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