1 /* 2 * Copyright (C) 2015 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.messaging.widget; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.database.Cursor; 22 import android.graphics.Bitmap; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.text.Spannable; 26 import android.text.SpannableString; 27 import android.text.TextUtils; 28 import android.text.format.DateUtils; 29 import android.text.format.Formatter; 30 import android.text.style.ForegroundColorSpan; 31 import android.view.View; 32 import android.widget.RemoteViews; 33 import android.widget.RemoteViewsService; 34 35 import com.android.messaging.R; 36 import com.android.messaging.datamodel.MessagingContentProvider; 37 import com.android.messaging.datamodel.data.ConversationMessageData; 38 import com.android.messaging.datamodel.data.MessageData; 39 import com.android.messaging.datamodel.data.MessagePartData; 40 import com.android.messaging.datamodel.media.ImageResource; 41 import com.android.messaging.datamodel.media.MediaRequest; 42 import com.android.messaging.datamodel.media.MediaResourceManager; 43 import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor; 44 import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor; 45 import com.android.messaging.datamodel.media.UriImageRequestDescriptor; 46 import com.android.messaging.datamodel.media.VideoThumbnailRequest; 47 import com.android.messaging.sms.MmsUtils; 48 import com.android.messaging.ui.UIIntents; 49 import com.android.messaging.util.AvatarUriUtil; 50 import com.android.messaging.util.Dates; 51 import com.android.messaging.util.LogUtil; 52 import com.android.messaging.util.OsUtil; 53 import com.android.messaging.util.PhoneUtils; 54 55 import java.util.List; 56 57 public class WidgetConversationService extends RemoteViewsService { 58 private static final String TAG = LogUtil.BUGLE_WIDGET_TAG; 59 60 private static final int IMAGE_ATTACHMENT_SIZE = 400; 61 62 @Override onGetViewFactory(Intent intent)63 public RemoteViewsFactory onGetViewFactory(Intent intent) { 64 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 65 LogUtil.v(TAG, "onGetViewFactory intent: " + intent); 66 } 67 return new WidgetConversationFactory(getApplicationContext(), intent); 68 } 69 70 /** 71 * Remote Views Factory for the conversation widget. 72 */ 73 private static class WidgetConversationFactory extends BaseWidgetFactory { 74 private ImageResource mImageResource; 75 private String mConversationId; 76 WidgetConversationFactory(Context context, Intent intent)77 public WidgetConversationFactory(Context context, Intent intent) { 78 super(context, intent); 79 80 mConversationId = intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); 81 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 82 LogUtil.v(TAG, "BugleFactory intent: " + intent + "widget id: " + mAppWidgetId); 83 } 84 mIconSize = (int) context.getResources() 85 .getDimension(R.dimen.contact_icon_view_normal_size); 86 } 87 88 @Override onCreate()89 public void onCreate() { 90 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 91 LogUtil.v(TAG, "onCreate"); 92 } 93 super.onCreate(); 94 95 // If the conversation for this widget has been removed, we want to update the widget to 96 // "Tap to configure" mode. 97 if (!WidgetConversationProvider.isWidgetConfigured(mAppWidgetId)) { 98 WidgetConversationProvider.rebuildWidget(mContext, mAppWidgetId); 99 } 100 } 101 102 @Override doQuery()103 protected Cursor doQuery() { 104 if (TextUtils.isEmpty(mConversationId)) { 105 LogUtil.w(TAG, "doQuery no conversation id"); 106 return null; 107 } 108 final Uri uri = MessagingContentProvider.buildConversationMessagesUri(mConversationId); 109 if (uri != null) { 110 LogUtil.w(TAG, "doQuery uri: " + uri.toString()); 111 } 112 return mContext.getContentResolver().query(uri, 113 ConversationMessageData.getProjection(), 114 null, // where 115 null, // selection args 116 null // sort order 117 ); 118 } 119 120 /** 121 * @return the {@link RemoteViews} for a specific position in the list. 122 */ 123 @Override getViewAt(final int originalPosition)124 public RemoteViews getViewAt(final int originalPosition) { 125 synchronized (sWidgetLock) { 126 // "View more messages" view. 127 if (mCursor == null 128 || (mShouldShowViewMore && originalPosition == 0)) { 129 return getViewMoreItemsView(); 130 } 131 // The message cursor is in reverse order for performance reasons. 132 final int position = getCount() - originalPosition - 1; 133 if (!mCursor.moveToPosition(position)) { 134 // If we ever fail to move to a position, return the "View More messages" 135 // view. 136 LogUtil.w(TAG, "Failed to move to position: " + position); 137 return getViewMoreItemsView(); 138 } 139 140 final ConversationMessageData message = new ConversationMessageData(); 141 message.bind(mCursor); 142 143 // Inflate and fill out the remote view 144 final RemoteViews remoteViews = new RemoteViews( 145 mContext.getPackageName(), message.getIsIncoming() ? 146 R.layout.widget_message_item_incoming : 147 R.layout.widget_message_item_outgoing); 148 149 final boolean hasUnreadMessages = false; //!message.getIsRead(); 150 151 // Date 152 remoteViews.setTextViewText(R.id.date, boldifyIfUnread( 153 Dates.getWidgetTimeString(message.getReceivedTimeStamp(), 154 false /*abbreviated*/), 155 hasUnreadMessages)); 156 157 // On click intent. 158 final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext, 159 mConversationId, null /* draft */); 160 161 // Attachments 162 int attachmentStringId = 0; 163 remoteViews.setViewVisibility(R.id.attachmentFrame, View.GONE); 164 165 int scrollToPosition = originalPosition; 166 final int cursorCount = mCursor.getCount(); 167 if (cursorCount > MAX_ITEMS_TO_SHOW) { 168 scrollToPosition += cursorCount - MAX_ITEMS_TO_SHOW; 169 } 170 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 171 LogUtil.v(TAG, "getViewAt position: " + originalPosition + 172 " computed position: " + position + 173 " scrollToPosition: " + scrollToPosition + 174 " cursorCount: " + cursorCount + 175 " MAX_ITEMS_TO_SHOW: " + MAX_ITEMS_TO_SHOW); 176 } 177 178 intent.putExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, scrollToPosition); 179 if (message.hasAttachments()) { 180 final List<MessagePartData> attachments = message.getAttachments(); 181 for (MessagePartData part : attachments) { 182 final boolean videoWithThumbnail = part.isVideo() 183 && (VideoThumbnailRequest.shouldShowIncomingVideoThumbnails() 184 || !message.getIsIncoming()); 185 if (part.isImage() || videoWithThumbnail) { 186 final Uri uri = part.getContentUri(); 187 remoteViews.setViewVisibility(R.id.attachmentFrame, View.VISIBLE); 188 remoteViews.setViewVisibility(R.id.playButton, part.isVideo() ? 189 View.VISIBLE : View.GONE); 190 remoteViews.setImageViewBitmap(R.id.attachment, 191 getAttachmentBitmap(part)); 192 intent.putExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_URI , 193 uri.toString()); 194 intent.putExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_TYPE , 195 part.getContentType()); 196 break; 197 } else if (part.isVideo()) { 198 attachmentStringId = R.string.conversation_list_snippet_video; 199 break; 200 } 201 if (part.isAudio()) { 202 attachmentStringId = R.string.conversation_list_snippet_audio_clip; 203 break; 204 } 205 if (part.isVCard()) { 206 attachmentStringId = R.string.conversation_list_snippet_vcard; 207 break; 208 } 209 } 210 } 211 212 remoteViews.setOnClickFillInIntent(message.getIsIncoming() ? 213 R.id.widget_message_item_incoming : 214 R.id.widget_message_item_outgoing, 215 intent); 216 217 // Avatar 218 boolean includeAvatar; 219 if (OsUtil.isAtLeastJB()) { 220 final Bundle options = mAppWidgetManager.getAppWidgetOptions(mAppWidgetId); 221 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 222 LogUtil.v(TAG, "getViewAt BugleWidgetProvider.WIDGET_SIZE_KEY: " + 223 options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY)); 224 } 225 226 includeAvatar = options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY) 227 == BugleWidgetProvider.SIZE_LARGE; 228 } else { 229 includeAvatar = true; 230 } 231 232 // Show the avatar (and shadow) when grande size, otherwise hide it. 233 remoteViews.setViewVisibility(R.id.avatarView, includeAvatar ? 234 View.VISIBLE : View.GONE); 235 remoteViews.setViewVisibility(R.id.avatarShadow, includeAvatar ? 236 View.VISIBLE : View.GONE); 237 238 final Uri avatarUri = AvatarUriUtil.createAvatarUri( 239 message.getSenderProfilePhotoUri(), 240 message.getSenderFullName(), 241 message.getSenderNormalizedDestination(), 242 message.getSenderContactLookupKey()); 243 244 remoteViews.setImageViewBitmap(R.id.avatarView, includeAvatar ? 245 getAvatarBitmap(avatarUri) : null); 246 247 String text = message.getText(); 248 if (attachmentStringId != 0) { 249 final String attachment = mContext.getString(attachmentStringId); 250 if (!TextUtils.isEmpty(text)) { 251 text += '\n' + attachment; 252 } else { 253 text = attachment; 254 } 255 } 256 257 remoteViews.setViewVisibility(R.id.message, View.VISIBLE); 258 updateViewContent(text, message, remoteViews); 259 260 return remoteViews; 261 } 262 } 263 264 // updateViewContent figures out what to show in the message and date fields based on 265 // the message status. This code came from ConversationMessageView.updateViewContent, but 266 // had to be simplified to work with our simple widget list item. 267 // updateViewContent also builds the accessibility content description for the list item. updateViewContent(final String messageText, final ConversationMessageData message, final RemoteViews remoteViews)268 private void updateViewContent(final String messageText, 269 final ConversationMessageData message, 270 final RemoteViews remoteViews) { 271 int titleResId = -1; 272 int statusResId = -1; 273 boolean showInRed = false; 274 String statusText = null; 275 switch(message.getStatus()) { 276 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 277 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 278 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 279 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 280 titleResId = R.string.message_title_downloading; 281 statusResId = R.string.message_status_downloading; 282 break; 283 284 case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: 285 if (!OsUtil.isSecondaryUser()) { 286 titleResId = R.string.message_title_manual_download; 287 statusResId = R.string.message_status_download; 288 } 289 break; 290 291 case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: 292 if (!OsUtil.isSecondaryUser()) { 293 titleResId = R.string.message_title_download_failed; 294 statusResId = R.string.message_status_download_error; 295 showInRed = true; 296 } 297 break; 298 299 case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: 300 if (!OsUtil.isSecondaryUser()) { 301 titleResId = R.string.message_title_download_failed; 302 statusResId = R.string.message_status_download; 303 showInRed = true; 304 } 305 break; 306 307 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 308 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 309 statusResId = R.string.message_status_sending; 310 break; 311 312 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 313 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 314 statusResId = R.string.message_status_send_retrying; 315 break; 316 317 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 318 statusResId = R.string.message_status_send_failed_emergency_number; 319 showInRed = true; 320 break; 321 322 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 323 // don't show the error state unless we're the default sms app 324 if (PhoneUtils.getDefault().isDefaultSmsApp()) { 325 statusResId = MmsUtils.mapRawStatusToErrorResourceId( 326 message.getStatus(), message.getRawTelephonyStatus()); 327 showInRed = true; 328 break; 329 } 330 // FALL THROUGH HERE 331 332 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 333 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 334 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 335 default: 336 if (!message.getCanClusterWithNextMessage()) { 337 statusText = Dates.getWidgetTimeString(message.getReceivedTimeStamp(), 338 false /*abbreviated*/).toString(); 339 } 340 break; 341 } 342 343 // Build the content description while we're populating the various fields. 344 final StringBuilder description = new StringBuilder(); 345 final String separator = mContext.getString(R.string.enumeration_comma); 346 // Sender information 347 final boolean hasPlainTextMessage = !(TextUtils.isEmpty(message.getText())); 348 if (message.getIsIncoming()) { 349 int senderResId = hasPlainTextMessage 350 ? R.string.incoming_text_sender_content_description 351 : R.string.incoming_sender_content_description; 352 description.append(mContext.getString(senderResId, message.getSenderDisplayName())); 353 } else { 354 int senderResId = hasPlainTextMessage 355 ? R.string.outgoing_text_sender_content_description 356 : R.string.outgoing_sender_content_description; 357 description.append(mContext.getString(senderResId)); 358 } 359 360 final boolean titleVisible = (titleResId >= 0); 361 if (titleVisible) { 362 final String titleText = mContext.getString(titleResId); 363 remoteViews.setTextViewText(R.id.message, titleText); 364 365 final String mmsInfoText = mContext.getString( 366 R.string.mms_info, 367 Formatter.formatFileSize(mContext, message.getSmsMessageSize()), 368 DateUtils.formatDateTime( 369 mContext, 370 message.getMmsExpiry(), 371 DateUtils.FORMAT_SHOW_DATE | 372 DateUtils.FORMAT_SHOW_TIME | 373 DateUtils.FORMAT_NUMERIC_DATE | 374 DateUtils.FORMAT_NO_YEAR)); 375 remoteViews.setTextViewText(R.id.date, mmsInfoText); 376 description.append(separator); 377 description.append(mmsInfoText); 378 } else if (!TextUtils.isEmpty(messageText)) { 379 remoteViews.setTextViewText(R.id.message, messageText); 380 description.append(separator); 381 description.append(messageText); 382 } else { 383 remoteViews.setViewVisibility(R.id.message, View.GONE); 384 } 385 386 final String subjectText = MmsUtils.cleanseMmsSubject(mContext.getResources(), 387 message.getMmsSubject()); 388 if (!TextUtils.isEmpty(subjectText)) { 389 description.append(separator); 390 description.append(subjectText); 391 } 392 393 if (statusResId >= 0) { 394 statusText = mContext.getString(statusResId); 395 final Spannable colorStr = new SpannableString(statusText); 396 if (showInRed) { 397 colorStr.setSpan(new ForegroundColorSpan( 398 mContext.getResources().getColor(R.color.timestamp_text_failed)), 399 0, statusText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 400 } 401 remoteViews.setTextViewText(R.id.date, colorStr); 402 description.append(separator); 403 description.append(colorStr); 404 } else { 405 description.append(separator); 406 description.append(Dates.getWidgetTimeString(message.getReceivedTimeStamp(), 407 false /*abbreviated*/)); 408 } 409 410 if (message.hasAttachments()) { 411 final List<MessagePartData> attachments = message.getAttachments(); 412 int stringId; 413 for (MessagePartData part : attachments) { 414 if (part.isImage()) { 415 stringId = R.string.conversation_list_snippet_picture; 416 } else if (part.isVideo()) { 417 stringId = R.string.conversation_list_snippet_video; 418 } else if (part.isAudio()) { 419 stringId = R.string.conversation_list_snippet_audio_clip; 420 } else if (part.isVCard()) { 421 stringId = R.string.conversation_list_snippet_vcard; 422 } else { 423 stringId = 0; 424 } 425 if (stringId > 0) { 426 description.append(separator); 427 description.append(mContext.getString(stringId)); 428 } 429 } 430 } 431 remoteViews.setContentDescription(message.getIsIncoming() ? 432 R.id.widget_message_item_incoming : 433 R.id.widget_message_item_outgoing, description); 434 } 435 getAttachmentBitmap(final MessagePartData part)436 private Bitmap getAttachmentBitmap(final MessagePartData part) { 437 UriImageRequestDescriptor descriptor; 438 if (part.isImage()) { 439 descriptor = new MessagePartImageRequestDescriptor(part, 440 IMAGE_ATTACHMENT_SIZE, // desiredWidth 441 IMAGE_ATTACHMENT_SIZE, // desiredHeight 442 true // isStatic 443 ); 444 } else if (part.isVideo()) { 445 descriptor = new MessagePartVideoThumbnailRequestDescriptor(part); 446 } else { 447 return null; 448 } 449 450 final MediaRequest<ImageResource> imageRequest = 451 descriptor.buildSyncMediaRequest(mContext); 452 final ImageResource imageResource = 453 MediaResourceManager.get().requestMediaResourceSync(imageRequest); 454 if (imageResource != null && imageResource.getBitmap() != null) { 455 setImageResource(imageResource); 456 return Bitmap.createBitmap(imageResource.getBitmap()); 457 } else { 458 releaseImageResource(); 459 return null; 460 } 461 } 462 463 /** 464 * @return the "View more messages" view. When the user taps this item, they're 465 * taken to the conversation in Bugle. 466 */ 467 @Override getViewMoreItemsView()468 protected RemoteViews getViewMoreItemsView() { 469 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 470 LogUtil.v(TAG, "getViewMoreConversationsView"); 471 } 472 final RemoteViews view = new RemoteViews(mContext.getPackageName(), 473 R.layout.widget_loading); 474 view.setTextViewText( 475 R.id.loading_text, mContext.getText(R.string.view_more_messages)); 476 477 // Tapping this "More messages" item should take us to the conversation. 478 final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext, 479 mConversationId, null /* draft */); 480 view.setOnClickFillInIntent(R.id.widget_loading, intent); 481 return view; 482 } 483 484 @Override getLoadingView()485 public RemoteViews getLoadingView() { 486 final RemoteViews view = new RemoteViews(mContext.getPackageName(), 487 R.layout.widget_loading); 488 view.setTextViewText( 489 R.id.loading_text, mContext.getText(R.string.loading_messages)); 490 return view; 491 } 492 493 @Override getViewTypeCount()494 public int getViewTypeCount() { 495 return 3; // Number of different list items that can be returned - 496 // 1- incoming list item 497 // 2- outgoing list item 498 // 3- more items list item 499 } 500 501 @Override getMainLayoutId()502 protected int getMainLayoutId() { 503 return R.layout.widget_conversation; 504 } 505 setImageResource(final ImageResource resource)506 private void setImageResource(final ImageResource resource) { 507 if (mImageResource != resource) { 508 // Clear out any information for what is currently used 509 releaseImageResource(); 510 mImageResource = resource; 511 } 512 } 513 releaseImageResource()514 private void releaseImageResource() { 515 if (mImageResource != null) { 516 mImageResource.release(); 517 } 518 mImageResource = null; 519 } 520 } 521 522 } 523