| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.messaging.widget; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.text.format.Formatter; |
| import android.text.style.ForegroundColorSpan; |
| import android.view.View; |
| import android.widget.RemoteViews; |
| import android.widget.RemoteViewsService; |
| |
| import com.android.messaging.R; |
| import com.android.messaging.datamodel.MessagingContentProvider; |
| import com.android.messaging.datamodel.data.ConversationMessageData; |
| import com.android.messaging.datamodel.data.MessageData; |
| import com.android.messaging.datamodel.data.MessagePartData; |
| import com.android.messaging.datamodel.media.ImageResource; |
| import com.android.messaging.datamodel.media.MediaRequest; |
| import com.android.messaging.datamodel.media.MediaResourceManager; |
| import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor; |
| import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor; |
| import com.android.messaging.datamodel.media.UriImageRequestDescriptor; |
| import com.android.messaging.datamodel.media.VideoThumbnailRequest; |
| import com.android.messaging.sms.MmsUtils; |
| import com.android.messaging.ui.UIIntents; |
| import com.android.messaging.util.AvatarUriUtil; |
| import com.android.messaging.util.Dates; |
| import com.android.messaging.util.LogUtil; |
| import com.android.messaging.util.OsUtil; |
| import com.android.messaging.util.PhoneUtils; |
| |
| import java.util.List; |
| |
| public class WidgetConversationService extends RemoteViewsService { |
| private static final String TAG = LogUtil.BUGLE_WIDGET_TAG; |
| |
| private static final int IMAGE_ATTACHMENT_SIZE = 400; |
| |
| @Override |
| public RemoteViewsFactory onGetViewFactory(Intent intent) { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "onGetViewFactory intent: " + intent); |
| } |
| return new WidgetConversationFactory(getApplicationContext(), intent); |
| } |
| |
| /** |
| * Remote Views Factory for the conversation widget. |
| */ |
| private static class WidgetConversationFactory extends BaseWidgetFactory { |
| private ImageResource mImageResource; |
| private String mConversationId; |
| |
| public WidgetConversationFactory(Context context, Intent intent) { |
| super(context, intent); |
| |
| mConversationId = intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "BugleFactory intent: " + intent + "widget id: " + mAppWidgetId); |
| } |
| mIconSize = (int) context.getResources() |
| .getDimension(R.dimen.contact_icon_view_normal_size); |
| } |
| |
| @Override |
| public void onCreate() { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "onCreate"); |
| } |
| super.onCreate(); |
| |
| // If the conversation for this widget has been removed, we want to update the widget to |
| // "Tap to configure" mode. |
| if (!WidgetConversationProvider.isWidgetConfigured(mAppWidgetId)) { |
| WidgetConversationProvider.rebuildWidget(mContext, mAppWidgetId); |
| } |
| } |
| |
| @Override |
| protected Cursor doQuery() { |
| if (TextUtils.isEmpty(mConversationId)) { |
| LogUtil.w(TAG, "doQuery no conversation id"); |
| return null; |
| } |
| final Uri uri = MessagingContentProvider.buildConversationMessagesUri(mConversationId); |
| if (uri != null) { |
| LogUtil.w(TAG, "doQuery uri: " + uri.toString()); |
| } |
| return mContext.getContentResolver().query(uri, |
| ConversationMessageData.getProjection(), |
| null, // where |
| null, // selection args |
| null // sort order |
| ); |
| } |
| |
| /** |
| * @return the {@link RemoteViews} for a specific position in the list. |
| */ |
| @Override |
| public RemoteViews getViewAt(final int originalPosition) { |
| synchronized (sWidgetLock) { |
| // "View more messages" view. |
| if (mCursor == null |
| || (mShouldShowViewMore && originalPosition == 0)) { |
| return getViewMoreItemsView(); |
| } |
| // The message cursor is in reverse order for performance reasons. |
| final int position = getCount() - originalPosition - 1; |
| if (!mCursor.moveToPosition(position)) { |
| // If we ever fail to move to a position, return the "View More messages" |
| // view. |
| LogUtil.w(TAG, "Failed to move to position: " + position); |
| return getViewMoreItemsView(); |
| } |
| |
| final ConversationMessageData message = new ConversationMessageData(); |
| message.bind(mCursor); |
| |
| // Inflate and fill out the remote view |
| final RemoteViews remoteViews = new RemoteViews( |
| mContext.getPackageName(), message.getIsIncoming() ? |
| R.layout.widget_message_item_incoming : |
| R.layout.widget_message_item_outgoing); |
| |
| final boolean hasUnreadMessages = false; //!message.getIsRead(); |
| |
| // Date |
| remoteViews.setTextViewText(R.id.date, boldifyIfUnread( |
| Dates.getWidgetTimeString(message.getReceivedTimeStamp(), |
| false /*abbreviated*/), |
| hasUnreadMessages)); |
| |
| // On click intent. |
| final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext, |
| mConversationId, null /* draft */); |
| |
| // Attachments |
| int attachmentStringId = 0; |
| remoteViews.setViewVisibility(R.id.attachmentFrame, View.GONE); |
| |
| int scrollToPosition = originalPosition; |
| final int cursorCount = mCursor.getCount(); |
| if (cursorCount > MAX_ITEMS_TO_SHOW) { |
| scrollToPosition += cursorCount - MAX_ITEMS_TO_SHOW; |
| } |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "getViewAt position: " + originalPosition + |
| " computed position: " + position + |
| " scrollToPosition: " + scrollToPosition + |
| " cursorCount: " + cursorCount + |
| " MAX_ITEMS_TO_SHOW: " + MAX_ITEMS_TO_SHOW); |
| } |
| |
| intent.putExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, scrollToPosition); |
| if (message.hasAttachments()) { |
| final List<MessagePartData> attachments = message.getAttachments(); |
| for (MessagePartData part : attachments) { |
| final boolean videoWithThumbnail = part.isVideo() |
| && (VideoThumbnailRequest.shouldShowIncomingVideoThumbnails() |
| || !message.getIsIncoming()); |
| if (part.isImage() || videoWithThumbnail) { |
| final Uri uri = part.getContentUri(); |
| remoteViews.setViewVisibility(R.id.attachmentFrame, View.VISIBLE); |
| remoteViews.setViewVisibility(R.id.playButton, part.isVideo() ? |
| View.VISIBLE : View.GONE); |
| remoteViews.setImageViewBitmap(R.id.attachment, |
| getAttachmentBitmap(part)); |
| intent.putExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_URI , |
| uri.toString()); |
| intent.putExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_TYPE , |
| part.getContentType()); |
| break; |
| } else if (part.isVideo()) { |
| attachmentStringId = R.string.conversation_list_snippet_video; |
| break; |
| } |
| if (part.isAudio()) { |
| attachmentStringId = R.string.conversation_list_snippet_audio_clip; |
| break; |
| } |
| if (part.isVCard()) { |
| attachmentStringId = R.string.conversation_list_snippet_vcard; |
| break; |
| } |
| } |
| } |
| |
| remoteViews.setOnClickFillInIntent(message.getIsIncoming() ? |
| R.id.widget_message_item_incoming : |
| R.id.widget_message_item_outgoing, |
| intent); |
| |
| // Avatar |
| boolean includeAvatar; |
| if (OsUtil.isAtLeastJB()) { |
| final Bundle options = mAppWidgetManager.getAppWidgetOptions(mAppWidgetId); |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "getViewAt BugleWidgetProvider.WIDGET_SIZE_KEY: " + |
| options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY)); |
| } |
| |
| includeAvatar = options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY) |
| == BugleWidgetProvider.SIZE_LARGE; |
| } else { |
| includeAvatar = true; |
| } |
| |
| // Show the avatar (and shadow) when grande size, otherwise hide it. |
| remoteViews.setViewVisibility(R.id.avatarView, includeAvatar ? |
| View.VISIBLE : View.GONE); |
| remoteViews.setViewVisibility(R.id.avatarShadow, includeAvatar ? |
| View.VISIBLE : View.GONE); |
| |
| final Uri avatarUri = AvatarUriUtil.createAvatarUri( |
| message.getSenderProfilePhotoUri(), |
| message.getSenderFullName(), |
| message.getSenderNormalizedDestination(), |
| message.getSenderContactLookupKey()); |
| |
| remoteViews.setImageViewBitmap(R.id.avatarView, includeAvatar ? |
| getAvatarBitmap(avatarUri) : null); |
| |
| String text = message.getText(); |
| if (attachmentStringId != 0) { |
| final String attachment = mContext.getString(attachmentStringId); |
| if (!TextUtils.isEmpty(text)) { |
| text += '\n' + attachment; |
| } else { |
| text = attachment; |
| } |
| } |
| |
| remoteViews.setViewVisibility(R.id.message, View.VISIBLE); |
| updateViewContent(text, message, remoteViews); |
| |
| return remoteViews; |
| } |
| } |
| |
| // updateViewContent figures out what to show in the message and date fields based on |
| // the message status. This code came from ConversationMessageView.updateViewContent, but |
| // had to be simplified to work with our simple widget list item. |
| // updateViewContent also builds the accessibility content description for the list item. |
| private void updateViewContent(final String messageText, |
| final ConversationMessageData message, |
| final RemoteViews remoteViews) { |
| int titleResId = -1; |
| int statusResId = -1; |
| boolean showInRed = false; |
| String statusText = null; |
| switch(message.getStatus()) { |
| case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: |
| case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: |
| case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: |
| case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: |
| titleResId = R.string.message_title_downloading; |
| statusResId = R.string.message_status_downloading; |
| break; |
| |
| case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: |
| if (!OsUtil.isSecondaryUser()) { |
| titleResId = R.string.message_title_manual_download; |
| statusResId = R.string.message_status_download; |
| } |
| break; |
| |
| case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: |
| if (!OsUtil.isSecondaryUser()) { |
| titleResId = R.string.message_title_download_failed; |
| statusResId = R.string.message_status_download_error; |
| showInRed = true; |
| } |
| break; |
| |
| case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: |
| if (!OsUtil.isSecondaryUser()) { |
| titleResId = R.string.message_title_download_failed; |
| statusResId = R.string.message_status_download; |
| showInRed = true; |
| } |
| break; |
| |
| case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: |
| case MessageData.BUGLE_STATUS_OUTGOING_SENDING: |
| statusResId = R.string.message_status_sending; |
| break; |
| |
| case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: |
| case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: |
| statusResId = R.string.message_status_send_retrying; |
| break; |
| |
| case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: |
| statusResId = R.string.message_status_send_failed_emergency_number; |
| showInRed = true; |
| break; |
| |
| case MessageData.BUGLE_STATUS_OUTGOING_FAILED: |
| // don't show the error state unless we're the default sms app |
| if (PhoneUtils.getDefault().isDefaultSmsApp()) { |
| statusResId = MmsUtils.mapRawStatusToErrorResourceId( |
| message.getStatus(), message.getRawTelephonyStatus()); |
| showInRed = true; |
| break; |
| } |
| // FALL THROUGH HERE |
| |
| case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: |
| case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: |
| case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: |
| default: |
| if (!message.getCanClusterWithNextMessage()) { |
| statusText = Dates.getWidgetTimeString(message.getReceivedTimeStamp(), |
| false /*abbreviated*/).toString(); |
| } |
| break; |
| } |
| |
| // Build the content description while we're populating the various fields. |
| final StringBuilder description = new StringBuilder(); |
| final String separator = mContext.getString(R.string.enumeration_comma); |
| // Sender information |
| final boolean hasPlainTextMessage = !(TextUtils.isEmpty(message.getText())); |
| if (message.getIsIncoming()) { |
| int senderResId = hasPlainTextMessage |
| ? R.string.incoming_text_sender_content_description |
| : R.string.incoming_sender_content_description; |
| description.append(mContext.getString(senderResId, message.getSenderDisplayName())); |
| } else { |
| int senderResId = hasPlainTextMessage |
| ? R.string.outgoing_text_sender_content_description |
| : R.string.outgoing_sender_content_description; |
| description.append(mContext.getString(senderResId)); |
| } |
| |
| final boolean titleVisible = (titleResId >= 0); |
| if (titleVisible) { |
| final String titleText = mContext.getString(titleResId); |
| remoteViews.setTextViewText(R.id.message, titleText); |
| |
| final String mmsInfoText = mContext.getString( |
| R.string.mms_info, |
| Formatter.formatFileSize(mContext, message.getSmsMessageSize()), |
| DateUtils.formatDateTime( |
| mContext, |
| message.getMmsExpiry(), |
| DateUtils.FORMAT_SHOW_DATE | |
| DateUtils.FORMAT_SHOW_TIME | |
| DateUtils.FORMAT_NUMERIC_DATE | |
| DateUtils.FORMAT_NO_YEAR)); |
| remoteViews.setTextViewText(R.id.date, mmsInfoText); |
| description.append(separator); |
| description.append(mmsInfoText); |
| } else if (!TextUtils.isEmpty(messageText)) { |
| remoteViews.setTextViewText(R.id.message, messageText); |
| description.append(separator); |
| description.append(messageText); |
| } else { |
| remoteViews.setViewVisibility(R.id.message, View.GONE); |
| } |
| |
| final String subjectText = MmsUtils.cleanseMmsSubject(mContext.getResources(), |
| message.getMmsSubject()); |
| if (!TextUtils.isEmpty(subjectText)) { |
| description.append(separator); |
| description.append(subjectText); |
| } |
| |
| if (statusResId >= 0) { |
| statusText = mContext.getString(statusResId); |
| final Spannable colorStr = new SpannableString(statusText); |
| if (showInRed) { |
| colorStr.setSpan(new ForegroundColorSpan( |
| mContext.getResources().getColor(R.color.timestamp_text_failed)), |
| 0, statusText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| remoteViews.setTextViewText(R.id.date, colorStr); |
| description.append(separator); |
| description.append(colorStr); |
| } else { |
| description.append(separator); |
| description.append(Dates.getWidgetTimeString(message.getReceivedTimeStamp(), |
| false /*abbreviated*/)); |
| } |
| |
| if (message.hasAttachments()) { |
| final List<MessagePartData> attachments = message.getAttachments(); |
| int stringId; |
| for (MessagePartData part : attachments) { |
| if (part.isImage()) { |
| stringId = R.string.conversation_list_snippet_picture; |
| } else if (part.isVideo()) { |
| stringId = R.string.conversation_list_snippet_video; |
| } else if (part.isAudio()) { |
| stringId = R.string.conversation_list_snippet_audio_clip; |
| } else if (part.isVCard()) { |
| stringId = R.string.conversation_list_snippet_vcard; |
| } else { |
| stringId = 0; |
| } |
| if (stringId > 0) { |
| description.append(separator); |
| description.append(mContext.getString(stringId)); |
| } |
| } |
| } |
| remoteViews.setContentDescription(message.getIsIncoming() ? |
| R.id.widget_message_item_incoming : |
| R.id.widget_message_item_outgoing, description); |
| } |
| |
| private Bitmap getAttachmentBitmap(final MessagePartData part) { |
| UriImageRequestDescriptor descriptor; |
| if (part.isImage()) { |
| descriptor = new MessagePartImageRequestDescriptor(part, |
| IMAGE_ATTACHMENT_SIZE, // desiredWidth |
| IMAGE_ATTACHMENT_SIZE, // desiredHeight |
| true // isStatic |
| ); |
| } else if (part.isVideo()) { |
| descriptor = new MessagePartVideoThumbnailRequestDescriptor(part); |
| } else { |
| return null; |
| } |
| |
| final MediaRequest<ImageResource> imageRequest = |
| descriptor.buildSyncMediaRequest(mContext); |
| final ImageResource imageResource = |
| MediaResourceManager.get().requestMediaResourceSync(imageRequest); |
| if (imageResource != null && imageResource.getBitmap() != null) { |
| setImageResource(imageResource); |
| return Bitmap.createBitmap(imageResource.getBitmap()); |
| } else { |
| releaseImageResource(); |
| return null; |
| } |
| } |
| |
| /** |
| * @return the "View more messages" view. When the user taps this item, they're |
| * taken to the conversation in Bugle. |
| */ |
| @Override |
| protected RemoteViews getViewMoreItemsView() { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "getViewMoreConversationsView"); |
| } |
| final RemoteViews view = new RemoteViews(mContext.getPackageName(), |
| R.layout.widget_loading); |
| view.setTextViewText( |
| R.id.loading_text, mContext.getText(R.string.view_more_messages)); |
| |
| // Tapping this "More messages" item should take us to the conversation. |
| final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext, |
| mConversationId, null /* draft */); |
| view.setOnClickFillInIntent(R.id.widget_loading, intent); |
| return view; |
| } |
| |
| @Override |
| public RemoteViews getLoadingView() { |
| final RemoteViews view = new RemoteViews(mContext.getPackageName(), |
| R.layout.widget_loading); |
| view.setTextViewText( |
| R.id.loading_text, mContext.getText(R.string.loading_messages)); |
| return view; |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| return 3; // Number of different list items that can be returned - |
| // 1- incoming list item |
| // 2- outgoing list item |
| // 3- more items list item |
| } |
| |
| @Override |
| protected int getMainLayoutId() { |
| return R.layout.widget_conversation; |
| } |
| |
| private void setImageResource(final ImageResource resource) { |
| if (mImageResource != resource) { |
| // Clear out any information for what is currently used |
| releaseImageResource(); |
| mImageResource = resource; |
| } |
| } |
| |
| private void releaseImageResource() { |
| if (mImageResource != null) { |
| mImageResource.release(); |
| } |
| mImageResource = null; |
| } |
| } |
| |
| } |