RecentFolderList.java revision 177097fad8fc26b8a215f9f1af6dd5fd2c8eb06c
1/**
2 * Copyright (c) 2011, Google Inc.
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 */
16
17package com.android.mail.ui;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.AsyncTask;
24
25import com.android.mail.content.ObjectCursor;
26import com.android.mail.providers.Account;
27import com.android.mail.providers.AccountObserver;
28import com.android.mail.providers.Folder;
29import com.android.mail.providers.Settings;
30import com.android.mail.utils.LogUtils;
31import com.android.mail.utils.LruCache;
32import com.android.mail.utils.Utils;
33import com.google.common.collect.Lists;
34
35import java.util.ArrayList;
36import java.util.Collections;
37import java.util.Comparator;
38import java.util.List;
39import java.util.concurrent.atomic.AtomicInteger;
40
41/**
42 * A self-updating list of folder canonical names for the N most recently touched folders, ordered
43 * from least-recently-touched to most-recently-touched. N is a fixed size determined upon
44 * creation.
45 *
46 * RecentFoldersCache returns lists of this type, and will keep them updated when observers are
47 * registered on them.
48 *
49 */
50public final class RecentFolderList {
51    private static final String TAG = "RecentFolderList";
52    /** The application context */
53    private final Context mContext;
54    /** The current account */
55    private Account mAccount = null;
56
57    /** The actual cache: map of folder URIs to folder objects. */
58    private final LruCache<String, RecentFolderListEntry> mFolderCache;
59    /**
60     *  We want to show at most five recent folders
61     */
62    private final static int MAX_RECENT_FOLDERS = 5;
63    /**
64     *  We exclude the default inbox for the account and the current folder; these might be the
65     *  same, but we'll allow for both
66     */
67    private final static int MAX_EXCLUDED_FOLDERS = 2;
68
69    private final AccountObserver mAccountObserver = new AccountObserver() {
70        @Override
71        public void onChanged(Account newAccount) {
72            setCurrentAccount(newAccount);
73        }
74    };
75
76    /**
77     * Compare based on alphanumeric name of the folder, ignoring case.
78     */
79    private static final Comparator<Folder> ALPHABET_IGNORECASE = new Comparator<Folder>() {
80        @Override
81        public int compare(Folder lhs, Folder rhs) {
82            return lhs.name.compareToIgnoreCase(rhs.name);
83        }
84    };
85    /**
86     * Class to store the recent folder list asynchronously.
87     */
88    private class StoreRecent extends AsyncTask<Void, Void, Void> {
89        /**
90         * Copy {@link RecentFolderList#mAccount} in case the account changes between when the
91         * AsyncTask is created and when it is executed.
92         */
93        @SuppressWarnings("hiding")
94        private final Account mAccount;
95        private final Folder mFolder;
96
97        /**
98         * Create a new asynchronous task to store the recent folder list. Both the account
99         * and the folder should be non-null.
100         * @param account
101         * @param folder
102         */
103        public StoreRecent(Account account, Folder folder) {
104            assert (account != null && folder != null);
105            mAccount = account;
106            mFolder = folder;
107        }
108
109        @Override
110        protected Void doInBackground(Void... v) {
111            final Uri uri = mAccount.recentFolderListUri;
112            if (!Utils.isEmpty(uri)) {
113                ContentValues values = new ContentValues();
114                // Only the folder URIs are provided. Providers are free to update their specific
115                // information, though most will probably write the current timestamp.
116                values.put(mFolder.uri.toString(), 0);
117                LogUtils.i(TAG, "Save: %s", mFolder.name);
118                mContext.getContentResolver().update(uri, values, null, null);
119            }
120            return null;
121        }
122    }
123
124    /**
125     * Create a Recent Folder List from the given account. This will query the UIProvider to
126     * retrieve the RecentFolderList from persistent storage (if any).
127     * @param context
128     */
129    public RecentFolderList(Context context) {
130        mFolderCache = new LruCache<String, RecentFolderListEntry>(
131                MAX_RECENT_FOLDERS + MAX_EXCLUDED_FOLDERS);
132        mContext = context;
133    }
134
135    /**
136     * Initialize the {@link RecentFolderList} with a controllable activity.
137     * @param activity
138     */
139    public void initialize(ControllableActivity activity){
140        setCurrentAccount(mAccountObserver.initialize(activity.getAccountController()));
141    }
142
143    /**
144     * Change the current account. When a cursor over the recent folders for this account is
145     * available, the client <b>must</b> call {@link
146     * #loadFromUiProvider(com.android.mail.content.ObjectCursor)} with the updated
147     * cursor. Till then, the recent account list will be empty.
148     * @param account the new current account
149     */
150    private void setCurrentAccount(Account account) {
151        final boolean accountSwitched = (mAccount == null) || !mAccount.matches(account);
152        mAccount = account;
153        // Clear the cache only if we moved from alice@example.com -> alice@work.com
154        if (accountSwitched) {
155            mFolderCache.clear();
156        }
157    }
158
159    /**
160     * Load the account information from the UI provider given the cursor over the recent folders.
161     * @param c a cursor over the recent folders.
162     */
163    public void loadFromUiProvider(ObjectCursor<Folder> c) {
164        if (mAccount == null || c == null) {
165            LogUtils.e(TAG, "RecentFolderList.loadFromUiProvider: bad input. mAccount=%s,cursor=%s",
166                    mAccount, c);
167            return;
168        }
169        LogUtils.d(TAG, "Number of recents = %d", c.getCount());
170        if (!c.moveToLast()) {
171            LogUtils.e(TAG, "Not able to move to last in recent labels cursor");
172            return;
173        }
174        // Add them backwards, since the most recent values are at the beginning in the cursor.
175        // This enables older values to fall off the LRU cache. Also, read all values, just in case
176        // there are duplicates in the cursor.
177        do {
178            final Folder folder = c.getModel();
179            final RecentFolderListEntry entry = new RecentFolderListEntry(folder);
180            mFolderCache.putElement(folder.uri.toString(), entry);
181            LogUtils.v(TAG, "Account %s, Recent: %s", mAccount.name, folder.name);
182        } while (c.moveToPrevious());
183    }
184
185    /**
186     * Marks the given folder as 'accessed' by the user interface, its entry is updated in the
187     * recent folder list, and the current time is written to the provider. This should never
188     * be called with a null folder.
189     * @param folder the folder we touched
190     */
191    public void touchFolder(Folder folder, Account account) {
192        // We haven't got a valid account yet, cannot proceed.
193        if (mAccount == null || !mAccount.equals(account)) {
194            if (account != null) {
195                setCurrentAccount(account);
196            } else {
197                LogUtils.w(TAG, "No account set for setting recent folders?");
198                return;
199            }
200        }
201        assert (folder != null);
202        final RecentFolderListEntry entry = new RecentFolderListEntry(folder);
203        mFolderCache.putElement(folder.uri.toString(), entry);
204        new StoreRecent(mAccount, folder).execute();
205    }
206
207    /**
208     * Generate a sorted list of recent folders, excluding the passed in folder (if any) and
209     * default inbox for the current account. This must be called <em>after</em>
210     * {@link #setCurrentAccount(Account)} has been called.
211     * Returns a list of size {@value #MAX_RECENT_FOLDERS} or smaller.
212     * @param excludedFolderUri the uri of folder to be excluded (typically the current folder)
213     */
214    public ArrayList<Folder> getRecentFolderList(Uri excludedFolderUri) {
215        final ArrayList<Uri> excludedUris = new ArrayList<Uri>();
216        if (excludedFolderUri != null) {
217            excludedUris.add(excludedFolderUri);
218        }
219        final Uri defaultInbox = (mAccount == null) ?
220                Uri.EMPTY : Settings.getDefaultInboxUri(mAccount.settings);
221        if (!defaultInbox.equals(Uri.EMPTY)) {
222            excludedUris.add(defaultInbox);
223        }
224        final List<RecentFolderListEntry> recent = Lists.newArrayList();
225        recent.addAll(mFolderCache.values());
226        Collections.sort(recent);
227
228        final ArrayList<Folder> recentFolders = Lists.newArrayList();
229        for (final RecentFolderListEntry entry : recent) {
230            if (!excludedUris.contains(entry.mFolder.uri)) {
231                recentFolders.add(entry.mFolder);
232            }
233            if (recentFolders.size() == MAX_RECENT_FOLDERS) {
234                break;
235            }
236        }
237
238        // Sort the values as the very last step.
239        Collections.sort(recentFolders, ALPHABET_IGNORECASE);
240
241        return recentFolders;
242    }
243
244    /**
245     * Destroys this instance. The object is unusable after this has been called.
246     */
247    public void destroy() {
248        mAccountObserver.unregisterAndDestroy();
249    }
250
251    private static class RecentFolderListEntry implements Comparable<RecentFolderListEntry> {
252        private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger();
253
254        private final Folder mFolder;
255        private final int mSequence;
256
257        RecentFolderListEntry(Folder folder) {
258            mFolder = folder;
259            mSequence = SEQUENCE_GENERATOR.getAndIncrement();
260        }
261
262        /**
263         * Ensure that RecentFolderListEntry objects with greater sequence number will appear
264         * before objects with lower sequence numbers
265         */
266        @Override
267        public int compareTo(RecentFolderListEntry t) {
268            return t.mSequence - mSequence;
269        }
270    }
271}
272