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