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