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