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