1 /*
2  * Copyright (C) 2012 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.mms.widget;
18 
19 import android.app.PendingIntent;
20 import android.appwidget.AppWidgetManager;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.database.Cursor;
25 import android.provider.Telephony.Threads;
26 import android.text.Spannable;
27 import android.text.SpannableStringBuilder;
28 import android.text.style.ForegroundColorSpan;
29 import android.text.style.TextAppearanceSpan;
30 import android.util.Log;
31 import android.view.View;
32 import android.widget.RemoteViews;
33 import android.widget.RemoteViewsService;
34 
35 import com.android.mms.LogTag;
36 import com.android.mms.R;
37 import com.android.mms.data.Contact;
38 import com.android.mms.data.Conversation;
39 import com.android.mms.ui.ConversationList;
40 import com.android.mms.ui.ConversationListItem;
41 import com.android.mms.ui.MessageUtils;
42 
43 public class MmsWidgetService extends RemoteViewsService {
44     private static final String TAG = LogTag.TAG;
45 
46     /**
47      * Lock to avoid race condition between widgets.
48      */
49     private static final Object sWidgetLock = new Object();
50 
51     @Override
onGetViewFactory(Intent intent)52     public RemoteViewsFactory onGetViewFactory(Intent intent) {
53         if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
54             Log.v(TAG, "onGetViewFactory intent: " + intent);
55         }
56         return new MmsFactory(getApplicationContext(), intent);
57     }
58 
59     /**
60      * Remote Views Factory for Mms Widget.
61      */
62     private static class MmsFactory
63             implements RemoteViewsService.RemoteViewsFactory, Contact.UpdateListener {
64         private static final int MAX_CONVERSATIONS_COUNT = 25;
65         private final Context mContext;
66         private final int mAppWidgetId;
67         private boolean mShouldShowViewMore;
68         private Cursor mConversationCursor;
69         private int mUnreadConvCount;
70         private final AppWidgetManager mAppWidgetManager;
71 
72         // Static colors
73         private static int SUBJECT_TEXT_COLOR_READ;
74         private static int SUBJECT_TEXT_COLOR_UNREAD;
75         private static int SENDERS_TEXT_COLOR_READ;
76         private static int SENDERS_TEXT_COLOR_UNREAD;
77 
MmsFactory(Context context, Intent intent)78         public MmsFactory(Context context, Intent intent) {
79             mContext = context;
80             mAppWidgetId = intent.getIntExtra(
81                     AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
82             mAppWidgetManager = AppWidgetManager.getInstance(context);
83             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
84                 Log.v(TAG, "MmsFactory intent: " + intent + "widget id: " + mAppWidgetId);
85             }
86             // Initialize colors
87             Resources res = context.getResources();
88             SENDERS_TEXT_COLOR_READ = res.getColor(R.color.widget_sender_text_color_read);
89             SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_sender_text_color_unread);
90             SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.widget_subject_text_color_read);
91             SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_subject_text_color_unread);
92         }
93 
94         @Override
onCreate()95         public void onCreate() {
96             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
97                 Log.v(TAG, "onCreate");
98             }
99             Contact.addListener(this);
100         }
101 
102         @Override
onDestroy()103         public void onDestroy() {
104             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
105                 Log.v(TAG, "onDestroy");
106             }
107             synchronized (sWidgetLock) {
108                 if (mConversationCursor != null && !mConversationCursor.isClosed()) {
109                     mConversationCursor.close();
110                     mConversationCursor = null;
111                 }
112                 Contact.removeListener(this);
113             }
114         }
115 
116         @Override
onDataSetChanged()117         public void onDataSetChanged() {
118             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
119                 Log.v(TAG, "onDataSetChanged");
120             }
121             synchronized (sWidgetLock) {
122                 if (mConversationCursor != null) {
123                     mConversationCursor.close();
124                     mConversationCursor = null;
125                 }
126                 mConversationCursor = queryAllConversations();
127                 mUnreadConvCount = queryUnreadCount();
128                 onLoadComplete();
129             }
130         }
131 
queryAllConversations()132         private Cursor queryAllConversations() {
133             return mContext.getContentResolver().query(
134                     Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION,
135                     null, null, null);
136         }
137 
queryUnreadCount()138         private int queryUnreadCount() {
139             Cursor cursor = null;
140             int unreadCount = 0;
141             try {
142                 cursor = mContext.getContentResolver().query(
143                     Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION,
144                     Threads.READ + "=0", null, null);
145                 if (cursor != null) {
146                     unreadCount = cursor.getCount();
147                 }
148             } finally {
149                 if (cursor != null) {
150                     cursor.close();
151                 }
152             }
153             return unreadCount;
154         }
155 
156         /**
157          * Returns the number of items should be shown in the widget list.  This method also updates
158          * the boolean that indicates whether the "show more" item should be shown.
159          * @return the number of items to be displayed in the list.
160          */
161         @Override
getCount()162         public int getCount() {
163             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
164                 Log.v(TAG, "getCount");
165             }
166             synchronized (sWidgetLock) {
167                 if (mConversationCursor == null) {
168                     return 0;
169                 }
170                 final int count = getConversationCount();
171                 mShouldShowViewMore = count < mConversationCursor.getCount();
172                 return count + (mShouldShowViewMore ? 1 : 0);
173             }
174         }
175 
176         /**
177          * Returns the number of conversations that should be shown in the widget.  This method
178          * doesn't update the boolean that indicates that the "show more" item should be included
179          * in the list.
180          * @return
181          */
182         private int getConversationCount() {
183             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
184                 Log.v(TAG, "getConversationCount");
185             }
186 
187             return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT);
188         }
189 
190         /*
191          * Add color to a given text
192          */
193         private SpannableStringBuilder addColor(CharSequence text, int color) {
194             SpannableStringBuilder builder = new SpannableStringBuilder(text);
195             if (color != 0) {
196                 builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
197                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
198             }
199             return builder;
200         }
201 
202         /**
203          * @return the {@link RemoteViews} for a specific position in the list.
204          */
205         @Override
206         public RemoteViews getViewAt(int position) {
207             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
208                 Log.v(TAG, "getViewAt position: " + position);
209             }
210             synchronized (sWidgetLock) {
211                 // "View more conversations" view.
212                 if (mConversationCursor == null
213                         || (mShouldShowViewMore && position >= getConversationCount())) {
214                     return getViewMoreConversationsView();
215                 }
216 
217                 if (!mConversationCursor.moveToPosition(position)) {
218                     // If we ever fail to move to a position, return the "View More conversations"
219                     // view.
220                     Log.w(TAG, "Failed to move to position: " + position);
221                     return getViewMoreConversationsView();
222                 }
223 
224                 Conversation conv = Conversation.from(mContext, mConversationCursor);
225 
226                 // Inflate and fill out the remote view
227                 RemoteViews remoteViews = new RemoteViews(
228                         mContext.getPackageName(), R.layout.widget_conversation);
229 
230                 if (conv.hasUnreadMessages()) {
231                     remoteViews.setViewVisibility(R.id.widget_unread_background, View.VISIBLE);
232                     remoteViews.setViewVisibility(R.id.widget_read_background, View.GONE);
233                 } else {
234                     remoteViews.setViewVisibility(R.id.widget_unread_background, View.GONE);
235                     remoteViews.setViewVisibility(R.id.widget_read_background, View.VISIBLE);
236                 }
237                 boolean hasAttachment = conv.hasAttachment();
238                 remoteViews.setViewVisibility(R.id.attachment, hasAttachment ? View.VISIBLE :
239                     View.GONE);
240 
241                 // Date
242                 remoteViews.setTextViewText(R.id.date,
243                         addColor(MessageUtils.formatTimeStampString(mContext, conv.getDate()),
244                                 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD :
245                                     SUBJECT_TEXT_COLOR_READ));
246 
247                 // From
248                 int color = conv.hasUnreadMessages() ? SENDERS_TEXT_COLOR_UNREAD :
249                         SENDERS_TEXT_COLOR_READ;
250                 SpannableStringBuilder from = addColor(conv.getRecipients().formatNames(", "),
251                         color);
252 
253                 if (conv.hasDraft()) {
254                     from.append(mContext.getResources().getString(R.string.draft_separator));
255                     int before = from.length();
256                     from.append(mContext.getResources().getString(R.string.has_draft));
257                     from.setSpan(new TextAppearanceSpan(mContext,
258                             android.R.style.TextAppearance_Small, color), before,
259                             from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
260                     from.setSpan(new ForegroundColorSpan(
261                             mContext.getResources().getColor(R.drawable.text_color_red)),
262                             before, from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
263                 }
264 
265                 // Unread messages are shown in bold
266                 if (conv.hasUnreadMessages()) {
267                     from.setSpan(ConversationListItem.STYLE_BOLD, 0, from.length(),
268                             Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
269                 }
270                 remoteViews.setTextViewText(R.id.from, from);
271 
272                 // Subject
273                 remoteViews.setTextViewText(R.id.subject,
274                         addColor(conv.getSnippet(),
275                                 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD :
276                                     SUBJECT_TEXT_COLOR_READ));
277 
278                 // On click intent.
279                 Intent clickIntent = new Intent(Intent.ACTION_VIEW);
280                 clickIntent.setType("vnd.android-dir/mms-sms");
281                 clickIntent.putExtra("thread_id", conv.getThreadId());
282 
283                 remoteViews.setOnClickFillInIntent(R.id.widget_conversation, clickIntent);
284 
285                 return remoteViews;
286             }
287         }
288 
289         /**
290          * @return the "View more conversations" view. When the user taps this item, they're
291          * taken to the messaging app's conversation list.
292          */
getViewMoreConversationsView()293         private RemoteViews getViewMoreConversationsView() {
294             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
295                 Log.v(TAG, "getViewMoreConversationsView");
296             }
297             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
298             view.setTextViewText(
299                     R.id.loading_text, mContext.getText(R.string.view_more_conversations));
300             PendingIntent pendingIntent =
301                     PendingIntent.getActivity(mContext, 0, new Intent(mContext,
302                             ConversationList.class),
303                             PendingIntent.FLAG_UPDATE_CURRENT);
304             view.setOnClickPendingIntent(R.id.widget_loading, pendingIntent);
305             return view;
306         }
307 
308         @Override
getLoadingView()309         public RemoteViews getLoadingView() {
310             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
311             view.setTextViewText(
312                     R.id.loading_text, mContext.getText(R.string.loading_conversations));
313             return view;
314         }
315 
316         @Override
getViewTypeCount()317         public int getViewTypeCount() {
318             return 2;
319         }
320 
321         @Override
getItemId(int position)322         public long getItemId(int position) {
323             return position;
324         }
325 
326         @Override
hasStableIds()327         public boolean hasStableIds() {
328             return true;
329         }
330 
onLoadComplete()331         private void onLoadComplete() {
332             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
333                 Log.v(TAG, "onLoadComplete");
334             }
335             RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget);
336 
337             remoteViews.setViewVisibility(R.id.widget_unread_count, mUnreadConvCount > 0 ?
338                     View.VISIBLE : View.GONE);
339             if (mUnreadConvCount > 0) {
340                 remoteViews.setTextViewText(
341                         R.id.widget_unread_count, Integer.toString(mUnreadConvCount));
342             }
343 
344             mAppWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
345         }
346 
onUpdate(Contact updated)347         public void onUpdate(Contact updated) {
348             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
349                 Log.v(TAG, "onUpdate from Contact: " + updated);
350             }
351             mAppWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.conversation_list);
352         }
353 
354     }
355 }
356