blob: f2de5892b2d54bb22491d1c791af4bc25de005aa [file] [log] [blame]
/**
* 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<String, RecentFolderListEntry> 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<Folder> ALPHABET_IGNORECASE = new Comparator<Folder>() {
@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<Void, Void, Void> {
/**
* 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<String, RecentFolderListEntry>(
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 <b>must</b> 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<Folder> 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 <em>after</em>
* {@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<Folder> getRecentFolderList(final FolderUri excludedFolderUri) {
final ArrayList<FolderUri> excludedUris = new ArrayList<FolderUri>();
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<RecentFolderListEntry> recent = Lists.newArrayList();
recent.addAll(mFolderCache.values());
Collections.sort(recent);
final ArrayList<Folder> 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<RecentFolderListEntry> {
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;
}
}
}