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
17package com.example.android.weatherlistwidget;
18
19import android.app.PendingIntent;
20import android.appwidget.AppWidgetManager;
21import android.appwidget.AppWidgetProvider;
22import android.content.Context;
23import android.content.Intent;
24import android.content.ComponentName;
25import android.content.ContentValues;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.database.Cursor;
29import android.database.ContentObserver;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.HandlerThread;
34import android.widget.RemoteViews;
35import android.widget.Toast;
36
37import java.util.Random;
38
39/**
40 * Our data observer just notifies an update for all weather widgets when it detects a change.
41 */
42class WeatherDataProviderObserver extends ContentObserver {
43    private AppWidgetManager mAppWidgetManager;
44    private ComponentName mComponentName;
45
46    WeatherDataProviderObserver(AppWidgetManager mgr, ComponentName cn, Handler h) {
47        super(h);
48        mAppWidgetManager = mgr;
49        mComponentName = cn;
50    }
51
52    @Override
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 */
65public 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
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
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
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
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
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
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}