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;
+    }
+}