diff --git a/app/build.gradle b/app/build.gradle index 4d476dfe..9618e3f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,5 +29,7 @@ dependencies { compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.android.support:design:22.2.0' compile 'com.android.support:recyclerview-v7:22.2.0' + compile 'com.google.android.apps.muzei:muzei-api:2.0' compile 'com.google.android.gms:play-services-gcm:7.5.0' + compile 'com.google.android.gms:play-services-location:7.5.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ae3cdd4..a8893d32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,9 @@ android:protectionLevel="signature" /> + + + + + + + + + + + = Build.VERSION_CODES.HONEYCOMB) { + mAttribution = new ImageView(this); + mAttribution.setImageResource(R.drawable.powered_by_google_light); + + if (!Utility.isLocationLatLonAvailable(this)) { + mAttribution.setVisibility(View.GONE); + } + + setListFooter(mAttribution); + } } // Registers a shared preference change listener that gets notified when preferences change @@ -133,7 +155,17 @@ public boolean onPreferenceChange(Preference preference, Object value) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if ( key.equals(getString(R.string.pref_location_key)) ) { // we've changed the location - // first clear locationStatus + // Wipe out any potential PlacePicker latlng values so that we can use this text entry. + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(getString(R.string.pref_location_latitude)); + editor.remove(getString(R.string.pref_location_longitude)); + editor.commit(); + + // Remove attributions for our any PlacePicker locations. + if (mAttribution != null) { + mAttribution.setVisibility(View.GONE); + } + Utility.resetLocationStatus(this); SunshineSyncAdapter.syncImmediately(this); } else if ( key.equals(getString(R.string.pref_units_key)) ) { @@ -154,4 +186,59 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin public Intent getParentActivityIntent() { return super.getParentActivityIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // Check to see if the result is from our Place Picker intent + if (requestCode == PLACE_PICKER_REQUEST) { + // Make sure the request was successful + if (resultCode == RESULT_OK) { + Place place = PlacePicker.getPlace(data, this); + String address = place.getAddress().toString(); + LatLng latLong = place.getLatLng(); + + // If the provided place doesn't have an address, we'll form a display-friendly + // string from the latlng values. + if (TextUtils.isEmpty(address)) { + address = String.format("(%.2f, %.2f)",latLong.latitude, latLong.longitude); + } + + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(getString(R.string.pref_location_key), address); + + // Also store the latitude and longitude so that we can use these to get a precise + // result from our weather service. We cannot expect the weather service to + // understand addresses that Google formats. + editor.putFloat(getString(R.string.pref_location_latitude), + (float) latLong.latitude); + editor.putFloat(getString(R.string.pref_location_longitude), + (float) latLong.longitude); + editor.commit(); + + // Tell the SyncAdapter that we've changed the location, so that we can update + // our UI with new values. We need to do this manually because we are responding + // to the PlacePicker widget result here instead of allowing the + // LocationEditTextPreference to handle these changes and invoke our callbacks. + Preference locationPreference = findPreference(getString(R.string.pref_location_key)); + setPreferenceSummary(locationPreference, address); + + // Add attributions for our new PlacePicker location. + if (mAttribution != null) { + mAttribution.setVisibility(View.VISIBLE); + } else { + // For pre-Honeycomb devices, we cannot add a footer, so we will use a snackbar + View rootView = findViewById(android.R.id.content); + Snackbar.make(rootView, getString(R.string.attribution_text), + Snackbar.LENGTH_LONG).show(); + } + + Utility.resetLocationStatus(this); + SunshineSyncAdapter.syncImmediately(this); + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } } diff --git a/app/src/main/java/com/example/android/sunshine/app/Utility.java b/app/src/main/java/com/example/android/sunshine/app/Utility.java index 4368a376..5f559620 100644 --- a/app/src/main/java/com/example/android/sunshine/app/Utility.java +++ b/app/src/main/java/com/example/android/sunshine/app/Utility.java @@ -30,6 +30,30 @@ import java.util.Locale; public class Utility { + // We'll default our latlong to 0. Yay, "Earth!" + public static float DEFAULT_LATLONG = 0F; + + public static boolean isLocationLatLonAvailable(Context context) { + SharedPreferences prefs + = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.contains(context.getString(R.string.pref_location_latitude)) + && prefs.contains(context.getString(R.string.pref_location_longitude)); + } + + public static float getLocationLatitude(Context context) { + SharedPreferences prefs + = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getFloat(context.getString(R.string.pref_location_latitude), + DEFAULT_LATLONG); + } + + public static float getLocationLongitude(Context context) { + SharedPreferences prefs + = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getFloat(context.getString(R.string.pref_location_longitude), + DEFAULT_LATLONG); + } + public static String getPreferredLocation(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); return prefs.getString(context.getString(R.string.pref_location_key), @@ -339,7 +363,7 @@ public static String getStringForWeatherCondition(Context context, int weatherId stringId = R.string.condition_2xx; } else if (weatherId >= 300 && weatherId <= 321) { stringId = R.string.condition_3xx; - } else switch(weatherId) { + } else switch (weatherId) { case 500: stringId = R.string.condition_500; break; @@ -502,6 +526,42 @@ public static String getStringForWeatherCondition(Context context, int weatherId return context.getString(stringId); } + /* + * Helper method to provide the correct image according to the weather condition id returned + * by the OpenWeatherMap call. + * + * @param weatherId from OpenWeatherMap API response + * @return A string URL to an appropriate image or null if no mapping is found + */ + public static String getImageUrlForWeatherCondition(int weatherId) { + // Based on weather code data found at: + // http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes + if (weatherId >= 200 && weatherId <= 232) { + return "http://upload.wikimedia.org/wikipedia/commons/2/28/Thunderstorm_in_Annemasse,_France.jpg"; + } else if (weatherId >= 300 && weatherId <= 321) { + return "http://upload.wikimedia.org/wikipedia/commons/a/a0/Rain_on_leaf_504605006.jpg"; + } else if (weatherId >= 500 && weatherId <= 504) { + return "http://upload.wikimedia.org/wikipedia/commons/6/6c/Rain-on-Thassos.jpg"; + } else if (weatherId == 511) { + return "http://upload.wikimedia.org/wikipedia/commons/b/b8/Fresh_snow.JPG"; + } else if (weatherId >= 520 && weatherId <= 531) { + return "http://upload.wikimedia.org/wikipedia/commons/6/6c/Rain-on-Thassos.jpg"; + } else if (weatherId >= 600 && weatherId <= 622) { + return "http://upload.wikimedia.org/wikipedia/commons/b/b8/Fresh_snow.JPG"; + } else if (weatherId >= 701 && weatherId <= 761) { + return "http://upload.wikimedia.org/wikipedia/commons/e/e6/Westminster_fog_-_London_-_UK.jpg"; + } else if (weatherId == 761 || weatherId == 781) { + return "http://upload.wikimedia.org/wikipedia/commons/d/dc/Raised_dust_ahead_of_a_severe_thunderstorm_1.jpg"; + } else if (weatherId == 800) { + return "http://upload.wikimedia.org/wikipedia/commons/7/7e/A_few_trees_and_the_sun_(6009964513).jpg"; + } else if (weatherId == 801) { + return "http://upload.wikimedia.org/wikipedia/commons/e/e7/Cloudy_Blue_Sky_(5031259890).jpg"; + } else if (weatherId >= 802 && weatherId <= 804) { + return "http://upload.wikimedia.org/wikipedia/commons/5/54/Cloudy_hills_in_Elis,_Greece_2.jpg"; + } + return null; + } + /** * Returns true if the network is available or about to become available. * diff --git a/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java b/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java new file mode 100644 index 00000000..dcfc24cb --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.sunshine.app.muzei; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; + +import com.example.android.sunshine.app.MainActivity; +import com.example.android.sunshine.app.Utility; +import com.example.android.sunshine.app.data.WeatherContract; +import com.example.android.sunshine.app.sync.SunshineSyncAdapter; +import com.google.android.apps.muzei.api.Artwork; +import com.google.android.apps.muzei.api.MuzeiArtSource; + +/** + * Muzei source that changes your background based on the current weather conditions + */ +public class WeatherMuzeiSource extends MuzeiArtSource { + private static final String[] FORECAST_COLUMNS = new String[]{ + WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, + WeatherContract.WeatherEntry.COLUMN_SHORT_DESC + }; + // these indices must match the projection + private static final int INDEX_WEATHER_ID = 0; + private static final int INDEX_SHORT_DESC = 1; + + public WeatherMuzeiSource() { + super("WeatherMuzeiSource"); + } + + @Override + protected void onHandleIntent(Intent intent) { + super.onHandleIntent(intent); + boolean dataUpdated = intent != null && + SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction()); + if (dataUpdated && isEnabled()) { + onUpdate(UPDATE_REASON_OTHER); + } + } + + @Override + protected void onUpdate(int reason) { + String location = Utility.getPreferredLocation(this); + Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate( + location, System.currentTimeMillis()); + Cursor cursor = getContentResolver().query(weatherForLocationUri, FORECAST_COLUMNS, null, + null, WeatherContract.WeatherEntry.COLUMN_DATE + " ASC"); + if (cursor.moveToFirst()) { + int weatherId = cursor.getInt(INDEX_WEATHER_ID); + String desc = cursor.getString(INDEX_SHORT_DESC); + + String imageUrl = Utility.getImageUrlForWeatherCondition(weatherId); + // Only publish a new wallpaper if we have a valid image + if (imageUrl != null) { + publishArtwork(new Artwork.Builder() + .imageUri(Uri.parse(imageUrl)) + .title(desc) + .byline(location) + .viewIntent(new Intent(this, MainActivity.class)) + .build()); + } + } + cursor.close(); + } +} diff --git a/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java b/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java index c9275d0e..4a1232a9 100644 --- a/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java +++ b/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java @@ -34,6 +34,7 @@ import com.example.android.sunshine.app.R; import com.example.android.sunshine.app.Utility; import com.example.android.sunshine.app.data.WeatherContract; +import com.example.android.sunshine.app.muzei.WeatherMuzeiSource; import org.json.JSONArray; import org.json.JSONException; @@ -92,7 +93,13 @@ public SunshineSyncAdapter(Context context, boolean autoInitialize) { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Log.d(LOG_TAG, "Starting sync"); - String locationQuery = Utility.getPreferredLocation(getContext()); + + // We no longer need just the location String, but also potentially the latitude and + // longitude, in case we are syncing based on a new Place Picker API result. + Context context = getContext(); + String locationQuery = Utility.getPreferredLocation(context); + String locationLatitude = String.valueOf(Utility.getLocationLatitude(context)); + String locationLongitude = String.valueOf(Utility.getLocationLongitude(context)); // These two need to be declared outside the try/catch // so that they can be closed in the finally block. @@ -113,13 +120,28 @@ public void onPerformSync(Account account, Bundle extras, String authority, Cont final String FORECAST_BASE_URL = "http://api.openweathermap.org/data/2.5/forecast/daily?"; final String QUERY_PARAM = "q"; + final String LAT_PARAM = "lat"; + final String LON_PARAM = "lon"; final String FORMAT_PARAM = "mode"; final String UNITS_PARAM = "units"; final String DAYS_PARAM = "cnt"; - Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon() - .appendQueryParameter(QUERY_PARAM, locationQuery) - .appendQueryParameter(FORMAT_PARAM, format) + Uri.Builder uriBuilder = Uri.parse(FORECAST_BASE_URL).buildUpon(); + + // Instead of always building the query based off of the location string, we want to + // potentially build a query using a lat/lon value. This will be the case when we are + // syncing based off of a new location from the Place Picker API. So we need to check + // if we have a lat/lon to work with, and use those when we do. Otherwise, the weather + // service may not understand the location address provided by the Place Picker API + // and the user could end up with no weather! The horror! + if (Utility.isLocationLatLonAvailable(context)) { + uriBuilder.appendQueryParameter(LAT_PARAM, locationLatitude) + .appendQueryParameter(LON_PARAM, locationLongitude); + } else { + uriBuilder.appendQueryParameter(QUERY_PARAM, locationQuery); + } + + Uri builtUri = uriBuilder.appendQueryParameter(FORMAT_PARAM, format) .appendQueryParameter(UNITS_PARAM, units) .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays)) .build(); @@ -226,6 +248,7 @@ private void getWeatherDataFromJson(String forecastJsonStr, try { JSONObject forecastJson = new JSONObject(forecastJsonStr); + Context context = getContext(); // do we have an error? if ( forecastJson.has(OWM_MESSAGE_CODE) ) { @@ -341,6 +364,7 @@ private void getWeatherDataFromJson(String forecastJsonStr, new String[] {Long.toString(dayTime.setJulianDay(julianStartDay-1))}); updateWidgets(); + updateMuzei(); notifyWeather(); } Log.d(LOG_TAG, "Sync Complete. " + cVVector.size() + " Inserted"); @@ -361,6 +385,16 @@ private void updateWidgets() { context.sendBroadcast(dataUpdatedIntent); } + private void updateMuzei() { + // Muzei is only compatible with Jelly Bean MR1+ devices, so there's no need to update the + // Muzei background on lower API level devices + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Context context = getContext(); + context.startService(new Intent(ACTION_DATA_UPDATED) + .setClass(context, WeatherMuzeiSource.class)); + } + } + private void notifyWeather() { Context context = getContext(); //checking the last update and notify if it' the first of the day diff --git a/app/src/main/res/drawable-hdpi/ic_current_location.png b/app/src/main/res/drawable-hdpi/ic_current_location.png new file mode 100644 index 00000000..85e38726 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_current_location.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_current_location.png b/app/src/main/res/drawable-mdpi/ic_current_location.png new file mode 100644 index 00000000..5684aa7d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_current_location.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_muzei.png b/app/src/main/res/drawable-nodpi/ic_muzei.png new file mode 100755 index 00000000..b0fe3c49 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_muzei.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_current_location.png b/app/src/main/res/drawable-xhdpi/ic_current_location.png new file mode 100644 index 00000000..7faa3455 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_current_location.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_current_location.png b/app/src/main/res/drawable-xxhdpi/ic_current_location.png new file mode 100644 index 00000000..d3a1ab08 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_current_location.png differ diff --git a/app/src/main/res/layout/pref_current_location.xml b/app/src/main/res/layout/pref_current_location.xml new file mode 100644 index 00000000..a5d79382 --- /dev/null +++ b/app/src/main/res/layout/pref_current_location.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74e7690a..93ab7dea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,9 +45,16 @@ loc-status + + loc-latitude + loc-longitude + 94043 + + Use my location + Invalid Location (%1$s)" Validating Location... (%1$s)" @@ -140,6 +147,9 @@ Sunshine Today Sunshine Details + + Today\'s weather + No Weather Information Available No weather information available. The network is not available to fetch weather data. @@ -223,4 +233,7 @@ Heads up: %1$s in %2$s! // TODO: Get the SenderID from the Developer Console + + Powered by Google +