blob: 5b8587b3d7fbc8d5534b633385a57ca7d2dff489 [file] [log] [blame]
/*
* 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;
}
}
}