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