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 package com.android.mail.widget;
17 
18 import android.app.PendingIntent;
19 import android.appwidget.AppWidgetManager;
20 import android.content.Context;
21 import android.content.CursorLoader;
22 import android.content.Intent;
23 import android.content.Loader;
24 import android.content.Loader.OnLoadCompleteListener;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.os.Looper;
29 import android.support.v4.app.TaskStackBuilder;
30 import android.text.SpannableString;
31 import android.text.SpannableStringBuilder;
32 import android.text.TextUtils;
33 import android.text.format.DateUtils;
34 import android.text.style.CharacterStyle;
35 import android.view.View;
36 import android.widget.RemoteViews;
37 import android.widget.RemoteViewsService;
38 
39 import com.android.mail.R;
40 import com.android.mail.browse.ConversationItemView;
41 import com.android.mail.browse.SendersView;
42 import com.android.mail.compose.ComposeActivity;
43 import com.android.mail.preferences.MailPrefs;
44 import com.android.mail.providers.Account;
45 import com.android.mail.providers.Conversation;
46 import com.android.mail.providers.Folder;
47 import com.android.mail.providers.UIProvider;
48 import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
49 import com.android.mail.providers.UIProvider.FolderType;
50 import com.android.mail.utils.AccountUtils;
51 import com.android.mail.utils.DelayedTaskHandler;
52 import com.android.mail.utils.FolderUri;
53 import com.android.mail.utils.LogTag;
54 import com.android.mail.utils.LogUtils;
55 import com.android.mail.utils.Utils;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 
60 public class WidgetService extends RemoteViewsService {
61     /**
62      * Lock to avoid race condition between widgets.
63      */
64     private static final Object sWidgetLock = new Object();
65 
66     private static final String LOG_TAG = LogTag.getLogTag();
67 
68     @Override
onGetViewFactory(Intent intent)69     public RemoteViewsFactory onGetViewFactory(Intent intent) {
70         return new MailFactory(getApplicationContext(), intent, this);
71     }
72 
configureValidAccountWidget(Context context, RemoteViews remoteViews, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, String folderName)73     protected void configureValidAccountWidget(Context context, RemoteViews remoteViews,
74             int appWidgetId, Account account, final int folderType, final int folderCapabilities,
75             final Uri folderUri, final Uri folderConversationListUri, String folderName) {
76         configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderType,
77                 folderCapabilities, folderUri, folderConversationListUri, folderName,
78                 WidgetService.class);
79     }
80 
81     /**
82      * Modifies the remoteView for the given account and folder.
83      */
configureValidAccountWidget(Context context, RemoteViews remoteViews, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, String folderDisplayName, Class<?> widgetService)84     public static void configureValidAccountWidget(Context context, RemoteViews remoteViews,
85             int appWidgetId, Account account, final int folderType, final int folderCapabilities,
86             final Uri folderUri, final Uri folderConversationListUri, String folderDisplayName,
87             Class<?> widgetService) {
88         remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
89 
90         // If the folder or account name are empty, we don't want to overwrite the valid data that
91         // had been saved previously.  Since the launcher will save the state of the remote views
92         // we should rely on the fact that valid data has been saved.  But we should still log this,
93         // as it shouldn't happen
94         if (TextUtils.isEmpty(folderDisplayName) || TextUtils.isEmpty(account.getDisplayName())) {
95             LogUtils.e(LOG_TAG, new Error(),
96                     "Empty folder or account name.  account: %s, folder: %s",
97                     account.getEmailAddress(), folderDisplayName);
98         }
99         if (!TextUtils.isEmpty(folderDisplayName)) {
100             remoteViews.setTextViewText(R.id.widget_folder, folderDisplayName);
101         }
102 
103         remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
104         remoteViews.setViewVisibility(R.id.conversation_list, View.VISIBLE);
105         remoteViews.setViewVisibility(R.id.empty_conversation_list, View.VISIBLE);
106         remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE);
107         remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
108         remoteViews.setEmptyView(R.id.conversation_list, R.id.empty_conversation_list);
109 
110         WidgetService.configureValidWidgetIntents(context, remoteViews, appWidgetId, account,
111                 folderType, folderCapabilities, folderUri, folderConversationListUri,
112                 folderDisplayName, widgetService);
113     }
114 
configureValidWidgetIntents(Context context, RemoteViews remoteViews, int appWidgetId, Account account, final int folderType, final int folderCapabilities, final Uri folderUri, final Uri folderConversationListUri, final String folderDisplayName, Class<?> serviceClass)115     public static void configureValidWidgetIntents(Context context, RemoteViews remoteViews,
116             int appWidgetId, Account account, final int folderType, final int folderCapabilities,
117             final Uri folderUri, final Uri folderConversationListUri,
118             final String folderDisplayName, Class<?> serviceClass) {
119         remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
120 
121 
122         // Launch an intent to avoid ANRs
123         final Intent intent = new Intent(context, serviceClass);
124         intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
125         intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
126         intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_TYPE, folderType);
127         intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CAPABILITIES, folderCapabilities);
128         intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_URI, folderUri);
129         intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI,
130                 folderConversationListUri);
131         intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName);
132         intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
133         remoteViews.setRemoteAdapter(R.id.conversation_list, intent);
134         // Open mail app when click on header
135         final Intent mailIntent = Utils.createViewFolderIntent(context, folderUri, account);
136         mailIntent.setPackage(context.getPackageName());
137         PendingIntent clickIntent = PendingIntent.getActivity(context, 0, mailIntent,
138                 PendingIntent.FLAG_UPDATE_CURRENT);
139         remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);
140 
141         // On click intent for Compose
142         final Intent composeIntent = new Intent();
143         composeIntent.setPackage(context.getPackageName());
144         composeIntent.setAction(Intent.ACTION_SEND);
145         composeIntent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
146         composeIntent.setData(account.composeIntentUri);
147         composeIntent.putExtra(ComposeActivity.EXTRA_FROM_EMAIL_TASK, true);
148         if (account.composeIntentUri != null) {
149             composeIntent.putExtra(Utils.EXTRA_COMPOSE_URI, account.composeIntentUri);
150         }
151 
152         // Build a task stack that forces the conversation list on the stack before the compose
153         // activity.
154         final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
155         clickIntent = taskStackBuilder.addNextIntent(mailIntent)
156                 .addNextIntent(composeIntent)
157                 .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
158         remoteViews.setOnClickPendingIntent(R.id.widget_compose, clickIntent);
159 
160         // On click intent for Conversation
161         final Intent conversationIntent = new Intent();
162         conversationIntent.setPackage(context.getPackageName());
163         conversationIntent.setAction(Intent.ACTION_VIEW);
164         clickIntent = PendingIntent.getActivity(context, 0, conversationIntent,
165                 PendingIntent.FLAG_UPDATE_CURRENT);
166         remoteViews.setPendingIntentTemplate(R.id.conversation_list, clickIntent);
167     }
168 
169     /**
170      * Persists the information about the specified widget.
171      */
saveWidgetInformation(Context context, int appWidgetId, Account account, final String folderUri)172     public static void saveWidgetInformation(Context context, int appWidgetId, Account account,
173                 final String folderUri) {
174         MailPrefs.get(context).configureWidget(appWidgetId, account, folderUri);
175     }
176 
177     /**
178      * Returns true if this widget id has been configured and saved.
179      */
isWidgetConfigured(Context context, int appWidgetId, Account account)180     public boolean isWidgetConfigured(Context context, int appWidgetId, Account account) {
181         return isAccountValid(context, account) &&
182                 MailPrefs.get(context).isWidgetConfigured(appWidgetId);
183     }
184 
isAccountValid(Context context, Account account)185     protected boolean isAccountValid(Context context, Account account) {
186         if (account != null) {
187             Account[] accounts = AccountUtils.getSyncingAccounts(context);
188             for (Account existing : accounts) {
189                 if (existing != null && account.uri.equals(existing.uri)) {
190                     return true;
191                 }
192             }
193         }
194         return false;
195     }
196 
197     /**
198      * Remote Views Factory for Mail Widget.
199      */
200     protected static class MailFactory
201             implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> {
202         private static final int MAX_CONVERSATIONS_COUNT = 25;
203         private static final int MAX_SENDERS_LENGTH = 25;
204 
205         private static final int FOLDER_LOADER_ID = 0;
206         private static final int CONVERSATION_CURSOR_LOADER_ID = 1;
207         private static final int ACCOUNT_LOADER_ID = 2;
208 
209         private final Context mContext;
210         private final int mAppWidgetId;
211         private final Account mAccount;
212         private final int mFolderType;
213         private final int mFolderCapabilities;
214         private final Uri mFolderUri;
215         private final Uri mFolderConversationListUri;
216         private final String mFolderDisplayName;
217         private final WidgetConversationListItemViewBuilder mWidgetConversationListItemViewBuilder;
218         private CursorLoader mConversationCursorLoader;
219         private Cursor mConversationCursor;
220         private CursorLoader mFolderLoader;
221         private CursorLoader mAccountLoader;
222         private FolderUpdateHandler mFolderUpdateHandler;
223         private int mFolderCount;
224         private boolean mShouldShowViewMore;
225         private boolean mFolderInformationShown = false;
226         private final WidgetService mService;
227         private String mSendersSplitToken;
228         private String mElidedPaddingToken;
229 
MailFactory(Context context, Intent intent, WidgetService service)230         public MailFactory(Context context, Intent intent, WidgetService service) {
231             mContext = context;
232             mAppWidgetId = intent.getIntExtra(
233                     AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
234             mAccount = Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT));
235             mFolderType = intent.getIntExtra(WidgetProvider.EXTRA_FOLDER_TYPE, FolderType.DEFAULT);
236             mFolderCapabilities = intent.getIntExtra(WidgetProvider.EXTRA_FOLDER_CAPABILITIES, 0);
237             mFolderDisplayName = intent.getStringExtra(WidgetProvider.EXTRA_FOLDER_DISPLAY_NAME);
238 
239             final Uri folderUri = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_URI);
240             final Uri folderConversationListUri =
241                     intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI);
242             if (folderUri != null && folderConversationListUri != null) {
243                 mFolderUri = folderUri;
244                 mFolderConversationListUri = folderConversationListUri;
245             } else {
246                 // This is a old intent created in version UR8 (or earlier).
247                 String folderString = intent.getStringExtra(Utils.EXTRA_FOLDER);
248                 //noinspection deprecation
249                 Folder folder = Folder.fromString(folderString);
250                 if (folder != null) {
251                     mFolderUri = folder.folderUri.fullUri;
252                     mFolderConversationListUri = folder.conversationListUri;
253                 } else {
254                     mFolderUri = Uri.EMPTY;
255                     mFolderConversationListUri = Uri.EMPTY;
256                     // this will mark the widget as unconfigured
257                     BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
258                             mFolderCapabilities, mFolderUri, mFolderConversationListUri,
259                             mFolderDisplayName);
260                 }
261             }
262 
263             mWidgetConversationListItemViewBuilder = new WidgetConversationListItemViewBuilder(
264                     context);
265             mService = service;
266         }
267 
268         @Override
onCreate()269         public void onCreate() {
270             // Save the map between widgetId and account to preference
271             saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolderUri.toString());
272 
273             // If the account of this widget has been removed, we want to update the widget to
274             // "Tap to configure" mode.
275             if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
276                 BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
277                         mFolderCapabilities, mFolderUri, mFolderConversationListUri,
278                         mFolderDisplayName);
279             }
280 
281             mFolderInformationShown = false;
282 
283             // We want to limit the query result to 25 and don't want these queries to cause network
284             // traffic
285             // We also want this cursor to receive notifications on all changes.  Any change that
286             // the user made locally, the default policy of the UI provider is to not send
287             // notifications for.  But in this case, since the widget is not using the
288             // ConversationCursor instance that the UI is using, the widget would not be updated.
289             final Uri.Builder builder = mFolderConversationListUri.buildUpon();
290             final String maxConversations = Integer.toString(MAX_CONVERSATIONS_COUNT);
291             final Uri widgetConversationQueryUri = builder
292                     .appendQueryParameter(ConversationListQueryParameters.LIMIT, maxConversations)
293                     .appendQueryParameter(ConversationListQueryParameters.USE_NETWORK,
294                             Boolean.FALSE.toString())
295                     .appendQueryParameter(ConversationListQueryParameters.ALL_NOTIFICATIONS,
296                             Boolean.TRUE.toString()).build();
297 
298             final Resources res = mContext.getResources();
299             mConversationCursorLoader = new CursorLoader(mContext, widgetConversationQueryUri,
300                     UIProvider.CONVERSATION_PROJECTION, null, null, null);
301             mConversationCursorLoader.registerListener(CONVERSATION_CURSOR_LOADER_ID, this);
302             mConversationCursorLoader.setUpdateThrottle(
303                     res.getInteger(R.integer.widget_refresh_delay_ms));
304             mConversationCursorLoader.startLoading();
305             mSendersSplitToken = res.getString(R.string.senders_split_token);
306             mElidedPaddingToken = res.getString(R.string.elided_padding_token);
307             mFolderLoader = new CursorLoader(mContext, mFolderUri, UIProvider.FOLDERS_PROJECTION,
308                     null, null, null);
309             mFolderLoader.registerListener(FOLDER_LOADER_ID, this);
310             mFolderUpdateHandler = new FolderUpdateHandler(
311                     res.getInteger(R.integer.widget_folder_refresh_delay_ms));
312             mFolderUpdateHandler.scheduleTask();
313 
314             mAccountLoader = new CursorLoader(mContext, mAccount.uri,
315                     UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null);
316             mAccountLoader.registerListener(ACCOUNT_LOADER_ID, this);
317             mAccountLoader.startLoading();
318         }
319 
320         @Override
onDestroy()321         public void onDestroy() {
322             synchronized (sWidgetLock) {
323                 if (mConversationCursorLoader != null) {
324                     mConversationCursorLoader.reset();
325                     mConversationCursorLoader.unregisterListener(this);
326                     mConversationCursorLoader = null;
327                 }
328 
329                 // The Loader should close the cursor, so just unset the reference
330                 // to it here.
331                 mConversationCursor = null;
332             }
333 
334             if (mFolderLoader != null) {
335                 mFolderLoader.reset();
336                 mFolderLoader.unregisterListener(this);
337                 mFolderLoader = null;
338             }
339 
340             if (mAccountLoader != null) {
341                 mAccountLoader.reset();
342                 mAccountLoader.unregisterListener(this);
343                 mAccountLoader = null;
344             }
345         }
346 
347         @Override
onDataSetChanged()348         public void onDataSetChanged() {
349             // We are not using this as signal to requery the cursor.  The query will be started
350             // in the following ways:
351             // 1) The Service is started and the loader is started in onCreate()
352             //       This will happen when the service is not running, and
353             //       AppWidgetManager#notifyAppWidgetViewDataChanged() is called
354             // 2) The service is running, with a previously created loader.  The loader is watching
355             //    for updates from the existing cursor.  If one is seen, the loader will load a new
356             //    cursor in the background.
357             mFolderUpdateHandler.scheduleTask();
358         }
359 
360         /**
361          * Returns the number of items should be shown in the widget list.  This method also updates
362          * the boolean that indicates whether the "show more" item should be shown.
363          * @return the number of items to be displayed in the list.
364          */
365         @Override
getCount()366         public int getCount() {
367             synchronized (sWidgetLock) {
368                 final int count = getConversationCount();
369                 final int cursorCount = mConversationCursor != null ?
370                         mConversationCursor.getCount() : 0;
371                 mShouldShowViewMore = count < cursorCount || count < mFolderCount;
372                 return count + (mShouldShowViewMore ? 1 : 0);
373             }
374         }
375 
376         /**
377          * Returns the number of conversations that should be shown in the widget.  This method
378          * doesn't update the boolean that indicates that the "show more" item should be included
379          * in the list.
380          * @return count
381          */
382         private int getConversationCount() {
383             synchronized (sWidgetLock) {
384                 final int cursorCount = mConversationCursor != null ?
385                         mConversationCursor.getCount() : 0;
386                 return Math.min(cursorCount, MAX_CONVERSATIONS_COUNT);
387             }
388         }
389 
390         /**
391          * @return the {@link RemoteViews} for a specific position in the list.
392          */
393         @Override
394         public RemoteViews getViewAt(int position) {
395             synchronized (sWidgetLock) {
396                 // "View more conversations" view.
397                 if (mConversationCursor == null || mConversationCursor.isClosed()
398                         || (mShouldShowViewMore && position >= getConversationCount())) {
399                     return getViewMoreConversationsView();
400                 }
401 
402                 if (!mConversationCursor.moveToPosition(position)) {
403                     // If we ever fail to move to a position, return the
404                     // "View More conversations"
405                     // view.
406                     LogUtils.e(LOG_TAG, "Failed to move to position %d in the cursor.", position);
407                     return getViewMoreConversationsView();
408                 }
409 
410                 Conversation conversation = new Conversation(mConversationCursor);
411                 // Split the senders and status from the instructions.
412 
413                 ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
414                 SendersView.format(mContext, conversation.conversationInfo, "",
415                         MAX_SENDERS_LENGTH, senders, null, null, mAccount,
416                         Folder.shouldShowRecipients(mFolderCapabilities), true);
417                 final SpannableStringBuilder senderBuilder = elideParticipants(senders);
418 
419                 // Get styled date.
420                 CharSequence date = DateUtils.getRelativeTimeSpanString(mContext,
421                         conversation.dateMs);
422 
423                 final int ignoreFolderType;
424                 if ((mFolderType & FolderType.INBOX) != 0) {
425                     ignoreFolderType = FolderType.INBOX;
426                 } else {
427                     ignoreFolderType = -1;
428                 }
429 
430                 // Load up our remote view.
431                 RemoteViews remoteViews = mWidgetConversationListItemViewBuilder.getStyledView(
432                         mContext, date, conversation, new FolderUri(mFolderUri), ignoreFolderType,
433                         senderBuilder,
434                         ConversationItemView.filterTag(mContext, conversation.subject));
435 
436                 // On click intent.
437                 remoteViews.setOnClickFillInIntent(R.id.widget_conversation_list_item,
438                         Utils.createViewConversationIntent(mContext, conversation, mFolderUri,
439                                 mAccount));
440 
441                 return remoteViews;
442             }
443         }
444 
445         private SpannableStringBuilder elideParticipants(List<SpannableString> parts) {
446             final SpannableStringBuilder builder = new SpannableStringBuilder();
447             SpannableString prevSender = null;
448 
449             boolean skipToHeader = false;
450 
451             // start with "To: " if we're showing recipients
452             if (Folder.shouldShowRecipients(mFolderCapabilities)) {
453                 builder.append(SendersView.getFormattedToHeader());
454                 skipToHeader = true;
455             }
456 
457             for (SpannableString sender : parts) {
458                 if (sender == null) {
459                     LogUtils.e(LOG_TAG, "null sender while iterating over styledSenders");
460                     continue;
461                 }
462                 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
463                 if (SendersView.sElidedString.equals(sender.toString())) {
464                     prevSender = sender;
465                     sender = copyStyles(spans, mElidedPaddingToken + sender + mElidedPaddingToken);
466                 } else if (!skipToHeader && builder.length() > 0
467                         && (prevSender == null || !SendersView.sElidedString.equals(prevSender
468                                 .toString()))) {
469                     prevSender = sender;
470                     sender = copyStyles(spans, mSendersSplitToken + sender);
471                 } else {
472                     prevSender = sender;
473                     skipToHeader = false;
474                 }
475                 builder.append(sender);
476             }
477             return builder;
478         }
479 
copyStyles(CharacterStyle[] spans, CharSequence newText)480         private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
481             SpannableString s = new SpannableString(newText);
482             if (spans != null && spans.length > 0) {
483                 s.setSpan(spans[0], 0, s.length(), 0);
484             }
485             return s;
486         }
487 
488         /**
489          * @return the "View more conversations" view.
490          */
getViewMoreConversationsView()491         private RemoteViews getViewMoreConversationsView() {
492             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
493             view.setTextViewText(
494                     R.id.loading_text, mContext.getText(R.string.view_more_conversations));
495             view.setOnClickFillInIntent(R.id.widget_loading,
496                     Utils.createViewFolderIntent(mContext, mFolderUri, mAccount));
497             return view;
498         }
499 
500         @Override
getLoadingView()501         public RemoteViews getLoadingView() {
502             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
503             view.setTextViewText(
504                     R.id.loading_text, mContext.getText(R.string.loading_conversation));
505             return view;
506         }
507 
508         @Override
getViewTypeCount()509         public int getViewTypeCount() {
510             return 2;
511         }
512 
513         @Override
getItemId(int position)514         public long getItemId(int position) {
515             return position;
516         }
517 
518         @Override
hasStableIds()519         public boolean hasStableIds() {
520             return false;
521         }
522 
523         @Override
onLoadComplete(Loader<Cursor> loader, Cursor data)524         public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
525             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
526             final RemoteViews remoteViews =
527                     new RemoteViews(mContext.getPackageName(), R.layout.widget);
528 
529             if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
530                 BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
531                         mFolderCapabilities, mFolderUri, mFolderConversationListUri,
532                         mFolderDisplayName);
533             }
534 
535             if (loader == mFolderLoader) {
536                 if (!isDataValid(data)) {
537                     // Our folder may have disappeared on us
538                     BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
539                             mFolderCapabilities, mFolderUri, mFolderConversationListUri,
540                             mFolderDisplayName);
541 
542                     return;
543                 }
544 
545                 final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
546                 final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN);
547                 mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
548 
549                 if (!mFolderInformationShown && !TextUtils.isEmpty(folderName) &&
550                         !TextUtils.isEmpty(mAccount.getDisplayName())) {
551                     // We want to do a full update to the widget at least once, as the widget
552                     // manager doesn't cache the state of the remote views when doing a partial
553                     // widget update. This causes the folder name to be shown as blank if the state
554                     // of the widget is restored.
555                     mService.configureValidAccountWidget(mContext, remoteViews, mAppWidgetId,
556                             mAccount, mFolderType, mFolderCapabilities, mFolderUri,
557                             mFolderConversationListUri, folderName);
558                     appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
559                     mFolderInformationShown = true;
560                 }
561 
562                 // There is no reason to overwrite a valid non-null folder name with an empty string
563                 if (!TextUtils.isEmpty(folderName)) {
564                     remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
565                     remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
566                     remoteViews.setTextViewText(R.id.widget_folder, folderName);
567                 } else {
568                     LogUtils.e(LOG_TAG, "Empty folder name");
569                 }
570 
571                 appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
572             } else if (loader == mConversationCursorLoader) {
573                 // We want to cache the new cursor
574                 synchronized (sWidgetLock) {
575                     if (!isDataValid(data)) {
576                         mConversationCursor = null;
577                     } else {
578                         mConversationCursor = data;
579                     }
580                 }
581 
582                 appWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId,
583                         R.id.conversation_list);
584 
585                 if (mConversationCursor == null || mConversationCursor.getCount() == 0) {
586                     remoteViews.setTextViewText(R.id.empty_conversation_list,
587                             mContext.getString(R.string.empty_folder));
588                     appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
589                 }
590             } else if (loader == mAccountLoader) {
591                 BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
592                         mFolderCapabilities, mFolderUri, mFolderConversationListUri,
593                         mFolderDisplayName);
594             }
595         }
596 
597         /**
598          * Returns a boolean indicating whether this cursor has valid data.
599          * Note: This seeks to the first position in the cursor
600          */
isDataValid(Cursor cursor)601         private static boolean isDataValid(Cursor cursor) {
602             return cursor != null && !cursor.isClosed() && cursor.moveToFirst();
603         }
604 
605         /**
606          * A {@link DelayedTaskHandler} to throttle folder update to a reasonable rate.
607          */
608         private class FolderUpdateHandler extends DelayedTaskHandler {
FolderUpdateHandler(int refreshDelay)609             public FolderUpdateHandler(int refreshDelay) {
610                 super(Looper.myLooper(), refreshDelay);
611             }
612 
613             @Override
performTask()614             protected void performTask() {
615                 // Start the loader. The cached data will be returned if present.
616                 if (mFolderLoader != null) {
617                     mFolderLoader.startLoading();
618                 }
619             }
620         }
621     }
622 }
623