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 import android.support.annotation.NonNull; 24 25 import com.android.mail.content.ObjectCursor; 26 import com.android.mail.providers.Account; 27 import com.android.mail.providers.AccountObserver; 28 import com.android.mail.providers.Folder; 29 import com.android.mail.providers.Settings; 30 import com.android.mail.providers.UIProvider.FolderType; 31 import com.android.mail.utils.FolderUri; 32 import com.android.mail.utils.LogUtils; 33 import com.android.mail.utils.LruCache; 34 import com.android.mail.utils.Utils; 35 import com.google.common.collect.Lists; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.List; 41 import java.util.concurrent.atomic.AtomicInteger; 42 43 /** 44 * A self-updating list of folder canonical names for the N most recently touched folders, ordered 45 * from least-recently-touched to most-recently-touched. N is a fixed size determined upon 46 * creation. 47 * 48 * RecentFoldersCache returns lists of this type, and will keep them updated when observers are 49 * registered on them. 50 * 51 */ 52 public final class RecentFolderList { 53 private static final String TAG = "RecentFolderList"; 54 /** The application context */ 55 private final Context mContext; 56 /** The current account */ 57 private Account mAccount = null; 58 59 /** The actual cache: map of folder URIs to folder objects. */ 60 private final LruCache<String, RecentFolderListEntry> mFolderCache; 61 /** 62 * We want to show at most five recent folders 63 */ 64 private final static int MAX_RECENT_FOLDERS = 5; 65 /** 66 * We exclude the default inbox for the account and the current folder; these might be the 67 * same, but we'll allow for both 68 */ 69 private final static int MAX_EXCLUDED_FOLDERS = 2; 70 71 private final AccountObserver mAccountObserver = new AccountObserver() { 72 @Override 73 public void onChanged(Account newAccount) { 74 setCurrentAccount(newAccount); 75 } 76 }; 77 78 /** 79 * Compare based on alphanumeric name of the folder, ignoring case. 80 */ 81 private static final Comparator<Folder> ALPHABET_IGNORECASE = new Comparator<Folder>() { 82 @Override 83 public int compare(Folder lhs, Folder rhs) { 84 return lhs.name.compareToIgnoreCase(rhs.name); 85 } 86 }; 87 /** 88 * Class to store the recent folder list asynchronously. 89 */ 90 private class StoreRecent extends AsyncTask<Void, Void, Void> { 91 /** 92 * Copy {@link RecentFolderList#mAccount} in case the account changes between when the 93 * AsyncTask is created and when it is executed. 94 */ 95 @SuppressWarnings("hiding") 96 private final Account mAccount; 97 private final Folder mFolder; 98 99 /** 100 * Create a new asynchronous task to store the recent folder list. Both the account 101 * and the folder should be non-null. 102 * @param account the current account for this folder. 103 * @param folder the folder which is to be stored. 104 */ StoreRecent(Account account, Folder folder)105 public StoreRecent(Account account, Folder folder) { 106 assert (account != null && folder != null); 107 mAccount = account; 108 mFolder = folder; 109 } 110 111 @Override doInBackground(Void... v)112 protected Void doInBackground(Void... v) { 113 final Uri uri = mAccount.recentFolderListUri; 114 if (!Utils.isEmpty(uri)) { 115 ContentValues values = new ContentValues(); 116 // Only the folder URIs are provided. Providers are free to update their specific 117 // information, though most will probably write the current timestamp. 118 values.put(mFolder.folderUri.fullUri.toString(), 0); 119 LogUtils.i(TAG, "Save: %s", mFolder.name); 120 mContext.getContentResolver().update(uri, values, null, null); 121 } 122 return null; 123 } 124 } 125 126 /** 127 * Create a Recent Folder List from the given account. This will query the UIProvider to 128 * retrieve the RecentFolderList from persistent storage (if any). 129 * @param context the context for the activity 130 */ RecentFolderList(Context context)131 public RecentFolderList(Context context) { 132 mFolderCache = new LruCache<String, RecentFolderListEntry>( 133 MAX_RECENT_FOLDERS + MAX_EXCLUDED_FOLDERS); 134 mContext = context; 135 } 136 137 /** 138 * Initialize the {@link RecentFolderList} with a controllable activity. 139 * @param activity the underlying activity 140 */ initialize(ControllableActivity activity)141 public void initialize(ControllableActivity activity){ 142 setCurrentAccount(mAccountObserver.initialize(activity.getAccountController())); 143 } 144 145 /** 146 * Change the current account. When a cursor over the recent folders for this account is 147 * available, the client <b>must</b> call {@link 148 * #loadFromUiProvider(com.android.mail.content.ObjectCursor)} with the updated 149 * cursor. Till then, the recent account list will be empty. 150 * @param account the new current account 151 */ setCurrentAccount(Account account)152 private void setCurrentAccount(Account account) { 153 final boolean accountSwitched = (mAccount == null) || !mAccount.matches(account); 154 mAccount = account; 155 // Clear the cache only if we moved from alice@example.com -> alice@work.com 156 if (accountSwitched) { 157 mFolderCache.clear(); 158 } 159 } 160 161 /** 162 * Load the account information from the UI provider given the cursor over the recent folders. 163 * @param c a cursor over the recent folders. 164 */ loadFromUiProvider(ObjectCursor<Folder> c)165 public void loadFromUiProvider(ObjectCursor<Folder> c) { 166 if (mAccount == null || c == null) { 167 LogUtils.e(TAG, "RecentFolderList.loadFromUiProvider: bad input. mAccount=%s,cursor=%s", 168 mAccount, c); 169 return; 170 } 171 LogUtils.d(TAG, "Number of recents = %d", c.getCount()); 172 if (!c.moveToLast()) { 173 LogUtils.e(TAG, "Not able to move to last in recent labels cursor"); 174 return; 175 } 176 // Add them backwards, since the most recent values are at the beginning in the cursor. 177 // This enables older values to fall off the LRU cache. Also, read all values, just in case 178 // there are duplicates in the cursor. 179 do { 180 final Folder folder = c.getModel(); 181 final RecentFolderListEntry entry = new RecentFolderListEntry(folder); 182 mFolderCache.putElement(folder.folderUri.fullUri.toString(), entry); 183 LogUtils.v(TAG, "Account %s, Recent: %s", mAccount.getEmailAddress(), folder.name); 184 } while (c.moveToPrevious()); 185 } 186 187 /** 188 * Marks the given folder as 'accessed' by the user interface, its entry is updated in the 189 * recent folder list, and the current time is written to the provider. This should never 190 * be called with a null folder. 191 * @param folder the folder we touched 192 */ touchFolder(@onNull Folder folder, Account account)193 public void touchFolder(@NonNull Folder folder, Account account) { 194 // We haven't got a valid account yet, cannot proceed. 195 if (mAccount == null || !mAccount.equals(account)) { 196 if (account != null) { 197 setCurrentAccount(account); 198 } else { 199 LogUtils.w(TAG, "No account set for setting recent folders?"); 200 return; 201 } 202 } 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 */ getRecentFolderList(final FolderUri excludedFolderUri)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 */ destroy()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 RecentFolderListEntry(Folder folder)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 compareTo(RecentFolderListEntry t)275 public int compareTo(RecentFolderListEntry t) { 276 return t.mSequence - mSequence; 277 } 278 } 279 } 280