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