Home | History | Annotate | Download | only in providers
      1 /*******************************************************************************
      2  *      Copyright (C) 2012 Google Inc.
      3  *      Licensed to The Android Open Source Project.
      4  *
      5  *      Licensed under the Apache License, Version 2.0 (the "License");
      6  *      you may not use this file except in compliance with the License.
      7  *      You may obtain a copy of the License at
      8  *
      9  *           http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *      Unless required by applicable law or agreed to in writing, software
     12  *      distributed under the License is distributed on an "AS IS" BASIS,
     13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *      See the License for the specific language governing permissions and
     15  *      limitations under the License.
     16  *******************************************************************************/
     17 
     18 package com.android.mail.providers;
     19 
     20 import android.app.LoaderManager;
     21 import android.content.Loader;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.support.annotation.NonNull;
     25 
     26 import com.android.mail.content.ObjectCursor;
     27 import com.android.mail.content.ObjectCursorLoader;
     28 import com.android.mail.ui.AbstractActivityController;
     29 import com.android.mail.ui.RestrictedActivity;
     30 import com.android.mail.utils.LogUtils;
     31 import com.google.common.collect.Lists;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Collections;
     35 import java.util.HashMap;
     36 import java.util.List;
     37 import java.util.Map;
     38 
     39 /**
     40  * A container to keep a list of Folder objects, with the ability to automatically keep in sync with
     41  * the folders in the providers.
     42  */
     43 public class FolderWatcher {
     44     public static final String FOLDER_URI = "FOLDER-URI";
     45     /** List of URIs that are watched. */
     46     private final List<Uri> mUris = new ArrayList<Uri>();
     47     /** Map returning the default inbox folder for each URI */
     48     private final Map<Uri, Folder> mInboxMap = new HashMap<Uri, Folder>();
     49     private final RestrictedActivity mActivity;
     50     /** Handles folder callbacks and reads unread counts. */
     51     private final UnreadLoads mUnreadCallback = new UnreadLoads();
     52 
     53     /**
     54      * The adapter that consumes this data. We use this only to notify the consumer that new data
     55      * is available.
     56      */
     57     private UnreadCountChangedListener mConsumer;
     58 
     59     private final static String LOG_TAG = LogUtils.TAG;
     60 
     61     public static interface UnreadCountChangedListener {
     62         void onUnreadCountChange();
     63     }
     64 
     65     /**
     66      * Create a {@link FolderWatcher}.
     67      * @param activity Upstream activity
     68      * @param listener A listener to be notified when the unread count changes
     69      */
     70     public FolderWatcher(
     71             RestrictedActivity activity, @NonNull UnreadCountChangedListener listener) {
     72         mActivity = activity;
     73         mConsumer = listener;
     74     }
     75 
     76     /**
     77      * Start watching all the accounts in this list and stop watching accounts NOT on this list.
     78      * Does nothing if the list of all accounts is null.
     79      * @param allAccounts all the current accounts on the device.
     80      */
     81     public void updateAccountList(Account[] allAccounts) {
     82         if (allAccounts == null) {
     83             return;
     84         }
     85         // Create list of Inbox URIs from the array of accounts.
     86         final List<Uri> newAccounts = new ArrayList<Uri>(allAccounts.length);
     87         for (final Account account : allAccounts) {
     88             newAccounts.add(account.settings.defaultInbox);
     89         }
     90         // Stop watching accounts not in the new list.
     91         final List<Uri> uriCopy = Collections.unmodifiableList(Lists.newArrayList(mUris));
     92         for (final Uri previous : uriCopy) {
     93             if (!newAccounts.contains(previous)) {
     94                 stopWatching(previous);
     95             }
     96         }
     97         // Add accounts in the new list, that are not already watched.
     98         for (final Uri fresh : newAccounts) {
     99             if (!mUris.contains(fresh)) {
    100                 startWatching(fresh);
    101             }
    102         }
    103     }
    104 
    105     /**
    106      * Starts watching the given URI for changes. It is NOT safe to call this method repeatedly
    107      * for the same URI.
    108      * @param uri the URI for an inbox whose unread count is to be watched
    109      */
    110     private void startWatching(Uri uri) {
    111         final int location = insertAtNextEmptyLocation(uri);
    112         LogUtils.d(LOG_TAG, "Watching %s, at position %d.", uri, location);
    113         // No inbox folder yet, put a safe placeholder for now.
    114         mInboxMap.put(uri, null);
    115         final LoaderManager lm = mActivity.getLoaderManager();
    116         final Bundle args = new Bundle();
    117         args.putString(FOLDER_URI, uri.toString());
    118         lm.initLoader(getLoaderFromPosition(location), args, mUnreadCallback);
    119     }
    120 
    121     /**
    122      * Locates the next empty position in {@link #mUris} and inserts the URI there, returning the
    123      * location.
    124      * @return location where the URI was inserted.
    125      */
    126     private int insertAtNextEmptyLocation(Uri newElement) {
    127         Uri uri;
    128         int location = -1;
    129         for (int size = mUris.size(), i = 0; i < size; i++) {
    130             uri = mUris.get(i);
    131             // Hole in the list, use this position
    132             if (uri == null) {
    133                 location = i;
    134                 break;
    135             }
    136         }
    137 
    138         if (location < 0) {
    139             // No hole found, return the current size;
    140             location = mUris.size();
    141             mUris.add(location, newElement);
    142         } else {
    143             mUris.set(location, newElement);
    144         }
    145         return location;
    146     }
    147 
    148     /**
    149      * Returns the loader ID for a position inside the {@link #mUris} table.
    150      * @param position position in the {@link #mUris} list
    151      * @return a loader id
    152      */
    153     private static int getLoaderFromPosition(int position) {
    154         return position + AbstractActivityController.LAST_LOADER_ID;
    155     }
    156 
    157     /**
    158      * Stops watching the given URI for folder changes. Subsequent calls to
    159      * {@link #getUnreadCount(Account)} for this uri will return null.
    160      * @param uri the URI for a folder
    161      */
    162     private void stopWatching(Uri uri) {
    163         if (uri == null) {
    164             return;
    165         }
    166 
    167         final int id = mUris.indexOf(uri);
    168         // Does not exist in the list, we have stopped watching it already.
    169         if (id < 0) {
    170             return;
    171         }
    172         // Destroy the loader before removing references to the object.
    173         final LoaderManager lm = mActivity.getLoaderManager();
    174         lm.destroyLoader(getLoaderFromPosition(id));
    175         mInboxMap.remove(uri);
    176         mUris.set(id, null);
    177     }
    178 
    179     /**
    180      * Returns the unread count for the default inbox for the account given. The account must be
    181      * watched with {@link #updateAccountList(Account[])}. If the account was not in an account
    182      * list passed previously, this method returns zero.
    183      * @param account an account whose unread count we wisht to track
    184      * @return the unread count if the account was in array passed previously to {@link
    185      * #updateAccountList(Account[])}. Zero otherwise.
    186      */
    187     public final int getUnreadCount(Account account) {
    188         final Folder f = getDefaultInbox(account);
    189         if (f != null) {
    190             return f.unreadCount;
    191         }
    192         return 0;
    193     }
    194 
    195     public final Folder getDefaultInbox(Account account) {
    196         final Uri uri = account.settings.defaultInbox;
    197         if (mInboxMap.containsKey(uri)) {
    198             final Folder candidate = mInboxMap.get(uri);
    199             if (candidate != null) {
    200                 return candidate;
    201             }
    202         }
    203         return null;
    204     }
    205 
    206     /**
    207      * Class to perform {@link LoaderManager.LoaderCallbacks} for populating unread counts.
    208      */
    209     private class UnreadLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
    210         // TODO(viki): Fix http://b/8494129 and read only the URI and unread count.
    211         /** Only interested in the folder unread count, but asking for everything due to
    212          * bug 8494129. */
    213         private final String[] projection = UIProvider.FOLDERS_PROJECTION;
    214 
    215         @Override
    216         public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
    217             final Uri uri = Uri.parse(args.getString(FOLDER_URI));
    218             return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), uri, projection,
    219                     Folder.FACTORY);
    220         }
    221 
    222         @Override
    223         public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
    224             if (data == null || data.getCount() <= 0 || !data.moveToFirst()) {
    225                 return;
    226             }
    227             final Folder f = data.getModel();
    228             final Uri uri = f.folderUri.getComparisonUri();
    229             final int unreadCount = f.unreadCount;
    230             final Folder previousFolder = mInboxMap.get(uri);
    231             final boolean unreadCountChanged = previousFolder == null
    232                     || unreadCount != previousFolder.unreadCount;
    233             mInboxMap.put(uri, f);
    234             // Once we have updated data, we notify the parent class that something new appeared.
    235             if (unreadCountChanged) {
    236                 mConsumer.onUnreadCountChange();
    237             }
    238         }
    239 
    240         @Override
    241         public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
    242             // Do nothing.
    243         }
    244     }
    245 }
    246