• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.widget;
18 
19 import com.android.email.Email;
20 import com.android.email.R;
21 import com.android.email.ResourceHelper;
22 import com.android.email.activity.MessageCompose;
23 import com.android.email.activity.UiUtilities;
24 import com.android.email.activity.Welcome;
25 import com.android.email.provider.WidgetProvider.WidgetService;
26 import com.android.emailcommon.Logging;
27 import com.android.emailcommon.provider.Account;
28 import com.android.emailcommon.provider.EmailContent.Message;
29 import com.android.emailcommon.provider.Mailbox;
30 import com.android.emailcommon.utility.EmailAsyncTask;
31 
32 import android.app.PendingIntent;
33 import android.appwidget.AppWidgetManager;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.Loader;
37 import android.content.Loader.OnLoadCompleteListener;
38 import android.content.res.Resources;
39 import android.database.Cursor;
40 import android.graphics.Typeface;
41 import android.net.Uri;
42 import android.net.Uri.Builder;
43 import android.text.Spannable;
44 import android.text.SpannableString;
45 import android.text.SpannableStringBuilder;
46 import android.text.TextUtils;
47 import android.text.format.DateUtils;
48 import android.text.style.AbsoluteSizeSpan;
49 import android.text.style.ForegroundColorSpan;
50 import android.text.style.StyleSpan;
51 import android.util.Log;
52 import android.view.View;
53 import android.widget.RemoteViews;
54 import android.widget.RemoteViewsService;
55 
56 import java.util.List;
57 
58 /**
59  * The email widget.
60  * <p><em>NOTE</em>: All methods must be called on the UI thread so synchronization is NOT required
61  * in this class)
62  */
63 public class EmailWidget implements RemoteViewsService.RemoteViewsFactory,
64         OnLoadCompleteListener<Cursor> {
65     public static final String TAG = "EmailWidget";
66 
67     /**
68      * When handling clicks in a widget ListView, a single PendingIntent template is provided to
69      * RemoteViews, and the individual "on click" actions are distinguished via a "fillInIntent"
70      * on each list element; when a click is received, this "fillInIntent" is merged with the
71      * PendingIntent using Intent.fillIn().  Since this mechanism does NOT preserve the Extras
72      * Bundle, we instead encode information about the action (e.g. view, reply, etc.) and its
73      * arguments (e.g. messageId, mailboxId, etc.) in an Uri which is added to the Intent via
74      * Intent.setDataAndType()
75      *
76      * The mime type MUST be set in the Intent, even though we do not use it; therefore, it's value
77      * is entirely arbitrary.
78      *
79      * Our "command" Uri is NOT used by the system in any manner, and is therefore constrained only
80      * in the requirement that it be syntactically valid.
81      *
82      * We use the following convention for our commands:
83      *     widget://command/<command>/<arg1>[/<arg2>]
84      */
85     private static final String WIDGET_DATA_MIME_TYPE = "com.android.email/widget_data";
86 
87     private static final Uri COMMAND_URI = Uri.parse("widget://command");
88 
89     // Command names and Uri's built upon COMMAND_URI
90     private static final String COMMAND_NAME_VIEW_MESSAGE = "view_message";
91     private static final Uri COMMAND_URI_VIEW_MESSAGE =
92             COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build();
93 
94     // TODO Can this be moved to the loader and made a database 'LIMIT'?
95     private static final int MAX_MESSAGE_LIST_COUNT = 25;
96 
97     private static String sSubjectSnippetDivider;
98     @SuppressWarnings("unused")
99     private static String sConfigureText;
100     private static int sSenderFontSize;
101     private static int sSubjectFontSize;
102     private static int sDateFontSize;
103     private static int sDefaultTextColor;
104     private static int sLightTextColor;
105 
106     private final Context mContext;
107     private final AppWidgetManager mWidgetManager;
108 
109     // The widget identifier
110     private final int mWidgetId;
111 
112     // The widget's loader (derived from ThrottlingCursorLoader)
113     private final EmailWidgetLoader mLoader;
114     private final ResourceHelper mResourceHelper;
115 
116     /** The account ID of this widget. May be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. */
117     private long mAccountId = Account.NO_ACCOUNT;
118     /** The display name of this account */
119     private String mAccountName;
120     /** The display name of this mailbox */
121     private String mMailboxName;
122 
123     /**
124      * The cursor for the messages, with some extra info such as the number of accounts.
125      *
126      * Note this cursor can be closed any time by the loader.  Always use {@link #isCursorValid()}
127      * before touching its contents.
128      */
129     private EmailWidgetLoader.WidgetCursor mCursor;
130 
EmailWidget(Context context, int _widgetId)131     public EmailWidget(Context context, int _widgetId) {
132         super();
133         if (Email.DEBUG) {
134             Log.d(TAG, "Creating EmailWidget with id = " + _widgetId);
135         }
136         mContext = context.getApplicationContext();
137         mWidgetManager = AppWidgetManager.getInstance(mContext);
138 
139         mWidgetId = _widgetId;
140         mLoader = new EmailWidgetLoader(mContext);
141         mLoader.registerListener(0, this);
142         if (sSubjectSnippetDivider == null) {
143             // Initialize string, color, dimension resources
144             Resources res = mContext.getResources();
145             sSubjectSnippetDivider =
146                 res.getString(R.string.message_list_subject_snippet_divider);
147             sSenderFontSize = res.getDimensionPixelSize(R.dimen.widget_senders_font_size);
148             sSubjectFontSize = res.getDimensionPixelSize(R.dimen.widget_subject_font_size);
149             sDateFontSize = res.getDimensionPixelSize(R.dimen.widget_date_font_size);
150             sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
151             sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
152             sLightTextColor = res.getColor(R.color.widget_light_text_color);
153             sConfigureText =  res.getString(R.string.widget_other_views);
154         }
155         mResourceHelper = ResourceHelper.getInstance(mContext);
156     }
157 
158     /**
159      * Start loading the data.  At this point nothing on the widget changes -- the current view
160      * will remain valid until the loader loads the latest data.
161      */
start()162     public void start() {
163         long accountId = WidgetManager.loadAccountIdPref(mContext, mWidgetId);
164         long mailboxId = WidgetManager.loadMailboxIdPref(mContext, mWidgetId);
165         // Legacy support; if preferences haven't been saved for this widget, load something
166         if (accountId == Account.NO_ACCOUNT) {
167             accountId = Account.ACCOUNT_ID_COMBINED_VIEW;
168             mailboxId = Mailbox.QUERY_ALL_INBOXES;
169         }
170         mAccountId = accountId;
171         mLoader.load(mAccountId, mailboxId);
172     }
173 
isCursorValid()174     private boolean isCursorValid() {
175         return mCursor != null && !mCursor.isClosed();
176     }
177 
178     /**
179      * Called when the loader finished loading data.  Update the widget.
180      */
181     @Override
onLoadComplete(Loader<Cursor> loader, Cursor cursor)182     public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
183         mCursor = (EmailWidgetLoader.WidgetCursor) cursor;   // Save away the cursor
184         mAccountName = mCursor.getAccountName();
185         mMailboxName = mCursor.getMailboxName();
186         updateHeader();
187         mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
188     }
189 
190     /**
191      * Convenience method for creating an onClickPendingIntent that launches another activity
192      * directly.
193      *
194      * @param views The RemoteViews we're inflating
195      * @param buttonId the id of the button view
196      * @param intent The intent to be used when launching the activity
197      */
setActivityIntent(RemoteViews views, int buttonId, Intent intent)198     private void setActivityIntent(RemoteViews views, int buttonId, Intent intent) {
199         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // just in case intent comes without it
200         PendingIntent pendingIntent =
201                 PendingIntent.getActivity(mContext, (int) mAccountId, intent,
202                         PendingIntent.FLAG_UPDATE_CURRENT);
203         views.setOnClickPendingIntent(buttonId, pendingIntent);
204     }
205 
206     /**
207      * Convenience method for constructing a fillInIntent for a given list view element.
208      * Appends the command and any arguments to a base Uri.
209      *
210      * @param views the RemoteViews we are inflating
211      * @param viewId the id of the view
212      * @param baseUri the base uri for the command
213      * @param args any arguments to the command
214      */
setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args)215     private void setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args) {
216         Intent intent = new Intent();
217         Builder builder = baseUri.buildUpon();
218         for (String arg: args) {
219             builder.appendPath(arg);
220         }
221         intent.setDataAndType(builder.build(), WIDGET_DATA_MIME_TYPE);
222         views.setOnClickFillInIntent(viewId, intent);
223     }
224 
225     /**
226      * Called back by {@link com.android.email.provider.WidgetProvider.WidgetService} to
227      * handle intents created by remote views.
228      */
processIntent(Context context, Intent intent)229     public static boolean processIntent(Context context, Intent intent) {
230         final Uri data = intent.getData();
231         if (data == null) {
232             return false;
233         }
234         List<String> pathSegments = data.getPathSegments();
235         // Our path segments are <command>, <arg1> [, <arg2>]
236         // First, a quick check of Uri validity
237         if (pathSegments.size() < 2) {
238             throw new IllegalArgumentException();
239         }
240         String command = pathSegments.get(0);
241         // Ignore unknown action names
242         try {
243             final long arg1 = Long.parseLong(pathSegments.get(1));
244             if (EmailWidget.COMMAND_NAME_VIEW_MESSAGE.equals(command)) {
245                 // "view", <message id>, <mailbox id>
246                 openMessage(context, Long.parseLong(pathSegments.get(2)), arg1);
247             }
248         } catch (NumberFormatException e) {
249             // Shouldn't happen as we construct all of the Uri's
250             return false;
251         }
252         return true;
253     }
254 
openMessage(final Context context, final long mailboxId, final long messageId)255     private static void openMessage(final Context context, final long mailboxId,
256             final long messageId) {
257         EmailAsyncTask.runAsyncParallel(new Runnable() {
258             @Override
259             public void run() {
260                 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
261                 if (mailbox == null) return;
262                 context.startActivity(Welcome.createOpenMessageIntent(context, mailbox.mAccountKey,
263                         mailboxId, messageId));
264             }
265         });
266     }
267 
setupTitleAndCount(RemoteViews views)268     private void setupTitleAndCount(RemoteViews views) {
269         // Set up the title (view type + count of messages)
270         views.setTextViewText(R.id.widget_title, mMailboxName);
271         // TODO Temporary UX; need to make this visible and create the correct UX
272         //views.setTextViewText(R.id.widget_tap, sConfigureText);
273         views.setViewVisibility(R.id.widget_tap, View.VISIBLE);
274         views.setTextViewText(R.id.widget_tap, mAccountName);
275         String count = "";
276         if (isCursorValid()) {
277             count = UiUtilities.getMessageCountForUi(mContext, mCursor.getMessageCount(), false);
278         }
279         views.setTextViewText(R.id.widget_count, count);
280     }
281 
282     /**
283      * Update the "header" of the widget (i.e. everything that doesn't include the scrolling
284      * message list)
285      */
updateHeader()286     private void updateHeader() {
287         if (Email.DEBUG) {
288             Log.d(TAG, "#updateHeader(); widgetId: " + mWidgetId);
289         }
290 
291         // Get the widget layout
292         RemoteViews views =
293                 new RemoteViews(mContext.getPackageName(), R.layout.widget);
294 
295         // Set up the list with an adapter
296         Intent intent = new Intent(mContext, WidgetService.class);
297         intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId);
298         intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
299         views.setRemoteAdapter(R.id.message_list, intent);
300 
301         setupTitleAndCount(views);
302 
303         if (isCursorValid()) {
304             // Show compose icon & message list
305             if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
306                 // Don't allow compose for "combined" view
307                 views.setViewVisibility(R.id.widget_compose, View.INVISIBLE);
308             } else {
309                 views.setViewVisibility(R.id.widget_compose, View.VISIBLE);
310             }
311             views.setViewVisibility(R.id.message_list, View.VISIBLE);
312             views.setViewVisibility(R.id.tap_to_configure, View.GONE);
313             // Create click intent for "compose email" target
314             intent = MessageCompose.getMessageComposeIntent(mContext, mAccountId);
315             setActivityIntent(views, R.id.widget_compose, intent);
316             // Create click intent for logo to open inbox
317             intent = Welcome.createOpenAccountInboxIntent(mContext, mAccountId);
318             setActivityIntent(views, R.id.widget_logo, intent);
319         } else {
320             // TODO This really should never happen ... probably can remove the else block
321             // Hide compose icon & show "touch to configure" text
322             views.setViewVisibility(R.id.widget_compose, View.INVISIBLE);
323             views.setViewVisibility(R.id.message_list, View.GONE);
324             views.setViewVisibility(R.id.tap_to_configure, View.VISIBLE);
325             // Create click intent for "touch to configure" target
326             intent = Welcome.createOpenAccountInboxIntent(mContext, -1);
327             setActivityIntent(views, R.id.tap_to_configure, intent);
328         }
329 
330         // Use a bare intent for our template; we need to fill everything in
331         intent = new Intent(mContext, WidgetService.class);
332         PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, intent,
333                 PendingIntent.FLAG_UPDATE_CURRENT);
334         views.setPendingIntentTemplate(R.id.message_list, pendingIntent);
335 
336         // And finally update the widget
337         mWidgetManager.updateAppWidget(mWidgetId, views);
338     }
339 
340     /**
341      * Add size and color styling to text
342      *
343      * @param text the text to style
344      * @param size the font size for this text
345      * @param color the color for this text
346      * @return a CharSequence quitable for use in RemoteViews.setTextViewText()
347      */
addStyle(CharSequence text, int size, int color)348     private CharSequence addStyle(CharSequence text, int size, int color) {
349         SpannableStringBuilder builder = new SpannableStringBuilder(text);
350         builder.setSpan(
351                 new AbsoluteSizeSpan(size), 0, text.length(),
352                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
353         if (color != 0) {
354             builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
355                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
356         }
357         return builder;
358     }
359 
360     /**
361      * Create styled text for our combination subject and snippet
362      *
363      * @param subject the message's subject (or null)
364      * @param snippet the message's snippet (or null)
365      * @param read whether or not the message is read
366      * @return a CharSequence suitable for use in RemoteViews.setTextViewText()
367      */
getStyledSubjectSnippet(String subject, String snippet, boolean read)368     private CharSequence getStyledSubjectSnippet(String subject, String snippet, boolean read) {
369         SpannableStringBuilder ssb = new SpannableStringBuilder();
370         boolean hasSubject = false;
371         if (!TextUtils.isEmpty(subject)) {
372             SpannableString ss = new SpannableString(subject);
373             ss.setSpan(new StyleSpan(read ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
374                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
375             ss.setSpan(new ForegroundColorSpan(sDefaultTextColor), 0, ss.length(),
376                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
377             ssb.append(ss);
378             hasSubject = true;
379         }
380         if (!TextUtils.isEmpty(snippet)) {
381             if (hasSubject) {
382                 ssb.append(sSubjectSnippetDivider);
383             }
384             SpannableString ss = new SpannableString(snippet);
385             ss.setSpan(new ForegroundColorSpan(sLightTextColor), 0, snippet.length(),
386                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
387             ssb.append(ss);
388         }
389         return addStyle(ssb, sSubjectFontSize, 0);
390     }
391 
392     @Override
getViewAt(int position)393     public RemoteViews getViewAt(int position) {
394         // Use the cursor to set up the widget
395         if (!isCursorValid() || !mCursor.moveToPosition(position)) {
396             return getLoadingView();
397         }
398         RemoteViews views =
399             new RemoteViews(mContext.getPackageName(), R.layout.widget_list_item);
400         boolean isUnread = mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAG_READ) != 1;
401         int drawableId = R.drawable.message_list_read_selector;
402         if (isUnread) {
403             drawableId = R.drawable.message_list_unread_selector;
404         }
405         views.setInt(R.id.widget_message, "setBackgroundResource", drawableId);
406 
407         // Add style to sender
408         String cursorString =
409                 mCursor.isNull(EmailWidgetLoader.WIDGET_COLUMN_DISPLAY_NAME)
410                     ? ""    // an empty string
411                     : mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_DISPLAY_NAME);
412         SpannableStringBuilder from = new SpannableStringBuilder(cursorString);
413         from.setSpan(
414                 isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0,
415                 from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
416         CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor);
417         views.setTextViewText(R.id.widget_from, styledFrom);
418 
419         long timestamp = mCursor.getLong(EmailWidgetLoader.WIDGET_COLUMN_TIMESTAMP);
420         // Get a nicely formatted date string (relative to today)
421         String date = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString();
422         // Add style to date
423         CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor);
424         views.setTextViewText(R.id.widget_date, styledDate);
425 
426         // Add style to subject/snippet
427         String subject = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_SUBJECT);
428         String snippet = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_SNIPPET);
429         CharSequence subjectAndSnippet = getStyledSubjectSnippet(subject, snippet, !isUnread);
430         views.setTextViewText(R.id.widget_subject, subjectAndSnippet);
431 
432         int messageFlags = mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAGS);
433         boolean hasInvite = (messageFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
434         views.setViewVisibility(R.id.widget_invite, hasInvite ? View.VISIBLE : View.GONE);
435 
436         boolean hasAttachment =
437                 mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAG_ATTACHMENT) != 0;
438         views.setViewVisibility(R.id.widget_attachment,
439                 hasAttachment ? View.VISIBLE : View.GONE);
440 
441         if (mAccountId != Account.ACCOUNT_ID_COMBINED_VIEW) {
442             views.setViewVisibility(R.id.color_chip, View.INVISIBLE);
443         } else {
444             long accountId = mCursor.getLong(EmailWidgetLoader.WIDGET_COLUMN_ACCOUNT_KEY);
445             int colorId = mResourceHelper.getAccountColorId(accountId);
446             if (colorId != ResourceHelper.UNDEFINED_RESOURCE_ID) {
447                 // Color defined by resource ID, so, use it
448                 views.setViewVisibility(R.id.color_chip, View.VISIBLE);
449                 views.setImageViewResource(R.id.color_chip, colorId);
450             } else {
451                 // Color not defined by resource ID, nothing we can do, so, hide the chip
452                 views.setViewVisibility(R.id.color_chip, View.INVISIBLE);
453             }
454         }
455 
456         // Set button intents for view, reply, and delete
457         String messageId = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_ID);
458         String mailboxId = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_MAILBOX_KEY);
459         setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE,
460                 messageId, mailboxId);
461 
462         return views;
463     }
464 
465     @Override
getCount()466     public int getCount() {
467         if (!isCursorValid()) return 0;
468         return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT);
469     }
470 
471     @Override
getItemId(int position)472     public long getItemId(int position) {
473         return position;
474     }
475 
476     @Override
getLoadingView()477     public RemoteViews getLoadingView() {
478         RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
479         view.setTextViewText(R.id.loading_text, mContext.getString(R.string.widget_loading));
480         return view;
481     }
482 
483     @Override
getViewTypeCount()484     public int getViewTypeCount() {
485         // Regular list view and the "loading" view
486         return 2;
487     }
488 
489     @Override
hasStableIds()490     public boolean hasStableIds() {
491         return true;
492     }
493 
494     @Override
onDataSetChanged()495     public void onDataSetChanged() {
496     }
497 
onDeleted()498     public void onDeleted() {
499         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
500             Log.d(TAG, "#onDeleted(); widgetId: " + mWidgetId);
501         }
502 
503         if (mLoader != null) {
504             mLoader.reset();
505         }
506     }
507 
508     @Override
onDestroy()509     public void onDestroy() {
510         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
511             Log.d(TAG, "#onDestroy(); widgetId: " + mWidgetId);
512         }
513 
514         if (mLoader != null) {
515             mLoader.reset();
516         }
517     }
518 
519     @Override
onCreate()520     public void onCreate() {
521         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
522             Log.d(TAG, "#onCreate(); widgetId: " + mWidgetId);
523         }
524     }
525 
526     @Override
toString()527     public String toString() {
528         return "View=" + mAccountName;
529     }
530 }
531