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.widget.BaseAdapter;
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 BaseAdapter mConsumer;
58
59    private final static String LOG_TAG = LogUtils.TAG;
60
61    /**
62     * Create a {@link FolderWatcher}.
63     * @param activity Upstream activity
64     * @param consumer If non-null, a consumer to be notified when the unread count changes
65     */
66    public FolderWatcher(RestrictedActivity activity, BaseAdapter consumer) {
67        mActivity = activity;
68        mConsumer = consumer;
69    }
70
71    /**
72     * Start watching all the accounts in this list and stop watching accounts NOT on this list.
73     * Does nothing if the list of all accounts is null.
74     * @param allAccounts all the current accounts on the device.
75     */
76    public void updateAccountList(Account[] allAccounts) {
77        if (allAccounts == null) {
78            return;
79        }
80        // Create list of Inbox URIs from the array of accounts.
81        final List<Uri> newAccounts = new ArrayList<Uri>(allAccounts.length);
82        for (final Account account : allAccounts) {
83            newAccounts.add(account.settings.defaultInbox);
84        }
85        // Stop watching accounts not in the new list.
86        final List<Uri> uriCopy = Collections.unmodifiableList(Lists.newArrayList(mUris));
87        for (final Uri previous : uriCopy) {
88            if (!newAccounts.contains(previous)) {
89                stopWatching(previous);
90            }
91        }
92        // Add accounts in the new list, that are not already watched.
93        for (final Uri fresh : newAccounts) {
94            if (!mUris.contains(fresh)) {
95                startWatching(fresh);
96            }
97        }
98    }
99
100    /**
101     * Starts watching the given URI for changes. It is NOT safe to call this method repeatedly
102     * for the same URI.
103     * @param uri the URI for an inbox whose unread count is to be watched
104     */
105    private void startWatching(Uri uri) {
106        final int location = insertAtNextEmptyLocation(uri);
107        LogUtils.d(LOG_TAG, "Watching %s, at position %d.", uri, location);
108        // No inbox folder yet, put a safe placeholder for now.
109        mInboxMap.put(uri, null);
110        final LoaderManager lm = mActivity.getLoaderManager();
111        final Bundle args = new Bundle();
112        args.putString(FOLDER_URI, uri.toString());
113        lm.initLoader(getLoaderFromPosition(location), args, mUnreadCallback);
114    }
115
116    /**
117     * Locates the next empty position in {@link #mUris} and inserts the URI there, returning the
118     * location.
119     * @return location where the URI was inserted.
120     */
121    private int insertAtNextEmptyLocation(Uri newElement) {
122        Uri uri;
123        int location = -1;
124        for (int size = mUris.size(), i = 0; i < size; i++) {
125            uri = mUris.get(i);
126            // Hole in the list, use this position
127            if (uri == null) {
128                location = i;
129                break;
130            }
131        }
132
133        if (location < 0) {
134            // No hole found, return the current size;
135            location = mUris.size();
136            mUris.add(location, newElement);
137        } else {
138            mUris.set(location, newElement);
139        }
140        return location;
141    }
142
143    /**
144     * Returns the loader ID for a position inside the {@link #mUris} table.
145     * @param position position in the {@link #mUris} list
146     * @return a loader id
147     */
148    private static int getLoaderFromPosition(int position) {
149        return position + AbstractActivityController.LAST_LOADER_ID;
150    }
151
152    /**
153     * Stops watching the given URI for folder changes. Subsequent calls to
154     * {@link #getUnreadCount(Account)} for this uri will return null.
155     * @param uri the URI for a folder
156     */
157    private void stopWatching(Uri uri) {
158        if (uri == null) {
159            return;
160        }
161
162        final int id = mUris.indexOf(uri);
163        // Does not exist in the list, we have stopped watching it already.
164        if (id < 0) {
165            return;
166        }
167        // Destroy the loader before removing references to the object.
168        final LoaderManager lm = mActivity.getLoaderManager();
169        lm.destroyLoader(getLoaderFromPosition(id));
170        mInboxMap.remove(uri);
171        mUris.set(id, null);
172    }
173
174    /**
175     * Returns the unread count for the default inbox for the account given. The account must be
176     * watched with {@link #updateAccountList(Account[])}. If the account was not in an account
177     * list passed previously, this method returns zero.
178     * @param account an account whose unread count we wisht to track
179     * @return the unread count if the account was in array passed previously to {@link
180     * #updateAccountList(Account[])}. Zero otherwise.
181     */
182    public final int getUnreadCount(Account account) {
183        final Folder f = getDefaultInbox(account);
184        if (f != null) {
185            return f.unreadCount;
186        }
187        return 0;
188    }
189
190    public final Folder getDefaultInbox(Account account) {
191        final Uri uri = account.settings.defaultInbox;
192        if (mInboxMap.containsKey(uri)) {
193            final Folder candidate = mInboxMap.get(uri);
194            if (candidate != null) {
195                return candidate;
196            }
197        }
198        return null;
199    }
200
201    /**
202     * Class to perform {@link LoaderManager.LoaderCallbacks} for populating unread counts.
203     */
204    private class UnreadLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
205        // TODO(viki): Fix http://b/8494129 and read only the URI and unread count.
206        /** Only interested in the folder unread count, but asking for everything due to
207         * bug 8494129. */
208        private final String[] projection = UIProvider.FOLDERS_PROJECTION;
209
210        @Override
211        public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
212            final Uri uri = Uri.parse(args.getString(FOLDER_URI));
213            return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), uri, projection,
214                    Folder.FACTORY);
215        }
216
217        @Override
218        public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
219            if (data == null || data.getCount() <= 0 || !data.moveToFirst()) {
220                return;
221            }
222            final Folder f = data.getModel();
223            final Uri uri = f.folderUri.getComparisonUri();
224            final int unreadCount = f.unreadCount;
225            final Folder previousFolder = mInboxMap.get(uri);
226            final boolean unreadCountChanged = previousFolder == null
227                    || unreadCount != previousFolder.unreadCount;
228            mInboxMap.put(uri, f);
229            // Once we have updated data, we notify the parent class that something new appeared.
230            if (unreadCountChanged) {
231                mConsumer.notifyDataSetChanged();
232            }
233        }
234
235        @Override
236        public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
237            // Do nothing.
238        }
239    }
240}
241