diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 00000000..2a6bcb28
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @udacity/active-public-content
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 4d476dfe..79af6450 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,6 +18,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ buildTypes.each {
+ it.buildConfigField 'String', 'OPEN_WEATHER_MAP_API_KEY', MyOpenWeatherMapApiKey
+ }
}
dependencies {
@@ -29,5 +32,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 4a106a1c..a8893d32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,9 @@
android:protectionLevel="signature" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
index 7c8b3705..8bcb4b93 100644
--- a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
+++ b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
@@ -127,13 +127,16 @@ public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder
mCursor.moveToPosition(position);
int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
int defaultImage;
+ boolean useLongToday;
switch (getItemViewType(position)) {
case VIEW_TYPE_TODAY:
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
+ useLongToday = true;
break;
default:
defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
+ useLongToday = false;
}
if ( Utility.usingLocalGraphics(mContext) ) {
@@ -154,7 +157,7 @@ public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder
long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
// Find TextView and set formatted date on it
- forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis));
+ forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis, useLongToday));
// Read weather forecast from cursor
String description = Utility.getStringForWeatherCondition(mContext, weatherId);
diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
index 13ed1d2b..2c4537e3 100644
--- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
+++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
@@ -26,7 +26,6 @@
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.design.widget.AppBarLayout;
-import android.support.design.widget.CoordinatorLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
@@ -55,12 +54,11 @@
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks, SharedPreferences.OnSharedPreferenceChangeListener {
public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
private ForecastAdapter mForecastAdapter;
-
private RecyclerView mRecyclerView;
- private int mPosition = RecyclerView.NO_POSITION;
private boolean mUseTodayLayout, mAutoSelectView;
private int mChoiceMode;
private boolean mHoldForTransition;
+ private long mInitialSelectedDate = -1;
private static final String SELECTED_KEY = "selected_position";
@@ -196,7 +194,6 @@ public void onClick(Long date, ForecastAdapter.ForecastAdapterViewHolder vh) {
locationSetting, date),
vh
);
- mPosition = vh.getAdapterPosition();
}
}, emptyView, mChoiceMode);
@@ -246,11 +243,6 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// or magically appeared to take advantage of room, but data or place in the app was never
// actually *lost*.
if (savedInstanceState != null) {
- if (savedInstanceState.containsKey(SELECTED_KEY)) {
- // The Recycler View probably hasn't even been populated yet. Actually perform the
- // swapout in onLoadFinished.
- mPosition = savedInstanceState.getInt(SELECTED_KEY);
- }
mForecastAdapter.onRestoreInstanceState(savedInstanceState);
}
@@ -304,11 +296,6 @@ private void openPreferredLocationInMap() {
@Override
public void onSaveInstanceState(Bundle outState) {
// When tablets rotate, the currently selected list item needs to be saved.
- // When no item is selected, mPosition will be set to RecyclerView.NO_POSITION,
- // so check for that before storing.
- if (mPosition != RecyclerView.NO_POSITION) {
- outState.putInt(SELECTED_KEY, mPosition);
- }
mForecastAdapter.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
@@ -340,11 +327,6 @@ public Loader onCreateLoader(int i, Bundle bundle) {
@Override
public void onLoadFinished(Loader loader, Cursor data) {
mForecastAdapter.swapCursor(data);
- if (mPosition != RecyclerView.NO_POSITION) {
- // If we don't need to restart the loader, and there's a desired position to restore
- // to, do so now.
- mRecyclerView.smoothScrollToPosition(mPosition);
- }
updateEmptyView();
if ( data.getCount() == 0 ) {
getActivity().supportStartPostponedEnterTransition();
@@ -356,11 +338,27 @@ public boolean onPreDraw() {
// we see Children.
if (mRecyclerView.getChildCount() > 0) {
mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
- int itemPosition = mForecastAdapter.getSelectedItemPosition();
- if ( RecyclerView.NO_POSITION == itemPosition ) itemPosition = 0;
- RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(itemPosition);
- if ( null != vh && mAutoSelectView ) {
- mForecastAdapter.selectView( vh );
+ int position = mForecastAdapter.getSelectedItemPosition();
+ if (position == RecyclerView.NO_POSITION &&
+ -1 != mInitialSelectedDate) {
+ Cursor data = mForecastAdapter.getCursor();
+ int count = data.getCount();
+ int dateColumn = data.getColumnIndex(WeatherContract.WeatherEntry.COLUMN_DATE);
+ for ( int i = 0; i < count; i++ ) {
+ data.moveToPosition(i);
+ if ( data.getLong(dateColumn) == mInitialSelectedDate ) {
+ position = i;
+ break;
+ }
+ }
+ }
+ if (position == RecyclerView.NO_POSITION) position = 0;
+ // If we don't need to restart the loader, and there's a desired position to restore
+ // to, do so now.
+ mRecyclerView.smoothScrollToPosition(position);
+ RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(position);
+ if (null != vh && mAutoSelectView) {
+ mForecastAdapter.selectView(vh);
}
if ( mHoldForTransition ) {
getActivity().supportStartPostponedEnterTransition();
@@ -396,6 +394,10 @@ public void setUseTodayLayout(boolean useTodayLayout) {
}
}
+ public void setInitialSelectedDate(long initialSelectedDate) {
+ mInitialSelectedDate = initialSelectedDate;
+ }
+
/*
Updates the empty list view with contextually relevant information that the user can
use to determine why they aren't seeing weather.
diff --git a/app/src/main/java/com/example/android/sunshine/app/LocationEditTextPreference.java b/app/src/main/java/com/example/android/sunshine/app/LocationEditTextPreference.java
index eb84c05e..b91398c8 100644
--- a/app/src/main/java/com/example/android/sunshine/app/LocationEditTextPreference.java
+++ b/app/src/main/java/com/example/android/sunshine/app/LocationEditTextPreference.java
@@ -15,6 +15,7 @@
*/
package com.example.android.sunshine.app;
+import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
@@ -24,9 +25,17 @@
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
+import com.google.android.gms.common.GooglePlayServicesRepairableException;
+import com.google.android.gms.location.places.ui.PlacePicker;
+
public class LocationEditTextPreference extends EditTextPreference {
static final private int DEFAULT_MINIMUM_LOCATION_LENGTH = 2;
private int mMinLength;
@@ -42,8 +51,57 @@ public LocationEditTextPreference(Context context, AttributeSet attrs) {
} finally {
a.recycle();
}
+
+ // Check to see if Google Play services is available. The Place Picker API is available
+ // through Google Play services, so if this is false, we'll just carry on as though this
+ // feature does not exist. If it is true, however, we can add a widget to our preference.
+ GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+ int resultCode = apiAvailability.isGooglePlayServicesAvailable(getContext());
+ if (resultCode == ConnectionResult.SUCCESS) {
+ // Add the get current location widget to our location preference
+ setWidgetLayoutResource(R.layout.pref_current_location);
+ }
}
+ @Override
+ protected View onCreateView(ViewGroup parent) {
+ View view = super.onCreateView(parent);
+ View currentLocation = view.findViewById(R.id.current_location);
+ currentLocation.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Context context = getContext();
+
+ // Launch the Place Picker so that the user can specify their location, and then
+ // return the result to SettingsActivity.
+ PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder();
+
+
+ // We are in a view right now, not an activity. So we need to get ourselves
+ // an activity that we can use to start our Place Picker intent. By using
+ // SettingsActivity in this way, we can ensure the result of the Place Picker
+ // intent comes to the right place for us to process it.
+ Activity settingsActivity = (SettingsActivity) context;
+ try {
+ settingsActivity.startActivityForResult(
+ builder.build(context), SettingsActivity.PLACE_PICKER_REQUEST);
+
+ } catch (GooglePlayServicesNotAvailableException
+ | GooglePlayServicesRepairableException e) {
+ // What did you do?? This is why we check Google Play services in onResume!!!
+ // The difference in these exception types is the difference between pausing
+ // for a moment to prompt the user to update/install/enable Play services vs
+ // complete and utter failure.
+ // If you prefer to manage Google Play services dynamically, then you can do so
+ // by responding to these exceptions in the right moment. But I prefer a cleaner
+ // user experience, which is why you check all of this when the app resumes,
+ // and then disable/enable features based on that availability.
+ }
+ }
+ });
+
+ return view;
+ }
@Override
protected void showDialog(Bundle state) {
diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java
index b7c718b9..a2f0b780 100644
--- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java
+++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java
@@ -30,6 +30,7 @@
import android.view.MenuItem;
import android.view.View;
+import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.gcm.RegistrationIntentService;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
import com.google.android.gms.common.ConnectionResult;
@@ -49,6 +50,7 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLocation = Utility.getPreferredLocation(this);
+ Uri contentUri = getIntent() != null ? getIntent().getData() : null;
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
@@ -64,8 +66,14 @@ protected void onCreate(Bundle savedInstanceState) {
// adding or replacing the detail fragment using a
// fragment transaction.
if (savedInstanceState == null) {
+ DetailFragment fragment = new DetailFragment();
+ if (contentUri != null) {
+ Bundle args = new Bundle();
+ args.putParcelable(DetailFragment.DETAIL_URI, contentUri);
+ fragment.setArguments(args);
+ }
getSupportFragmentManager().beginTransaction()
- .replace(R.id.weather_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG)
+ .replace(R.id.weather_detail_container, fragment, DETAILFRAGMENT_TAG)
.commit();
}
} else {
@@ -76,6 +84,10 @@ protected void onCreate(Bundle savedInstanceState) {
ForecastFragment forecastFragment = ((ForecastFragment)getSupportFragmentManager()
.findFragmentById(R.id.fragment_forecast));
forecastFragment.setUseTodayLayout(!mTwoPane);
+ if (contentUri != null) {
+ forecastFragment.setInitialSelectedDate(
+ WeatherContract.WeatherEntry.getDateFromUri(contentUri));
+ }
SunshineSyncAdapter.initializeSyncAdapter(this);
diff --git a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
index afd1cace..ae20b520 100644
--- a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
+++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
@@ -24,9 +24,16 @@
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
-
+import android.support.design.widget.Snackbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
+import com.google.android.gms.location.places.Place;
+import com.google.android.gms.location.places.ui.PlacePicker;
+import com.google.android.gms.maps.model.LatLng;
/**
* A {@link PreferenceActivity} that presents a set of application settings.
@@ -38,6 +45,8 @@
*/
public class SettingsActivity extends PreferenceActivity
implements Preference.OnPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
+ protected final static int PLACE_PICKER_REQUEST = 9090;
+ private ImageView mAttribution;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -50,6 +59,19 @@ public void onCreate(Bundle savedInstanceState) {
bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key)));
bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key)));
bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_art_pack_key)));
+
+
+ // If we are using a PlacePicker location, we need to show attributions.
+ if (Build.VERSION.SDK_INT >= 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 7e1dacbe..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),
@@ -72,7 +96,7 @@ static String formatDate(long dateInMilliseconds) {
* @param dateInMillis The date in milliseconds
* @return a user-friendly representation of the date.
*/
- public static String getFriendlyDayString(Context context, long dateInMillis) {
+ public static String getFriendlyDayString(Context context, long dateInMillis, boolean displayLongToday) {
// The day string for forecast uses the following logic:
// For today: "Today, June 8"
// For tomorrow: "Tomorrow"
@@ -87,7 +111,7 @@ public static String getFriendlyDayString(Context context, long dateInMillis) {
// If the date we're building the String for is today's date, the format
// is "Today, June 24"
- if (julianDay == currentJulianDay) {
+ if (displayLongToday && julianDay == currentJulianDay) {
String today = context.getString(R.string.today);
int formatId = R.string.format_full_friendly_date;
return String.format(context.getString(
@@ -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 e2482357..9aa52c96 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
@@ -30,10 +30,12 @@
import android.util.Log;
import com.bumptech.glide.Glide;
+import com.example.android.sunshine.app.BuildConfig;
import com.example.android.sunshine.app.MainActivity;
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;
@@ -52,6 +54,8 @@
public class SunshineSyncAdapter extends AbstractThreadedSyncAdapter {
public final String LOG_TAG = SunshineSyncAdapter.class.getSimpleName();
+ public static final String ACTION_DATA_UPDATED =
+ "com.example.android.sunshine.app.ACTION_DATA_UPDATED";
// Interval at which to sync with the weather, in seconds.
// 60 seconds (1 minute) * 180 = 3 hours
public static final int SYNC_INTERVAL = 60 * 180;
@@ -90,7 +94,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.
@@ -111,15 +121,32 @@ 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";
+ final String APPID_PARAM = "APPID";
+
+ 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 = Uri.parse(FORECAST_BASE_URL).buildUpon()
- .appendQueryParameter(QUERY_PARAM, locationQuery)
- .appendQueryParameter(FORMAT_PARAM, format)
+ Uri builtUri = uriBuilder.appendQueryParameter(FORMAT_PARAM, format)
.appendQueryParameter(UNITS_PARAM, units)
.appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
+ .appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY)
.build();
URL url = new URL(builtUri.toString());
@@ -224,6 +251,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) ) {
@@ -338,6 +366,8 @@ private void getWeatherDataFromJson(String forecastJsonStr,
WeatherContract.WeatherEntry.COLUMN_DATE + " <= ?",
new String[] {Long.toString(dayTime.setJulianDay(julianStartDay-1))});
+ updateWidgets();
+ updateMuzei();
notifyWeather();
}
Log.d(LOG_TAG, "Sync Complete. " + cVVector.size() + " Inserted");
@@ -350,6 +380,24 @@ private void getWeatherDataFromJson(String forecastJsonStr,
}
}
+ private void updateWidgets() {
+ Context context = getContext();
+ // Setting the package ensures that only components in our app will receive the broadcast
+ Intent dataUpdatedIntent = new Intent(ACTION_DATA_UPDATED)
+ .setPackage(context.getPackageName());
+ 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/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java
new file mode 100644
index 00000000..85896eba
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java
@@ -0,0 +1,89 @@
+package com.example.android.sunshine.app.widget;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.TaskStackBuilder;
+import android.widget.RemoteViews;
+
+import com.example.android.sunshine.app.DetailActivity;
+import com.example.android.sunshine.app.MainActivity;
+import com.example.android.sunshine.app.R;
+import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
+
+/**
+ * Provider for a scrollable weather detail widget
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class DetailWidgetProvider extends AppWidgetProvider {
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ // Perform this loop procedure for each App Widget that belongs to this provider
+ for (int appWidgetId : appWidgetIds) {
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_detail);
+
+ // Create an Intent to launch MainActivity
+ Intent intent = new Intent(context, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.widget, pendingIntent);
+
+ // Set up the collection
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ setRemoteAdapter(context, views);
+ } else {
+ setRemoteAdapterV11(context, views);
+ }
+ boolean useDetailActivity = context.getResources()
+ .getBoolean(R.bool.use_detail_activity);
+ Intent clickIntentTemplate = useDetailActivity
+ ? new Intent(context, DetailActivity.class)
+ : new Intent(context, MainActivity.class);
+ PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
+ .addNextIntentWithParentStack(clickIntentTemplate)
+ .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setPendingIntentTemplate(R.id.widget_list, clickPendingIntentTemplate);
+ views.setEmptyView(R.id.widget_list, R.id.widget_empty);
+
+ // Tell the AppWidgetManager to perform an update on the current app widget
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+ }
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ super.onReceive(context, intent);
+ if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(
+ new ComponentName(context, getClass()));
+ appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
+ }
+ }
+
+ /**
+ * Sets the remote adapter used to fill in the list items
+ *
+ * @param views RemoteViews to set the RemoteAdapter
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setRemoteAdapter(Context context, @NonNull final RemoteViews views) {
+ views.setRemoteAdapter(R.id.widget_list,
+ new Intent(context, DetailWidgetRemoteViewsService.class));
+ }
+
+ /**
+ * Sets the remote adapter used to fill in the list items
+ *
+ * @param views RemoteViews to set the RemoteAdapter
+ */
+ @SuppressWarnings("deprecation")
+ private void setRemoteAdapterV11(Context context, @NonNull final RemoteViews views) {
+ views.setRemoteAdapter(0, R.id.widget_list,
+ new Intent(context, DetailWidgetRemoteViewsService.class));
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java
new file mode 100644
index 00000000..f31c3ba9
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java
@@ -0,0 +1,175 @@
+package com.example.android.sunshine.app.widget;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+import android.widget.AdapterView;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.Target;
+import com.example.android.sunshine.app.R;
+import com.example.android.sunshine.app.Utility;
+import com.example.android.sunshine.app.data.WeatherContract;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * RemoteViewsService controlling the data being shown in the scrollable weather detail widget
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class DetailWidgetRemoteViewsService extends RemoteViewsService {
+ public final String LOG_TAG = DetailWidgetRemoteViewsService.class.getSimpleName();
+ private static final String[] FORECAST_COLUMNS = {
+ WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
+ WeatherContract.WeatherEntry.COLUMN_DATE,
+ WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
+ WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
+ WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
+ WeatherContract.WeatherEntry.COLUMN_MIN_TEMP
+ };
+ // these indices must match the projection
+ static final int INDEX_WEATHER_ID = 0;
+ static final int INDEX_WEATHER_DATE = 1;
+ static final int INDEX_WEATHER_CONDITION_ID = 2;
+ static final int INDEX_WEATHER_DESC = 3;
+ static final int INDEX_WEATHER_MAX_TEMP = 4;
+ static final int INDEX_WEATHER_MIN_TEMP = 5;
+
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ return new RemoteViewsFactory() {
+ private Cursor data = null;
+
+ @Override
+ public void onCreate() {
+ // Nothing to do
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ if (data != null) {
+ data.close();
+ }
+ // This method is called by the app hosting the widget (e.g., the launcher)
+ // However, our ContentProvider is not exported so it doesn't have access to the
+ // data. Therefore we need to clear (and finally restore) the calling identity so
+ // that calls use our process and permission
+ final long identityToken = Binder.clearCallingIdentity();
+ String location = Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
+ Uri weatherForLocationUri = WeatherContract.WeatherEntry
+ .buildWeatherLocationWithStartDate(location, System.currentTimeMillis());
+ data = getContentResolver().query(weatherForLocationUri,
+ FORECAST_COLUMNS,
+ null,
+ null,
+ WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (data != null) {
+ data.close();
+ data = null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return data == null ? 0 : data.getCount();
+ }
+
+ @Override
+ public RemoteViews getViewAt(int position) {
+ if (position == AdapterView.INVALID_POSITION ||
+ data == null || !data.moveToPosition(position)) {
+ return null;
+ }
+ RemoteViews views = new RemoteViews(getPackageName(),
+ R.layout.widget_detail_list_item);
+ int weatherId = data.getInt(INDEX_WEATHER_CONDITION_ID);
+ int weatherArtResourceId = Utility.getIconResourceForWeatherCondition(weatherId);
+ Bitmap weatherArtImage = null;
+ if ( !Utility.usingLocalGraphics(DetailWidgetRemoteViewsService.this) ) {
+ String weatherArtResourceUrl = Utility.getArtUrlForWeatherCondition(
+ DetailWidgetRemoteViewsService.this, weatherId);
+ try {
+ weatherArtImage = Glide.with(DetailWidgetRemoteViewsService.this)
+ .load(weatherArtResourceUrl)
+ .asBitmap()
+ .error(weatherArtResourceId)
+ .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Error retrieving large icon from " + weatherArtResourceUrl, e);
+ }
+ }
+ String description = data.getString(INDEX_WEATHER_DESC);
+ long dateInMillis = data.getLong(INDEX_WEATHER_DATE);
+ String formattedDate = Utility.getFriendlyDayString(
+ DetailWidgetRemoteViewsService.this, dateInMillis, false);
+ double maxTemp = data.getDouble(INDEX_WEATHER_MAX_TEMP);
+ double minTemp = data.getDouble(INDEX_WEATHER_MIN_TEMP);
+ String formattedMaxTemperature =
+ Utility.formatTemperature(DetailWidgetRemoteViewsService.this, maxTemp);
+ String formattedMinTemperature =
+ Utility.formatTemperature(DetailWidgetRemoteViewsService.this, minTemp);
+ if (weatherArtImage != null) {
+ views.setImageViewBitmap(R.id.widget_icon, weatherArtImage);
+ } else {
+ views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ setRemoteContentDescription(views, description);
+ }
+ views.setTextViewText(R.id.widget_date, formattedDate);
+ views.setTextViewText(R.id.widget_description, description);
+ views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
+ views.setTextViewText(R.id.widget_low_temperature, formattedMinTemperature);
+
+ final Intent fillInIntent = new Intent();
+ String locationSetting =
+ Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
+ Uri weatherUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
+ locationSetting,
+ dateInMillis);
+ fillInIntent.setData(weatherUri);
+ views.setOnClickFillInIntent(R.id.widget_list_item, fillInIntent);
+ return views;
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private void setRemoteContentDescription(RemoteViews views, String description) {
+ views.setContentDescription(R.id.widget_icon, description);
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ return new RemoteViews(getPackageName(), R.layout.widget_detail_list_item);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (data.moveToPosition(position))
+ return data.getLong(INDEX_WEATHER_ID);
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java
new file mode 100644
index 00000000..0ab88541
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java
@@ -0,0 +1,151 @@
+/*
+ * 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.widget;
+
+import android.annotation.TargetApi;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.widget.RemoteViews;
+
+import com.example.android.sunshine.app.MainActivity;
+import com.example.android.sunshine.app.R;
+import com.example.android.sunshine.app.Utility;
+import com.example.android.sunshine.app.data.WeatherContract;
+
+/**
+ * IntentService which handles updating all Today widgets with the latest data
+ */
+public class TodayWidgetIntentService extends IntentService {
+ private static final String[] FORECAST_COLUMNS = {
+ WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
+ WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
+ WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
+ WeatherContract.WeatherEntry.COLUMN_MIN_TEMP
+ };
+ // these indices must match the projection
+ private static final int INDEX_WEATHER_ID = 0;
+ private static final int INDEX_SHORT_DESC = 1;
+ private static final int INDEX_MAX_TEMP = 2;
+ private static final int INDEX_MIN_TEMP = 3;
+
+ public TodayWidgetIntentService() {
+ super("TodayWidgetIntentService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Retrieve all of the Today widget ids: these are the widgets we need to update
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
+ TodayWidgetProvider.class));
+
+ // Get today's data from the ContentProvider
+ String location = Utility.getPreferredLocation(this);
+ Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
+ location, System.currentTimeMillis());
+ Cursor data = getContentResolver().query(weatherForLocationUri, FORECAST_COLUMNS, null,
+ null, WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
+ if (data == null) {
+ return;
+ }
+ if (!data.moveToFirst()) {
+ data.close();
+ return;
+ }
+
+ // Extract the weather data from the Cursor
+ int weatherId = data.getInt(INDEX_WEATHER_ID);
+ int weatherArtResourceId = Utility.getArtResourceForWeatherCondition(weatherId);
+ String description = data.getString(INDEX_SHORT_DESC);
+ double maxTemp = data.getDouble(INDEX_MAX_TEMP);
+ double minTemp = data.getDouble(INDEX_MIN_TEMP);
+ String formattedMaxTemperature = Utility.formatTemperature(this, maxTemp);
+ String formattedMinTemperature = Utility.formatTemperature(this, minTemp);
+ data.close();
+
+ // Perform this loop procedure for each Today widget
+ for (int appWidgetId : appWidgetIds) {
+ // Find the correct layout based on the widget's width
+ int widgetWidth = getWidgetWidth(appWidgetManager, appWidgetId);
+ int defaultWidth = getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ int largeWidth = getResources().getDimensionPixelSize(R.dimen.widget_today_large_width);
+ int layoutId;
+ if (widgetWidth >= largeWidth) {
+ layoutId = R.layout.widget_today_large;
+ } else if (widgetWidth >= defaultWidth) {
+ layoutId = R.layout.widget_today;
+ } else {
+ layoutId = R.layout.widget_today_small;
+ }
+ RemoteViews views = new RemoteViews(getPackageName(), layoutId);
+
+ // Add the data to the RemoteViews
+ views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
+ // Content Descriptions for RemoteViews were only added in ICS MR1
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ setRemoteContentDescription(views, description);
+ }
+ views.setTextViewText(R.id.widget_description, description);
+ views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
+ views.setTextViewText(R.id.widget_low_temperature, formattedMinTemperature);
+
+ // Create an Intent to launch MainActivity
+ Intent launchIntent = new Intent(this, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
+ views.setOnClickPendingIntent(R.id.widget, pendingIntent);
+
+ // Tell the AppWidgetManager to perform an update on the current app widget
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+ }
+
+ private int getWidgetWidth(AppWidgetManager appWidgetManager, int appWidgetId) {
+ // Prior to Jelly Bean, widgets were always their default size
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ return getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ }
+ // For Jelly Bean and higher devices, widgets can be resized - the current size can be
+ // retrieved from the newly added App Widget Options
+ return getWidgetWidthFromOptions(appWidgetManager, appWidgetId);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private int getWidgetWidthFromOptions(AppWidgetManager appWidgetManager, int appWidgetId) {
+ Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
+ if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)) {
+ int minWidthDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
+ // The width returned is in dp, but we'll convert it to pixels to match the other widths
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, minWidthDp,
+ displayMetrics);
+ }
+ return getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private void setRemoteContentDescription(RemoteViews views, String description) {
+ views.setContentDescription(R.id.widget_icon, description);
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java
index 04ef261b..81c1dfe6 100644
--- a/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java
@@ -15,55 +15,39 @@
*/
package com.example.android.sunshine.app.widget;
-import android.annotation.TargetApi;
-import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
-import android.os.Build;
-import android.widget.RemoteViews;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
-import com.example.android.sunshine.app.MainActivity;
-import com.example.android.sunshine.app.R;
-import com.example.android.sunshine.app.Utility;
+import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
/**
- * Provider for a widget showing today's weather.
+ * Provider for a horizontally expandable widget showing today's weather.
+ *
+ * Delegates widget updating to {@link TodayWidgetIntentService} to ensure that
+ * data retrieval is done on a background thread
*/
public class TodayWidgetProvider extends AppWidgetProvider {
+ @Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- int weatherArtResourceId = R.drawable.art_clear;
- String description = "Clear";
- double maxTemp = 24;
- String formattedMaxTemperature = Utility.formatTemperature(context, maxTemp);
-
- // Perform this loop procedure for each Today widget
- for (int appWidgetId : appWidgetIds) {
- int layoutId = R.layout.widget_today_small;
- RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
-
- // Add the data to the RemoteViews
- views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
- // Content Descriptions for RemoteViews were only added in ICS MR1
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
- setRemoteContentDescription(views, description);
- }
- views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
-
- // Create an Intent to launch MainActivity
- Intent launchIntent = new Intent(context, MainActivity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, 0);
- views.setOnClickPendingIntent(R.id.widget, pendingIntent);
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
+ }
- // Tell the AppWidgetManager to perform an update on the current app widget
- appWidgetManager.updateAppWidget(appWidgetId, views);
- }
+ @Override
+ public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
+ int appWidgetId, Bundle newOptions) {
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
}
- @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
- private void setRemoteContentDescription(RemoteViews views, String description) {
- views.setContentDescription(R.id.widget_icon, description);
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ super.onReceive(context, intent);
+ if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
+ }
}
}
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-nodpi/widget_preview_detail.png b/app/src/main/res/drawable-nodpi/widget_preview_detail.png
new file mode 100644
index 00000000..7e5a7969
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/widget_preview_detail.png differ
diff --git a/app/src/main/res/drawable-nodpi/widget_preview_today.png b/app/src/main/res/drawable-nodpi/widget_preview_today.png
new file mode 100644
index 00000000..7be01f0d
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/widget_preview_today.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/layout/widget_detail.xml b/app/src/main/res/layout/widget_detail.xml
new file mode 100644
index 00000000..364d33c0
--- /dev/null
+++ b/app/src/main/res/layout/widget_detail.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_detail_list_item.xml b/app/src/main/res/layout/widget_detail_list_item.xml
new file mode 100644
index 00000000..2231b659
--- /dev/null
+++ b/app/src/main/res/layout/widget_detail_list_item.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_today.xml b/app/src/main/res/layout/widget_today.xml
new file mode 100644
index 00000000..caf01ed0
--- /dev/null
+++ b/app/src/main/res/layout/widget_today.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_today_large.xml b/app/src/main/res/layout/widget_today_large.xml
new file mode 100644
index 00000000..3838aafd
--- /dev/null
+++ b/app/src/main/res/layout/widget_today_large.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml
new file mode 100644
index 00000000..a2b6639a
--- /dev/null
+++ b/app/src/main/res/values-sw600dp/bools.xml
@@ -0,0 +1,4 @@
+
+
+ false
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v11/bools.xml b/app/src/main/res/values-v11/bools.xml
new file mode 100644
index 00000000..d187294a
--- /dev/null
+++ b/app/src/main/res/values-v11/bools.xml
@@ -0,0 +1,4 @@
+
+
+ true
+
\ No newline at end of file
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
new file mode 100644
index 00000000..11c11a98
--- /dev/null
+++ b/app/src/main/res/values/bools.xml
@@ -0,0 +1,5 @@
+
+
+ false
+ true
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 12b07d17..b547465a 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -54,6 +54,24 @@
360dp
- 40dp
+ 110dp
40dp
+ 40dp
+ @dimen/widget_today_default_height
+ 220dp
+
+ 250dp
+ 180dp
+ 220dp
+ @dimen/widget_detail_default_height
+
+
+ 24dp
+
+
+ 38dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5a7c9ff2..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)"
@@ -138,6 +145,10 @@
Sunshine Today
+ Sunshine Details
+
+
+ Today\'s weather
No Weather Information Available
@@ -222,4 +233,7 @@
Heads up: %1$s in %2$s!
// TODO: Get the SenderID from the Developer Console
+
+ Powered by Google
+
diff --git a/app/src/main/res/xml/widget_info_detail.xml b/app/src/main/res/xml/widget_info_detail.xml
new file mode 100644
index 00000000..54ad27db
--- /dev/null
+++ b/app/src/main/res/xml/widget_info_detail.xml
@@ -0,0 +1,29 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/widget_info_today.xml b/app/src/main/res/xml/widget_info_today.xml
index 9812f0b5..c36d5643 100644
--- a/app/src/main/res/xml/widget_info_today.xml
+++ b/app/src/main/res/xml/widget_info_today.xml
@@ -14,7 +14,13 @@
limitations under the License.
-->
\ No newline at end of file
+ android:previewImage="@drawable/widget_preview_today"
+ android:resizeMode="horizontal"
+ android:updatePeriodMillis="0"
+ tools:ignore="UnusedAttribute" />