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