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