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