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