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