WidgetProvider.java revision 3db3e4b795c08122d1c9d4fc105150231795448b
1/* 2 * Copyright (C) 2010 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 */ 16 17package com.android.email.provider; 18 19import com.android.email.Email; 20import com.android.email.R; 21import com.android.email.ResourceHelper; 22import com.android.email.Utility; 23import com.android.email.activity.MessageCompose; 24import com.android.email.activity.Welcome; 25import com.android.email.data.ThrottlingCursorLoader; 26import com.android.email.provider.EmailContent.Account; 27import com.android.email.provider.EmailContent.AccountColumns; 28import com.android.email.provider.EmailContent.Mailbox; 29import com.android.email.provider.EmailContent.Message; 30import com.android.email.provider.EmailContent.MessageColumns; 31 32import android.app.Activity; 33import android.app.PendingIntent; 34import android.app.Service; 35import android.appwidget.AppWidgetManager; 36import android.appwidget.AppWidgetProvider; 37import android.content.ContentResolver; 38import android.content.ContentUris; 39import android.content.Context; 40import android.content.Intent; 41import android.content.Loader; 42import android.content.res.Resources; 43import android.database.Cursor; 44import android.graphics.Typeface; 45import android.net.Uri; 46import android.net.Uri.Builder; 47import android.os.AsyncTask; 48import android.os.Bundle; 49import android.text.Spannable; 50import android.text.SpannableString; 51import android.text.SpannableStringBuilder; 52import android.text.TextUtils; 53import android.text.format.DateUtils; 54import android.text.style.AbsoluteSizeSpan; 55import android.text.style.ForegroundColorSpan; 56import android.text.style.StyleSpan; 57import android.util.Log; 58import android.view.View; 59import android.widget.RemoteViews; 60import android.widget.RemoteViewsService; 61 62import java.util.HashMap; 63import java.util.List; 64 65public class WidgetProvider extends AppWidgetProvider { 66 private static final String TAG = "WidgetProvider"; 67 68 /** 69 * When handling clicks in a widget ListView, a single PendingIntent template is provided to 70 * RemoteViews, and the individual "on click" actions are distinguished via a "fillInIntent" 71 * on each list element; when a click is received, this "fillInIntent" is merged with the 72 * PendingIntent using Intent.fillIn(). Since this mechanism does NOT preserve the Extras 73 * Bundle, we instead encode information about the action (e.g. view, reply, etc.) and its 74 * arguments (e.g. messageId, mailboxId, etc.) in an Uri which is added to the Intent via 75 * Intent.setDataAndType() 76 * 77 * The mime type MUST be set in the Intent, even though we do not use it; therefore, it's value 78 * is entirely arbitrary. 79 * 80 * Our "command" Uri is NOT used by the system in any manner, and is therefore constrained only 81 * in the requirement that it be syntactically valid. 82 * 83 * We use the following convention for our commands: 84 * widget://command/<command>/<arg1>[/<arg2>] 85 */ 86 private static final String WIDGET_DATA_MIME_TYPE = "com.android.email/widget_data"; 87 private static final Uri COMMAND_URI = Uri.parse("widget://command"); 88 89 // Command names and Uri's built upon COMMAND_URI 90 private static final String COMMAND_NAME_SWITCH_LIST_VIEW = "switch_list_view"; 91 private static final Uri COMMAND_URI_SWITCH_LIST_VIEW = 92 COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_SWITCH_LIST_VIEW).build(); 93 private static final String COMMAND_NAME_VIEW_MESSAGE = "view_message"; 94 private static final Uri COMMAND_URI_VIEW_MESSAGE = 95 COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build(); 96 97 private static final int TOTAL_COUNT_UNKNOWN = -1; 98 private static final int MAX_MESSAGE_LIST_COUNT = 25; 99 100 private static final String[] NO_ARGUMENTS = new String[0]; 101 private static final String SORT_TIMESTAMP_DESCENDING = MessageColumns.TIMESTAMP + " DESC"; 102 private static final String SORT_ID_ASCENDING = AccountColumns.ID + " ASC"; 103 private static final String[] ID_NAME_PROJECTION = {Account.RECORD_ID, Account.DISPLAY_NAME}; 104 private static final int ID_NAME_COLUMN_ID = 0; 105 private static final int ID_NAME_COLUMN_NAME = 1; 106 107 108 // Map holding our instantiated widgets, accessed by widget id 109 private static HashMap<Integer, EmailWidget> sWidgetMap = new HashMap<Integer, EmailWidget>(); 110 private static AppWidgetManager sWidgetManager; 111 private static Context sContext; 112 private static ContentResolver sResolver; 113 114 private static int sSenderFontSize; 115 private static int sSubjectFontSize; 116 private static int sDateFontSize; 117 private static int sDefaultTextColor; 118 private static int sLightTextColor; 119 private static String sSubjectSnippetDivider; 120 private static String sConfigureText; 121 122 /** 123 * Types of views that we're prepared to show in the widget - all mail, unread mail, and starred 124 * mail; we rotate between them. Each ViewType is composed of a selection string and a title. 125 */ 126 public enum ViewType { 127 ALL_INBOX(null, NO_ARGUMENTS, R.string.widget_all_inbox), 128 UNREAD(MessageColumns.FLAG_READ + "=0", NO_ARGUMENTS, R.string.widget_unread), 129 STARRED(Message.ALL_FAVORITE_SELECTION, NO_ARGUMENTS, R.string.widget_starred), 130 ACCOUNT(MessageColumns.ACCOUNT_KEY + "=?", new String[1], 0); 131 132 private final String selection; 133 private final String[] selectionArgs; 134 private final int titleResource; 135 private String title; 136 137 ViewType(String _selection, String[] _selectionArgs, int _titleResource) { 138 selection = _selection; 139 selectionArgs = _selectionArgs; 140 titleResource = _titleResource; 141 } 142 143 public String getTitle(Context context) { 144 if (title == null && titleResource != 0) { 145 title = context.getString(titleResource); 146 } 147 return title; 148 } 149 150 public String getSelection(Context context) { 151 // For "all inbox", we define a special selection 152 if (this == ViewType.ALL_INBOX) { 153 // Rebuild selection every time in case accounts have been added or removed 154 return Utility.buildMailboxIdSelection(context, Mailbox.QUERY_ALL_INBOXES); 155 } 156 return selection; 157 } 158 } 159 160 static class EmailWidget implements RemoteViewsService.RemoteViewsFactory { 161 // The widget identifier 162 private final int mWidgetId; 163 164 // The cursor underlying the message list for this widget; this must only be modified while 165 // holding mCursorLock 166 private volatile Cursor mCursor; 167 // A lock on our cursor, which is used in the UI thread while inflating views, and by 168 // our Loader in the background 169 private final Object mCursorLock = new Object(); 170 // Number of records in the cursor 171 private int mCursorCount = TOTAL_COUNT_UNKNOWN; 172 // The widget's loader (derived from ThrottlingCursorLoader) 173 private WidgetLoader mLoader; 174 private final ResourceHelper mResourceHelper; 175 176 // The current view type (all mail, unread, or starred for now) 177 /*package*/ ViewType mViewType = ViewType.STARRED; 178 179 // The projection to be used by the WidgetLoader 180 public static final String[] WIDGET_PROJECTION = new String[] { 181 EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP, 182 MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, 183 MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET, 184 MessageColumns.ACCOUNT_KEY, MessageColumns.FLAGS 185 }; 186 public static final int WIDGET_COLUMN_ID = 0; 187 public static final int WIDGET_COLUMN_DISPLAY_NAME = 1; 188 public static final int WIDGET_COLUMN_TIMESTAMP = 2; 189 public static final int WIDGET_COLUMN_SUBJECT = 3; 190 public static final int WIDGET_COLUMN_FLAG_READ = 4; 191 public static final int WIDGET_COLUMN_FLAG_FAVORITE = 5; 192 public static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6; 193 public static final int WIDGET_COLUMN_MAILBOX_KEY = 7; 194 public static final int WIDGET_COLUMN_SNIPPET = 8; 195 public static final int WIDGET_COLUMN_ACCOUNT_KEY = 9; 196 public static final int WIDGET_COLUMN_FLAGS = 10; 197 198 public EmailWidget(int _widgetId) { 199 super(); 200 if (Email.DEBUG) { 201 Log.d(TAG, "Creating EmailWidget with id = " + _widgetId); 202 } 203 mWidgetId = _widgetId; 204 mLoader = new WidgetLoader(); 205 if (sSubjectSnippetDivider == null) { 206 // Initialize string, color, dimension resources 207 Resources res = sContext.getResources(); 208 sSubjectSnippetDivider = 209 res.getString(R.string.message_list_subject_snippet_divider); 210 sSenderFontSize = res.getDimensionPixelSize(R.dimen.widget_senders_font_size); 211 sSubjectFontSize = res.getDimensionPixelSize(R.dimen.widget_subject_font_size); 212 sDateFontSize = res.getDimensionPixelSize(R.dimen.widget_date_font_size); 213 sDefaultTextColor = res.getColor(R.color.widget_default_text_color); 214 sDefaultTextColor = res.getColor(R.color.widget_default_text_color); 215 sLightTextColor = res.getColor(R.color.widget_light_text_color); 216 sConfigureText = res.getString(R.string.widget_other_views); 217 } 218 mResourceHelper = ResourceHelper.getInstance(sContext); 219 } 220 221 /** 222 * The ThrottlingCursorLoader does all of the heavy lifting in managing the data loading 223 * task; all we need is to register a listener so that we're notified when the load is 224 * complete. 225 */ 226 final class WidgetLoader extends ThrottlingCursorLoader { 227 protected WidgetLoader() { 228 super(sContext, Message.CONTENT_URI, WIDGET_PROJECTION, mViewType.selection, 229 mViewType.selectionArgs, SORT_TIMESTAMP_DESCENDING); 230 registerListener(0, new OnLoadCompleteListener<Cursor>() { 231 @Override 232 public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) { 233 synchronized (mCursorLock) { 234 // Save away the cursor 235 mCursor = cursor; 236 // Reset the notification Uri to our Message table notifier URI 237 mCursor.setNotificationUri(sResolver, Message.NOTIFIER_URI); 238 // Save away the count (for display) 239 mCursorCount = mCursor.getCount(); 240 if (Email.DEBUG) { 241 Log.d(TAG, "onLoadComplete, count = " + cursor.getCount()); 242 } 243 } 244 RemoteViews views = 245 new RemoteViews(sContext.getPackageName(), R.layout.widget); 246 setupTitleAndCount(views); 247 sWidgetManager.partiallyUpdateAppWidget(mWidgetId, views); 248 sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); 249 } 250 }); 251 } 252 253 /** 254 * Stop any pending load, reset selection parameters, and start loading 255 * Must be called from the UI thread 256 * @param viewType the current ViewType 257 */ 258 private void load(ViewType viewType) { 259 reset(); 260 setSelection(viewType.getSelection(sContext)); 261 setSelectionArgs(viewType.selectionArgs); 262 startLoading(); 263 } 264 } 265 266 /** 267 * Initialize to first appropriate view (depending on the number of accounts) 268 */ 269 private void init() { 270 new WidgetViewSwitcher(this).execute(); 271 } 272 273 /** 274 * Reset cursor and cursor count, notify widget that list data is invalid, and start loading 275 * with our current ViewType 276 */ 277 private void loadView() { 278 synchronized(mCursorLock) { 279 mCursorCount = TOTAL_COUNT_UNKNOWN; 280 mCursor = null; 281 sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); 282 mLoader.load(mViewType); 283 } 284 } 285 286 /** 287 * Switch to the next widget view (all -> account1 -> ... -> account n -> unread -> starred) 288 * This must be called on a background thread 289 */ 290 public synchronized void switchToNextView() { 291 switch(mViewType) { 292 // If we're in starred and there is more than one account, go to "all mail" 293 // Otherwise, fall through to the accounts themselves 294 case STARRED: 295 if (EmailContent.count(sContext, Account.CONTENT_URI) > 1) { 296 mViewType = ViewType.ALL_INBOX; 297 break; 298 } 299 //$FALL-THROUGH$ 300 case ALL_INBOX: 301 ViewType.ACCOUNT.selectionArgs[0] = "0"; 302 //$FALL-THROUGH$ 303 case ACCOUNT: 304 // Find the next account (or, if none, default to UNREAD) 305 String idString = ViewType.ACCOUNT.selectionArgs[0]; 306 Cursor c = sResolver.query(Account.CONTENT_URI, ID_NAME_PROJECTION, "_id>?", 307 new String[] {idString}, SORT_ID_ASCENDING); 308 try { 309 if (c.moveToFirst()) { 310 mViewType = ViewType.ACCOUNT; 311 mViewType.selectionArgs[0] = c.getString(ID_NAME_COLUMN_ID); 312 mViewType.title = c.getString(ID_NAME_COLUMN_NAME); 313 } else { 314 mViewType = ViewType.UNREAD; 315 } 316 } finally { 317 c.close(); 318 } 319 break; 320 case UNREAD: 321 mViewType = ViewType.STARRED; 322 break; 323 } 324 } 325 326 /** 327 * Convenience method for creating an onClickPendingIntent that executes a command via 328 * our command Uri. Used for the "next view" command; appends the widget id to the command 329 * Uri. 330 * 331 * @param views The RemoteViews we're inflating 332 * @param buttonId the id of the button view 333 * @param data the command Uri 334 */ 335 private void setCommandIntent(RemoteViews views, int buttonId, Uri data) { 336 Intent intent = new Intent(sContext, WidgetService.class); 337 intent.setDataAndType(ContentUris.withAppendedId(data, mWidgetId), 338 WIDGET_DATA_MIME_TYPE); 339 PendingIntent pendingIntent = PendingIntent.getService(sContext, 0, intent, 340 PendingIntent.FLAG_UPDATE_CURRENT); 341 views.setOnClickPendingIntent(buttonId, pendingIntent); 342 } 343 344 /** 345 * Convenience method for creating an onClickPendingIntent that launches another activity 346 * directly. Used for the "Compose" button 347 * 348 * @param views The RemoteViews we're inflating 349 * @param buttonId the id of the button view 350 * @param activityClass the class of the activity to be launched 351 */ 352 private void setActivityIntent(RemoteViews views, int buttonId, 353 Class<? extends Activity> activityClass) { 354 Intent intent = new Intent(sContext, activityClass); 355 PendingIntent pendingIntent = PendingIntent.getActivity(sContext, 0, intent, 0); 356 views.setOnClickPendingIntent(buttonId, pendingIntent); 357 } 358 359 /** 360 * Convenience method for constructing a fillInIntent for a given list view element. 361 * Appends the command and any arguments to a base Uri. 362 * 363 * @param views the RemoteViews we are inflating 364 * @param viewId the id of the view 365 * @param baseUri the base uri for the command 366 * @param args any arguments to the command 367 */ 368 private void setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args) { 369 Intent intent = new Intent(); 370 Builder builder = baseUri.buildUpon(); 371 for (String arg: args) { 372 builder.appendPath(arg); 373 } 374 intent.setDataAndType(builder.build(), WIDGET_DATA_MIME_TYPE); 375 views.setOnClickFillInIntent(viewId, intent); 376 } 377 378 private void setupTitleAndCount(RemoteViews views) { 379 // Set up the title (view type + count of messages) 380 views.setTextViewText(R.id.widget_title, mViewType.getTitle(sContext)); 381 views.setTextViewText(R.id.widget_tap, sConfigureText); 382 String count = ""; 383 if (mCursorCount != TOTAL_COUNT_UNKNOWN) { 384 count = Integer.toString(mCursor.getCount()); 385 } 386 views.setTextViewText(R.id.widget_count, count); 387 } 388 /** 389 * Update the "header" of the widget (i.e. everything that doesn't include the scrolling 390 * message list) 391 */ 392 private void updateHeader() { 393 if (Email.DEBUG) { 394 Log.d(TAG, "updateWidget " + mWidgetId); 395 } 396 397 // Get the widget layout 398 RemoteViews views = new RemoteViews(sContext.getPackageName(), R.layout.widget); 399 400 // Set up the list with an adapter 401 Intent intent = new Intent(sContext, WidgetService.class); 402 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId); 403 intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); 404 views.setRemoteAdapter(mWidgetId, R.id.message_list, intent); 405 406 setupTitleAndCount(views); 407 408 // Set up "new" button (compose new message) and "next view" button 409 setActivityIntent(views, R.id.widget_compose, MessageCompose.class); 410 setCommandIntent(views, R.id.widget_logo, COMMAND_URI_SWITCH_LIST_VIEW); 411 412 // Use a bare intent for our template; we need to fill everything in 413 intent = new Intent(sContext, WidgetService.class); 414 PendingIntent pendingIntent = 415 PendingIntent.getService(sContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 416 views.setPendingIntentTemplate(R.id.message_list, pendingIntent); 417 418 // And finally update the widget 419 sWidgetManager.updateAppWidget(mWidgetId, views); 420 } 421 422 /** 423 * Add size and color styling to text 424 * 425 * @param text the text to style 426 * @param size the font size for this text 427 * @param color the color for this text 428 * @return a CharSequence quitable for use in RemoteViews.setTextViewText() 429 */ 430 private CharSequence addStyle(CharSequence text, int size, int color) { 431 SpannableStringBuilder builder = new SpannableStringBuilder(text); 432 builder.setSpan( 433 new AbsoluteSizeSpan(size), 0, text.length(), 434 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 435 if (color != 0) { 436 builder.setSpan(new ForegroundColorSpan(color), 0, text.length(), 437 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 438 } 439 return builder; 440 } 441 442 /** 443 * Create styled text for our combination subject and snippet 444 * 445 * @param subject the message's subject (or null) 446 * @param snippet the message's snippet (or null) 447 * @param read whether or not the message is read 448 * @return a CharSequence suitable for use in RemoteViews.setTextViewText() 449 */ 450 private CharSequence getStyledSubjectSnippet (String subject, String snippet, 451 boolean read) { 452 SpannableStringBuilder ssb = new SpannableStringBuilder(); 453 boolean hasSubject = false; 454 if (!TextUtils.isEmpty(subject)) { 455 SpannableString ss = new SpannableString(subject); 456 ss.setSpan(new StyleSpan(read ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(), 457 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 458 ss.setSpan(new ForegroundColorSpan(sDefaultTextColor), 0, ss.length(), 459 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 460 ssb.append(ss); 461 hasSubject = true; 462 } 463 if (!TextUtils.isEmpty(snippet)) { 464 if (hasSubject) { 465 ssb.append(sSubjectSnippetDivider); 466 } 467 SpannableString ss = new SpannableString(snippet); 468 ss.setSpan(new ForegroundColorSpan(sLightTextColor), 0, snippet.length(), 469 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 470 ssb.append(ss); 471 } 472 return addStyle(ssb, sSubjectFontSize, 0); 473 } 474 475 /* (non-Javadoc) 476 * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int) 477 */ 478 public RemoteViews getViewAt(int position) { 479 // Use the cursor to set up the widget 480 synchronized (mCursorLock) { 481 if (mCursor == null || mCursor.isClosed() || !mCursor.moveToPosition(position)) { 482 return getLoadingView(); 483 } 484 RemoteViews views = 485 new RemoteViews(sContext.getPackageName(), R.layout.widget_list_item); 486 boolean isUnread = mCursor.getInt(WIDGET_COLUMN_FLAG_READ) != 1; 487 int drawableId = R.drawable.widget_read_conversation_selector; 488 if (isUnread) { 489 drawableId = R.drawable.widget_unread_conversation_selector; 490 } 491 views.setInt(R.id.widget_message, "setBackgroundResource", drawableId); 492 493 // Add style to sender 494 SpannableStringBuilder from = 495 new SpannableStringBuilder(mCursor.getString(WIDGET_COLUMN_DISPLAY_NAME)); 496 from.setSpan( 497 isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0, 498 from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 499 CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor); 500 views.setTextViewText(R.id.widget_from, styledFrom); 501 502 long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP); 503 // Get a nicely formatted date string (relative to today) 504 String date = DateUtils.getRelativeTimeSpanString(sContext, timestamp).toString(); 505 // Add style to date 506 CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor); 507 views.setTextViewText(R.id.widget_date, styledDate); 508 509 // Add style to subject/snippet 510 String subject = mCursor.getString(WIDGET_COLUMN_SUBJECT); 511 String snippet = mCursor.getString(WIDGET_COLUMN_SNIPPET); 512 CharSequence subjectAndSnippet = 513 getStyledSubjectSnippet(subject, snippet, !isUnread); 514 views.setTextViewText(R.id.widget_subject, subjectAndSnippet); 515 516 int messageFlags = mCursor.getInt(WIDGET_COLUMN_FLAGS); 517 boolean hasInvite = (messageFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; 518 views.setViewVisibility(R.id.widget_invite, hasInvite ? View.VISIBLE : View.GONE); 519 520 boolean hasAttachment = mCursor.getInt(WIDGET_COLUMN_FLAG_ATTACHMENT) != 0; 521 views.setViewVisibility(R.id.widget_attachment, 522 hasAttachment ? View.VISIBLE : View.GONE); 523 524 if (mViewType == ViewType.ACCOUNT) { 525 views.setViewVisibility(R.id.color_chip, View.INVISIBLE); 526 } else { 527 long accountId = mCursor.getLong(WIDGET_COLUMN_ACCOUNT_KEY); 528 int colorId = mResourceHelper.getAccountColorId(accountId); 529 if (colorId != ResourceHelper.UNDEFINED_RESOURCE_ID) { 530 // Color defined by resource ID, so, use it 531 views.setViewVisibility(R.id.color_chip, View.VISIBLE); 532 views.setImageViewResource(R.id.color_chip, colorId); 533 } else { 534 // Color not defined by resource ID, nothing we can do, so, hide the chip 535 views.setViewVisibility(R.id.color_chip, View.INVISIBLE); 536 } 537 } 538 539 // Set button intents for view, reply, and delete 540 String messageId = mCursor.getString(WIDGET_COLUMN_ID); 541 String mailboxId = mCursor.getString(WIDGET_COLUMN_MAILBOX_KEY); 542 setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE, messageId, 543 mailboxId); 544 545 return views; 546 } 547 } 548 549 @Override 550 public int getCount() { 551 if (mCursor == null) return 0; 552 return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT); 553 } 554 555 @Override 556 public long getItemId(int position) { 557 return position; 558 } 559 560 @Override 561 public RemoteViews getLoadingView() { 562 RemoteViews view = new RemoteViews(sContext.getPackageName(), R.layout.widget_loading); 563 view.setTextViewText(R.id.loading_text, sContext.getString(R.string.widget_loading)); 564 return view; 565 } 566 567 @Override 568 public int getViewTypeCount() { 569 // Regular list view and the "loading" view 570 return 2; 571 } 572 573 @Override 574 public boolean hasStableIds() { 575 return true; 576 } 577 578 @Override 579 public void onDataSetChanged() { 580 } 581 582 private void onDeleted() { 583 if (mLoader != null) { 584 mLoader.stopLoading(); 585 } 586 sWidgetMap.remove(mWidgetId); 587 } 588 589 @Override 590 public void onDestroy() { 591 if (mLoader != null) { 592 mLoader.stopLoading(); 593 } 594 sWidgetMap.remove(mWidgetId); 595 } 596 597 @Override 598 public void onCreate() { 599 } 600 } 601 602 private static synchronized void update(Context context, int[] appWidgetIds) { 603 for (int widgetId: appWidgetIds) { 604 getOrCreateWidget(context, widgetId).updateHeader(); 605 } 606 } 607 608 /** 609 * Force a context for widgets (used by unit tests) 610 * @param context the Context to set 611 */ 612 /*package*/ static void setContextForTest(Context context) { 613 sContext = context; 614 sResolver = context.getContentResolver(); 615 sWidgetManager = AppWidgetManager.getInstance(context); 616 } 617 618 /*package*/ static EmailWidget getOrCreateWidget(Context context, int widgetId) { 619 // Lazily initialize these 620 if (sContext == null) { 621 sContext = context.getApplicationContext(); 622 sWidgetManager = AppWidgetManager.getInstance(context); 623 sResolver = context.getContentResolver(); 624 } 625 EmailWidget widget = sWidgetMap.get(widgetId); 626 if (widget == null) { 627 if (Email.DEBUG) { 628 Log.d(TAG, "Creating EmailWidget for id #" + widgetId); 629 } 630 widget = new EmailWidget(widgetId); 631 widget.init(); 632 sWidgetMap.put(widgetId, widget); 633 } 634 return widget; 635 } 636 637 @Override 638 public void onDisabled(Context context) { 639 super.onDisabled(context); 640 if (Email.DEBUG) { 641 Log.d(TAG, "onDisabled"); 642 } 643 context.stopService(new Intent(context, WidgetService.class)); 644 } 645 646 @Override 647 public void onEnabled(final Context context) { 648 super.onEnabled(context); 649 if (Email.DEBUG) { 650 Log.d(TAG, "onEnabled"); 651 } 652 } 653 654 @Override 655 public void onReceive(final Context context, Intent intent) { 656 String action = intent.getAction(); 657 if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { 658 Bundle extras = intent.getExtras(); 659 if (extras != null) { 660 final int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); 661 if (appWidgetIds != null && appWidgetIds.length > 0) { 662 update(context, appWidgetIds); 663 } 664 } 665 } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { 666 Bundle extras = intent.getExtras(); 667 if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) { 668 final int widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); 669 // Find the widget in the map 670 EmailWidget widget = sWidgetMap.get(widgetId); 671 if (widget != null) { 672 // Stop loading and remove the widget from the map 673 widget.onDeleted(); 674 } 675 } 676 } 677 } 678 679 /** 680 * Utility class to handle switching widget views; in the background, we access the database 681 * to determine account status, etc. In the foreground, we start up the Loader with new 682 * parameters 683 */ 684 /*package*/ static class WidgetViewSwitcher extends AsyncTask<Void, Void, Void> { 685 private final EmailWidget mWidget; 686 private boolean mLoadAfterSwitch = true; 687 688 public WidgetViewSwitcher(EmailWidget widget) { 689 mWidget = widget; 690 } 691 692 /*package*/ void disableLoadAfterSwitchForTest() { 693 mLoadAfterSwitch = false; 694 } 695 696 @Override 697 protected Void doInBackground(Void... params) { 698 mWidget.switchToNextView(); 699 return null; 700 } 701 702 @Override 703 protected void onPostExecute(Void param) { 704 if (isCancelled()) { 705 return; 706 } 707 if (mLoadAfterSwitch) { 708 mWidget.loadView(); 709 } 710 } 711 } 712 713 /** 714 * We use the WidgetService for two purposes: 715 * 1) To provide a widget factory for RemoteViews, and 716 * 2) To process our command Uri's (i.e. take actions on user clicks) 717 */ 718 public static class WidgetService extends RemoteViewsService { 719 @Override 720 public RemoteViewsFactory onGetViewFactory(Intent intent) { 721 // Which widget do we want (nice alliteration, huh?) 722 int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); 723 if (widgetId == -1) return null; 724 // Find the existing widget or create it 725 return getOrCreateWidget(this, widgetId); 726 } 727 728 @Override 729 public void startActivity(Intent intent) { 730 // Since we're not calling startActivity from an Activity, we need the new task flag 731 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 732 super.startActivity(intent); 733 } 734 735 @Override 736 public int onStartCommand(Intent intent, int flags, int startId) { 737 Uri data = intent.getData(); 738 if (data == null) return Service.START_NOT_STICKY; 739 List<String> pathSegments = data.getPathSegments(); 740 // Our path segments are <command>, <arg1> [, <arg2>] 741 // First, a quick check of Uri validity 742 if (pathSegments.size() < 2) { 743 throw new IllegalArgumentException(); 744 } 745 String command = pathSegments.get(0); 746 // Ignore unknown action names 747 try { 748 final long arg1 = Long.parseLong(pathSegments.get(1)); 749 if (COMMAND_NAME_VIEW_MESSAGE.equals(command)) { 750 // "view", <message id>, <mailbox id> 751 final long mailboxId = Long.parseLong(pathSegments.get(2)); 752 final long messageId = arg1; 753 Utility.runAsync(new Runnable() { 754 @Override 755 public void run() { 756 openMessage(mailboxId, messageId); 757 } 758 }); 759 } else if (COMMAND_NAME_SWITCH_LIST_VIEW.equals(command)) { 760 // "next_view", <widget id> 761 EmailWidget widget = sWidgetMap.get((int)arg1); 762 if (widget != null) { 763 WidgetViewSwitcher switcher = new WidgetViewSwitcher(widget); 764 switcher.execute(); 765 } 766 } 767 } catch (NumberFormatException e) { 768 // Shouldn't happen as we construct all of the Uri's 769 } 770 return Service.START_NOT_STICKY; 771 } 772 773 private void openMessage(long mailboxId, long messageId) { 774 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId); 775 if (mailbox == null) return; 776 startActivity(Welcome.createOpenMessageIntent(this, mailbox.mAccountKey, mailboxId, 777 messageId)); 778 } 779 } 780} 781