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