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 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 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 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 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 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. 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 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 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 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 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 501 protected int getMainLayoutId() { 502 return R.layout.widget_conversation; 503 } 504 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 513 private void releaseImageResource() { 514 if (mImageResource != null) { 515 mImageResource.release(); 516 } 517 mImageResource = null; 518 } 519 } 520 521 } 522