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
17package com.android.mms.widget;
18
19import android.app.PendingIntent;
20import android.appwidget.AppWidgetManager;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.provider.Telephony.Threads;
26import android.text.Spannable;
27import android.text.SpannableStringBuilder;
28import android.text.style.ForegroundColorSpan;
29import android.text.style.TextAppearanceSpan;
30import android.util.Log;
31import android.view.View;
32import android.widget.RemoteViews;
33import android.widget.RemoteViewsService;
34
35import com.android.mms.LogTag;
36import com.android.mms.R;
37import com.android.mms.data.Contact;
38import com.android.mms.data.Conversation;
39import com.android.mms.ui.ConversationList;
40import com.android.mms.ui.ConversationListItem;
41import com.android.mms.ui.MessageUtils;
42
43public class MmsWidgetService extends RemoteViewsService {
44    private static final String TAG = "MmsWidgetService";
45
46    /**
47     * Lock to avoid race condition between widgets.
48     */
49    private static final Object sWidgetLock = new Object();
50
51    @Override
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
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
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
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
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
132        private Cursor queryAllConversations() {
133            return mContext.getContentResolver().query(
134                    Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION,
135                    null, null, null);
136        }
137
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
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         */
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
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
317        public int getViewTypeCount() {
318            return 2;
319        }
320
321        @Override
322        public long getItemId(int position) {
323            return position;
324        }
325
326        @Override
327        public boolean hasStableIds() {
328            return true;
329        }
330
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
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