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_INCOMING_COMPLETE: 334 default: 335 if (!message.getCanClusterWithNextMessage()) { 336 statusText = Dates.getWidgetTimeString(message.getReceivedTimeStamp(), 337 false /*abbreviated*/).toString(); 338 } 339 break; 340 } 341 342 // Build the content description while we're populating the various fields. 343 final StringBuilder description = new StringBuilder(); 344 final String separator = mContext.getString(R.string.enumeration_comma); 345 // Sender information 346 final boolean hasPlainTextMessage = !(TextUtils.isEmpty(message.getText())); 347 if (message.getIsIncoming()) { 348 int senderResId = hasPlainTextMessage 349 ? R.string.incoming_text_sender_content_description 350 : R.string.incoming_sender_content_description; 351 description.append(mContext.getString(senderResId, message.getSenderDisplayName())); 352 } else { 353 int senderResId = hasPlainTextMessage 354 ? R.string.outgoing_text_sender_content_description 355 : R.string.outgoing_sender_content_description; 356 description.append(mContext.getString(senderResId)); 357 } 358 359 final boolean titleVisible = (titleResId >= 0); 360 if (titleVisible) { 361 final String titleText = mContext.getString(titleResId); 362 remoteViews.setTextViewText(R.id.message, titleText); 363 364 final String mmsInfoText = mContext.getString( 365 R.string.mms_info, 366 Formatter.formatFileSize(mContext, message.getSmsMessageSize()), 367 DateUtils.formatDateTime( 368 mContext, 369 message.getMmsExpiry(), 370 DateUtils.FORMAT_SHOW_DATE | 371 DateUtils.FORMAT_SHOW_TIME | 372 DateUtils.FORMAT_NUMERIC_DATE | 373 DateUtils.FORMAT_NO_YEAR)); 374 remoteViews.setTextViewText(R.id.date, mmsInfoText); 375 description.append(separator); 376 description.append(mmsInfoText); 377 } else if (!TextUtils.isEmpty(messageText)) { 378 remoteViews.setTextViewText(R.id.message, messageText); 379 description.append(separator); 380 description.append(messageText); 381 } else { 382 remoteViews.setViewVisibility(R.id.message, View.GONE); 383 } 384 385 final String subjectText = MmsUtils.cleanseMmsSubject(mContext.getResources(), 386 message.getMmsSubject()); 387 if (!TextUtils.isEmpty(subjectText)) { 388 description.append(separator); 389 description.append(subjectText); 390 } 391 392 if (statusResId >= 0) { 393 statusText = mContext.getString(statusResId); 394 final Spannable colorStr = new SpannableString(statusText); 395 if (showInRed) { 396 colorStr.setSpan(new ForegroundColorSpan( 397 mContext.getResources().getColor(R.color.timestamp_text_failed)), 398 0, statusText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 399 } 400 remoteViews.setTextViewText(R.id.date, colorStr); 401 description.append(separator); 402 description.append(colorStr); 403 } else { 404 description.append(separator); 405 description.append(Dates.getWidgetTimeString(message.getReceivedTimeStamp(), 406 false /*abbreviated*/)); 407 } 408 409 if (message.hasAttachments()) { 410 final List<MessagePartData> attachments = message.getAttachments(); 411 int stringId; 412 for (MessagePartData part : attachments) { 413 if (part.isImage()) { 414 stringId = R.string.conversation_list_snippet_picture; 415 } else if (part.isVideo()) { 416 stringId = R.string.conversation_list_snippet_video; 417 } else if (part.isAudio()) { 418 stringId = R.string.conversation_list_snippet_audio_clip; 419 } else if (part.isVCard()) { 420 stringId = R.string.conversation_list_snippet_vcard; 421 } else { 422 stringId = 0; 423 } 424 if (stringId > 0) { 425 description.append(separator); 426 description.append(mContext.getString(stringId)); 427 } 428 } 429 } 430 remoteViews.setContentDescription(message.getIsIncoming() ? 431 R.id.widget_message_item_incoming : 432 R.id.widget_message_item_outgoing, description); 433 } 434 getAttachmentBitmap(final MessagePartData part)435 private Bitmap getAttachmentBitmap(final MessagePartData part) { 436 UriImageRequestDescriptor descriptor; 437 if (part.isImage()) { 438 descriptor = new MessagePartImageRequestDescriptor(part, 439 IMAGE_ATTACHMENT_SIZE, // desiredWidth 440 IMAGE_ATTACHMENT_SIZE, // desiredHeight 441 true // isStatic 442 ); 443 } else if (part.isVideo()) { 444 descriptor = new MessagePartVideoThumbnailRequestDescriptor(part); 445 } else { 446 return null; 447 } 448 449 final MediaRequest<ImageResource> imageRequest = 450 descriptor.buildSyncMediaRequest(mContext); 451 final ImageResource imageResource = 452 MediaResourceManager.get().requestMediaResourceSync(imageRequest); 453 if (imageResource != null && imageResource.getBitmap() != null) { 454 setImageResource(imageResource); 455 return Bitmap.createBitmap(imageResource.getBitmap()); 456 } else { 457 releaseImageResource(); 458 return null; 459 } 460 } 461 462 /** 463 * @return the "View more messages" view. When the user taps this item, they're 464 * taken to the conversation in Bugle. 465 */ 466 @Override getViewMoreItemsView()467 protected RemoteViews getViewMoreItemsView() { 468 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 469 LogUtil.v(TAG, "getViewMoreConversationsView"); 470 } 471 final RemoteViews view = new RemoteViews(mContext.getPackageName(), 472 R.layout.widget_loading); 473 view.setTextViewText( 474 R.id.loading_text, mContext.getText(R.string.view_more_messages)); 475 476 // Tapping this "More messages" item should take us to the conversation. 477 final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext, 478 mConversationId, null /* draft */); 479 view.setOnClickFillInIntent(R.id.widget_loading, intent); 480 return view; 481 } 482 483 @Override getLoadingView()484 public RemoteViews getLoadingView() { 485 final RemoteViews view = new RemoteViews(mContext.getPackageName(), 486 R.layout.widget_loading); 487 view.setTextViewText( 488 R.id.loading_text, mContext.getText(R.string.loading_messages)); 489 return view; 490 } 491 492 @Override getViewTypeCount()493 public int getViewTypeCount() { 494 return 3; // Number of different list items that can be returned - 495 // 1- incoming list item 496 // 2- outgoing list item 497 // 3- more items list item 498 } 499 500 @Override getMainLayoutId()501 protected int getMainLayoutId() { 502 return R.layout.widget_conversation; 503 } 504 setImageResource(final ImageResource resource)505 private void setImageResource(final ImageResource resource) { 506 if (mImageResource != resource) { 507 // Clear out any information for what is currently used 508 releaseImageResource(); 509 mImageResource = resource; 510 } 511 } 512 releaseImageResource()513 private void releaseImageResource() { 514 if (mImageResource != null) { 515 mImageResource.release(); 516 } 517 mImageResource = null; 518 } 519 } 520 521 } 522