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