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