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 {
onUnreadCountChange()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      */
FolderWatcher( RestrictedActivity activity, @NonNull UnreadCountChangedListener listener)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      */
updateAccountList(Account[] allAccounts)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      */
startWatching(Uri uri)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      */
insertAtNextEmptyLocation(Uri newElement)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      */
getLoaderFromPosition(int position)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      */
stopWatching(Uri uri)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      */
getUnreadCount(Account account)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 
getDefaultInbox(Account account)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data)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
onLoaderReset(Loader<ObjectCursor<Folder>> loader)241         public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
242             // Do nothing.
243         }
244     }
245 }
246