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