Integrate Detail widget, showing a full list of weather information and allowing clicking through to individual days
This commit is contained in:
@@ -122,6 +122,23 @@
|
|||||||
android:resource="@xml/widget_info_today" />
|
android:resource="@xml/widget_info_today" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<service android:name=".widget.TodayWidgetIntentService" />
|
<service android:name=".widget.TodayWidgetIntentService" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.DetailWidgetProvider"
|
||||||
|
android:label="@string/title_widget_detail"
|
||||||
|
android:enabled="@bool/widget_detail_enabled" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info_detail" />
|
||||||
|
</receiver>
|
||||||
|
<service
|
||||||
|
android:name=".widget.DetailWidgetRemoteViewsService"
|
||||||
|
android:enabled="@bool/widget_detail_enabled"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -127,13 +127,16 @@ public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.Foreca
|
|||||||
mCursor.moveToPosition(position);
|
mCursor.moveToPosition(position);
|
||||||
int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
|
int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
|
||||||
int defaultImage;
|
int defaultImage;
|
||||||
|
boolean useLongToday;
|
||||||
|
|
||||||
switch (getItemViewType(position)) {
|
switch (getItemViewType(position)) {
|
||||||
case VIEW_TYPE_TODAY:
|
case VIEW_TYPE_TODAY:
|
||||||
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
|
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
|
||||||
|
useLongToday = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
|
defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
|
||||||
|
useLongToday = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( Utility.usingLocalGraphics(mContext) ) {
|
if ( Utility.usingLocalGraphics(mContext) ) {
|
||||||
@@ -154,7 +157,7 @@ public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.Foreca
|
|||||||
long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
|
long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
|
||||||
|
|
||||||
// Find TextView and set formatted date on it
|
// 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
|
// Read weather forecast from cursor
|
||||||
String description = Utility.getStringForWeatherCondition(mContext, weatherId);
|
String description = Utility.getStringForWeatherCondition(mContext, weatherId);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.design.widget.AppBarLayout;
|
import android.support.design.widget.AppBarLayout;
|
||||||
import android.support.design.widget.CoordinatorLayout;
|
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
@@ -55,12 +54,11 @@ import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
|
|||||||
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, SharedPreferences.OnSharedPreferenceChangeListener {
|
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
|
public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
|
||||||
private ForecastAdapter mForecastAdapter;
|
private ForecastAdapter mForecastAdapter;
|
||||||
|
|
||||||
private RecyclerView mRecyclerView;
|
private RecyclerView mRecyclerView;
|
||||||
private int mPosition = RecyclerView.NO_POSITION;
|
|
||||||
private boolean mUseTodayLayout, mAutoSelectView;
|
private boolean mUseTodayLayout, mAutoSelectView;
|
||||||
private int mChoiceMode;
|
private int mChoiceMode;
|
||||||
private boolean mHoldForTransition;
|
private boolean mHoldForTransition;
|
||||||
|
private long mInitialSelectedDate = -1;
|
||||||
|
|
||||||
private static final String SELECTED_KEY = "selected_position";
|
private static final String SELECTED_KEY = "selected_position";
|
||||||
|
|
||||||
@@ -196,7 +194,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
|
|||||||
locationSetting, date),
|
locationSetting, date),
|
||||||
vh
|
vh
|
||||||
);
|
);
|
||||||
mPosition = vh.getAdapterPosition();
|
|
||||||
}
|
}
|
||||||
}, emptyView, mChoiceMode);
|
}, 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
|
// or magically appeared to take advantage of room, but data or place in the app was never
|
||||||
// actually *lost*.
|
// actually *lost*.
|
||||||
if (savedInstanceState != null) {
|
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);
|
mForecastAdapter.onRestoreInstanceState(savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,11 +296,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
// When tablets rotate, the currently selected list item needs to be saved.
|
// 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);
|
mForecastAdapter.onSaveInstanceState(outState);
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
}
|
}
|
||||||
@@ -340,11 +327,6 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||||
mForecastAdapter.swapCursor(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();
|
updateEmptyView();
|
||||||
if ( data.getCount() == 0 ) {
|
if ( data.getCount() == 0 ) {
|
||||||
getActivity().supportStartPostponedEnterTransition();
|
getActivity().supportStartPostponedEnterTransition();
|
||||||
@@ -356,11 +338,27 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
|
|||||||
// we see Children.
|
// we see Children.
|
||||||
if (mRecyclerView.getChildCount() > 0) {
|
if (mRecyclerView.getChildCount() > 0) {
|
||||||
mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
|
mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
int itemPosition = mForecastAdapter.getSelectedItemPosition();
|
int position = mForecastAdapter.getSelectedItemPosition();
|
||||||
if ( RecyclerView.NO_POSITION == itemPosition ) itemPosition = 0;
|
if (position == RecyclerView.NO_POSITION &&
|
||||||
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(itemPosition);
|
-1 != mInitialSelectedDate) {
|
||||||
if ( null != vh && mAutoSelectView ) {
|
Cursor data = mForecastAdapter.getCursor();
|
||||||
mForecastAdapter.selectView( vh );
|
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 ) {
|
if ( mHoldForTransition ) {
|
||||||
getActivity().supportStartPostponedEnterTransition();
|
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
|
Updates the empty list view with contextually relevant information that the user can
|
||||||
use to determine why they aren't seeing weather.
|
use to determine why they aren't seeing weather.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.example.android.sunshine.app.data.WeatherContract;
|
||||||
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
|
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
import com.google.android.gms.common.GooglePlayServicesUtil;
|
import com.google.android.gms.common.GooglePlayServicesUtil;
|
||||||
@@ -63,6 +64,7 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
mLocation = Utility.getPreferredLocation(this);
|
mLocation = Utility.getPreferredLocation(this);
|
||||||
|
Uri contentUri = getIntent() != null ? getIntent().getData() : null;
|
||||||
|
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
|
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
|
// adding or replacing the detail fragment using a
|
||||||
// fragment transaction.
|
// fragment transaction.
|
||||||
if (savedInstanceState == null) {
|
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()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.weather_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG)
|
.replace(R.id.weather_detail_container, fragment, DETAILFRAGMENT_TAG)
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -90,6 +98,10 @@ public class MainActivity extends AppCompatActivity implements ForecastFragment.
|
|||||||
ForecastFragment forecastFragment = ((ForecastFragment)getSupportFragmentManager()
|
ForecastFragment forecastFragment = ((ForecastFragment)getSupportFragmentManager()
|
||||||
.findFragmentById(R.id.fragment_forecast));
|
.findFragmentById(R.id.fragment_forecast));
|
||||||
forecastFragment.setUseTodayLayout(!mTwoPane);
|
forecastFragment.setUseTodayLayout(!mTwoPane);
|
||||||
|
if (contentUri != null) {
|
||||||
|
forecastFragment.setInitialSelectedDate(
|
||||||
|
WeatherContract.WeatherEntry.getDateFromUri(contentUri));
|
||||||
|
}
|
||||||
|
|
||||||
SunshineSyncAdapter.initializeSyncAdapter(this);
|
SunshineSyncAdapter.initializeSyncAdapter(this);
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class Utility {
|
|||||||
* @param dateInMillis The date in milliseconds
|
* @param dateInMillis The date in milliseconds
|
||||||
* @return a user-friendly representation of the date.
|
* @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:
|
// The day string for forecast uses the following logic:
|
||||||
// For today: "Today, June 8"
|
// For today: "Today, June 8"
|
||||||
// For tomorrow: "Tomorrow"
|
// 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
|
// If the date we're building the String for is today's date, the format
|
||||||
// is "Today, June 24"
|
// is "Today, June 24"
|
||||||
if (julianDay == currentJulianDay) {
|
if (displayLongToday && julianDay == currentJulianDay) {
|
||||||
String today = context.getString(R.string.today);
|
String today = context.getString(R.string.today);
|
||||||
int formatId = R.string.format_full_friendly_date;
|
int formatId = R.string.format_full_friendly_date;
|
||||||
return String.format(context.getString(
|
return String.format(context.getString(
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable-nodpi/widget_preview_detail.png
Normal file
BIN
app/src/main/res/drawable-nodpi/widget_preview_detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
58
app/src/main/res/layout/widget_detail.xml
Normal file
58
app/src/main/res/layout/widget_detail.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/widget_margin">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/widget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/abc_action_bar_default_height_material"
|
||||||
|
android:background="@color/primary">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/ic_logo"
|
||||||
|
/>
|
||||||
|
</FrameLayout>
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@color/primary_light">
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/widget_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:divider="@null"
|
||||||
|
android:dividerHeight="0dp"
|
||||||
|
tools:listitem="@layout/widget_detail_list_item"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_empty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:fontFamily="sans-serif-condensed"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:text="@string/empty_forecast_list"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
93
app/src/main/res/layout/widget_detail_list_item.xml
Normal file
93
app/src/main/res/layout/widget_detail_list_item.xml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<!-- Layout for weather forecast list item for future day (not today) -->
|
||||||
|
<!-- Make the background of our selector a non-transparent color -->
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/widget_list_item"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/touch_selector_white"
|
||||||
|
>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
|
android:paddingLeft="@dimen/abc_list_item_padding_horizontal_material"
|
||||||
|
android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_icon"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_width="@dimen/list_icon"
|
||||||
|
android:layout_height="@dimen/list_icon"
|
||||||
|
android:layout_marginRight="@dimen/abc_list_item_padding_horizontal_material"
|
||||||
|
android:layout_marginEnd="@dimen/abc_list_item_padding_horizontal_material"
|
||||||
|
tools:src="@drawable/ic_clear"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="7"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_date"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||||
|
android:textColor="@color/primary_text"
|
||||||
|
android:text="Today, May 21"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="@color/secondary_text"
|
||||||
|
tools:text="@string/condition_500"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_high_temperature"
|
||||||
|
android:layout_width="@dimen/forecast_widget_text_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="right"
|
||||||
|
android:layout_marginRight="@dimen/forecast_temperature_space"
|
||||||
|
android:layout_marginEnd="@dimen/forecast_temperature_space"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:textColor="@color/primary_text"
|
||||||
|
android:textSize="@dimen/forecast_widget_text_size"
|
||||||
|
tools:text="10"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_low_temperature"
|
||||||
|
android:layout_width="@dimen/forecast_widget_text_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="right"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:textColor="@color/forecast_low_text"
|
||||||
|
android:textSize="@dimen/forecast_widget_text_size"
|
||||||
|
tools:text="7"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
4
app/src/main/res/values-sw600dp/bools.xml
Normal file
4
app/src/main/res/values-sw600dp/bools.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="use_detail_activity">false</bool>
|
||||||
|
</resources>
|
||||||
4
app/src/main/res/values-v11/bools.xml
Normal file
4
app/src/main/res/values-v11/bools.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="widget_detail_enabled">true</bool>
|
||||||
|
</resources>
|
||||||
5
app/src/main/res/values/bools.xml
Normal file
5
app/src/main/res/values/bools.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="widget_detail_enabled">false</bool>
|
||||||
|
<bool name="use_detail_activity">true</bool>
|
||||||
|
</resources>
|
||||||
@@ -59,4 +59,19 @@
|
|||||||
<dimen name="widget_today_min_resize_width">40dp</dimen>
|
<dimen name="widget_today_min_resize_width">40dp</dimen>
|
||||||
<dimen name="widget_today_min_resize_height">@dimen/widget_today_default_height</dimen>
|
<dimen name="widget_today_min_resize_height">@dimen/widget_today_default_height</dimen>
|
||||||
<dimen name="widget_today_large_width">220dp</dimen>
|
<dimen name="widget_today_large_width">220dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="widget_detail_default_width">250dp</dimen>
|
||||||
|
<dimen name="widget_detail_default_height">180dp</dimen>
|
||||||
|
<dimen name="widget_detail_min_resize_width">220dp</dimen>
|
||||||
|
<dimen name="widget_detail_min_resize_height">@dimen/widget_detail_default_height</dimen>
|
||||||
|
|
||||||
|
<!-- Text Sizes - We are using DP here rather than SP because these are already large
|
||||||
|
font sizes, and going larger will cause lots of view problems. This is only for
|
||||||
|
the large forecast numbers in the forecast list -->
|
||||||
|
<dimen name="forecast_widget_text_size">24dp</dimen>
|
||||||
|
|
||||||
|
<!-- This is an odd width, but we're trying to match the font closely to keep things working
|
||||||
|
on devices that don't yet have Roboto -->
|
||||||
|
<dimen name="forecast_widget_text_width">38dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
|
|
||||||
<!-- Strings related to Widgets -->
|
<!-- Strings related to Widgets -->
|
||||||
<string name="title_widget_today">Sunshine Today</string>
|
<string name="title_widget_today">Sunshine Today</string>
|
||||||
|
<string name="title_widget_detail">Sunshine Details</string>
|
||||||
|
|
||||||
<!-- Empty Weather Database -->
|
<!-- Empty Weather Database -->
|
||||||
<string name="empty_forecast_list">No Weather Information Available</string>
|
<string name="empty_forecast_list">No Weather Information Available</string>
|
||||||
|
|||||||
29
app/src/main/res/xml/widget_info_detail.xml
Normal file
29
app/src/main/res/xml/widget_info_detail.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:initialKeyguardLayout="@layout/widget_detail"
|
||||||
|
android:initialLayout="@layout/widget_detail"
|
||||||
|
android:minHeight="@dimen/widget_detail_default_height"
|
||||||
|
android:minResizeHeight="@dimen/widget_detail_min_resize_height"
|
||||||
|
android:minResizeWidth="@dimen/widget_detail_min_resize_width"
|
||||||
|
android:minWidth="@dimen/widget_detail_default_width"
|
||||||
|
android:previewImage="@drawable/widget_preview_detail"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="0"
|
||||||
|
android:widgetCategory="home_screen|keyguard"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
Reference in New Issue
Block a user