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