blob: d62af712e1590e3b4924118a4b8297f8f930677f [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Loader;
import android.content.Loader.OnLoadCompleteListener;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.support.v4.app.TaskStackBuilder;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.CharacterStyle;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import com.android.mail.R;
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.SendersView;
import com.android.mail.compose.ComposeActivity;
import com.android.mail.preferences.MailPrefs;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.utils.AccountUtils;
import com.android.mail.utils.DelayedTaskHandler;
import com.android.mail.utils.FolderUri;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class WidgetService extends RemoteViewsService {
/**
* Lock to avoid race condition between widgets.
*/
private static final Object sWidgetLock = new Object();
private static final String LOG_TAG = LogTag.getLogTag();
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new MailFactory(getApplicationContext(), intent, this);
}
protected void configureValidAccountWidget(Context context, RemoteViews remoteViews,
int appWidgetId, Account account, final int folderType, final int folderCapabilities,
final Uri folderUri, final Uri folderConversationListUri, String folderName) {
configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderType,
folderCapabilities, folderUri, folderConversationListUri, folderName,
WidgetService.class);
}
/**
* Modifies the remoteView for the given account and folder.
*/
public static void 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) {
remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
// If the folder or account name are empty, we don't want to overwrite the valid data that
// had been saved previously. Since the launcher will save the state of the remote views
// we should rely on the fact that valid data has been saved. But we should still log this,
// as it shouldn't happen
if (TextUtils.isEmpty(folderDisplayName) || TextUtils.isEmpty(account.getDisplayName())) {
LogUtils.e(LOG_TAG, new Error(),
"Empty folder or account name. account: %s, folder: %s",
account.getEmailAddress(), folderDisplayName);
}
if (!TextUtils.isEmpty(folderDisplayName)) {
remoteViews.setTextViewText(R.id.widget_folder, folderDisplayName);
}
remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
remoteViews.setViewVisibility(R.id.conversation_list, View.VISIBLE);
remoteViews.setViewVisibility(R.id.empty_conversation_list, View.VISIBLE);
remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE);
remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
remoteViews.setEmptyView(R.id.conversation_list, R.id.empty_conversation_list);
WidgetService.configureValidWidgetIntents(context, remoteViews, appWidgetId, account,
folderType, folderCapabilities, folderUri, folderConversationListUri,
folderDisplayName, widgetService);
}
public static void 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) {
remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
// Launch an intent to avoid ANRs
final Intent intent = new Intent(context, serviceClass);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_TYPE, folderType);
intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CAPABILITIES, folderCapabilities);
intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_URI, folderUri);
intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI,
folderConversationListUri);
intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
remoteViews.setRemoteAdapter(R.id.conversation_list, intent);
// Open mail app when click on header
final Intent mailIntent = Utils.createViewFolderIntent(context, folderUri, account);
mailIntent.setPackage(context.getPackageName());
PendingIntent clickIntent = PendingIntent.getActivity(context, 0, mailIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);
// On click intent for Compose
final Intent composeIntent = new Intent();
composeIntent.setPackage(context.getPackageName());
composeIntent.setAction(Intent.ACTION_SEND);
composeIntent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
composeIntent.setData(account.composeIntentUri);
composeIntent.putExtra(ComposeActivity.EXTRA_FROM_EMAIL_TASK, true);
if (account.composeIntentUri != null) {
composeIntent.putExtra(Utils.EXTRA_COMPOSE_URI, account.composeIntentUri);
}
// Build a task stack that forces the conversation list on the stack before the compose
// activity.
final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
clickIntent = taskStackBuilder.addNextIntent(mailIntent)
.addNextIntent(composeIntent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.widget_compose, clickIntent);
// On click intent for Conversation
final Intent conversationIntent = new Intent();
conversationIntent.setPackage(context.getPackageName());
conversationIntent.setAction(Intent.ACTION_VIEW);
clickIntent = PendingIntent.getActivity(context, 0, conversationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setPendingIntentTemplate(R.id.conversation_list, clickIntent);
}
/**
* Persists the information about the specified widget.
*/
public static void saveWidgetInformation(Context context, int appWidgetId, Account account,
final String folderUri) {
MailPrefs.get(context).configureWidget(appWidgetId, account, folderUri);
}
/**
* Returns true if this widget id has been configured and saved.
*/
public boolean isWidgetConfigured(Context context, int appWidgetId, Account account) {
return isAccountValid(context, account) &&
MailPrefs.get(context).isWidgetConfigured(appWidgetId);
}
protected boolean isAccountValid(Context context, Account account) {
if (account != null) {
Account[] accounts = AccountUtils.getSyncingAccounts(context);
for (Account existing : accounts) {
if (existing != null && account.uri.equals(existing.uri)) {
return true;
}
}
}
return false;
}
/**
* Remote Views Factory for Mail Widget.
*/
protected static class MailFactory
implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> {
private static final int MAX_CONVERSATIONS_COUNT = 25;
private static final int MAX_SENDERS_LENGTH = 25;
private static final int FOLDER_LOADER_ID = 0;
private static final int CONVERSATION_CURSOR_LOADER_ID = 1;
private static final int ACCOUNT_LOADER_ID = 2;
private static final int VIRTUALMAILBOX_LOADER_ID = 3;
private final Context mContext;
private final int mAppWidgetId;
private final Account mAccount;
private final int mFolderType;
private final int mFolderCapabilities;
private final Uri mFolderUri;
private final Uri mFolderConversationListUri;
private final String mFolderDisplayName;
private final WidgetConversationListItemViewBuilder mWidgetConversationListItemViewBuilder;
private CursorLoader mConversationCursorLoader;
private Cursor mConversationCursor;
private CursorLoader mFolderLoader;
private CursorLoader mAccountLoader;
private FolderUpdateHandler mFolderUpdateHandler;
private int mFolderCount;
private boolean mShouldShowViewMore;
private boolean mFolderInformationShown = false;
private final WidgetService mService;
private String mSendersSplitToken;
private String mElidedPaddingToken;
private UpdateFolderWidgetReceiver mUpdateFolderWidgetReceiver;
private CursorLoader mVirtualMailBoxLoader;
private int[] mWidgetIds;
private VirtualMailBoxLoaderCompleterListener mMailBoxLoaderCompleterListener;
public MailFactory(Context context, Intent intent, WidgetService service) {
mContext = context;
mAppWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
mAccount = Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT));
mFolderType = intent.getIntExtra(WidgetProvider.EXTRA_FOLDER_TYPE, FolderType.DEFAULT);
mFolderCapabilities = intent.getIntExtra(WidgetProvider.EXTRA_FOLDER_CAPABILITIES, 0);
mFolderDisplayName = intent.getStringExtra(WidgetProvider.EXTRA_FOLDER_DISPLAY_NAME);
final Uri folderUri = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_URI);
final Uri folderConversationListUri =
intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI);
if (folderUri != null && folderConversationListUri != null) {
mFolderUri = folderUri;
mFolderConversationListUri = folderConversationListUri;
} else {
// This is a old intent created in version UR8 (or earlier).
String folderString = intent.getStringExtra(Utils.EXTRA_FOLDER);
//noinspection deprecation
Folder folder = Folder.fromString(folderString);
if (folder != null) {
mFolderUri = folder.folderUri.fullUri;
mFolderConversationListUri = folder.conversationListUri;
} else {
mFolderUri = Uri.EMPTY;
mFolderConversationListUri = Uri.EMPTY;
// this will mark the widget as unconfigured
BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
mFolderCapabilities, mFolderUri, mFolderConversationListUri,
mFolderDisplayName);
}
}
mWidgetConversationListItemViewBuilder = new WidgetConversationListItemViewBuilder(
context);
mService = service;
}
@Override
public void onCreate() {
// Save the map between widgetId and account to preference
saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolderUri.toString());
// If the account of this widget has been removed, we want to update the widget to
// "Tap to configure" mode.
if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
mFolderCapabilities, mFolderUri, mFolderConversationListUri,
mFolderDisplayName);
}
mFolderInformationShown = false;
// We want to limit the query result to 25 and don't want these queries to cause network
// traffic
// We also want this cursor to receive notifications on all changes. Any change that
// the user made locally, the default policy of the UI provider is to not send
// notifications for. But in this case, since the widget is not using the
// ConversationCursor instance that the UI is using, the widget would not be updated.
final Uri.Builder builder = mFolderConversationListUri.buildUpon();
final String maxConversations = Integer.toString(MAX_CONVERSATIONS_COUNT);
final Uri widgetConversationQueryUri = builder
.appendQueryParameter(ConversationListQueryParameters.LIMIT, maxConversations)
.appendQueryParameter(ConversationListQueryParameters.USE_NETWORK,
Boolean.FALSE.toString())
.appendQueryParameter(ConversationListQueryParameters.ALL_NOTIFICATIONS,
Boolean.TRUE.toString()).build();
final Resources res = mContext.getResources();
mConversationCursorLoader = new CursorLoader(mContext, widgetConversationQueryUri,
UIProvider.CONVERSATION_PROJECTION, null, null, null);
mConversationCursorLoader.registerListener(CONVERSATION_CURSOR_LOADER_ID, this);
mConversationCursorLoader.setUpdateThrottle(
res.getInteger(R.integer.widget_refresh_delay_ms));
mConversationCursorLoader.startLoading();
mSendersSplitToken = res.getString(R.string.senders_split_token);
mElidedPaddingToken = res.getString(R.string.elided_padding_token);
mFolderLoader = new CursorLoader(mContext, mFolderUri, UIProvider.FOLDERS_PROJECTION,
null, null, null);
mFolderLoader.registerListener(FOLDER_LOADER_ID, this);
mFolderUpdateHandler = new FolderUpdateHandler(
res.getInteger(R.integer.widget_folder_refresh_delay_ms));
mFolderUpdateHandler.scheduleTask();
mAccountLoader = new CursorLoader(mContext, mAccount.uri,
UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null);
mAccountLoader.registerListener(ACCOUNT_LOADER_ID, this);
mAccountLoader.startLoading();
IntentFilter filter = new IntentFilter();
filter.addAction(Utils.ACTION_WIDGET_FOLDER_UPDATE);
mUpdateFolderWidgetReceiver = new UpdateFolderWidgetReceiver();
mContext.registerReceiver(mUpdateFolderWidgetReceiver, filter);
}
@Override
public void onDestroy() {
synchronized (sWidgetLock) {
if (mConversationCursorLoader != null) {
mConversationCursorLoader.reset();
mConversationCursorLoader.unregisterListener(this);
mConversationCursorLoader = null;
}
// The Loader should close the cursor, so just unset the reference
// to it here.
mConversationCursor = null;
}
if (mFolderLoader != null) {
mFolderLoader.reset();
mFolderLoader.unregisterListener(this);
mFolderLoader = null;
}
if (mAccountLoader != null) {
mAccountLoader.reset();
mAccountLoader.unregisterListener(this);
mAccountLoader = null;
}
if (mVirtualMailBoxLoader != null) {
mVirtualMailBoxLoader.reset();
if (mMailBoxLoaderCompleterListener != null) {
mVirtualMailBoxLoader.unregisterListener(mMailBoxLoaderCompleterListener);
}
mVirtualMailBoxLoader = null;
mMailBoxLoaderCompleterListener = null;
mWidgetIds = null;
}
if (mUpdateFolderWidgetReceiver != null) {
mContext.unregisterReceiver(mUpdateFolderWidgetReceiver);
}
}
@Override
public void onDataSetChanged() {
// We are not using this as signal to requery the cursor. The query will be started
// in the following ways:
// 1) The Service is started and the loader is started in onCreate()
// This will happen when the service is not running, and
// AppWidgetManager#notifyAppWidgetViewDataChanged() is called
// 2) The service is running, with a previously created loader. The loader is watching
// for updates from the existing cursor. If one is seen, the loader will load a new
// cursor in the background.
mFolderUpdateHandler.scheduleTask();
}
/**
* Returns the number of items should be shown in the widget list. This method also updates
* the boolean that indicates whether the "show more" item should be shown.
* @return the number of items to be displayed in the list.
*/
@Override
public int getCount() {
synchronized (sWidgetLock) {
final int count = getConversationCount();
final int cursorCount = mConversationCursor != null ?
mConversationCursor.getCount() : 0;
mShouldShowViewMore = count < cursorCount || count < mFolderCount;
return count + (mShouldShowViewMore ? 1 : 0);
}
}
/**
* Returns the number of conversations that should be shown in the widget. This method
* doesn't update the boolean that indicates that the "show more" item should be included
* in the list.
* @return count
*/
private int getConversationCount() {
synchronized (sWidgetLock) {
final int cursorCount = mConversationCursor != null ?
mConversationCursor.getCount() : 0;
return Math.min(cursorCount, MAX_CONVERSATIONS_COUNT);
}
}
/**
* @return the {@link RemoteViews} for a specific position in the list.
*/
@Override
public RemoteViews getViewAt(int position) {
synchronized (sWidgetLock) {
// "View more conversations" view.
if (mConversationCursor == null || mConversationCursor.isClosed()
|| (mShouldShowViewMore && position >= getConversationCount())) {
return getViewMoreConversationsView();
}
if (!mConversationCursor.moveToPosition(position)) {
// If we ever fail to move to a position, return the
// "View More conversations"
// view.
LogUtils.e(LOG_TAG, "Failed to move to position %d in the cursor.", position);
return getViewMoreConversationsView();
}
Conversation conversation = new Conversation(mConversationCursor);
// Split the senders and status from the instructions.
ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
SendersView.format(mContext, conversation.conversationInfo, "",
MAX_SENDERS_LENGTH, senders, null, null, mAccount,
Folder.shouldShowRecipients(mFolderCapabilities), true);
final SpannableStringBuilder senderBuilder = elideParticipants(senders);
// Get styled date.
CharSequence date = DateUtils.getRelativeTimeSpanString(mContext,
conversation.dateMs);
final int ignoreFolderType;
if ((mFolderType & FolderType.INBOX) != 0) {
ignoreFolderType = FolderType.INBOX;
} else {
ignoreFolderType = -1;
}
// Load up our remote view.
RemoteViews remoteViews = mWidgetConversationListItemViewBuilder.getStyledView(
mContext, date, conversation, new FolderUri(mFolderUri), ignoreFolderType,
senderBuilder,
ConversationItemView.filterTag(mContext, conversation.subject));
// On click intent.
remoteViews.setOnClickFillInIntent(R.id.widget_conversation_list_item,
Utils.createViewConversationIntent(mContext, conversation, mFolderUri,
mAccount));
return remoteViews;
}
}
private SpannableStringBuilder elideParticipants(List<SpannableString> parts) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString prevSender = null;
boolean skipToHeader = false;
// start with "To: " if we're showing recipients
if (Folder.shouldShowRecipients(mFolderCapabilities)) {
builder.append(SendersView.getFormattedToHeader());
skipToHeader = true;
}
for (SpannableString sender : parts) {
if (sender == null) {
LogUtils.e(LOG_TAG, "null sender while iterating over styledSenders");
continue;
}
CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
if (SendersView.sElidedString.equals(sender.toString())) {
prevSender = sender;
sender = copyStyles(spans, mElidedPaddingToken + sender + mElidedPaddingToken);
} else if (!skipToHeader && builder.length() > 0
&& (prevSender == null || !SendersView.sElidedString.equals(prevSender
.toString()))) {
prevSender = sender;
sender = copyStyles(spans, mSendersSplitToken + sender);
} else {
prevSender = sender;
skipToHeader = false;
}
builder.append(sender);
}
return builder;
}
private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
SpannableString s = new SpannableString(newText);
if (spans != null && spans.length > 0) {
s.setSpan(spans[0], 0, s.length(), 0);
}
return s;
}
/**
* @return the "View more conversations" view.
*/
private RemoteViews getViewMoreConversationsView() {
RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
view.setTextViewText(
R.id.loading_text, mContext.getText(R.string.view_more_conversations));
view.setOnClickFillInIntent(R.id.widget_loading,
Utils.createViewFolderIntent(mContext, mFolderUri, mAccount));
return view;
}
@Override
public RemoteViews getLoadingView() {
RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
view.setTextViewText(
R.id.loading_text, mContext.getText(R.string.loading_conversation));
return view;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
final RemoteViews remoteViews =
new RemoteViews(mContext.getPackageName(), R.layout.widget);
if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
mFolderCapabilities, mFolderUri, mFolderConversationListUri,
mFolderDisplayName);
}
if (loader == mFolderLoader) {
if (!isDataValid(data)) {
// Our folder may have disappeared on us
BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
mFolderCapabilities, mFolderUri, mFolderConversationListUri,
mFolderDisplayName);
return;
}
final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN);
mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
if (!mFolderInformationShown && !TextUtils.isEmpty(folderName) &&
!TextUtils.isEmpty(mAccount.getDisplayName())) {
// We want to do a full update to the widget at least once, as the widget
// manager doesn't cache the state of the remote views when doing a partial
// widget update. This causes the folder name to be shown as blank if the state
// of the widget is restored.
mService.configureValidAccountWidget(mContext, remoteViews, mAppWidgetId,
mAccount, mFolderType, mFolderCapabilities, mFolderUri,
mFolderConversationListUri, folderName);
appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
mFolderInformationShown = true;
}
// There is no reason to overwrite a valid non-null folder name with an empty string
if (!TextUtils.isEmpty(folderName)) {
remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
remoteViews.setTextViewText(R.id.widget_folder, folderName);
} else {
LogUtils.e(LOG_TAG, "Empty folder name");
}
appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
} else if (loader == mConversationCursorLoader) {
// We want to cache the new cursor
synchronized (sWidgetLock) {
if (!isDataValid(data)) {
mConversationCursor = null;
} else {
mConversationCursor = data;
}
}
appWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId,
R.id.conversation_list);
if (mConversationCursor == null || mConversationCursor.getCount() == 0) {
remoteViews.setTextViewText(R.id.empty_conversation_list,
mContext.getString(R.string.empty_folder));
appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
}
} else if (loader == mAccountLoader) {
BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
mFolderCapabilities, mFolderUri, mFolderConversationListUri,
mFolderDisplayName);
}
}
/**
* Returns a boolean indicating whether this cursor has valid data.
* Note: This seeks to the first position in the cursor
*/
private static boolean isDataValid(Cursor cursor) {
return cursor != null && !cursor.isClosed() && cursor.moveToFirst();
}
/**
* A {@link DelayedTaskHandler} to throttle folder update to a reasonable rate.
*/
private class FolderUpdateHandler extends DelayedTaskHandler {
public FolderUpdateHandler(int refreshDelay) {
super(Looper.myLooper(), refreshDelay);
}
@Override
protected void performTask() {
// Start the loader. The cached data will be returned if present.
if (mFolderLoader != null) {
mFolderLoader.startLoading();
}
}
}
public class UpdateFolderWidgetReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
if (intent.getAction() == Utils.ACTION_WIDGET_FOLDER_UPDATE) {
final Bundle extras = intent.getExtras();
if (extras == null) {
return;
}
final Uri accountUri = extras.getParcelable(Utils.EXTRA_ACCOUNT_URI);
final Uri folderUri = extras.getParcelable(Utils.EXTRA_FOLDER_URI);
final Uri conversationListUri =
extras.getParcelable(Utils.EXTRA_CONVERSATIONLIST_URI);
final boolean updateAllWidgets =
extras.getBoolean(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, false);
if (mVirtualMailBoxLoader != null && !mVirtualMailBoxLoader.isReset()) {
mVirtualMailBoxLoader.reset();
}
if (mVirtualMailBoxLoader == null) {
mVirtualMailBoxLoader = new CursorLoader(mContext,
builderConversationQueryUri(conversationListUri),
UIProvider.CONVERSATION_PROJECTION, null, null, null);
mMailBoxLoaderCompleterListener =
new VirtualMailBoxLoaderCompleterListener();
mVirtualMailBoxLoader.registerListener(VIRTUALMAILBOX_LOADER_ID,
mMailBoxLoaderCompleterListener);
} else {
mVirtualMailBoxLoader
.setUri(builderConversationQueryUri(conversationListUri));
}
mWidgetIds = getWidgetIds(accountUri, folderUri, updateAllWidgets);
mVirtualMailBoxLoader.setUpdateThrottle(
context.getResources().getInteger(R.integer.widget_refresh_delay_ms));
mVirtualMailBoxLoader.startLoading();
}
}
private int[] getWidgetIds(Uri accountUri, Uri folderUri, boolean updateAllWidgets) {
if (accountUri == null && Utils.isEmpty(folderUri) && !updateAllWidgets) {
return new int[0];
}
final Set<Integer> widgetsToUpdate = Sets.newHashSet();
for (int id : getCurrentWidgetIds(mContext)) {
// Retrieve the persisted information for this widget from
// preferences.
final String accountFolder = MailPrefs.get(mContext).getWidgetConfiguration(id);
// If the account matched, update the widget.
if (accountFolder != null) {
final String[] parsedInfo = TextUtils.split(accountFolder,
BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR);
boolean updateThis = updateAllWidgets;
if (!updateThis) {
if (accountUri != null &&
TextUtils.equals(accountUri.toString(), parsedInfo[0])) {
updateThis = true;
} else if (folderUri != null &&
TextUtils.equals(folderUri.toString(), parsedInfo[1])) {
updateThis = true;
}
}
if (updateThis) {
widgetsToUpdate.add(id);
}
}
}
if (widgetsToUpdate.size() == 0) {
return new int[0];
}
return Ints.toArray(widgetsToUpdate);
}
private int[] getCurrentWidgetIds(Context context) {
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
final ComponentName mailComponent = new ComponentName(context,
BaseWidgetProvider.getProviderName(context));
return appWidgetManager.getAppWidgetIds(mailComponent);
}
private Uri builderConversationQueryUri(Uri conversationListUri) {
final Uri.Builder builder = conversationListUri.buildUpon();
final String maxConversations = Integer.toString(MAX_CONVERSATIONS_COUNT);
final Uri widgetConversationQueryUri = builder
.appendQueryParameter(ConversationListQueryParameters.LIMIT,
maxConversations)
.appendQueryParameter(ConversationListQueryParameters.USE_NETWORK,
Boolean.FALSE.toString())
.appendQueryParameter(ConversationListQueryParameters.ALL_NOTIFICATIONS,
Boolean.TRUE.toString()).build();
return widgetConversationQueryUri;
}
}
private class VirtualMailBoxLoaderCompleterListener implements
OnLoadCompleteListener<Cursor> {
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
final RemoteViews remoteViews =
new RemoteViews(mContext.getPackageName(), R.layout.widget);
if (data != null) {
synchronized (sWidgetLock) {
if (!isDataValid(data)) {
mConversationCursor = null;
} else {
mConversationCursor = data;
}
}
appWidgetManager.notifyAppWidgetViewDataChanged(mWidgetIds,
R.id.conversation_list);
if (mConversationCursor == null || mConversationCursor.getCount() == 0) {
remoteViews.setTextViewText(R.id.empty_conversation_list,
mContext.getString(R.string.empty_folder));
appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
}
}
}
}
}
}