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