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