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