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