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