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