Home | History | Annotate | Download | only in widget
      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