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