Home | History | Annotate | Download | only in ui
      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          */
    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 (at) example.com -> alice (at) 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