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 androidx.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