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_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
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
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     */
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     */
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
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
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
202        public BulkUpdateAsyncTask(final Context context, final int[] appWidgetIds) {
203            mContext = context;
204            mAppWidgetIds = appWidgetIds;
205        }
206
207        @Override
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
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     */
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
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
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
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
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
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
390    private void migrateAllLegacyWidgetInformation(Context context) {
391        final int[] currentWidgetIds = getCurrentWidgetIds(context);
392        migrateLegacyWidgets(context, currentWidgetIds);
393    }
394
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
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     */
438    protected abstract void migrateLegacyWidgetInformation(Context context, int widgetId);
439}
440