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 = 163 conv.getIsFailedStatus() && !conv.getIsGroup() && isDefaultSmsApp; 164 final boolean showDraft = conv.getShowDraft() && isDefaultSmsApp; 165 remoteViews.setViewVisibility(R.id.conversation_failed_status_icon, 166 showError && includeAvatar ? 167 View.VISIBLE : View.GONE); 168 169 if (showError || showDraft) { 170 remoteViews.setViewVisibility(R.id.snippet, View.GONE); 171 remoteViews.setViewVisibility(R.id.errorBlock, View.VISIBLE); 172 remoteViews.setTextViewText(R.id.errorSnippet, getSnippetText(conv)); 173 174 if (showDraft) { 175 // Show italicized "Draft" on third line 176 final String text = resources.getString( 177 R.string.conversation_list_item_view_draft_message); 178 SpannableStringBuilder builder = new SpannableStringBuilder(text); 179 builder.setSpan(new StyleSpan(Typeface.ITALIC), 0, text.length(), 180 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 181 builder.setSpan(new ForegroundColorSpan( 182 resources.getColor(R.color.widget_text_color)), 183 0, text.length(), 184 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 185 remoteViews.setTextViewText(R.id.errorText, builder); 186 } else { 187 // Show error message on third line 188 int failureMessageId = R.string.message_status_download_failed; 189 if (conv.getIsMessageTypeOutgoing()) { 190 failureMessageId = MmsUtils.mapRawStatusToErrorResourceId( 191 conv.getMessageStatus(), 192 conv.getMessageRawTelephonyStatus()); 193 } 194 remoteViews.setTextViewText(R.id.errorText, 195 resources.getString(failureMessageId)); 196 } 197 } else { 198 remoteViews.setViewVisibility(R.id.errorBlock, View.GONE); 199 remoteViews.setViewVisibility(R.id.snippet, View.VISIBLE); 200 remoteViews.setTextViewText(R.id.snippet, 201 boldifyIfUnread(getSnippetText(conv), hasUnreadMessages)); 202 } 203 204 // Set the accessibility TalkBack text 205 remoteViews.setContentDescription(R.id.widget_conversation_list_item, 206 ConversationListItemView.buildContentDescription(mContext.getResources(), 207 conv, new TextPaint())); 208 209 return remoteViews; 210 } 211 } 212 getSnippetText(final ConversationListItemData conv)213 private String getSnippetText(final ConversationListItemData conv) { 214 String snippetText = conv.getShowDraft() ? 215 conv.getDraftSnippetText() : conv.getSnippetText(); 216 final String previewContentType = conv.getShowDraft() ? 217 conv.getDraftPreviewContentType() : conv.getPreviewContentType(); 218 if (TextUtils.isEmpty(snippetText)) { 219 Resources resources = mContext.getResources(); 220 // Use the attachment type as a snippet so the preview doesn't look odd 221 if (ContentType.isAudioType(previewContentType)) { 222 snippetText = resources.getString( 223 R.string.conversation_list_snippet_audio_clip); 224 } else if (ContentType.isImageType(previewContentType)) { 225 snippetText = resources.getString(R.string.conversation_list_snippet_picture); 226 } else if (ContentType.isVideoType(previewContentType)) { 227 snippetText = resources.getString(R.string.conversation_list_snippet_video); 228 } else if (ContentType.isVCardType(previewContentType)) { 229 snippetText = resources.getString(R.string.conversation_list_snippet_vcard); 230 } 231 } 232 return snippetText; 233 } 234 235 /** 236 * @return the "View more conversations" view. When the user taps this item, they're 237 * taken to the Bugle's conversation list. 238 */ 239 @Override getViewMoreItemsView()240 protected RemoteViews getViewMoreItemsView() { 241 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 242 LogUtil.v(TAG, "getViewMoreItemsView"); 243 } 244 final RemoteViews view = new RemoteViews(mContext.getPackageName(), 245 R.layout.widget_loading); 246 view.setTextViewText( 247 R.id.loading_text, mContext.getText(R.string.view_more_conversations)); 248 249 // Tapping this "More conversations" item should take us to the ConversationList. 250 // However, the list view is primed with an intent to go to the Conversation activity. 251 // Each normal conversation list item sets the fill-in intent with the 252 // ConversationId for that particular conversation. In other words, the only place 253 // we can go is the ConversationActivity. We add an extra here to tell the 254 // ConversationActivity to really take us to the ConversationListActivity. 255 final Intent intent = new Intent(); 256 intent.putExtra(UIIntents.UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST, true); 257 view.setOnClickFillInIntent(R.id.widget_loading, intent); 258 return view; 259 } 260 261 @Override getLoadingView()262 public RemoteViews getLoadingView() { 263 RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading); 264 view.setTextViewText( 265 R.id.loading_text, mContext.getText(R.string.loading_conversations)); 266 return view; 267 } 268 269 @Override getViewTypeCount()270 public int getViewTypeCount() { 271 return 2; 272 } 273 274 @Override getMainLayoutId()275 protected int getMainLayoutId() { 276 return R.layout.widget_conversation_list; 277 } 278 } 279 280 } 281