• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_OUTGOING_DELIVERED:
334                 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE:
335                 default:
336                     if (!message.getCanClusterWithNextMessage()) {
337                         statusText = Dates.getWidgetTimeString(message.getReceivedTimeStamp(),
338                                 false /*abbreviated*/).toString();
339                     }
340                     break;
341             }
342 
343             // Build the content description while we're populating the various fields.
344             final StringBuilder description = new StringBuilder();
345             final String separator = mContext.getString(R.string.enumeration_comma);
346             // Sender information
347             final boolean hasPlainTextMessage = !(TextUtils.isEmpty(message.getText()));
348             if (message.getIsIncoming()) {
349                 int senderResId = hasPlainTextMessage
350                     ? R.string.incoming_text_sender_content_description
351                     : R.string.incoming_sender_content_description;
352                 description.append(mContext.getString(senderResId, message.getSenderDisplayName()));
353             } else {
354                 int senderResId = hasPlainTextMessage
355                     ? R.string.outgoing_text_sender_content_description
356                     : R.string.outgoing_sender_content_description;
357                 description.append(mContext.getString(senderResId));
358             }
359 
360             final boolean titleVisible = (titleResId >= 0);
361             if (titleVisible) {
362                 final String titleText = mContext.getString(titleResId);
363                 remoteViews.setTextViewText(R.id.message, titleText);
364 
365                 final String mmsInfoText = mContext.getString(
366                         R.string.mms_info,
367                         Formatter.formatFileSize(mContext, message.getSmsMessageSize()),
368                         DateUtils.formatDateTime(
369                                 mContext,
370                                 message.getMmsExpiry(),
371                                 DateUtils.FORMAT_SHOW_DATE |
372                                 DateUtils.FORMAT_SHOW_TIME |
373                                 DateUtils.FORMAT_NUMERIC_DATE |
374                                 DateUtils.FORMAT_NO_YEAR));
375                 remoteViews.setTextViewText(R.id.date, mmsInfoText);
376                 description.append(separator);
377                 description.append(mmsInfoText);
378             } else if (!TextUtils.isEmpty(messageText)) {
379                 remoteViews.setTextViewText(R.id.message, messageText);
380                 description.append(separator);
381                 description.append(messageText);
382             } else {
383                 remoteViews.setViewVisibility(R.id.message, View.GONE);
384             }
385 
386             final String subjectText = MmsUtils.cleanseMmsSubject(mContext.getResources(),
387                     message.getMmsSubject());
388             if (!TextUtils.isEmpty(subjectText)) {
389                 description.append(separator);
390                 description.append(subjectText);
391             }
392 
393             if (statusResId >= 0) {
394                 statusText = mContext.getString(statusResId);
395                 final Spannable colorStr = new SpannableString(statusText);
396                 if (showInRed) {
397                     colorStr.setSpan(new ForegroundColorSpan(
398                             mContext.getResources().getColor(R.color.timestamp_text_failed)),
399                             0, statusText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
400                 }
401                 remoteViews.setTextViewText(R.id.date, colorStr);
402                 description.append(separator);
403                 description.append(colorStr);
404             } else {
405                 description.append(separator);
406                 description.append(Dates.getWidgetTimeString(message.getReceivedTimeStamp(),
407                         false /*abbreviated*/));
408             }
409 
410             if (message.hasAttachments()) {
411                 final List<MessagePartData> attachments = message.getAttachments();
412                 int stringId;
413                 for (MessagePartData part : attachments) {
414                     if (part.isImage()) {
415                         stringId = R.string.conversation_list_snippet_picture;
416                     } else if (part.isVideo()) {
417                         stringId = R.string.conversation_list_snippet_video;
418                     } else if (part.isAudio()) {
419                         stringId = R.string.conversation_list_snippet_audio_clip;
420                     } else if (part.isVCard()) {
421                         stringId = R.string.conversation_list_snippet_vcard;
422                     } else {
423                         stringId = 0;
424                     }
425                     if (stringId > 0) {
426                         description.append(separator);
427                         description.append(mContext.getString(stringId));
428                     }
429                 }
430             }
431             remoteViews.setContentDescription(message.getIsIncoming() ?
432                     R.id.widget_message_item_incoming :
433                         R.id.widget_message_item_outgoing, description);
434         }
435 
getAttachmentBitmap(final MessagePartData part)436         private Bitmap getAttachmentBitmap(final MessagePartData part) {
437             UriImageRequestDescriptor descriptor;
438             if (part.isImage()) {
439                 descriptor = new MessagePartImageRequestDescriptor(part,
440                         IMAGE_ATTACHMENT_SIZE, // desiredWidth
441                         IMAGE_ATTACHMENT_SIZE,  // desiredHeight
442                         true // isStatic
443                         );
444             } else if (part.isVideo()) {
445                 descriptor = new MessagePartVideoThumbnailRequestDescriptor(part);
446             } else {
447                 return null;
448             }
449 
450             final MediaRequest<ImageResource> imageRequest =
451                     descriptor.buildSyncMediaRequest(mContext);
452             final ImageResource imageResource =
453                     MediaResourceManager.get().requestMediaResourceSync(imageRequest);
454             if (imageResource != null && imageResource.getBitmap() != null) {
455                 setImageResource(imageResource);
456                 return Bitmap.createBitmap(imageResource.getBitmap());
457             } else {
458                 releaseImageResource();
459                 return null;
460             }
461         }
462 
463         /**
464          * @return the "View more messages" view. When the user taps this item, they're
465          * taken to the conversation in Bugle.
466          */
467         @Override
getViewMoreItemsView()468         protected RemoteViews getViewMoreItemsView() {
469             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
470                 LogUtil.v(TAG, "getViewMoreConversationsView");
471             }
472             final RemoteViews view = new RemoteViews(mContext.getPackageName(),
473                     R.layout.widget_loading);
474             view.setTextViewText(
475                     R.id.loading_text, mContext.getText(R.string.view_more_messages));
476 
477             // Tapping this "More messages" item should take us to the conversation.
478             final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext,
479                     mConversationId, null /* draft */);
480             view.setOnClickFillInIntent(R.id.widget_loading, intent);
481             return view;
482         }
483 
484         @Override
getLoadingView()485         public RemoteViews getLoadingView() {
486             final RemoteViews view = new RemoteViews(mContext.getPackageName(),
487                     R.layout.widget_loading);
488             view.setTextViewText(
489                     R.id.loading_text, mContext.getText(R.string.loading_messages));
490             return view;
491         }
492 
493         @Override
getViewTypeCount()494         public int getViewTypeCount() {
495             return 3;   // Number of different list items that can be returned -
496                         // 1- incoming list item
497                         // 2- outgoing list item
498                         // 3- more items list item
499         }
500 
501         @Override
getMainLayoutId()502         protected int getMainLayoutId() {
503             return R.layout.widget_conversation;
504         }
505 
setImageResource(final ImageResource resource)506         private void setImageResource(final ImageResource resource) {
507             if (mImageResource != resource) {
508                 // Clear out any information for what is currently used
509                 releaseImageResource();
510                 mImageResource = resource;
511             }
512         }
513 
releaseImageResource()514         private void releaseImageResource() {
515             if (mImageResource != null) {
516                 mImageResource.release();
517             }
518             mImageResource = null;
519         }
520     }
521 
522 }
523