diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d41a398..925588f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,6 +122,23 @@ android:resource="@xml/widget_info_today" /> + + + + + + + + + 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 7c8b370..8bcb4b9 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 class ForecastAdapter extends RecyclerView.Adapter, 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 class ForecastFragment extends Fragment implements LoaderManager.LoaderCa locationSetting, date), vh ); - mPosition = vh.getAdapterPosition(); } }, emptyView, mChoiceMode); @@ -246,11 +243,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa // 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 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa @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 class ForecastFragment extends Fragment implements LoaderManager.LoaderCa @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 class ForecastFragment extends Fragment implements LoaderManager.LoaderCa // 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 class ForecastFragment extends Fragment implements LoaderManager.LoaderCa } } + 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/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 6460c9e..d90301c 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 @@ -34,6 +34,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import com.example.android.sunshine.app.data.WeatherContract; import com.example.android.sunshine.app.sync.SunshineSyncAdapter; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; @@ -63,6 +64,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); @@ -78,8 +80,14 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment. // 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 { @@ -90,6 +98,10 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment. 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/Utility.java b/app/src/main/java/com/example/android/sunshine/app/Utility.java index 7e1dacb..4368a37 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 @@ -72,7 +72,7 @@ public class Utility { * @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 +87,7 @@ public class Utility { // 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( 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 0000000..85896eb --- /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 0000000..f31c3ba --- /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/res/drawable-nodpi/widget_preview_detail.png b/app/src/main/res/drawable-nodpi/widget_preview_detail.png new file mode 100644 index 0000000..7e5a796 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/widget_preview_detail.png differ 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 0000000..364d33c --- /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 0000000..2231b65 --- /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/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml new file mode 100644 index 0000000..a2b6639 --- /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 0000000..d187294 --- /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 0000000..11c11a9 --- /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 1c7cbb2..b547465 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -59,4 +59,19 @@ 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 27c9e53..52c512f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,6 +138,7 @@ Sunshine Today + Sunshine Details No Weather Information Available 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 0000000..54ad27d --- /dev/null +++ b/app/src/main/res/xml/widget_info_detail.xml @@ -0,0 +1,29 @@ + + + + \ No newline at end of file