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