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 onDeleted(Context context, int[] appWidgetIds)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 getProviderName(Context context)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 */ getCurrentWidgetIds(Context context)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 */ getWidgetInfo(Context context, int[] widgetIds)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 onReceive(Context context, Intent intent)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 onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)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 BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds)202 public BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds) { 203 mContext = context; 204 mAppWidgetIds = appWidgetIds; 205 } 206 207 @Override doInBackground(final Void... params)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 getAccountObject(Context context, String accountUri)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 */ updateWidget(Context context, int appWidgetId, Account account, final int folderType, final Uri folderUri, final Uri folderConversationListUri, final String folderDisplayName)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 validateAllWidgets(Context context, String accountMimeType)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 updateWidgetInternal(Context context, int appWidgetId, Account account, final int folderType, final Uri folderUri, final Uri folderConversationListUri, final String folderDisplayName)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 isAccountValid(Context context, Account account)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 isFolderValid(Context context, Uri folderUri)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 configureValidAccountWidget(Context context, RemoteViews remoteViews, int appWidgetId, Account account, final int folderType, final Uri folderUri, final Uri folderConversationListUri, String folderDisplayName)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 migrateAllLegacyWidgetInformation(Context context)390 private void migrateAllLegacyWidgetInformation(Context context) { 391 final int[] currentWidgetIds = getCurrentWidgetIds(context); 392 migrateLegacyWidgets(context, currentWidgetIds); 393 } 394 migrateLegacyWidgets(Context context, int[] widgetIds)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 validateAllWidgetInformation(Context context)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 */ migrateLegacyWidgetInformation(Context context, int widgetId)438 protected abstract void migrateLegacyWidgetInformation(Context context, int widgetId); 439 } 440