blob: 87d916aa58d4e9fcb97bdff4c13498dbfbaa9629 [file] [log] [blame]
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -08001/**
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
Vikram Aggarwal27e85f22012-03-05 16:29:17 -080019import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
Marc Blankbc748ac2012-03-21 10:08:53 -070022import android.net.Uri;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -080023import android.os.AsyncTask;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080024
25import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070026import com.android.mail.providers.AccountObserver;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080027import com.android.mail.providers.Folder;
Marc Blank167faa82012-03-21 13:11:53 -070028import com.android.mail.providers.Settings;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080029import com.android.mail.utils.LogUtils;
30import com.android.mail.utils.LruCache;
Marc Blank10ddc192012-04-16 09:47:51 -070031import com.android.mail.utils.Utils;
Paul Westbrook5a64acd2012-09-07 10:30:04 -070032import com.google.common.collect.Lists;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080033
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080034import java.util.ArrayList;
Marc Blank3232a962012-03-08 15:32:37 -080035import java.util.Collections;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080036import java.util.Comparator;
Marc Blank3232a962012-03-08 15:32:37 -080037import java.util.List;
Paul Westbrook5a64acd2012-09-07 10:30:04 -070038import java.util.concurrent.atomic.AtomicInteger;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080039
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 {
Marc Blankbc748ac2012-03-21 10:08:53 -070050 private static final String TAG = "RecentFolderList";
Vikram Aggarwal27e85f22012-03-05 16:29:17 -080051 /** The application context */
52 private final Context mContext;
53 /** The current account */
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -080054 private Account mAccount = null;
Marc Blankbc748ac2012-03-21 10:08:53 -070055
Vikram Aggarwal2c0032d2012-09-04 10:30:07 -070056 /** The actual cache: map of folder URIs to folder objects. */
Paul Westbrook5a64acd2012-09-07 10:30:04 -070057 private final LruCache<String, RecentFolderListEntry> mFolderCache;
Marc Blankbc748ac2012-03-21 10:08:53 -070058 /**
Marc Blank167faa82012-03-21 13:11:53 -070059 * We want to show at most five recent folders
Marc Blankbc748ac2012-03-21 10:08:53 -070060 */
Marc Blank167faa82012-03-21 13:11:53 -070061 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;
Marc Blankbc748ac2012-03-21 10:08:53 -070067
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070068 private final AccountObserver mAccountObserver = new AccountObserver() {
69 @Override
70 public void onChanged(Account newAccount) {
71 setCurrentAccount(newAccount);
72 }
73 };
74
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -080075 /**
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 };
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -080084 /**
85 * Class to store the recent folder list asynchronously.
86 */
Marc Blankbc748ac2012-03-21 10:08:53 -070087 private class StoreRecent extends AsyncTask<Void, Void, Void> {
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070088 /**
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;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -080095
Vikram Aggarwal792ccba2012-03-27 13:46:57 -070096 /**
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 */
Marc Blankbc748ac2012-03-21 10:08:53 -0700102 public StoreRecent(Account account, Folder folder) {
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700103 assert (account != null && folder != null);
Marc Blankc972b182012-03-12 18:03:43 -0700104 mAccount = account;
Marc Blankbc748ac2012-03-21 10:08:53 -0700105 mFolder = folder;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800106 }
107
108 @Override
Marc Blankbc748ac2012-03-21 10:08:53 -0700109 protected Void doInBackground(Void... v) {
Vikram Aggarwal677cbef2012-06-11 15:38:56 -0700110 final Uri uri = mAccount.recentFolderListUri;
Marc Blank10ddc192012-04-16 09:47:51 -0700111 if (!Utils.isEmpty(uri)) {
Marc Blankbc748ac2012-03-21 10:08:53 -0700112 ContentValues values = new ContentValues();
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -0700113 // 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);
Paul Westbrookad6a2752012-04-04 16:58:13 -0700116 LogUtils.i(TAG, "Save: %s", mFolder.name);
Marc Blankbc748ac2012-03-21 10:08:53 -0700117 mContext.getContentResolver().update(uri, values, null, null);
118 }
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800119 return null;
120 }
121 }
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800122
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).
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700126 * @param context
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800127 */
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700128 public RecentFolderList(Context context) {
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700129 mFolderCache = new LruCache<String, RecentFolderListEntry>(
130 MAX_RECENT_FOLDERS + MAX_EXCLUDED_FOLDERS);
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800131 mContext = context;
Vikram Aggarwal27e85f22012-03-05 16:29:17 -0800132 }
133
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800134 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700135 * 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 /**
Marc Blank167faa82012-03-21 13:11:53 -0700143 * 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
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800147 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700148 private void setCurrentAccount(Account account) {
Vikram Aggarwal9e357e62012-11-05 09:56:12 -0800149 final boolean accountSwitched = (mAccount == null) || account.uri.equals(mAccount.uri);
Vikram Aggarwal27e85f22012-03-05 16:29:17 -0800150 mAccount = account;
Vikram Aggarwal9e357e62012-11-05 09:56:12 -0800151 // Clear the cache only if we moved from alice@example.com -> alice@work.com
152 if (accountSwitched) {
153 mFolderCache.clear();
154 }
Vikram Aggarwal27e85f22012-03-05 16:29:17 -0800155 }
156
157 /**
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800158 * Load the account information from the UI provider given the cursor over the recent folders.
Marc Blank167faa82012-03-21 13:11:53 -0700159 * @param c a cursor over the recent folders.
Vikram Aggarwal27e85f22012-03-05 16:29:17 -0800160 */
Marc Blank167faa82012-03-21 13:11:53 -0700161 public void loadFromUiProvider(Cursor c) {
162 if (mAccount == null || c == null) {
Vikram Aggarwal9e357e62012-11-05 09:56:12 -0800163 LogUtils.e(TAG, "RecentFolderList.loadFromUiProvider: bad input. mAccount=%s,cursor=%s",
164 mAccount, c);
Vikram Aggarwal27e85f22012-03-05 16:29:17 -0800165 return;
Marc Blankbc748ac2012-03-21 10:08:53 -0700166 }
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -0700167 LogUtils.d(TAG, "Number of recents = %d", c.getCount());
Paul Westbrook317348a2012-08-24 17:35:01 -0700168 if (!c.moveToLast()) {
Vikram Aggarwal9e357e62012-11-05 09:56:12 -0800169 LogUtils.e(TAG, "Not able to move to last in recent labels cursor");
Paul Westbrook317348a2012-08-24 17:35:01 -0700170 return;
171 }
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700172 // Add them backwards, since the most recent values are at the beginning in the cursor.
173 // This enables older values to fall off the LRU cache. Also, read all values, just in case
174 // there are duplicates in the cursor.
175 do {
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -0700176 final Folder folder = new Folder(c);
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700177 final RecentFolderListEntry entry = new RecentFolderListEntry(folder);
178 mFolderCache.putElement(folder.uri.toString(), entry);
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -0700179 LogUtils.v(TAG, "Account %s, Recent: %s", mAccount.name, folder.name);
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700180 } while (c.moveToPrevious());
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800181 }
182
183 /**
Marc Blankbc748ac2012-03-21 10:08:53 -0700184 * Marks the given folder as 'accessed' by the user interface, its entry is updated in the
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700185 * recent folder list, and the current time is written to the provider. This should never
186 * be called with a null folder.
Marc Blank167faa82012-03-21 13:11:53 -0700187 * @param folder the folder we touched
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800188 */
Marc Blank2675dbc2012-04-03 10:17:13 -0700189 public void touchFolder(Folder folder, Account account) {
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700190 // We haven't got a valid account yet, cannot proceed.
Marc Blank2675dbc2012-04-03 10:17:13 -0700191 if (mAccount == null || !mAccount.equals(account)) {
192 if (account != null) {
193 setCurrentAccount(account);
194 } else {
195 LogUtils.w(TAG, "No account set for setting recent folders?");
196 return;
197 }
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700198 }
199 assert (folder != null);
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700200 final RecentFolderListEntry entry = new RecentFolderListEntry(folder);
201 mFolderCache.putElement(folder.uri.toString(), entry);
Marc Blankbc748ac2012-03-21 10:08:53 -0700202 new StoreRecent(mAccount, folder).execute();
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800203 }
204
205 /**
Marc Blank167faa82012-03-21 13:11:53 -0700206 * Generate a sorted list of recent folders, excluding the passed in folder (if any) and
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700207 * default inbox for the current account. This must be called <em>after</em>
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700208 * {@link #setCurrentAccount(Account)} has been called.
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700209 * Returns a list of size {@value #MAX_RECENT_FOLDERS} or smaller.
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700210 * @param excludedFolderUri the uri of folder to be excluded (typically the current folder)
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800211 */
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700212 public ArrayList<Folder> getRecentFolderList(Uri excludedFolderUri) {
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700213 final ArrayList<Uri> excludedUris = new ArrayList<Uri>();
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700214 if (excludedFolderUri != null) {
215 excludedUris.add(excludedFolderUri);
Marc Blank167faa82012-03-21 13:11:53 -0700216 }
Vikram Aggarwal9da85df2012-05-09 15:34:23 -0700217 final Uri defaultInbox = (mAccount == null) ?
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700218 Uri.EMPTY : Settings.getDefaultInboxUri(mAccount.settings);
Vikram Aggarwal1e57e672012-05-07 14:48:24 -0700219 if (!defaultInbox.equals(Uri.EMPTY)) {
Vikram Aggarwal1e57e672012-05-07 14:48:24 -0700220 excludedUris.add(defaultInbox);
Marc Blank167faa82012-03-21 13:11:53 -0700221 }
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700222 final List<RecentFolderListEntry> recent = Lists.newArrayList();
223 recent.addAll(mFolderCache.values());
224 Collections.sort(recent);
225
226 final ArrayList<Folder> recentFolders = Lists.newArrayList();
227 for (final RecentFolderListEntry entry : recent) {
228 if (!excludedUris.contains(entry.mFolder.uri)) {
229 recentFolders.add(entry.mFolder);
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800230 }
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700231 if (recentFolders.size() == MAX_RECENT_FOLDERS) {
232 break;
233 }
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800234 }
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700235
Vikram Aggarwalf991bee2012-08-24 09:11:52 -0700236 // Sort the values as the very last step.
237 Collections.sort(recentFolders, ALPHABET_IGNORECASE);
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700238
Marc Blank167faa82012-03-21 13:11:53 -0700239 return recentFolders;
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800240 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700241
242 /**
243 * Destroys this instance. The object is unusable after this has been called.
244 */
245 public void destroy() {
246 mAccountObserver.unregisterAndDestroy();
247 }
Paul Westbrook5a64acd2012-09-07 10:30:04 -0700248
249 private static class RecentFolderListEntry implements Comparable<RecentFolderListEntry> {
250 private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger();
251
252 private final Folder mFolder;
253 private final int mSequence;
254
255 RecentFolderListEntry(Folder folder) {
256 mFolder = folder;
257 mSequence = SEQUENCE_GENERATOR.getAndIncrement();
258 }
259
260 /**
261 * Ensure that RecentFolderListEntry objects with greater sequence number will appear
262 * before objects with lower sequence numbers
263 */
264 @Override
265 public int compareTo(RecentFolderListEntry t) {
266 return t.mSequence - mSequence;
267 }
268 }
Vikram Aggarwal1a4bcc02012-03-01 10:09:44 -0800269}