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