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