WidgetService.java revision 8a8c50d8fcc4f20549c9f395edbad017a940e72b
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 */
16package com.android.mail.widget;
17
18import com.android.mail.R;
19import com.android.mail.browse.ConversationCursor;
20import com.android.mail.providers.Account;
21import com.android.mail.providers.Conversation;
22import com.android.mail.providers.Folder;
23import com.android.mail.providers.UIProvider;
24import com.android.mail.utils.DelayedTaskHandler;
25import com.android.mail.utils.LogUtils;
26import com.android.mail.utils.Utils;
27
28import android.appwidget.AppWidgetManager;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.CursorLoader;
32import android.content.Intent;
33import android.content.Loader;
34import android.content.Loader.OnLoadCompleteListener;
35import android.database.Cursor;
36import android.net.Uri;
37import android.os.Looper;
38import android.text.SpannableStringBuilder;
39import android.text.TextUtils;
40import android.text.format.DateUtils;
41import android.view.View;
42import android.widget.RemoteViews;
43import android.widget.RemoteViewsService;
44
45import java.util.Map;
46
47public class WidgetService extends RemoteViewsService {
48    /**
49     * Lock to avoid race condition between widgets.
50     */
51    private static Object sWidgetLock = new Object();
52
53    @Override
54    public RemoteViewsFactory onGetViewFactory(Intent intent) {
55        return new MailFactory(getApplicationContext(), intent);
56    }
57
58    /**
59     * Remote Views Factory for Mail Widget.
60     */
61    private static class MailFactory
62            implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> {
63        private static final int MAX_CONVERSATIONS_COUNT = 25;
64        private static final int MAX_SENDERS_LENGTH = 25;
65        private static final String LOG_TAG = new LogUtils().getLogTag();
66
67        private final Context mContext;
68        private final int mAppWidgetId;
69        private final Account mAccount;
70        private final Folder mFolder;
71        private final WidgetConversationViewBuilder mWidgetConversationViewBuilder;
72        private Cursor mConversationCursor;
73        private CursorLoader mFolderLoader;
74        private FolderUpdateHandler mFolderUpdateHandler;
75        private int mFolderCount;
76        private boolean mShouldShowViewMore;
77        private boolean mFolderInformationShown = false;
78        private ContentResolver mResolver;
79
80        public MailFactory(Context context, Intent intent) {
81            mContext = context;
82            mAppWidgetId = intent.getIntExtra(
83                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
84            mAccount = intent.getParcelableExtra(WidgetProvider.EXTRA_ACCOUNT);
85            final Folder folder = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER);
86            mFolder = folder != null ? folder : mAccount.getAccountInbox();
87            mWidgetConversationViewBuilder = new WidgetConversationViewBuilder(mContext, mAccount);
88            mResolver = context.getContentResolver();
89        }
90
91        @Override
92        public void onCreate() {
93            // Save the map between widgetId and account to preference
94            BaseWidgetProvider.saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolder);
95
96            // If the account of this widget has been removed, we want to update the widget to
97            // "Tap to configure" mode.
98            if (!BaseWidgetProvider.isWidgetConfigured(mContext, mAppWidgetId, mAccount, mFolder)) {
99                BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolder);
100            }
101
102            mConversationCursor = mResolver.query(Uri.parse(mFolder.conversationListUri),
103                    UIProvider.CONVERSATION_PROJECTION, null, null, null);
104
105            mFolderLoader = new CursorLoader(mContext, Uri.parse(mFolder.uri),
106                    UIProvider.FOLDERS_PROJECTION, null, null, null);
107            mFolderLoader.registerListener(0, this);
108            mFolderUpdateHandler = new FolderUpdateHandler(mContext.getResources().getInteger(
109                    R.integer.widget_folder_refresh_delay_ms));
110            mFolderUpdateHandler.scheduleTask();
111
112        }
113
114        @Override
115        public void onDestroy() {
116            synchronized (sWidgetLock) {
117                if (mConversationCursor != null && !mConversationCursor.isClosed()) {
118                    mConversationCursor.close();
119                    mConversationCursor = null;
120                }
121            }
122
123            if (mFolderLoader != null) {
124                mFolderLoader.reset();
125                mFolderLoader = null;
126            }
127        }
128
129        @Override
130        public void onDataSetChanged() {
131            synchronized (sWidgetLock) {
132                // TODO: use loader manager.
133                mConversationCursor.requery();
134            }
135            mFolderUpdateHandler.scheduleTask();
136        }
137
138        /**
139         * Returns the number of items should be shown in the widget list.  This method also updates
140         * the boolean that indicates whether the "show more" item should be shown.
141         * @return the number of items to be displayed in the list.
142         */
143        @Override
144        public int getCount() {
145            synchronized (sWidgetLock) {
146                final int count = getConversationCount();
147                mShouldShowViewMore = count < mConversationCursor.getCount()
148                        || count < mFolderCount;
149                return count + (mShouldShowViewMore ? 1 : 0);
150            }
151        }
152
153        /**
154         * Returns the number of conversations that should be shown in the widget.  This method
155         * doesn't update the boolean that indicates that the "show more" item should be included
156         * in the list.
157         * @return
158         */
159        private int getConversationCount() {
160            synchronized (sWidgetLock) {
161                return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT);
162            }
163        }
164
165        /**
166         * @return the {@link RemoteViews} for a specific position in the list.
167         */
168        @Override
169        public RemoteViews getViewAt(int position) {
170            synchronized (sWidgetLock) {
171                // "View more conversations" view.
172                if (mConversationCursor == null
173                        || (mShouldShowViewMore && position >= getConversationCount())) {
174                    return getViewMoreConversationsView();
175                }
176
177                if (!mConversationCursor.moveToPosition(position)) {
178                    // If we ever fail to move to a position, return the "View More conversations"
179                    // view.
180                    LogUtils.e(LOG_TAG,
181                            "Failed to move to position %d in the cursor.", position);
182                    return getViewMoreConversationsView();
183                }
184
185                Conversation conversation = Conversation.from(mConversationCursor);
186                // Split the senders and status from the instructions.
187                SpannableStringBuilder senderBuilder = new SpannableStringBuilder();
188                SpannableStringBuilder statusBuilder = new SpannableStringBuilder();
189                Utils.getStyledSenderSnippet(mContext, conversation.senders, senderBuilder,
190                        statusBuilder, MAX_SENDERS_LENGTH, false, false, false);
191
192                // Get styled date.
193                CharSequence date = DateUtils.getRelativeTimeSpanString(
194                        mContext, conversation.dateMs);
195
196                // Load up our remote view.
197                RemoteViews remoteViews = mWidgetConversationViewBuilder.getStyledView(
198                        senderBuilder, statusBuilder, date, filterTag(conversation.subject),
199                        conversation.snippet, conversation.folderList, conversation.hasAttachments);
200
201                // On click intent.
202                remoteViews.setOnClickFillInIntent(R.id.widget_conversation,
203                        Utils.createViewConversationIntent(mContext, mAccount, mFolder,
204                                        conversation.id));
205
206                return remoteViews;
207            }
208        }
209
210        /**
211         * @return the "View more conversations" view.
212         */
213        private RemoteViews getViewMoreConversationsView() {
214            RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
215            view.setTextViewText(
216                    R.id.loading_text, mContext.getText(R.string.view_more_conversations));
217            view.setOnClickFillInIntent(R.id.widget_loading,
218                    Utils.createViewConversationIntent(mContext, mAccount, mFolder,
219                            UIProvider.INVALID_CONVERSATION_ID));
220            return view;
221        }
222
223        @Override
224        public RemoteViews getLoadingView() {
225            RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
226            view.setTextViewText(
227                    R.id.loading_text, mContext.getText(R.string.loading_conversation));
228            return view;
229        }
230
231        @Override
232        public int getViewTypeCount() {
233            return 2;
234        }
235
236        @Override
237        public long getItemId(int position) {
238            return position;
239        }
240
241        @Override
242        public boolean hasStableIds() {
243            return false;
244        }
245
246        @Override
247        public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
248            if (!data.moveToFirst()) {
249                return;
250            }
251
252            final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
253            final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN);
254            mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
255
256            RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget);
257            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
258
259            if (!mFolderInformationShown && !TextUtils.isEmpty(folderName)) {
260                // We want to do a full update to the widget at least once, as the widget
261                // manager doesn't cache the state of the remote views when doing a partial
262                // widget update. This causes the label name to be shown as blank if the state
263                // of the widget is restored.
264                BaseWidgetProvider.configureValidAccountWidget(
265                        mContext, remoteViews, mAppWidgetId, mAccount, mFolder, folderName);
266                appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
267                mFolderInformationShown = true;
268            }
269
270            remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
271            remoteViews.setTextViewText(R.id.widget_folder, folderName);
272            remoteViews.setViewVisibility(R.id.widget_unread_count, View.VISIBLE);
273            remoteViews.setTextViewText(
274                    R.id.widget_unread_count, Utils.getUnreadCountString(mContext, unreadCount));
275
276            appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
277        }
278
279        /**
280         * If the subject contains the tag of a mailing-list (text surrounded with []), return the
281         * subject with that tag ellipsized, e.g. "[android-gmail-team] Hello" -> "[andr...] Hello"
282         */
283        private static String filterTag(String subject) {
284            String result = subject;
285            if (subject.length() > 0 && subject.charAt(0) == '[') {
286                int end = subject.indexOf(']');
287                if (end > 0) {
288                    String tag = subject.substring(1, end);
289                    result = "[" + Utils.ellipsize(tag, 7) + "]" + subject.substring(end + 1);
290                }
291            }
292
293            return result;
294        }
295
296        /**
297         * A {@link DelayedTaskHandler} to throttle label update to a reasonable rate.
298         */
299        private class FolderUpdateHandler extends DelayedTaskHandler {
300            public FolderUpdateHandler(int refreshDelay) {
301                super(Looper.myLooper(), refreshDelay);
302            }
303
304            @Override
305            protected void performTask() {
306                // Start the loader. The cached data will be returned if present.
307                if (mFolderLoader != null) {
308                    mFolderLoader.startLoading();
309                }
310            }
311        }
312    }
313}
314