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. 90 */ getCurrentWidgetIds(Context context)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 */ getWidgetInfo(Context context, int[] widgetIds)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 onReceive(Context context, Intent intent)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 onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)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 BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds)204 public BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds) { 205 mContext = context; 206 mAppWidgetIds = appWidgetIds; 207 } 208 209 @Override doInBackground(final Void... params)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 getAccountObject(Context context, String accountUri)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 */ updateWidget(Context context, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, final String folderDisplayName)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 validateAllWidgets(Context context, String accountMimeType)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 updateWidgetInternal(Context context, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, final String folderDisplayName)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 isAccountValid(Context context, Account account)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 isFolderValid(Context context, Uri folderUri)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 configureValidAccountWidget(Context context, RemoteViews remoteViews, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, String folderDisplayName)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 migrateAllLegacyWidgetInformation(Context context)394 private void migrateAllLegacyWidgetInformation(Context context) { 395 final int[] currentWidgetIds = getCurrentWidgetIds(context); 396 migrateLegacyWidgets(context, currentWidgetIds); 397 } 398 migrateLegacyWidgets(Context context, int[] widgetIds)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 validateAllWidgetInformation(Context context)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 */ migrateLegacyWidgetInformation(Context context, int widgetId)443 protected abstract void migrateLegacyWidgetInformation(Context context, int widgetId); 444 } 445