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