WidgetService.java revision 8a8c50d8fcc4f20549c9f395edbad017a940e72b
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 */ 16package com.android.mail.widget; 17 18import com.android.mail.R; 19import com.android.mail.browse.ConversationCursor; 20import com.android.mail.providers.Account; 21import com.android.mail.providers.Conversation; 22import com.android.mail.providers.Folder; 23import com.android.mail.providers.UIProvider; 24import com.android.mail.utils.DelayedTaskHandler; 25import com.android.mail.utils.LogUtils; 26import com.android.mail.utils.Utils; 27 28import android.appwidget.AppWidgetManager; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.CursorLoader; 32import android.content.Intent; 33import android.content.Loader; 34import android.content.Loader.OnLoadCompleteListener; 35import android.database.Cursor; 36import android.net.Uri; 37import android.os.Looper; 38import android.text.SpannableStringBuilder; 39import android.text.TextUtils; 40import android.text.format.DateUtils; 41import android.view.View; 42import android.widget.RemoteViews; 43import android.widget.RemoteViewsService; 44 45import java.util.Map; 46 47public class WidgetService extends RemoteViewsService { 48 /** 49 * Lock to avoid race condition between widgets. 50 */ 51 private static Object sWidgetLock = new Object(); 52 53 @Override 54 public RemoteViewsFactory onGetViewFactory(Intent intent) { 55 return new MailFactory(getApplicationContext(), intent); 56 } 57 58 /** 59 * Remote Views Factory for Mail Widget. 60 */ 61 private static class MailFactory 62 implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> { 63 private static final int MAX_CONVERSATIONS_COUNT = 25; 64 private static final int MAX_SENDERS_LENGTH = 25; 65 private static final String LOG_TAG = new LogUtils().getLogTag(); 66 67 private final Context mContext; 68 private final int mAppWidgetId; 69 private final Account mAccount; 70 private final Folder mFolder; 71 private final WidgetConversationViewBuilder mWidgetConversationViewBuilder; 72 private Cursor mConversationCursor; 73 private CursorLoader mFolderLoader; 74 private FolderUpdateHandler mFolderUpdateHandler; 75 private int mFolderCount; 76 private boolean mShouldShowViewMore; 77 private boolean mFolderInformationShown = false; 78 private ContentResolver mResolver; 79 80 public MailFactory(Context context, Intent intent) { 81 mContext = context; 82 mAppWidgetId = intent.getIntExtra( 83 AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); 84 mAccount = intent.getParcelableExtra(WidgetProvider.EXTRA_ACCOUNT); 85 final Folder folder = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER); 86 mFolder = folder != null ? folder : mAccount.getAccountInbox(); 87 mWidgetConversationViewBuilder = new WidgetConversationViewBuilder(mContext, mAccount); 88 mResolver = context.getContentResolver(); 89 } 90 91 @Override 92 public void onCreate() { 93 // Save the map between widgetId and account to preference 94 BaseWidgetProvider.saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolder); 95 96 // If the account of this widget has been removed, we want to update the widget to 97 // "Tap to configure" mode. 98 if (!BaseWidgetProvider.isWidgetConfigured(mContext, mAppWidgetId, mAccount, mFolder)) { 99 BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolder); 100 } 101 102 mConversationCursor = mResolver.query(Uri.parse(mFolder.conversationListUri), 103 UIProvider.CONVERSATION_PROJECTION, null, null, null); 104 105 mFolderLoader = new CursorLoader(mContext, Uri.parse(mFolder.uri), 106 UIProvider.FOLDERS_PROJECTION, null, null, null); 107 mFolderLoader.registerListener(0, this); 108 mFolderUpdateHandler = new FolderUpdateHandler(mContext.getResources().getInteger( 109 R.integer.widget_folder_refresh_delay_ms)); 110 mFolderUpdateHandler.scheduleTask(); 111 112 } 113 114 @Override 115 public void onDestroy() { 116 synchronized (sWidgetLock) { 117 if (mConversationCursor != null && !mConversationCursor.isClosed()) { 118 mConversationCursor.close(); 119 mConversationCursor = null; 120 } 121 } 122 123 if (mFolderLoader != null) { 124 mFolderLoader.reset(); 125 mFolderLoader = null; 126 } 127 } 128 129 @Override 130 public void onDataSetChanged() { 131 synchronized (sWidgetLock) { 132 // TODO: use loader manager. 133 mConversationCursor.requery(); 134 } 135 mFolderUpdateHandler.scheduleTask(); 136 } 137 138 /** 139 * Returns the number of items should be shown in the widget list. This method also updates 140 * the boolean that indicates whether the "show more" item should be shown. 141 * @return the number of items to be displayed in the list. 142 */ 143 @Override 144 public int getCount() { 145 synchronized (sWidgetLock) { 146 final int count = getConversationCount(); 147 mShouldShowViewMore = count < mConversationCursor.getCount() 148 || count < mFolderCount; 149 return count + (mShouldShowViewMore ? 1 : 0); 150 } 151 } 152 153 /** 154 * Returns the number of conversations that should be shown in the widget. This method 155 * doesn't update the boolean that indicates that the "show more" item should be included 156 * in the list. 157 * @return 158 */ 159 private int getConversationCount() { 160 synchronized (sWidgetLock) { 161 return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT); 162 } 163 } 164 165 /** 166 * @return the {@link RemoteViews} for a specific position in the list. 167 */ 168 @Override 169 public RemoteViews getViewAt(int position) { 170 synchronized (sWidgetLock) { 171 // "View more conversations" view. 172 if (mConversationCursor == null 173 || (mShouldShowViewMore && position >= getConversationCount())) { 174 return getViewMoreConversationsView(); 175 } 176 177 if (!mConversationCursor.moveToPosition(position)) { 178 // If we ever fail to move to a position, return the "View More conversations" 179 // view. 180 LogUtils.e(LOG_TAG, 181 "Failed to move to position %d in the cursor.", position); 182 return getViewMoreConversationsView(); 183 } 184 185 Conversation conversation = Conversation.from(mConversationCursor); 186 // Split the senders and status from the instructions. 187 SpannableStringBuilder senderBuilder = new SpannableStringBuilder(); 188 SpannableStringBuilder statusBuilder = new SpannableStringBuilder(); 189 Utils.getStyledSenderSnippet(mContext, conversation.senders, senderBuilder, 190 statusBuilder, MAX_SENDERS_LENGTH, false, false, false); 191 192 // Get styled date. 193 CharSequence date = DateUtils.getRelativeTimeSpanString( 194 mContext, conversation.dateMs); 195 196 // Load up our remote view. 197 RemoteViews remoteViews = mWidgetConversationViewBuilder.getStyledView( 198 senderBuilder, statusBuilder, date, filterTag(conversation.subject), 199 conversation.snippet, conversation.folderList, conversation.hasAttachments); 200 201 // On click intent. 202 remoteViews.setOnClickFillInIntent(R.id.widget_conversation, 203 Utils.createViewConversationIntent(mContext, mAccount, mFolder, 204 conversation.id)); 205 206 return remoteViews; 207 } 208 } 209 210 /** 211 * @return the "View more conversations" view. 212 */ 213 private RemoteViews getViewMoreConversationsView() { 214 RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading); 215 view.setTextViewText( 216 R.id.loading_text, mContext.getText(R.string.view_more_conversations)); 217 view.setOnClickFillInIntent(R.id.widget_loading, 218 Utils.createViewConversationIntent(mContext, mAccount, mFolder, 219 UIProvider.INVALID_CONVERSATION_ID)); 220 return view; 221 } 222 223 @Override 224 public RemoteViews getLoadingView() { 225 RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading); 226 view.setTextViewText( 227 R.id.loading_text, mContext.getText(R.string.loading_conversation)); 228 return view; 229 } 230 231 @Override 232 public int getViewTypeCount() { 233 return 2; 234 } 235 236 @Override 237 public long getItemId(int position) { 238 return position; 239 } 240 241 @Override 242 public boolean hasStableIds() { 243 return false; 244 } 245 246 @Override 247 public void onLoadComplete(Loader<Cursor> loader, Cursor data) { 248 if (!data.moveToFirst()) { 249 return; 250 } 251 252 final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN); 253 final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN); 254 mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN); 255 256 RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget); 257 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 258 259 if (!mFolderInformationShown && !TextUtils.isEmpty(folderName)) { 260 // We want to do a full update to the widget at least once, as the widget 261 // manager doesn't cache the state of the remote views when doing a partial 262 // widget update. This causes the label name to be shown as blank if the state 263 // of the widget is restored. 264 BaseWidgetProvider.configureValidAccountWidget( 265 mContext, remoteViews, mAppWidgetId, mAccount, mFolder, folderName); 266 appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews); 267 mFolderInformationShown = true; 268 } 269 270 remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE); 271 remoteViews.setTextViewText(R.id.widget_folder, folderName); 272 remoteViews.setViewVisibility(R.id.widget_unread_count, View.VISIBLE); 273 remoteViews.setTextViewText( 274 R.id.widget_unread_count, Utils.getUnreadCountString(mContext, unreadCount)); 275 276 appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews); 277 } 278 279 /** 280 * If the subject contains the tag of a mailing-list (text surrounded with []), return the 281 * subject with that tag ellipsized, e.g. "[android-gmail-team] Hello" -> "[andr...] Hello" 282 */ 283 private static String filterTag(String subject) { 284 String result = subject; 285 if (subject.length() > 0 && subject.charAt(0) == '[') { 286 int end = subject.indexOf(']'); 287 if (end > 0) { 288 String tag = subject.substring(1, end); 289 result = "[" + Utils.ellipsize(tag, 7) + "]" + subject.substring(end + 1); 290 } 291 } 292 293 return result; 294 } 295 296 /** 297 * A {@link DelayedTaskHandler} to throttle label update to a reasonable rate. 298 */ 299 private class FolderUpdateHandler extends DelayedTaskHandler { 300 public FolderUpdateHandler(int refreshDelay) { 301 super(Looper.myLooper(), refreshDelay); 302 } 303 304 @Override 305 protected void performTask() { 306 // Start the loader. The cached data will be returned if present. 307 if (mFolderLoader != null) { 308 mFolderLoader.startLoading(); 309 } 310 } 311 } 312 } 313} 314