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