diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 651aaa01c..1cd69bcdc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,47 +14,70 @@
limitations under the License.
-->
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+ android:label="@string/account_shortcut_picker_name">
+
+
@@ -186,7 +215,8 @@
>
-
+
@@ -197,11 +227,16 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
@@ -290,7 +355,8 @@
android:enabled="true"
>
-
+
-
+
@@ -315,7 +382,8 @@
android:name="com.android.exchange.EmailSyncAdapterService"
android:exported="true">
-
+
@@ -326,7 +394,8 @@
android:name="com.android.exchange.ContactsSyncAdapterService"
android:exported="true">
-
+
@@ -337,7 +406,8 @@
android:name="com.android.exchange.CalendarSyncAdapterService"
android:exported="true">
-
+
@@ -357,7 +427,8 @@
android:enabled="true"
>
-
+
-
+
-
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable-hdpi/widget_bg.9.png b/res/drawable-hdpi/widget_bg.9.png
new file mode 100644
index 000000000..d9af8fbad
Binary files /dev/null and b/res/drawable-hdpi/widget_bg.9.png differ
diff --git a/res/drawable-hdpi/widget_bg_focus.9.png b/res/drawable-hdpi/widget_bg_focus.9.png
new file mode 100644
index 000000000..ee098af16
Binary files /dev/null and b/res/drawable-hdpi/widget_bg_focus.9.png differ
diff --git a/res/drawable-hdpi/widget_bg_press.9.png b/res/drawable-hdpi/widget_bg_press.9.png
new file mode 100644
index 000000000..03ca2a1f3
Binary files /dev/null and b/res/drawable-hdpi/widget_bg_press.9.png differ
diff --git a/res/drawable-hdpi/widget_bg_top.9.png b/res/drawable-hdpi/widget_bg_top.9.png
new file mode 100644
index 000000000..c55f80833
Binary files /dev/null and b/res/drawable-hdpi/widget_bg_top.9.png differ
diff --git a/res/drawable-mdpi/widget_bg.9.png b/res/drawable-mdpi/widget_bg.9.png
new file mode 100644
index 000000000..80491912d
Binary files /dev/null and b/res/drawable-mdpi/widget_bg.9.png differ
diff --git a/res/drawable-mdpi/widget_bg_focus.9.png b/res/drawable-mdpi/widget_bg_focus.9.png
new file mode 100644
index 000000000..f4bbb08eb
Binary files /dev/null and b/res/drawable-mdpi/widget_bg_focus.9.png differ
diff --git a/res/drawable-mdpi/widget_bg_press.9.png b/res/drawable-mdpi/widget_bg_press.9.png
new file mode 100644
index 000000000..d060b7755
Binary files /dev/null and b/res/drawable-mdpi/widget_bg_press.9.png differ
diff --git a/res/drawable-mdpi/widget_bg_top.9.png b/res/drawable-mdpi/widget_bg_top.9.png
new file mode 100644
index 000000000..af0f466c1
Binary files /dev/null and b/res/drawable-mdpi/widget_bg_top.9.png differ
diff --git a/res/drawable/widget_background.xml b/res/drawable/widget_background.xml
new file mode 100644
index 000000000..83f7a8b7b
--- /dev/null
+++ b/res/drawable/widget_background.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/widget.xml b/res/layout/widget.xml
new file mode 100644
index 000000000..2885ac079
--- /dev/null
+++ b/res/layout/widget.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/widget_list_item.xml b/res/layout/widget_list_item.xml
new file mode 100644
index 000000000..3332c1b6e
--- /dev/null
+++ b/res/layout/widget_list_item.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/widget_loading.xml b/res/layout/widget_loading.xml
new file mode 100644
index 000000000..a25e32de0
--- /dev/null
+++ b/res/layout/widget_loading.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0a3e88951..db5ef2b18 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1012,6 +1012,19 @@ save attachment.
%1$d of %2$s
+
+
+
+ Tap email icon for other views
+
+ All Mail
+
+ All Unread
+
+ All Starred
+
+ Loading\u2026
+
+
+
+
\ No newline at end of file
diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java
index 8ab1c6c2d..d485f0c25 100644
--- a/src/com/android/email/activity/MessageView.java
+++ b/src/com/android/email/activity/MessageView.java
@@ -62,13 +62,17 @@ public class MessageView extends MessageViewBase implements View.OnClickListener
* @param mailboxId identifies the sequence of messages used for newer/older navigation.
*/
public static void actionView(Context context, long messageId, long mailboxId) {
+ context.startActivity(getActionViewIntent(context, messageId, mailboxId));
+ }
+
+ public static Intent getActionViewIntent(Context context, long messageId, long mailboxId) {
if (messageId < 0) {
throw new IllegalArgumentException("MessageView invalid messageId " + messageId);
}
Intent i = new Intent(context, MessageView.class);
i.putExtra(EXTRA_MESSAGE_ID, messageId);
i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
- context.startActivity(i);
+ return i;
}
@Override
diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java
index 3ffbe822b..5e7ddf907 100644
--- a/src/com/android/email/provider/EmailContent.java
+++ b/src/com/android/email/provider/EmailContent.java
@@ -63,13 +63,16 @@ import java.util.UUID;
*/
public abstract class EmailContent {
public static final String AUTHORITY = EmailProvider.EMAIL_AUTHORITY;
+ public static final String NOTIFIER_AUTHORITY = EmailProvider.EMAIL_NOTIFIER_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
public static final String PARAMETER_LIMIT = "limit";
+ public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
+
// All classes share this
public static final String RECORD_ID = "_id";
- private static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
+ public static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
/**
* This projection can be used with any of the EmailContent classes, when all you need
@@ -448,7 +451,6 @@ public abstract class EmailContent {
public static final String DELETED_TABLE_NAME = "Message_Deletes";
// To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
- @SuppressWarnings("hiding")
public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message");
public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1);
public static final Uri SYNCED_CONTENT_URI =
@@ -457,6 +459,8 @@ public abstract class EmailContent {
Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage");
public static final Uri UPDATED_CONTENT_URI =
Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage");
+ public static final Uri NOTIFIER_URI =
+ Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message");
public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc";
@@ -925,7 +929,6 @@ public abstract class EmailContent {
public static final class Account extends EmailContent implements AccountColumns, Parcelable {
public static final String TABLE_NAME = "Account";
- @SuppressWarnings("hiding")
public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
public static final Uri ADD_TO_FIELD_URI =
Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField");
diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java
index 79559b358..f19f26481 100644
--- a/src/com/android/email/provider/EmailProvider.java
+++ b/src/com/android/email/provider/EmailProvider.java
@@ -37,6 +37,7 @@ import android.accounts.AccountManager;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
@@ -116,6 +117,10 @@ public class EmailProvider extends ContentProvider {
public static final int BODY_DATABASE_VERSION = 6;
public static final String EMAIL_AUTHORITY = "com.android.email.provider";
+ // The notifier authority is used to send notifications regarding changes to messages (insert,
+ // delete, or update) and is intended as an optimization for use by clients of message list
+ // cursors (initially, the email AppWidget).
+ public static final String EMAIL_NOTIFIER_AUTHORITY = "com.android.email.notifier";
private static final int ACCOUNT_BASE = 0;
private static final int ACCOUNT = ACCOUNT_BASE;
@@ -911,6 +916,7 @@ public class EmailProvider extends ContentProvider {
int table = match >> BASE_SHIFT;
String id = "0";
boolean messageDeletion = false;
+ ContentResolver resolver = context.getContentResolver();
if (Email.LOGD) {
Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match);
@@ -940,6 +946,7 @@ public class EmailProvider extends ContentProvider {
// Bodies are auto-deleted here; Attachments are auto-deleted via trigger
messageDeletion = true;
db.beginTransaction();
+ resolver.notifyChange(Message.NOTIFIER_URI, null);
break;
}
switch (match) {
@@ -1045,7 +1052,7 @@ public class EmailProvider extends ContentProvider {
}
// Notify all existing cursors.
- getContext().getContentResolver().notifyChange(EmailContent.CONTENT_URI, null);
+ resolver.notifyChange(EmailContent.CONTENT_URI, null);
return result;
}
@@ -1101,6 +1108,8 @@ public class EmailProvider extends ContentProvider {
if (Email.DEBUG_THREAD_CHECK) Email.warnIfUiThread();
int match = sURIMatcher.match(uri);
Context context = getContext();
+ ContentResolver resolver = context.getContentResolver();
+
// See the comment at delete(), above
SQLiteDatabase db = getDatabase(context);
int table = match >> BASE_SHIFT;
@@ -1121,10 +1130,12 @@ public class EmailProvider extends ContentProvider {
try {
switch (match) {
+ case MESSAGE:
+ resolver.notifyChange(Message.NOTIFIER_URI, null);
+ //$FALL-THROUGH$
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case BODY:
- case MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
@@ -1175,7 +1186,7 @@ public class EmailProvider extends ContentProvider {
}
// Notify all existing cursors.
- getContext().getContentResolver().notifyChange(EmailContent.CONTENT_URI, null);
+ resolver.notifyChange(EmailContent.CONTENT_URI, null);
return resultUri;
}
@@ -1360,6 +1371,7 @@ public class EmailProvider extends ContentProvider {
int match = sURIMatcher.match(uri);
Context context = getContext();
+ ContentResolver resolver = context.getContentResolver();
// See the comment at delete(), above
SQLiteDatabase db = getDatabase(context);
int table = match >> BASE_SHIFT;
@@ -1411,10 +1423,12 @@ public class EmailProvider extends ContentProvider {
db.setTransactionSuccessful();
db.endTransaction();
break;
- case BODY_ID:
- case MESSAGE_ID:
case SYNCED_MESSAGE_ID:
+ resolver.notifyChange(Message.NOTIFIER_URI, null);
+ //$FALL-THROUGH$
case UPDATED_MESSAGE_ID:
+ case MESSAGE_ID:
+ case BODY_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
@@ -1490,7 +1504,7 @@ public class EmailProvider extends ContentProvider {
throw e;
}
- context.getContentResolver().notifyChange(notificationUri, null);
+ resolver.notifyChange(notificationUri, null);
return result;
}
diff --git a/src/com/android/email/provider/WidgetProvider.java b/src/com/android/email/provider/WidgetProvider.java
new file mode 100644
index 000000000..653edb23f
--- /dev/null
+++ b/src/com/android/email/provider/WidgetProvider.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.provider;
+
+import com.android.email.Email;
+import com.android.email.R;
+import com.android.email.activity.MessageCompose;
+import com.android.email.activity.MessageView;
+import com.android.email.data.ThrottlingCursorLoader;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.Paint.Align;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.format.DateUtils;
+import android.text.style.StyleSpan;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class WidgetProvider extends AppWidgetProvider {
+ private static final String TAG = "WidgetProvider";
+
+ /**
+ * When handling clicks in a widget ListView, a single PendingIntent template is provided to
+ * RemoteViews, and the individual "on click" actions are distinguished via a "fillInIntent"
+ * on each list element; when a click is received, this "fillInIntent" is merged with the
+ * PendingIntent using Intent.fillIn(). Since this mechanism does NOT preserve the Extras
+ * Bundle, we instead encode information about the action (e.g. view, reply, etc.) and its
+ * arguments (e.g. messageId, mailboxId, etc.) in an Uri which is added to the Intent via
+ * Intent.setDataAndType()
+ *
+ * The mime type MUST be set in the Intent, even though we do not use it; therefore, it's value
+ * is entirely arbitrary.
+ *
+ * Our "command" Uri is NOT used by the system in any manner, and is therefore constrained only
+ * in the requirement that it be syntactically valid.
+ *
+ * We use the following convention for our commands:
+ * widget://command//[/]
+ */
+ private static final String WIDGET_DATA_MIME_TYPE = "com.android.email/widget_data";
+ private static final Uri COMMAND_URI = Uri.parse("widget://command");
+
+ // Command names and Uri's built upon COMMAND_URI
+ private static final String COMMAND_NAME_SWITCH_LIST_VIEW = "switch_list_view";
+ private static final Uri COMMAND_URI_SWITCH_LIST_VIEW =
+ COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_SWITCH_LIST_VIEW).build();
+ private static final String COMMAND_NAME_VIEW_MESSAGE = "view_message";
+ private static final Uri COMMAND_URI_VIEW_MESSAGE =
+ COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build();
+
+ private static final int TOTAL_COUNT_UNKNOWN = -1;
+ private static final int MAX_MESSAGE_LIST_COUNT = 25;
+
+ private static final String SORT_DESCENDING = MessageColumns.TIMESTAMP + " DESC";
+
+ // Map holding our instantiated widgets, accessed by widget id
+ private static HashMap sWidgetMap = new HashMap();
+ private static AppWidgetManager sWidgetManager;
+ private static Context sContext;
+ private static ContentResolver sResolver;
+ private static TextPaint sDatePaint = new TextPaint();
+
+ /**
+ * Types of views that we're prepared to show in the widget - all mail, unread mail, and starred
+ * mail; we rotate between them. Each ViewType is composed of a selection string and a title.
+ */
+ public enum ViewType {
+ ALL_MAIL(null, R.string.widget_all_mail),
+ UNREAD(MessageColumns.FLAG_READ + "=0", R.string.widget_unread),
+ STARRED(MessageColumns.FLAG_FAVORITE + "=1", R.string.widget_starred);
+
+ private final String selection;
+ private final int titleResource;
+ private String title;
+
+ ViewType(String _selection, int _titleResource) {
+ selection = _selection;
+ titleResource = _titleResource;
+ }
+
+ public String getTitle(Context context) {
+ if (title == null) {
+ title = context.getString(titleResource);
+ }
+ return title;
+ }
+ }
+
+ static class EmailWidget implements RemoteViewsService.RemoteViewsFactory {
+ // The widget identifier
+ private final int mWidgetId;
+
+ // The cursor underlying the message list for this widget; this must only be modified while
+ // holding mCursorLock
+ private volatile Cursor mCursor;
+ // A lock on our cursor, which is used in the UI thread while inflating views, and by
+ // our Loader in the background
+ private final Object mCursorLock = new Object();
+ // Number of records in the cursor
+ private int mCursorCount = TOTAL_COUNT_UNKNOWN;
+ // The widget's loader (derived from ThrottlingCursorLoader)
+ private WidgetLoader mLoader;
+
+ // The current view type (all mail, unread, or starred for now)
+ private ViewType mViewType = ViewType.ALL_MAIL;
+
+ // The projection to be used by the WidgetLoader
+ public static final String[] WIDGET_PROJECTION = new String[] {
+ EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
+ MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE,
+ MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET,
+ MessageColumns.ACCOUNT_KEY
+ };
+ public static final int WIDGET_COLUMN_ID = 0;
+ public static final int WIDGET_COLUMN_DISPLAY_NAME = 1;
+ public static final int WIDGET_COLUMN_TIMESTAMP = 2;
+ public static final int WIDGET_COLUMN_SUBJECT = 3;
+ public static final int WIDGET_COLUMN_FLAG_READ = 4;
+ public static final int WIDGET_COLUMN_FLAG_FAVORITE = 5;
+ public static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6;
+ public static final int WIDGET_COLUMN_MAILBOX_KEY = 7;
+ public static final int WIDGET_COLUMN_SNIPPET = 8;
+ public static final int WIDGET_COLUMN_ACCOUNT_KEY = 9;
+
+ public EmailWidget(int _widgetId) {
+ super();
+ if (Email.DEBUG) {
+ Log.d(TAG, "Creating EmailWidget with id = " + _widgetId);
+ }
+ mWidgetId = _widgetId;
+ mLoader = new WidgetLoader();
+ if (sDatePaint == null) {
+ sDatePaint = new TextPaint();
+ sDatePaint.setTypeface(Typeface.DEFAULT);
+ sDatePaint.setTextSize(14);
+ sDatePaint.setAntiAlias(true);
+ sDatePaint.setTextAlign(Align.RIGHT);
+ }
+ }
+
+ /**
+ * The ThrottlingCursorLoader does all of the heavy lifting in managing the data loading
+ * task; all we need is to register a listener so that we're notified when the load is
+ * complete.
+ */
+ final class WidgetLoader extends ThrottlingCursorLoader {
+ protected WidgetLoader() {
+ super(sContext, Message.CONTENT_URI, WIDGET_PROJECTION, mViewType.selection, null,
+ SORT_DESCENDING);
+ registerListener(0, new OnLoadCompleteListener() {
+ @Override
+ public void onLoadComplete(Loader loader, Cursor cursor) {
+ synchronized (mCursorLock) {
+ // Save away the cursor
+ mCursor = cursor;
+ // Reset the notification Uri to our Message table notifier URI
+ mCursor.setNotificationUri(sResolver, Message.NOTIFIER_URI);
+ // Save away the count (for display)
+ mCursorCount = mCursor.getCount();
+ if (Email.DEBUG) {
+ Log.d(TAG, "onLoadComplete, count = " + cursor.getCount());
+ }
+ }
+ RemoteViews views =
+ new RemoteViews(sContext.getPackageName(), R.layout.widget);
+ views.setTextViewText(R.id.widget_title,
+ mViewType.getTitle(sContext) + " (" + mCursorCount + ")");
+ sWidgetManager.partiallyUpdateAppWidget(mWidgetId, views);
+ sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
+ }
+ });
+ startLoading();
+ }
+
+ /**
+ * Convenience method that stops existing loading (if any), sets a (possibly new)
+ * selection criterion, and starts loading
+ *
+ * @param selection a valid query selection argument
+ */
+ void startLoadingWithSelection(String selection) {
+ stopLoading();
+ setSelection(selection);
+ startLoading();
+ }
+ }
+
+ /**
+ * Switch to the next widget view (cycles all -> unread -> starred)
+ */
+ public void switchToNextView() {
+ switch(mViewType) {
+ case ALL_MAIL:
+ mViewType = ViewType.UNREAD;
+ break;
+ case UNREAD:
+ mViewType = ViewType.STARRED;
+ break;
+ case STARRED:
+ mViewType = ViewType.ALL_MAIL;
+ break;
+ }
+ synchronized(mCursorLock) {
+ mCursorCount = TOTAL_COUNT_UNKNOWN;
+ invalidateCursorLocked();
+ mLoader.startLoadingWithSelection(mViewType.selection);
+ }
+ }
+
+ /**
+ * Invalidates the current cursor and tells the UI that the underlying data has changed.
+ * This method must be called while holding mCursorLock
+ */
+ private void invalidateCursorLocked() {
+ mCursor = null;
+ sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
+ }
+
+ private void setStyleSpan(SpannableString str, int typeface) {
+ int length = str.length();
+ str.setSpan(new StyleSpan(typeface), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private CharSequence formattedText(String str, int typeface) {
+ if (str == null) {
+ return "";
+ }
+ SpannableString ss = new SpannableString(str);
+ setStyleSpan(ss, typeface);
+ return ss;
+ }
+
+ private CharSequence formattedTextFromCursor(Cursor c, int column, int typeface) {
+ return formattedText(mCursor.getString(column), typeface);
+ }
+
+
+ /**
+ * Convenience method for creating an onClickPendingIntent that executes a command via
+ * our command Uri. Used for the "next view" command; appends the widget id to the command
+ * Uri.
+ *
+ * @param views The RemoteViews we're inflating
+ * @param buttonId the id of the button view
+ * @param data the command Uri
+ */
+ private void setCommandIntent(RemoteViews views, int buttonId, Uri data) {
+ Intent intent = new Intent(sContext, WidgetService.class);
+ intent.setDataAndType(ContentUris.withAppendedId(data, mWidgetId),
+ WIDGET_DATA_MIME_TYPE);
+ PendingIntent pendingIntent = PendingIntent.getService(sContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(buttonId, pendingIntent);
+ }
+
+ /**
+ * Convenience method for creating an onClickPendingIntent that launches another activity
+ * directly. Used for the "Compose" button
+ *
+ * @param views The RemoteViews we're inflating
+ * @param buttonId the id of the button view
+ * @param activityClass the class of the activity to be launched
+ */
+ private void setActivityIntent(RemoteViews views, int buttonId,
+ Class extends Activity> activityClass) {
+ Intent intent = new Intent(sContext, activityClass);
+ PendingIntent pendingIntent = PendingIntent.getActivity(sContext, 0, intent, 0);
+ views.setOnClickPendingIntent(buttonId, pendingIntent);
+ }
+
+ /**
+ * Convenience method for constructing a fillInIntent for a given list view element.
+ * Appends the command and any arguments to a base Uri.
+ *
+ * @param views the RemoteViews we are inflating
+ * @param viewId the id of the view
+ * @param baseUri the base uri for the command
+ * @param args any arguments to the command
+ */
+ private void setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args) {
+ Intent intent = new Intent();
+ Builder builder = baseUri.buildUpon();
+ for (String arg: args) {
+ builder.appendPath(arg);
+ }
+ intent.setDataAndType(builder.build(), WIDGET_DATA_MIME_TYPE);
+ views.setOnClickFillInIntent(viewId, intent);
+ }
+
+ /**
+ * Update the "header" of the widget (i.e. everything that doesn't include the scrolling
+ * message list)
+ */
+ private void updateHeader() {
+ if (Email.DEBUG) {
+ Log.d(TAG, "updateWidget " + mWidgetId);
+ }
+
+ // Get the widget layout
+ RemoteViews views = new RemoteViews(sContext.getPackageName(), R.layout.widget);
+
+ // Set up the list with an adapter
+ Intent intent = new Intent(sContext, WidgetService.class);
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId);
+ views.setRemoteAdapter(R.id.message_list, intent);
+
+ // Set up the title (view type + count of messages)
+ views.setTextViewText(R.id.widget_title,
+ mViewType.getTitle(sContext) + " (" + mCursorCount + ")");
+
+ // Set up "new" button (compose new message) and "next view" button
+ setActivityIntent(views, R.id.widget_compose, MessageCompose.class);
+ setCommandIntent(views, R.id.widget_logo, COMMAND_URI_SWITCH_LIST_VIEW);
+
+ // Use a bare intent for our template; we need to fill everything in
+ intent = new Intent(sContext, WidgetService.class);
+ PendingIntent pendingIntent =
+ PendingIntent.getService(sContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ views.setPendingIntentTemplate(R.id.message_list, pendingIntent);
+
+ // And finally update the widget
+ sWidgetManager.updateAppWidget(mWidgetId, views);
+ }
+
+ /* (non-Javadoc)
+ * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int)
+ */
+ public RemoteViews getViewAt(int position) {
+ // Use the cursor to set up the widget
+ synchronized (mCursorLock) {
+ if (mCursor == null || !mCursor.moveToPosition(position)) {
+ return getLoadingView();
+ }
+ RemoteViews views =
+ new RemoteViews(sContext.getPackageName(), R.layout.widget_list_item);
+
+ // Typeface for from, subject, and date (normal/bold) depends on whether the message
+ // is read/unread
+ int typeface = (mCursor.getInt(WIDGET_COLUMN_FLAG_READ) == 0) ? Typeface.BOLD
+ : Typeface.NORMAL;
+ views.setTextViewText(R.id.widget_from,
+ formattedTextFromCursor(mCursor, WIDGET_COLUMN_DISPLAY_NAME, typeface));
+ views.setTextViewText(R.id.widget_subject,
+ formattedTextFromCursor(mCursor, WIDGET_COLUMN_SUBJECT, typeface));
+
+ long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP);
+ // Get a nicely formatted date string (relative to today)
+ String date = DateUtils.getRelativeTimeSpanString(sContext, timestamp).toString();
+ views.setTextViewText(R.id.widget_date, TextUtils.ellipsize(date, sDatePaint, 64,
+ TruncateAt.END));
+
+ // Set button intents for view, reply, and delete
+ String messageId = mCursor.getString(WIDGET_COLUMN_ID);
+ String mailboxId = mCursor.getString(WIDGET_COLUMN_MAILBOX_KEY);
+ setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE, messageId,
+ mailboxId);
+
+ return views;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (mCursor == null) return 0;
+ return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ RemoteViews view = new RemoteViews(sContext.getPackageName(), R.layout.widget_loading);
+ view.setTextViewText(R.id.loading_text, sContext.getString(R.string.widget_loading));
+ return view;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ // Regular list view and the "loading" view
+ return 2;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mLoader != null) {
+ mLoader.stopLoading();
+ }
+ sWidgetMap.remove(mWidgetId);
+ }
+
+ @Override
+ public void onCreate() {
+ }
+ }
+
+ private static synchronized void update(Context context, AppWidgetManager widgetManager,
+ int[] appWidgetIds) {
+ for (int widgetId: appWidgetIds) {
+ getOrCreateWidget(widgetId).updateHeader();
+ }
+ }
+
+ private static EmailWidget getOrCreateWidget(int widgetId) {
+ EmailWidget widget = sWidgetMap.get(widgetId);
+ if (widget == null) {
+ if (Email.DEBUG) {
+ Log.d(TAG, "Creating EmailWidget for id #" + widgetId);
+ }
+ widget = new EmailWidget(widgetId);
+ sWidgetMap.put(widgetId, widget);
+ }
+ return widget;
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+ super.onDisabled(context);
+ if (Email.DEBUG) {
+ Log.d(TAG, "onDisabled");
+ }
+ context.stopService(new Intent(context, WidgetService.class));
+ }
+
+ @Override
+ public void onEnabled(final Context context) {
+ super.onEnabled(context);
+ if (Email.DEBUG) {
+ Log.d(TAG, "onEnabled");
+ }
+ context.startService(new Intent(context, WidgetService.class));
+ }
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ String action = intent.getAction();
+ if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ final int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ if (appWidgetIds != null && appWidgetIds.length > 0) {
+ if (sWidgetManager == null) {
+ sWidgetManager = AppWidgetManager.getInstance(context);
+ sContext = context.getApplicationContext();
+ sResolver = sContext.getContentResolver();
+ }
+ context.startService(new Intent(context, WidgetService.class));
+ update(sContext, sWidgetManager, appWidgetIds);
+ }
+ }
+ } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
+ Bundle extras = intent.getExtras();
+ if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
+ final int widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
+ // Find the widget in the map
+ EmailWidget widget = sWidgetMap.get(widgetId);
+ if (widget != null) {
+ // Stop loading and remove the widget from the map
+ widget.onDestroy();
+ }
+ }
+ }
+ }
+
+ /**
+ * We use the WidgetService for two purposes:
+ * 1) To provide a widget factory for RemoteViews, and
+ * 2) To process our command Uri's (i.e. take actions on user clicks)
+ */
+ public static class WidgetService extends RemoteViewsService {
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ // Which widget do we want (nice alliteration, huh?)
+ int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+ if (widgetId == -1) return null;
+ // Find the existing widget or create it
+ EmailWidget widget = sWidgetMap.get(widgetId);
+ if (widget == null) {
+ throw new IllegalStateException("onGetViewFactory, widget does not exist");
+ }
+ return widget;
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ // Since we're not calling startActivity from an Activity, we need the new task flag
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ super.startActivity(intent);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Uri data = intent.getData();
+ if (Email.DEBUG) {
+ Log.d(TAG, "Executing: " + data);
+ }
+ if (data == null) return Service.START_NOT_STICKY;
+ List pathSegments = data.getPathSegments();
+ // Our path segments are , [, ]
+ // First, a quick check of Uri validity
+ if (pathSegments.size() < 2) {
+ throw new IllegalArgumentException();
+ }
+ String command = pathSegments.get(0);
+ // Ignore unknown action names
+ try {
+ long arg1 = Long.parseLong(pathSegments.get(1));
+ if (COMMAND_NAME_VIEW_MESSAGE.equals(command)) {
+ // "view", ,
+ Intent i = MessageView.getActionViewIntent(this, arg1,
+ Long.parseLong(pathSegments.get(2)));
+ startActivity(i);
+ } else if (COMMAND_NAME_SWITCH_LIST_VIEW.equals(command)) {
+ // "next_view",
+ EmailWidget widget = sWidgetMap.get((int)arg1);
+ if (widget != null) {
+ widget.switchToNextView();
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Shouldn't happen as we construct all of the Uri's
+ }
+ return Service.START_NOT_STICKY;
+ }
+
+ }
+}