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