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
18package com.android.mail.providers;
19
20import android.app.LoaderManager;
21import android.content.Loader;
22import android.net.Uri;
23import android.os.Bundle;
24import android.support.annotation.NonNull;
25
26import com.android.mail.content.ObjectCursor;
27import com.android.mail.content.ObjectCursorLoader;
28import com.android.mail.ui.AbstractActivityController;
29import com.android.mail.ui.RestrictedActivity;
30import com.android.mail.utils.LogUtils;
31import com.google.common.collect.Lists;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.HashMap;
36import java.util.List;
37import 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 */
43public 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