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
17package com.android.messaging.widget;
18
19import android.content.Context;
20import android.content.Intent;
21import android.database.Cursor;
22import android.graphics.Bitmap;
23import android.net.Uri;
24import android.os.Bundle;
25import android.text.Spannable;
26import android.text.SpannableString;
27import android.text.TextUtils;
28import android.text.format.DateUtils;
29import android.text.format.Formatter;
30import android.text.style.ForegroundColorSpan;
31import android.view.View;
32import android.widget.RemoteViews;
33import android.widget.RemoteViewsService;
34
35import com.android.messaging.R;
36import com.android.messaging.datamodel.MessagingContentProvider;
37import com.android.messaging.datamodel.data.ConversationMessageData;
38import com.android.messaging.datamodel.data.MessageData;
39import com.android.messaging.datamodel.data.MessagePartData;
40import com.android.messaging.datamodel.media.ImageResource;
41import com.android.messaging.datamodel.media.MediaRequest;
42import com.android.messaging.datamodel.media.MediaResourceManager;
43import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor;
44import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor;
45import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
46import com.android.messaging.datamodel.media.VideoThumbnailRequest;
47import com.android.messaging.sms.MmsUtils;
48import com.android.messaging.ui.UIIntents;
49import com.android.messaging.util.AvatarUriUtil;
50import com.android.messaging.util.Dates;
51import com.android.messaging.util.LogUtil;
52import com.android.messaging.util.OsUtil;
53import com.android.messaging.util.PhoneUtils;
54
55import java.util.List;
56
57public 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