1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.weatherlistwidget;
18 
19 import android.app.PendingIntent;
20 import android.appwidget.AppWidgetManager;
21 import android.appwidget.AppWidgetProvider;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.ComponentName;
25 import android.content.ContentValues;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.database.Cursor;
29 import android.database.ContentObserver;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.HandlerThread;
34 import android.widget.RemoteViews;
35 import android.widget.Toast;
36 
37 import java.util.Random;
38 
39 /**
40  * Our data observer just notifies an update for all weather widgets when it detects a change.
41  */
42 class WeatherDataProviderObserver extends ContentObserver {
43     private AppWidgetManager mAppWidgetManager;
44     private ComponentName mComponentName;
45 
WeatherDataProviderObserver(AppWidgetManager mgr, ComponentName cn, Handler h)46     WeatherDataProviderObserver(AppWidgetManager mgr, ComponentName cn, Handler h) {
47         super(h);
48         mAppWidgetManager = mgr;
49         mComponentName = cn;
50     }
51 
52     @Override
onChange(boolean selfChange)53     public void onChange(boolean selfChange) {
54         // The data has changed, so notify the widget that the collection view needs to be updated.
55         // In response, the factory's onDataSetChanged() will be called which will requery the
56         // cursor for the new data.
57         mAppWidgetManager.notifyAppWidgetViewDataChanged(
58                 mAppWidgetManager.getAppWidgetIds(mComponentName), R.id.weather_list);
59     }
60 }
61 
62 /**
63  * The weather widget's AppWidgetProvider.
64  */
65 public class WeatherWidgetProvider extends AppWidgetProvider {
66     public static String CLICK_ACTION = "com.example.android.weatherlistwidget.CLICK";
67     public static String REFRESH_ACTION = "com.example.android.weatherlistwidget.REFRESH";
68     public static String EXTRA_DAY_ID = "com.example.android.weatherlistwidget.day";
69 
70     private static HandlerThread sWorkerThread;
71     private static Handler sWorkerQueue;
72     private static WeatherDataProviderObserver sDataObserver;
73     private static final int sMaxDegrees = 96;
74 
75     private boolean mIsLargeLayout = true;
76     private int mHeaderWeatherState = 0;
77 
WeatherWidgetProvider()78     public WeatherWidgetProvider() {
79         // Start the worker thread
80         sWorkerThread = new HandlerThread("WeatherWidgetProvider-worker");
81         sWorkerThread.start();
82         sWorkerQueue = new Handler(sWorkerThread.getLooper());
83     }
84 
85     // XXX: clear the worker queue if we are destroyed?
86 
87     @Override
onEnabled(Context context)88     public void onEnabled(Context context) {
89         // Register for external updates to the data to trigger an update of the widget.  When using
90         // content providers, the data is often updated via a background service, or in response to
91         // user interaction in the main app.  To ensure that the widget always reflects the current
92         // state of the data, we must listen for changes and update ourselves accordingly.
93         final ContentResolver r = context.getContentResolver();
94         if (sDataObserver == null) {
95             final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
96             final ComponentName cn = new ComponentName(context, WeatherWidgetProvider.class);
97             sDataObserver = new WeatherDataProviderObserver(mgr, cn, sWorkerQueue);
98             r.registerContentObserver(WeatherDataProvider.CONTENT_URI, true, sDataObserver);
99         }
100     }
101 
102     @Override
onReceive(Context ctx, Intent intent)103     public void onReceive(Context ctx, Intent intent) {
104         final String action = intent.getAction();
105         if (action.equals(REFRESH_ACTION)) {
106             // BroadcastReceivers have a limited amount of time to do work, so for this sample, we
107             // are triggering an update of the data on another thread.  In practice, this update
108             // can be triggered from a background service, or perhaps as a result of user actions
109             // inside the main application.
110             final Context context = ctx;
111             sWorkerQueue.removeMessages(0);
112             sWorkerQueue.post(new Runnable() {
113                 @Override
114                 public void run() {
115                     final ContentResolver r = context.getContentResolver();
116                     final Cursor c = r.query(WeatherDataProvider.CONTENT_URI, null, null, null,
117                             null);
118                     final int count = c.getCount();
119 
120                     // We disable the data changed observer temporarily since each of the updates
121                     // will trigger an onChange() in our data observer.
122                     r.unregisterContentObserver(sDataObserver);
123                     for (int i = 0; i < count; ++i) {
124                         final Uri uri = ContentUris.withAppendedId(WeatherDataProvider.CONTENT_URI, i);
125                         final ContentValues values = new ContentValues();
126                         values.put(WeatherDataProvider.Columns.TEMPERATURE,
127                                 new Random().nextInt(sMaxDegrees));
128                         r.update(uri, values, null, null);
129                     }
130                     r.registerContentObserver(WeatherDataProvider.CONTENT_URI, true, sDataObserver);
131 
132                     final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
133                     final ComponentName cn = new ComponentName(context, WeatherWidgetProvider.class);
134                     mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.weather_list);
135                 }
136             });
137 
138             final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
139                     AppWidgetManager.INVALID_APPWIDGET_ID);
140         } else if (action.equals(CLICK_ACTION)) {
141             // Show a toast
142             final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
143                     AppWidgetManager.INVALID_APPWIDGET_ID);
144             final String day = intent.getStringExtra(EXTRA_DAY_ID);
145             final String formatStr = ctx.getResources().getString(R.string.toast_format_string);
146             Toast.makeText(ctx, String.format(formatStr, day), Toast.LENGTH_SHORT).show();
147         }
148 
149         super.onReceive(ctx, intent);
150     }
151 
buildLayout(Context context, int appWidgetId, boolean largeLayout)152     private RemoteViews buildLayout(Context context, int appWidgetId, boolean largeLayout) {
153         RemoteViews rv;
154         if (largeLayout) {
155             // Specify the service to provide data for the collection widget.  Note that we need to
156             // embed the appWidgetId via the data otherwise it will be ignored.
157             final Intent intent = new Intent(context, WeatherWidgetService.class);
158             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
159             intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
160             rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
161             rv.setRemoteAdapter(appWidgetId, R.id.weather_list, intent);
162 
163             // Set the empty view to be displayed if the collection is empty.  It must be a sibling
164             // view of the collection view.
165             rv.setEmptyView(R.id.weather_list, R.id.empty_view);
166 
167             // Bind a click listener template for the contents of the weather list.  Note that we
168             // need to update the intent's data if we set an extra, since the extras will be
169             // ignored otherwise.
170             final Intent onClickIntent = new Intent(context, WeatherWidgetProvider.class);
171             onClickIntent.setAction(WeatherWidgetProvider.CLICK_ACTION);
172             onClickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
173             onClickIntent.setData(Uri.parse(onClickIntent.toUri(Intent.URI_INTENT_SCHEME)));
174             final PendingIntent onClickPendingIntent = PendingIntent.getBroadcast(context, 0,
175                     onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
176             rv.setPendingIntentTemplate(R.id.weather_list, onClickPendingIntent);
177 
178             // Bind the click intent for the refresh button on the widget
179             final Intent refreshIntent = new Intent(context, WeatherWidgetProvider.class);
180             refreshIntent.setAction(WeatherWidgetProvider.REFRESH_ACTION);
181             final PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0,
182                     refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
183             rv.setOnClickPendingIntent(R.id.refresh, refreshPendingIntent);
184 
185             // Restore the minimal header
186             rv.setTextViewText(R.id.city_name, context.getString(R.string.city_name));
187         } else {
188             rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout_small);
189 
190             // Update the header to reflect the weather for "today"
191             Cursor c = context.getContentResolver().query(WeatherDataProvider.CONTENT_URI, null,
192                     null, null, null);
193             if (c.moveToPosition(0)) {
194                 int tempColIndex = c.getColumnIndex(WeatherDataProvider.Columns.TEMPERATURE);
195                 int temp = c.getInt(tempColIndex);
196                 String formatStr = context.getResources().getString(R.string.header_format_string);
197                 String header = String.format(formatStr, temp,
198                         context.getString(R.string.city_name));
199                 rv.setTextViewText(R.id.city_name, header);
200             }
201             c.close();
202         }
203         return rv;
204     }
205 
206     @Override
onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)207     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
208         // Update each of the widgets with the remote adapter
209         for (int i = 0; i < appWidgetIds.length; ++i) {
210             RemoteViews layout = buildLayout(context, appWidgetIds[i], mIsLargeLayout);
211             appWidgetManager.updateAppWidget(appWidgetIds[i], layout);
212         }
213         super.onUpdate(context, appWidgetManager, appWidgetIds);
214     }
215 
216     @Override
onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions)217     public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
218             int appWidgetId, Bundle newOptions) {
219 
220         int minWidth = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
221         int maxWidth = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH);
222         int minHeight = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
223         int maxHeight = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT);
224 
225         RemoteViews layout;
226         if (minHeight < 100) {
227             mIsLargeLayout = false;
228         } else {
229             mIsLargeLayout = true;
230         }
231         layout = buildLayout(context, appWidgetId, mIsLargeLayout);
232         appWidgetManager.updateAppWidget(appWidgetId, layout);
233     }
234 }