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 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; + } + + } +}