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