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