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 
     24 import com.android.mail.content.ObjectCursor;
     25 import com.android.mail.providers.Account;
     26 import com.android.mail.providers.AccountObserver;
     27 import com.android.mail.providers.Folder;
     28 import com.android.mail.providers.Settings;
     29 import com.android.mail.providers.UIProvider.FolderType;
     30 import com.android.mail.utils.FolderUri;
     31 import com.android.mail.utils.LogUtils;
     32 import com.android.mail.utils.LruCache;
     33 import com.android.mail.utils.Utils;
     34 import com.google.common.collect.Lists;
     35 
     36 import java.util.ArrayList;
     37 import java.util.Collections;
     38 import java.util.Comparator;
     39 import java.util.List;
     40 import 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  */
     51 public 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 (at) example.com -> alice (at) 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.name, 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