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 
17 package com.android.mail.ui;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.net.Uri;
22 import android.os.AsyncTask;
23 import android.support.annotation.NonNull;
24 
25 import com.android.mail.content.ObjectCursor;
26 import com.android.mail.providers.Account;
27 import com.android.mail.providers.AccountObserver;
28 import com.android.mail.providers.Folder;
29 import com.android.mail.providers.Settings;
30 import com.android.mail.providers.UIProvider.FolderType;
31 import com.android.mail.utils.FolderUri;
32 import com.android.mail.utils.LogUtils;
33 import com.android.mail.utils.LruCache;
34 import com.android.mail.utils.Utils;
35 import com.google.common.collect.Lists;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.List;
41 import 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  */
52 public 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          */
StoreRecent(Account account, Folder folder)105         public StoreRecent(Account account, Folder folder) {
106             assert (account != null && folder != null);
107             mAccount = account;
108             mFolder = folder;
109         }
110 
111         @Override
doInBackground(Void... v)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      */
RecentFolderList(Context context)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      */
initialize(ControllableActivity activity)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      */
setCurrentAccount(Account account)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      */
loadFromUiProvider(ObjectCursor<Folder> c)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      */
touchFolder(@onNull Folder folder, Account account)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      */
getRecentFolderList(final FolderUri excludedFolderUri)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      */
destroy()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 
RecentFolderListEntry(Folder folder)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
compareTo(RecentFolderListEntry t)275         public int compareTo(RecentFolderListEntry t) {
276             return t.mSequence - mSequence;
277         }
278     }
279 }
280