/** * Copyright (c) 2011, Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.ui; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; import com.android.mail.content.ObjectCursor; import com.android.mail.providers.Account; import com.android.mail.providers.AccountObserver; import com.android.mail.providers.Folder; import com.android.mail.providers.Settings; import com.android.mail.providers.UIProvider.FolderType; import com.android.mail.utils.FolderUri; import com.android.mail.utils.LogUtils; import com.android.mail.utils.LruCache; import com.android.mail.utils.Utils; import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * A self-updating list of folder canonical names for the N most recently touched folders, ordered * from least-recently-touched to most-recently-touched. N is a fixed size determined upon * creation. * * RecentFoldersCache returns lists of this type, and will keep them updated when observers are * registered on them. * */ public final class RecentFolderList { private static final String TAG = "RecentFolderList"; /** The application context */ private final Context mContext; /** The current account */ private Account mAccount = null; /** The actual cache: map of folder URIs to folder objects. */ private final LruCache mFolderCache; /** * We want to show at most five recent folders */ private final static int MAX_RECENT_FOLDERS = 5; /** * We exclude the default inbox for the account and the current folder; these might be the * same, but we'll allow for both */ private final static int MAX_EXCLUDED_FOLDERS = 2; private final AccountObserver mAccountObserver = new AccountObserver() { @Override public void onChanged(Account newAccount) { setCurrentAccount(newAccount); } }; /** * Compare based on alphanumeric name of the folder, ignoring case. */ private static final Comparator ALPHABET_IGNORECASE = new Comparator() { @Override public int compare(Folder lhs, Folder rhs) { return lhs.name.compareToIgnoreCase(rhs.name); } }; /** * Class to store the recent folder list asynchronously. */ private class StoreRecent extends AsyncTask { /** * Copy {@link RecentFolderList#mAccount} in case the account changes between when the * AsyncTask is created and when it is executed. */ @SuppressWarnings("hiding") private final Account mAccount; private final Folder mFolder; /** * Create a new asynchronous task to store the recent folder list. Both the account * and the folder should be non-null. * @param account the current account for this folder. * @param folder the folder which is to be stored. */ public StoreRecent(Account account, Folder folder) { assert (account != null && folder != null); mAccount = account; mFolder = folder; } @Override protected Void doInBackground(Void... v) { final Uri uri = mAccount.recentFolderListUri; if (!Utils.isEmpty(uri)) { ContentValues values = new ContentValues(); // Only the folder URIs are provided. Providers are free to update their specific // information, though most will probably write the current timestamp. values.put(mFolder.folderUri.fullUri.toString(), 0); LogUtils.i(TAG, "Save: %s", mFolder.name); mContext.getContentResolver().update(uri, values, null, null); } return null; } } /** * Create a Recent Folder List from the given account. This will query the UIProvider to * retrieve the RecentFolderList from persistent storage (if any). * @param context the context for the activity */ public RecentFolderList(Context context) { mFolderCache = new LruCache( MAX_RECENT_FOLDERS + MAX_EXCLUDED_FOLDERS); mContext = context; } /** * Initialize the {@link RecentFolderList} with a controllable activity. * @param activity the underlying activity */ public void initialize(ControllableActivity activity){ setCurrentAccount(mAccountObserver.initialize(activity.getAccountController())); } /** * Change the current account. When a cursor over the recent folders for this account is * available, the client must call {@link * #loadFromUiProvider(com.android.mail.content.ObjectCursor)} with the updated * cursor. Till then, the recent account list will be empty. * @param account the new current account */ private void setCurrentAccount(Account account) { final boolean accountSwitched = (mAccount == null) || !mAccount.matches(account); mAccount = account; // Clear the cache only if we moved from alice@example.com -> alice@work.com if (accountSwitched) { mFolderCache.clear(); } } /** * Load the account information from the UI provider given the cursor over the recent folders. * @param c a cursor over the recent folders. */ public void loadFromUiProvider(ObjectCursor c) { if (mAccount == null || c == null) { LogUtils.e(TAG, "RecentFolderList.loadFromUiProvider: bad input. mAccount=%s,cursor=%s", mAccount, c); return; } LogUtils.d(TAG, "Number of recents = %d", c.getCount()); if (!c.moveToLast()) { LogUtils.e(TAG, "Not able to move to last in recent labels cursor"); return; } // Add them backwards, since the most recent values are at the beginning in the cursor. // This enables older values to fall off the LRU cache. Also, read all values, just in case // there are duplicates in the cursor. do { final Folder folder = c.getModel(); final RecentFolderListEntry entry = new RecentFolderListEntry(folder); mFolderCache.putElement(folder.folderUri.fullUri.toString(), entry); LogUtils.v(TAG, "Account %s, Recent: %s", mAccount.getEmailAddress(), folder.name); } while (c.moveToPrevious()); } /** * Marks the given folder as 'accessed' by the user interface, its entry is updated in the * recent folder list, and the current time is written to the provider. This should never * be called with a null folder. * @param folder the folder we touched */ public void touchFolder(@NonNull Folder folder, Account account) { // We haven't got a valid account yet, cannot proceed. if (mAccount == null || !mAccount.equals(account)) { if (account != null) { setCurrentAccount(account); } else { LogUtils.w(TAG, "No account set for setting recent folders?"); return; } } if (folder.isProviderFolder() || folder.isType(FolderType.SEARCH)) { LogUtils.d(TAG, "Not touching recent folder because it's provider or search folder"); return; } final RecentFolderListEntry entry = new RecentFolderListEntry(folder); mFolderCache.putElement(folder.folderUri.fullUri.toString(), entry); new StoreRecent(mAccount, folder).execute(); } /** * Generate a sorted list of recent folders, excluding the passed in folder (if any) and * default inbox for the current account. This must be called after * {@link #setCurrentAccount(Account)} has been called. * Returns a list of size {@value #MAX_RECENT_FOLDERS} or smaller. * @param excludedFolderUri the uri of folder to be excluded (typically the current folder) */ public ArrayList getRecentFolderList(final FolderUri excludedFolderUri) { final ArrayList excludedUris = new ArrayList(); if (excludedFolderUri != null) { excludedUris.add(excludedFolderUri); } final FolderUri defaultInbox = (mAccount == null) ? FolderUri.EMPTY : new FolderUri(Settings.getDefaultInboxUri(mAccount.settings)); if (!defaultInbox.equals(FolderUri.EMPTY)) { excludedUris.add(defaultInbox); } final List recent = Lists.newArrayList(); recent.addAll(mFolderCache.values()); Collections.sort(recent); final ArrayList recentFolders = Lists.newArrayList(); for (final RecentFolderListEntry entry : recent) { if (!excludedUris.contains(entry.mFolder.folderUri)) { recentFolders.add(entry.mFolder); } if (recentFolders.size() == MAX_RECENT_FOLDERS) { break; } } // Sort the values as the very last step. Collections.sort(recentFolders, ALPHABET_IGNORECASE); return recentFolders; } /** * Destroys this instance. The object is unusable after this has been called. */ public void destroy() { mAccountObserver.unregisterAndDestroy(); } private static class RecentFolderListEntry implements Comparable { private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger(); private final Folder mFolder; private final int mSequence; RecentFolderListEntry(Folder folder) { mFolder = folder; mSequence = SEQUENCE_GENERATOR.getAndIncrement(); } /** * Ensure that RecentFolderListEntry objects with greater sequence number will appear * before objects with lower sequence numbers */ @Override public int compareTo(RecentFolderListEntry t) { return t.mSequence - mSequence; } } }