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.content.res.Resources;
22 import android.database.Cursor;
23 import android.graphics.Typeface;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.text.Spannable;
27 import android.text.SpannableStringBuilder;
28 import android.text.TextPaint;
29 import android.text.TextUtils;
30 import android.text.style.ForegroundColorSpan;
31 import android.text.style.StyleSpan;
32 import android.view.View;
33 import android.widget.RemoteViews;
34 import android.widget.RemoteViewsService;
35 
36 import com.android.messaging.R;
37 import com.android.messaging.datamodel.MessagingContentProvider;
38 import com.android.messaging.datamodel.data.ConversationListData;
39 import com.android.messaging.datamodel.data.ConversationListItemData;
40 import com.android.messaging.sms.MmsUtils;
41 import com.android.messaging.ui.UIIntents;
42 import com.android.messaging.ui.conversationlist.ConversationListItemView;
43 import com.android.messaging.util.ContentType;
44 import com.android.messaging.util.Dates;
45 import com.android.messaging.util.LogUtil;
46 import com.android.messaging.util.OsUtil;
47 import com.android.messaging.util.PhoneUtils;
48 
49 public class WidgetConversationListService extends RemoteViewsService {
50     private static final String TAG = LogUtil.BUGLE_WIDGET_TAG;
51 
52     @Override
onGetViewFactory(Intent intent)53     public RemoteViewsFactory onGetViewFactory(Intent intent) {
54         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
55             LogUtil.v(TAG, "onGetViewFactory intent: " + intent);
56         }
57         return new WidgetConversationListFactory(getApplicationContext(), intent);
58     }
59 
60     /**
61      * Remote Views Factory for Bugle Widget.
62      */
63     private static class WidgetConversationListFactory extends BaseWidgetFactory {
64 
WidgetConversationListFactory(Context context, Intent intent)65         public WidgetConversationListFactory(Context context, Intent intent) {
66             super(context, intent);
67         }
68 
69         @Override
doQuery()70         protected Cursor doQuery() {
71             return  mContext.getContentResolver().query(MessagingContentProvider.CONVERSATIONS_URI,
72                     ConversationListItemData.PROJECTION,
73                     ConversationListData.WHERE_NOT_ARCHIVED,
74                     null,       // selection args
75                     ConversationListData.SORT_ORDER);
76         }
77 
78         /**
79          * @return the {@link RemoteViews} for a specific position in the list.
80          */
81         @Override
getViewAt(int position)82         public RemoteViews getViewAt(int position) {
83             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
84                 LogUtil.v(TAG, "getViewAt position: " + position);
85             }
86             synchronized (sWidgetLock) {
87                 // "View more conversations" view.
88                 if (mCursor == null
89                         || (mShouldShowViewMore && position >= getItemCount())) {
90                     return getViewMoreItemsView();
91                 }
92 
93                 if (!mCursor.moveToPosition(position)) {
94                     // If we ever fail to move to a position, return the "View More conversations"
95                     // view.
96                     LogUtil.w(TAG, "Failed to move to position: " + position);
97                     return getViewMoreItemsView();
98                 }
99 
100                 final ConversationListItemData conv = new ConversationListItemData();
101                 conv.bind(mCursor);
102 
103                 // Inflate and fill out the remote view
104                 final RemoteViews remoteViews = new RemoteViews(
105                         mContext.getPackageName(), R.layout.widget_conversation_list_item);
106 
107                 final boolean hasUnreadMessages = !conv.getIsRead();
108                 final Resources resources = mContext.getResources();
109                 final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
110 
111                 final String timeStamp = conv.getIsSendRequested() ?
112                         resources.getString(R.string.message_status_sending) :
113                             Dates.getWidgetTimeString(conv.getTimestamp(), true /*abbreviated*/)
114                                 .toString();
115                 // Date/Timestamp or Sending or Error state -- all shown in the date item
116                 remoteViews.setTextViewText(R.id.date,
117                         boldifyIfUnread(timeStamp, hasUnreadMessages));
118 
119                 // From
120                 remoteViews.setTextViewText(R.id.from,
121                         boldifyIfUnread(conv.getName(), hasUnreadMessages));
122 
123                 // Notifications turned off mini-bell icon
124                 remoteViews.setViewVisibility(R.id.conversation_notification_bell,
125                         conv.getNotificationEnabled() ? View.GONE : View.VISIBLE);
126 
127                 // On click intent.
128                 final Intent intent = UIIntents.get().getIntentForConversationActivity(mContext,
129                         conv.getConversationId(), null /* draft */);
130 
131                 remoteViews.setOnClickFillInIntent(R.id.widget_conversation_list_item, intent);
132 
133                 // Avatar
134                 boolean includeAvatar;
135                 if (OsUtil.isAtLeastJB()) {
136                     final Bundle options = mAppWidgetManager.getAppWidgetOptions(mAppWidgetId);
137                     if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
138                         LogUtil.v(TAG, "getViewAt BugleWidgetProvider.WIDGET_SIZE_KEY: " +
139                                 options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY));
140                     }
141 
142                     includeAvatar = options.getInt(BugleWidgetProvider.WIDGET_SIZE_KEY) ==
143                             BugleWidgetProvider.SIZE_LARGE;
144                 } else {
145                     includeAvatar = true;;
146                 }
147 
148                 // Show the avatar when grande size, otherwise hide it.
149                 remoteViews.setViewVisibility(R.id.avatarView, includeAvatar ?
150                         View.VISIBLE : View.GONE);
151 
152                 Uri iconUri = null;
153                 if (conv.getIcon() != null) {
154                     iconUri = Uri.parse(conv.getIcon());
155                 }
156                 remoteViews.setImageViewBitmap(R.id.avatarView, includeAvatar ?
157                         getAvatarBitmap(iconUri) : null);
158 
159                 // Error
160                 // Only show the fail icon if it is not a group conversation.
161                 // And also require that we be the default sms app.
162                 final boolean showError = conv.getIsFailedStatus() &&
163                         isDefaultSmsApp;
164                 final boolean showDraft = conv.getShowDraft() &&
165                         isDefaultSmsApp;
166                 remoteViews.setViewVisibility(R.id.conversation_failed_status_icon,
167                         showError && includeAvatar ?
168                         View.VISIBLE : View.GONE);
169 
170                 if (showError || showDraft) {
171                     remoteViews.setViewVisibility(R.id.snippet, View.GONE);
172                     remoteViews.setViewVisibility(R.id.errorBlock, View.VISIBLE);
173                     remoteViews.setTextViewText(R.id.errorSnippet, getSnippetText(conv));
174 
175                     if (showDraft) {
176                         // Show italicized "Draft" on third line
177                         final String text = resources.getString(
178                                 R.string.conversation_list_item_view_draft_message);
179                         SpannableStringBuilder builder = new SpannableStringBuilder(text);
180                         builder.setSpan(new StyleSpan(Typeface.ITALIC), 0, text.length(),
181                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
182                         builder.setSpan(new ForegroundColorSpan(
183                                     resources.getColor(R.color.widget_text_color)),
184                                 0, text.length(),
185                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
186                         remoteViews.setTextViewText(R.id.errorText, builder);
187                     } else {
188                         // Show error message on third line
189                        int failureMessageId = R.string.message_status_download_failed;
190                         if (conv.getIsMessageTypeOutgoing()) {
191                             failureMessageId = MmsUtils.mapRawStatusToErrorResourceId(
192                                     conv.getMessageStatus(),
193                                     conv.getMessageRawTelephonyStatus());
194                         }
195                         remoteViews.setTextViewText(R.id.errorText,
196                                 resources.getString(failureMessageId));
197                     }
198                 } else {
199                     remoteViews.setViewVisibility(R.id.errorBlock, View.GONE);
200                     remoteViews.setViewVisibility(R.id.snippet, View.VISIBLE);
201                     remoteViews.setTextViewText(R.id.snippet,
202                             boldifyIfUnread(getSnippetText(conv), hasUnreadMessages));
203                 }
204 
205                 // Set the accessibility TalkBack text
206                 remoteViews.setContentDescription(R.id.widget_conversation_list_item,
207                         ConversationListItemView.buildContentDescription(mContext.getResources(),
208                                 conv, new TextPaint()));
209 
210                 return remoteViews;
211             }
212         }
213 
getSnippetText(final ConversationListItemData conv)214         private String getSnippetText(final ConversationListItemData conv) {
215             String snippetText = conv.getShowDraft() ?
216                     conv.getDraftSnippetText() : conv.getSnippetText();
217             final String previewContentType = conv.getShowDraft() ?
218                     conv.getDraftPreviewContentType() : conv.getPreviewContentType();
219             if (TextUtils.isEmpty(snippetText)) {
220                 Resources resources = mContext.getResources();
221                 // Use the attachment type as a snippet so the preview doesn't look odd
222                 if (ContentType.isAudioType(previewContentType)) {
223                     snippetText = resources.getString(
224                             R.string.conversation_list_snippet_audio_clip);
225                 } else if (ContentType.isImageType(previewContentType)) {
226                     snippetText = resources.getString(R.string.conversation_list_snippet_picture);
227                 } else if (ContentType.isVideoType(previewContentType)) {
228                     snippetText = resources.getString(R.string.conversation_list_snippet_video);
229                 } else if (ContentType.isVCardType(previewContentType)) {
230                     snippetText = resources.getString(R.string.conversation_list_snippet_vcard);
231                 }
232             }
233             return snippetText;
234         }
235 
236         /**
237          * @return the "View more conversations" view. When the user taps this item, they're
238          * taken to the Bugle's conversation list.
239          */
240         @Override
getViewMoreItemsView()241         protected RemoteViews getViewMoreItemsView() {
242             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
243                 LogUtil.v(TAG, "getViewMoreItemsView");
244             }
245             final RemoteViews view = new RemoteViews(mContext.getPackageName(),
246                     R.layout.widget_loading);
247             view.setTextViewText(
248                     R.id.loading_text, mContext.getText(R.string.view_more_conversations));
249 
250             // Tapping this "More conversations" item should take us to the ConversationList.
251             // However, the list view is primed with an intent to go to the Conversation activity.
252             // Each normal conversation list item sets the fill-in intent with the
253             // ConversationId for that particular conversation. In other words, the only place
254             // we can go is the ConversationActivity. We add an extra here to tell the
255             // ConversationActivity to really take us to the ConversationListActivity.
256             final Intent intent = new Intent();
257             intent.putExtra(UIIntents.UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST, true);
258             view.setOnClickFillInIntent(R.id.widget_loading, intent);
259             return view;
260         }
261 
262         @Override
getLoadingView()263         public RemoteViews getLoadingView() {
264             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
265             view.setTextViewText(
266                     R.id.loading_text, mContext.getText(R.string.loading_conversations));
267             return view;
268         }
269 
270         @Override
getViewTypeCount()271         public int getViewTypeCount() {
272             return 2;
273         }
274 
275         @Override
getMainLayoutId()276         protected int getMainLayoutId() {
277             return R.layout.widget_conversation_list;
278         }
279     }
280 
281 }
282