First implementation of recent folders.
Things still to be done:
0. Modify the UiProvider to include recent folders in the provider contract.
1. Save/Restore the recent folder list by delegating to the provider.
Change-Id: Ie36566d93bb95b35375498caeef16fa4559e85d6
diff --git a/src/com/android/mail/AccountSpinnerAdapter.java b/src/com/android/mail/AccountSpinnerAdapter.java
index a3107a3..a296dbe 100644
--- a/src/com/android/mail/AccountSpinnerAdapter.java
+++ b/src/com/android/mail/AccountSpinnerAdapter.java
@@ -29,6 +29,8 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.RecentFolderList;
+import com.android.mail.utils.LogUtils;
/**
* An adapter to return the list of accounts and folders for the Account Spinner.
@@ -54,15 +56,15 @@
*/
static final int NAME_COLUMN = 2;
/**
- * Fake folders for now
+ * An object that provides a collection of recent folders, per account.
*/
- private final String[] mFolders;
+ private RecentFolderList mRecentFolders;
/**
- * Fake unread counts
+ * The actual collection of sorted recent folders obtained from {@link #mRecentFolders}
*/
- private final int[] mUnreadCounts = {
- 0, 2, 42
- };
+ private Folder[] mRecentFolderList = new Folder[0];
+
+ /** The folder currently being viewed */
private Folder mCurrentFolder;
/**
@@ -117,11 +119,6 @@
public AccountSpinnerAdapter(Context context) {
mInflater = LayoutInflater.from(context);
- // Fake folder information
- mFolders = new String[3];
- mFolders[0] = "Drafts -fake";
- mFolders[1] = "Sent -fake";
- mFolders[1] = "Starred -fake";
}
/**
@@ -140,6 +137,7 @@
*/
public void setCurrentFolder(Folder folder) {
mCurrentFolder = folder;
+ mRecentFolderList = mRecentFolders.changeCurrentFolder(folder);
}
/**
@@ -148,12 +146,15 @@
*/
public void setCurrentAccount(Account account) {
mCurrentAccount = account;
+ mRecentFolders = new RecentFolderList(mCurrentAccount);
+ mRecentFolderList = mRecentFolders.getSortedArray(mCurrentFolder);
+ notifyDataSetChanged();
}
@Override
public int getCount() {
- // All the accounts, plus one header, and optionally some folders
- return mNumAccounts + 1 + mFolders.length;
+ // All the accounts, plus one header, plus recent folders
+ return mNumAccounts + 1 + mRecentFolderList.length;
}
@Override
@@ -164,7 +165,10 @@
case TYPE_HEADER:
return "account spinner header";
default:
- return mFolders[position - mNumAccounts - 1];
+ // The first few positions have accounts, and then the header.
+ final int offset = position - mNumAccounts - 1;
+ // Return the name of the folder at this location.
+ return mRecentFolderList[offset].name;
}
}
@@ -210,8 +214,9 @@
// Change the name of the current folder
final int offset = position - mNumAccounts - 1;
accountName = (mCurrentAccount == null) ? "" : mCurrentAccount.name;
- folderName = mFolders[offset];
- unreadCount = mUnreadCounts[offset];
+ final Folder folder = mRecentFolderList[offset];
+ folderName = folder.name;
+ unreadCount = folder.unreadCount;
break;
}
@@ -286,8 +291,9 @@
break;
case TYPE_FOLDER:
final int offset = position - mNumAccounts - 1;
- textLabel = mFolders[offset];
- unreadCount = mUnreadCounts[offset];
+ final Folder folder = mRecentFolderList[offset];
+ textLabel = folder.name;
+ unreadCount = folder.unreadCount;
break;
}
diff --git a/src/com/android/mail/ui/FolderSelectorAdapter.java b/src/com/android/mail/ui/FolderSelectorAdapter.java
index 0068279..6bf389d 100644
--- a/src/com/android/mail/ui/FolderSelectorAdapter.java
+++ b/src/com/android/mail/ui/FolderSelectorAdapter.java
@@ -39,7 +39,7 @@
import java.util.Set;
/**
- * An adapter for translating a {@link LabelList} to a set of selectable views to be used for
+ * An adapter for translating a {@link FolderList} to a set of selectable views to be used for
* applying labels to one or more conversations.
*/
public class FolderSelectorAdapter extends BaseAdapter {
diff --git a/src/com/android/mail/ui/RecentFolderList.java b/src/com/android/mail/ui/RecentFolderList.java
new file mode 100644
index 0000000..5406993
--- /dev/null
+++ b/src/com/android/mail/ui/RecentFolderList.java
@@ -0,0 +1,102 @@
+/**
+ * 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.text.TextUtils;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Folder;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.LruCache;
+
+import java.util.Collections;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Comparator;
+
+/**
+ * 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 LOG_TAG = new LogUtils().getLogTag();
+ /**
+ * 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);
+ }
+ };
+ private final LruCache<String, Folder> mFolderCache;
+
+ /**
+ * Create a Recent Folder List from the given account. This will query the UIProvider to
+ * retrieve the RecentFolderList from persistent storage (if any).
+ * @param account
+ */
+ public RecentFolderList(Account account) {
+ // We want to show five recent folders, and one space for the current folder (not displayed
+ // to user).
+ final int NUM_ACCOUNTS = 5 + 1;
+ mFolderCache = new LruCache<String, Folder>(NUM_ACCOUNTS);
+ }
+
+ /**
+ * Changes the current folder and returns the updated list of recent folders, <b>not</b>
+ * including the current folder.
+ * @param folder the folder we have changed to.
+ */
+ public Folder[] changeCurrentFolder(Folder folder) {
+ mFolderCache.putElement(folder.id, folder);
+ return getSortedArray(folder);
+ }
+
+ /**
+ * Requests the UIProvider to save this RecentFolderList to persistent storage.
+ */
+ public void save() {
+ // TODO: Implement
+ }
+
+ /**
+ * Generate a sorted array of recent folders, <b>not</b> including the current folder.
+ * @param currentFolder the current folder being displayed.
+ */
+ public Folder[] getSortedArray(Folder currentFolder) {
+ final int spaceForCurrentFolder =
+ (currentFolder != null && mFolderCache.getElement(currentFolder.id) != null)
+ ? 1 : 0;
+ final int numRecents = mFolderCache.size() - spaceForCurrentFolder;
+ final Folder[] folders = new Folder[numRecents];
+ int i = 0;
+ final List<Folder> recent = new ArrayList<Folder>(mFolderCache.values());
+ Collections.sort(recent, ALPHABET_IGNORECASE);
+ for (Folder f : recent) {
+ if (!TextUtils.equals(f.id, currentFolder.id)) {
+ folders[i++] = f;
+ }
+ }
+ return folders;
+ }
+}
diff --git a/src/com/android/mail/utils/LruCache.java b/src/com/android/mail/utils/LruCache.java
new file mode 100644
index 0000000..22c9e6f
--- /dev/null
+++ b/src/com/android/mail/utils/LruCache.java
@@ -0,0 +1,108 @@
+/**
+ * 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.utils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A simple in-memory LRU cache, which is trivial to implement on top
+ * of JDK's {@link LinkedHashMap}.
+ *
+ * LRU policy is ensured by the underlying LinkedHashMap functionality.
+ */
+public final class LruCache<K, V> extends LinkedHashMap<K, V> {
+ private static final long serialVersionUID = 1L;
+ private final int maxCapacity;
+
+ /**
+ * Creates an instance of LRUCache, with given capacity.
+ *
+ * @param capacity maximum number of elements in the cache. This is also
+ * used as initialCapacity i.e. memory is allocated upfront.
+ */
+ public LruCache(int capacity) {
+ this(capacity, capacity);
+ }
+
+ /**
+ * Creates an instance of LRUCache, with given capacity.
+ *
+ * @param initialCapacity initial capacity of the cache.
+ * @param maxCapacity maximum number of elements in the cache.
+ */
+ private LruCache(int initialCapacity, int maxCapacity) {
+ super(initialCapacity, (float) 0.75, true);
+ this.maxCapacity = maxCapacity;
+ }
+
+ // These methods are needed because the default implementation of LinkedHashMap is *not*
+ // synchronized.
+ /**
+ * Gets an element from the cache, returning the element if found, or null if not
+ * @param key
+ * @return
+ */
+ public synchronized V getElement(K key) {
+ return get(key);
+ }
+
+ /**
+ * Puts an element into the cache.
+ * @param key
+ * @param value, a non-null value.
+ */
+ public synchronized void putElement(K key, V value) {
+ put(key, value);
+ }
+
+ /**
+ * Removes an element from the cache. Returns the removed element, or null if no such element
+ * existed in the cache.
+ * @param key
+ * @return
+ */
+ public synchronized V removeElement(K key) {
+ return remove(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Necessary to override because HashMap increases the capacity of the
+ * hashtable before inserting the elements. However, here we call put() which
+ * checks if we can remove the eldest element before increasing the capacity.
+ */
+ @Override
+ public synchronized void putAll(Map<? extends K, ? extends V> m) {
+ for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
+ put(e.getKey(), e.getValue());
+ }
+ }
+
+ /**
+ * This method is called by LinkedHashMap to check whether the eldest entry
+ * should be removed.
+ *
+ * @param eldest
+ * @return true if element should be removed.
+ */
+ @Override
+ protected synchronized boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > maxCapacity;
+ }
+}