1 /* 2 * Copyright (C) 2012 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.mms.widget; 18 19 import android.app.PendingIntent; 20 import android.appwidget.AppWidgetManager; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.res.Resources; 24 import android.database.Cursor; 25 import android.provider.Telephony.Threads; 26 import android.text.Spannable; 27 import android.text.SpannableStringBuilder; 28 import android.text.style.ForegroundColorSpan; 29 import android.text.style.TextAppearanceSpan; 30 import android.util.Log; 31 import android.view.View; 32 import android.widget.RemoteViews; 33 import android.widget.RemoteViewsService; 34 35 import com.android.mms.LogTag; 36 import com.android.mms.R; 37 import com.android.mms.data.Contact; 38 import com.android.mms.data.Conversation; 39 import com.android.mms.ui.ConversationList; 40 import com.android.mms.ui.ConversationListItem; 41 import com.android.mms.ui.MessageUtils; 42 import com.android.mms.util.SmileyParser; 43 44 public class MmsWidgetService extends RemoteViewsService { 45 private static final String TAG = "MmsWidgetService"; 46 47 /** 48 * Lock to avoid race condition between widgets. 49 */ 50 private static final Object sWidgetLock = new Object(); 51 52 @Override 53 public RemoteViewsFactory onGetViewFactory(Intent intent) { 54 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 55 Log.v(TAG, "onGetViewFactory intent: " + intent); 56 } 57 return new MmsFactory(getApplicationContext(), intent); 58 } 59 60 /** 61 * Remote Views Factory for Mms Widget. 62 */ 63 private static class MmsFactory 64 implements RemoteViewsService.RemoteViewsFactory, Contact.UpdateListener { 65 private static final int MAX_CONVERSATIONS_COUNT = 25; 66 private final Context mContext; 67 private final int mAppWidgetId; 68 private boolean mShouldShowViewMore; 69 private Cursor mConversationCursor; 70 private int mUnreadConvCount; 71 private final AppWidgetManager mAppWidgetManager; 72 73 // Static colors 74 private static int SUBJECT_TEXT_COLOR_READ; 75 private static int SUBJECT_TEXT_COLOR_UNREAD; 76 private static int SENDERS_TEXT_COLOR_READ; 77 private static int SENDERS_TEXT_COLOR_UNREAD; 78 79 public MmsFactory(Context context, Intent intent) { 80 mContext = context; 81 mAppWidgetId = intent.getIntExtra( 82 AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); 83 mAppWidgetManager = AppWidgetManager.getInstance(context); 84 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 85 Log.v(TAG, "MmsFactory intent: " + intent + "widget id: " + mAppWidgetId); 86 } 87 // Initialize colors 88 Resources res = context.getResources(); 89 SENDERS_TEXT_COLOR_READ = res.getColor(R.color.widget_sender_text_color_read); 90 SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_sender_text_color_unread); 91 SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.widget_subject_text_color_read); 92 SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_subject_text_color_unread); 93 } 94 95 @Override 96 public void onCreate() { 97 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 98 Log.v(TAG, "onCreate"); 99 } 100 Contact.addListener(this); 101 } 102 103 @Override 104 public void onDestroy() { 105 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 106 Log.v(TAG, "onDestroy"); 107 } 108 synchronized (sWidgetLock) { 109 if (mConversationCursor != null && !mConversationCursor.isClosed()) { 110 mConversationCursor.close(); 111 mConversationCursor = null; 112 } 113 Contact.removeListener(this); 114 } 115 } 116 117 @Override 118 public void onDataSetChanged() { 119 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 120 Log.v(TAG, "onDataSetChanged"); 121 } 122 synchronized (sWidgetLock) { 123 if (mConversationCursor != null) { 124 mConversationCursor.close(); 125 mConversationCursor = null; 126 } 127 mConversationCursor = queryAllConversations(); 128 mUnreadConvCount = queryUnreadCount(); 129 onLoadComplete(); 130 } 131 } 132 133 private Cursor queryAllConversations() { 134 return mContext.getContentResolver().query( 135 Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION, 136 null, null, null); 137 } 138 139 private int queryUnreadCount() { 140 Cursor cursor = null; 141 int unreadCount = 0; 142 try { 143 cursor = mContext.getContentResolver().query( 144 Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION, 145 Threads.READ + "=0", null, null); 146 if (cursor != null) { 147 unreadCount = cursor.getCount(); 148 } 149 } finally { 150 if (cursor != null) { 151 cursor.close(); 152 } 153 } 154 return unreadCount; 155 } 156 157 /** 158 * Returns the number of items should be shown in the widget list. This method also updates 159 * the boolean that indicates whether the "show more" item should be shown. 160 * @return the number of items to be displayed in the list. 161 */ 162 @Override 163 public int getCount() { 164 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 165 Log.v(TAG, "getCount"); 166 } 167 synchronized (sWidgetLock) { 168 if (mConversationCursor == null) { 169 return 0; 170 } 171 final int count = getConversationCount(); 172 mShouldShowViewMore = count < mConversationCursor.getCount(); 173 return count + (mShouldShowViewMore ? 1 : 0); 174 } 175 } 176 177 /** 178 * Returns the number of conversations that should be shown in the widget. This method 179 * doesn't update the boolean that indicates that the "show more" item should be included 180 * in the list. 181 * @return 182 */ 183 private int getConversationCount() { 184 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 185 Log.v(TAG, "getConversationCount"); 186 } 187 188 return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT); 189 } 190 191 /* 192 * Add color to a given text 193 */ 194 private SpannableStringBuilder addColor(CharSequence text, int color) { 195 SpannableStringBuilder builder = new SpannableStringBuilder(text); 196 if (color != 0) { 197 builder.setSpan(new ForegroundColorSpan(color), 0, text.length(), 198 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 199 } 200 return builder; 201 } 202 203 /** 204 * @return the {@link RemoteViews} for a specific position in the list. 205 */ 206 @Override 207 public RemoteViews getViewAt(int position) { 208 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 209 Log.v(TAG, "getViewAt position: " + position); 210 } 211 synchronized (sWidgetLock) { 212 // "View more conversations" view. 213 if (mConversationCursor == null 214 || (mShouldShowViewMore && position >= getConversationCount())) { 215 return getViewMoreConversationsView(); 216 } 217 218 if (!mConversationCursor.moveToPosition(position)) { 219 // If we ever fail to move to a position, return the "View More conversations" 220 // view. 221 Log.w(TAG, "Failed to move to position: " + position); 222 return getViewMoreConversationsView(); 223 } 224 225 Conversation conv = Conversation.from(mContext, mConversationCursor); 226 227 // Inflate and fill out the remote view 228 RemoteViews remoteViews = new RemoteViews( 229 mContext.getPackageName(), R.layout.widget_conversation); 230 231 if (conv.hasUnreadMessages()) { 232 remoteViews.setViewVisibility(R.id.widget_unread_background, View.VISIBLE); 233 remoteViews.setViewVisibility(R.id.widget_read_background, View.GONE); 234 } else { 235 remoteViews.setViewVisibility(R.id.widget_unread_background, View.GONE); 236 remoteViews.setViewVisibility(R.id.widget_read_background, View.VISIBLE); 237 } 238 boolean hasAttachment = conv.hasAttachment(); 239 remoteViews.setViewVisibility(R.id.attachment, hasAttachment ? View.VISIBLE : 240 View.GONE); 241 242 // Date 243 remoteViews.setTextViewText(R.id.date, 244 addColor(MessageUtils.formatTimeStampString(mContext, conv.getDate()), 245 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD : 246 SUBJECT_TEXT_COLOR_READ)); 247 248 // From 249 int color = conv.hasUnreadMessages() ? SENDERS_TEXT_COLOR_UNREAD : 250 SENDERS_TEXT_COLOR_READ; 251 SpannableStringBuilder from = addColor(conv.getRecipients().formatNames(", "), 252 color); 253 254 if (conv.hasDraft()) { 255 from.append(mContext.getResources().getString(R.string.draft_separator)); 256 int before = from.length(); 257 from.append(mContext.getResources().getString(R.string.has_draft)); 258 from.setSpan(new TextAppearanceSpan(mContext, 259 android.R.style.TextAppearance_Small, color), before, 260 from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 261 from.setSpan(new ForegroundColorSpan( 262 mContext.getResources().getColor(R.drawable.text_color_red)), 263 before, from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 264 } 265 266 // Unread messages are shown in bold 267 if (conv.hasUnreadMessages()) { 268 from.setSpan(ConversationListItem.STYLE_BOLD, 0, from.length(), 269 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 270 } 271 remoteViews.setTextViewText(R.id.from, from); 272 273 // Subject 274 // TODO: the SmileyParser inserts image spans but they don't seem to make it 275 // into the remote view. 276 SmileyParser parser = SmileyParser.getInstance(); 277 remoteViews.setTextViewText(R.id.subject, 278 addColor(parser.addSmileySpans(conv.getSnippet()), 279 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD : 280 SUBJECT_TEXT_COLOR_READ)); 281 282 // On click intent. 283 Intent clickIntent = new Intent(Intent.ACTION_VIEW); 284 clickIntent.setType("vnd.android-dir/mms-sms"); 285 clickIntent.putExtra("thread_id", conv.getThreadId()); 286 287 remoteViews.setOnClickFillInIntent(R.id.widget_conversation, clickIntent); 288 289 return remoteViews; 290 } 291 } 292 293 /** 294 * @return the "View more conversations" view. When the user taps this item, they're 295 * taken to the messaging app's conversation list. 296 */ 297 private RemoteViews getViewMoreConversationsView() { 298 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 299 Log.v(TAG, "getViewMoreConversationsView"); 300 } 301 RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading); 302 view.setTextViewText( 303 R.id.loading_text, mContext.getText(R.string.view_more_conversations)); 304 PendingIntent pendingIntent = 305 PendingIntent.getActivity(mContext, 0, new Intent(mContext, 306 ConversationList.class), 307 PendingIntent.FLAG_UPDATE_CURRENT); 308 view.setOnClickPendingIntent(R.id.widget_loading, pendingIntent); 309 return view; 310 } 311 312 @Override 313 public RemoteViews getLoadingView() { 314 RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading); 315 view.setTextViewText( 316 R.id.loading_text, mContext.getText(R.string.loading_conversations)); 317 return view; 318 } 319 320 @Override 321 public int getViewTypeCount() { 322 return 2; 323 } 324 325 @Override 326 public long getItemId(int position) { 327 return position; 328 } 329 330 @Override 331 public boolean hasStableIds() { 332 return true; 333 } 334 335 private void onLoadComplete() { 336 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 337 Log.v(TAG, "onLoadComplete"); 338 } 339 RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget); 340 341 remoteViews.setViewVisibility(R.id.widget_unread_count, mUnreadConvCount > 0 ? 342 View.VISIBLE : View.GONE); 343 if (mUnreadConvCount > 0) { 344 remoteViews.setTextViewText( 345 R.id.widget_unread_count, Integer.toString(mUnreadConvCount)); 346 } 347 348 mAppWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews); 349 } 350 351 public void onUpdate(Contact updated) { 352 if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) { 353 Log.v(TAG, "onUpdate from Contact: " + updated); 354 } 355 mAppWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.conversation_list); 356 } 357 358 } 359 } 360