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 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 */ 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 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 }