1 /* 2 * Copyright (C) 2012 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.android.mail.widget; 18 19 import android.app.PendingIntent; 20 import android.appwidget.AppWidgetManager; 21 import android.appwidget.AppWidgetProvider; 22 import android.content.ComponentName; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.Bundle; 30 import android.text.TextUtils; 31 import android.view.View; 32 import android.widget.RemoteViews; 33 34 import com.android.mail.R; 35 import com.android.mail.preferences.MailPrefs; 36 import com.android.mail.providers.Account; 37 import com.android.mail.providers.Folder; 38 import com.android.mail.providers.UIProvider; 39 import com.android.mail.providers.UIProvider.FolderType; 40 import com.android.mail.ui.MailboxSelectionActivity; 41 import com.android.mail.utils.AccountUtils; 42 import com.android.mail.utils.LogTag; 43 import com.android.mail.utils.LogUtils; 44 import com.android.mail.utils.Utils; 45 import com.google.common.collect.Sets; 46 import com.google.common.primitives.Ints; 47 48 import java.util.Set; 49 50 public abstract class BaseWidgetProvider extends AppWidgetProvider { 51 public static final String EXTRA_FOLDER_TYPE = "folder-type"; 52 public static final String EXTRA_FOLDER_URI = "folder-uri"; 53 public static final String EXTRA_FOLDER_CONVERSATION_LIST_URI = "folder-conversation-list-uri"; 54 public static final String EXTRA_FOLDER_DISPLAY_NAME = "folder-display-name"; 55 public static final String EXTRA_UPDATE_ALL_WIDGETS = "update-all-widgets"; 56 public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-"; 57 58 public static final String ACCOUNT_FOLDER_PREFERENCE_SEPARATOR = " "; 59 60 61 protected static final String ACTION_UPDATE_WIDGET = "com.android.mail.ACTION_UPDATE_WIDGET"; 62 protected static final String 63 ACTION_VALIDATE_ALL_WIDGETS = "com.android.mail.ACTION_VALIDATE_ALL_WIDGETS"; 64 protected static final String EXTRA_WIDGET_ID = "widgetId"; 65 66 private static final String LOG_TAG = LogTag.getLogTag(); 67 68 /** 69 * Remove preferences when deleting widget 70 */ 71 @Override 72 public void onDeleted(Context context, int[] appWidgetIds) { 73 super.onDeleted(context, appWidgetIds); 74 75 // TODO: (mindyp) save widget information. 76 MailPrefs.get(context).clearWidgets(appWidgetIds); 77 } 78 79 public static String getProviderName(Context context) { 80 return context.getString(R.string.widget_provider); 81 } 82 83 /** 84 * Note: this method calls {@link BaseWidgetProvider#getProviderName} and thus returns widget 85 * IDs based on the widget_provider string resource. When subclassing, be sure to either 86 * override this method or provide the correct provider name in the string resource. 87 * 88 * @return the list ids for the currently configured widgets. 89 */ 90 protected int[] getCurrentWidgetIds(Context context) { 91 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 92 final ComponentName mailComponent = new ComponentName(context, getProviderName(context)); 93 return appWidgetManager.getAppWidgetIds(mailComponent); 94 } 95 96 /** 97 * Get an array of account/mailbox string pairs for currently configured widgets 98 * @return the account/mailbox string pairs 99 */ 100 static public String[][] getWidgetInfo(Context context, int[] widgetIds) { 101 final String[][] widgetInfo = new String[widgetIds.length][2]; 102 for (int i = 0; i < widgetIds.length; i++) { 103 // Retrieve the persisted information for this widget from 104 // preferences. 105 final String accountFolder = MailPrefs.get(context).getWidgetConfiguration( 106 widgetIds[i]); 107 // If the account matched, update the widget. 108 if (accountFolder != null) { 109 widgetInfo[i] = TextUtils.split(accountFolder, ACCOUNT_FOLDER_PREFERENCE_SEPARATOR); 110 } 111 } 112 return widgetInfo; 113 } 114 115 /** 116 * Catches ACTION_NOTIFY_DATASET_CHANGED intent and update the corresponding 117 * widgets. 118 */ 119 @Override 120 public void onReceive(Context context, Intent intent) { 121 // We want to migrate any legacy Email widget information to the new format 122 migrateAllLegacyWidgetInformation(context); 123 124 super.onReceive(context, intent); 125 LogUtils.d(LOG_TAG, "BaseWidgetProvider.onReceive: %s", intent); 126 127 final String action = intent.getAction(); 128 if (ACTION_UPDATE_WIDGET.equals(action)) { 129 final int widgetId = intent.getIntExtra(EXTRA_WIDGET_ID, -1); 130 final Account account = Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)); 131 final int folderType = intent.getIntExtra(EXTRA_FOLDER_TYPE, FolderType.DEFAULT); 132 final Uri folderUri = intent.getParcelableExtra(EXTRA_FOLDER_URI); 133 final Uri folderConversationListUri = 134 intent.getParcelableExtra(EXTRA_FOLDER_CONVERSATION_LIST_URI); 135 final String folderDisplayName = intent.getStringExtra(EXTRA_FOLDER_DISPLAY_NAME); 136 137 if (widgetId != -1 && account != null && folderUri != null) { 138 updateWidgetInternal(context, widgetId, account, folderType, folderUri, 139 folderConversationListUri, folderDisplayName); 140 } 141 } else if (ACTION_VALIDATE_ALL_WIDGETS.equals(action)) { 142 validateAllWidgetInformation(context); 143 } else if (Utils.ACTION_NOTIFY_DATASET_CHANGED.equals(action)) { 144 // Receive notification for a certain account. 145 final Bundle extras = intent.getExtras(); 146 final Uri accountUri = extras.getParcelable(Utils.EXTRA_ACCOUNT_URI); 147 final Uri folderUri = extras.getParcelable(Utils.EXTRA_FOLDER_URI); 148 final boolean updateAllWidgets = extras.getBoolean(EXTRA_UPDATE_ALL_WIDGETS, false); 149 150 if (accountUri == null && Utils.isEmpty(folderUri) && !updateAllWidgets) { 151 return; 152 } 153 final Set<Integer> widgetsToUpdate = Sets.newHashSet(); 154 for (int id : getCurrentWidgetIds(context)) { 155 // Retrieve the persisted information for this widget from 156 // preferences. 157 final String accountFolder = MailPrefs.get(context).getWidgetConfiguration(id); 158 // If the account matched, update the widget. 159 if (accountFolder != null) { 160 final String[] parsedInfo = TextUtils.split(accountFolder, 161 ACCOUNT_FOLDER_PREFERENCE_SEPARATOR); 162 boolean updateThis = updateAllWidgets; 163 if (!updateThis) { 164 if (accountUri != null && 165 TextUtils.equals(accountUri.toString(), parsedInfo[0])) { 166 updateThis = true; 167 } else if (folderUri != null && 168 TextUtils.equals(folderUri.toString(), parsedInfo[1])) { 169 updateThis = true; 170 } 171 } 172 if (updateThis) { 173 widgetsToUpdate.add(id); 174 } 175 } 176 } 177 if (widgetsToUpdate.size() > 0) { 178 final int[] widgets = Ints.toArray(widgetsToUpdate); 179 AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(widgets, 180 R.id.conversation_list); 181 } 182 } 183 } 184 185 /** 186 * Update all widgets in the list 187 */ 188 @Override 189 public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 190 migrateLegacyWidgets(context, appWidgetIds); 191 192 super.onUpdate(context, appWidgetManager, appWidgetIds); 193 // Update each of the widgets with a remote adapter 194 195 new BulkUpdateAsyncTask(context, appWidgetIds).execute((Void[]) null); 196 } 197 198 private class BulkUpdateAsyncTask extends AsyncTask<Void, Void, Void> { 199 private final Context mContext; 200 private final int[] mAppWidgetIds; 201 202 public BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds) { 203 mContext = context; 204 mAppWidgetIds = appWidgetIds; 205 } 206 207 @Override 208 protected Void doInBackground(final Void... params) { 209 for (int i = 0; i < mAppWidgetIds.length; ++i) { 210 // Get the account for this widget from preference 211 final String accountFolder = MailPrefs.get(mContext).getWidgetConfiguration( 212 mAppWidgetIds[i]); 213 String accountUri = null; 214 Uri folderUri = null; 215 if (!TextUtils.isEmpty(accountFolder)) { 216 final String[] parsedInfo = TextUtils.split(accountFolder, 217 ACCOUNT_FOLDER_PREFERENCE_SEPARATOR); 218 if (parsedInfo.length == 2) { 219 accountUri = parsedInfo[0]; 220 folderUri = Uri.parse(parsedInfo[1]); 221 } else { 222 accountUri = accountFolder; 223 folderUri = Uri.EMPTY; 224 } 225 } 226 // account will be null the first time a widget is created. This is 227 // OK, as isAccountValid will return false, allowing the widget to 228 // be configured. 229 230 // Lookup the account by URI. 231 Account account = null; 232 if (!TextUtils.isEmpty(accountUri)) { 233 account = getAccountObject(mContext, accountUri); 234 } 235 if (Utils.isEmpty(folderUri) && account != null) { 236 folderUri = account.settings.defaultInbox; 237 } 238 239 Folder folder = null; 240 241 if (folderUri != null) { 242 final Cursor folderCursor = 243 mContext.getContentResolver().query(folderUri, 244 UIProvider.FOLDERS_PROJECTION, null, null, null); 245 246 try { 247 if (folderCursor.moveToFirst()) { 248 folder = new Folder(folderCursor); 249 } 250 } finally { 251 folderCursor.close(); 252 } 253 } 254 255 updateWidgetInternal(mContext, mAppWidgetIds[i], account, 256 folder == null ? FolderType.DEFAULT : folder.type, folderUri, 257 folder == null ? null : folder.conversationListUri, folder == null ? null 258 : folder.name); 259 } 260 261 return null; 262 } 263 264 } 265 266 protected Account getAccountObject(Context context, String accountUri) { 267 final ContentResolver resolver = context.getContentResolver(); 268 Account account = null; 269 Cursor accountCursor = null; 270 try { 271 accountCursor = resolver.query(Uri.parse(accountUri), 272 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 273 if (accountCursor != null) { 274 if (accountCursor.moveToFirst()) { 275 account = new Account(accountCursor); 276 } 277 } 278 } finally { 279 if (accountCursor != null) { 280 accountCursor.close(); 281 } 282 } 283 return account; 284 } 285 286 /** 287 * Update the widget appWidgetId with the given account and folder 288 */ 289 public static void updateWidget(Context context, int appWidgetId, Account account, 290 final int folderType, final Uri folderUri, final Uri folderConversationListUri, 291 final String folderDisplayName) { 292 if (account == null || folderUri == null) { 293 LogUtils.e(LOG_TAG, 294 "Missing account or folder. account: %s folder %s", account, folderUri); 295 return; 296 } 297 final Intent updateWidgetIntent = new Intent(ACTION_UPDATE_WIDGET); 298 299 updateWidgetIntent.setType(account.mimeType); 300 updateWidgetIntent.putExtra(EXTRA_WIDGET_ID, appWidgetId); 301 updateWidgetIntent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize()); 302 updateWidgetIntent.putExtra(EXTRA_FOLDER_TYPE, folderType); 303 updateWidgetIntent.putExtra(EXTRA_FOLDER_URI, folderUri); 304 updateWidgetIntent.putExtra(EXTRA_FOLDER_CONVERSATION_LIST_URI, folderConversationListUri); 305 updateWidgetIntent.putExtra(EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName); 306 307 context.sendBroadcast(updateWidgetIntent); 308 } 309 310 public static void validateAllWidgets(Context context, String accountMimeType) { 311 final Intent migrateAllWidgetsIntent = new Intent(ACTION_VALIDATE_ALL_WIDGETS); 312 migrateAllWidgetsIntent.setType(accountMimeType); 313 context.sendBroadcast(migrateAllWidgetsIntent); 314 } 315 316 protected void updateWidgetInternal(Context context, int appWidgetId, Account account, 317 final int folderType, final Uri folderUri, final Uri folderConversationListUri, 318 final String folderDisplayName) { 319 final RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget); 320 321 final boolean isAccountValid = isAccountValid(context, account); 322 if (!isAccountValid || Utils.isEmpty(folderUri)) { 323 // Widget has not been configured yet 324 remoteViews.setViewVisibility(R.id.widget_folder, View.GONE); 325 remoteViews.setViewVisibility(R.id.widget_account_noflip, View.GONE); 326 remoteViews.setViewVisibility(R.id.widget_account_unread_flipper, View.GONE); 327 remoteViews.setViewVisibility(R.id.widget_compose, View.GONE); 328 remoteViews.setViewVisibility(R.id.conversation_list, View.GONE); 329 remoteViews.setViewVisibility(R.id.empty_conversation_list, View.GONE); 330 remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE); 331 remoteViews.setViewVisibility(R.id.widget_configuration, View.VISIBLE); 332 333 remoteViews.setTextViewText(R.id.empty_conversation_list, 334 context.getString(R.string.loading_conversations)); 335 336 final Intent configureIntent = new Intent(context, MailboxSelectionActivity.class); 337 configureIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 338 configureIntent.setData(Uri.parse(configureIntent.toUri(Intent.URI_INTENT_SCHEME))); 339 configureIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); 340 PendingIntent clickIntent = PendingIntent.getActivity(context, 0, configureIntent, 341 PendingIntent.FLAG_UPDATE_CURRENT); 342 remoteViews.setOnClickPendingIntent(R.id.widget_configuration, clickIntent); 343 } else { 344 // Set folder to a space here to avoid flicker. 345 configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderType, 346 folderUri, folderConversationListUri, 347 folderDisplayName == null ? " " : folderDisplayName); 348 349 } 350 AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews); 351 } 352 353 protected boolean isAccountValid(Context context, Account account) { 354 if (account != null) { 355 Account[] accounts = AccountUtils.getSyncingAccounts(context); 356 for (Account existing : accounts) { 357 if (existing != null && account.uri.equals(existing.uri)) { 358 return true; 359 } 360 } 361 } 362 return false; 363 } 364 365 protected boolean isFolderValid(Context context, Uri folderUri) { 366 if (folderUri != null) { 367 final Cursor folderCursor = 368 context.getContentResolver().query(folderUri, 369 UIProvider.FOLDERS_PROJECTION, null, null, null); 370 371 try { 372 if (folderCursor.moveToFirst()) { 373 return true; 374 } 375 } finally { 376 folderCursor.close(); 377 } 378 } 379 return false; 380 } 381 382 protected void configureValidAccountWidget(Context context, RemoteViews remoteViews, 383 int appWidgetId, Account account, final int folderType, final Uri folderUri, 384 final Uri folderConversationListUri, String folderDisplayName) { 385 WidgetService.configureValidAccountWidget(context, remoteViews, appWidgetId, account, 386 folderType, folderUri, folderConversationListUri, folderDisplayName, 387 WidgetService.class); 388 } 389 390 private void migrateAllLegacyWidgetInformation(Context context) { 391 final int[] currentWidgetIds = getCurrentWidgetIds(context); 392 migrateLegacyWidgets(context, currentWidgetIds); 393 } 394 395 private void migrateLegacyWidgets(Context context, int[] widgetIds) { 396 for (int widgetId : widgetIds) { 397 // We only want to bother to attempt to upgrade a widget if we don't already 398 // have information about. 399 if (!MailPrefs.get(context).isWidgetConfigured(widgetId)) { 400 migrateLegacyWidgetInformation(context, widgetId); 401 } 402 } 403 } 404 405 private void validateAllWidgetInformation(Context context) { 406 final int[] widgetIds = getCurrentWidgetIds(context); 407 for (int widgetId : widgetIds) { 408 final String accountFolder = MailPrefs.get(context).getWidgetConfiguration(widgetId); 409 String accountUri = null; 410 Uri folderUri = null; 411 if (!TextUtils.isEmpty(accountFolder)) { 412 final String[] parsedInfo = TextUtils.split(accountFolder, 413 ACCOUNT_FOLDER_PREFERENCE_SEPARATOR); 414 if (parsedInfo.length == 2) { 415 accountUri = parsedInfo[0]; 416 folderUri = Uri.parse(parsedInfo[1]); 417 } else { 418 accountUri = accountFolder; 419 folderUri = Uri.EMPTY; 420 } 421 } 422 423 Account account = null; 424 if (!TextUtils.isEmpty(accountUri)) { 425 account = getAccountObject(context, accountUri); 426 } 427 428 // unconfigure the widget if it is not valid 429 if (!isAccountValid(context, account) || !isFolderValid(context, folderUri)) { 430 updateWidgetInternal(context, widgetId, null, FolderType.DEFAULT, null, null, null); 431 } 432 } 433 } 434 435 /** 436 * Abstract method allowing extending classes to perform widget migration 437 */ 438 protected abstract void migrateLegacyWidgetInformation(Context context, int widgetId); 439 } 440