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