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