Home | History | Annotate | Download | only in widget
      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
     69     public RemoteViewsFactory onGetViewFactory(Intent intent) {
     70         return new MailFactory(getApplicationContext(), intent, this);
     71     }
     72 
     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      */
     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 
    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      */
    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      */
    180     public boolean isWidgetConfigured(Context context, int appWidgetId, Account account) {
    181         return isAccountValid(context, account) &&
    182                 MailPrefs.get(context).isWidgetConfigured(appWidgetId);
    183     }
    184 
    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 
    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
    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
    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
    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
    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 
    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          */
    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
    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
    509         public int getViewTypeCount() {
    510             return 2;
    511         }
    512 
    513         @Override
    514         public long getItemId(int position) {
    515             return position;
    516         }
    517 
    518         @Override
    519         public boolean hasStableIds() {
    520             return false;
    521         }
    522 
    523         @Override
    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          */
    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 {
    609             public FolderUpdateHandler(int refreshDelay) {
    610                 super(Looper.myLooper(), refreshDelay);
    611             }
    612 
    613             @Override
    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