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