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.widget.BaseAdapter; 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 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