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