Better handling of deletions of accounts

Bug: 6141367
Change-Id: If212a91e772a48717ac77326cd6ea1f1ad5ebfa5
diff --git a/src/com/android/mail/providers/AccountCacheProvider.java b/src/com/android/mail/providers/AccountCacheProvider.java
index 67c80d0..62cab37 100644
--- a/src/com/android/mail/providers/AccountCacheProvider.java
+++ b/src/com/android/mail/providers/AccountCacheProvider.java
@@ -41,10 +41,12 @@
 import com.google.common.collect.Sets;
 
 import java.lang.IllegalStateException;
-import java.lang.Override;
+import java.lang.StringBuilder;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
+
 
 
 /**
@@ -64,10 +66,7 @@
 
     private final static String LOG_TAG = new LogUtils().getLogTag();
 
-    private final Map<Uri, Account> mAccountCache = Maps.newHashMap();
-
-    // Map from content provider query uri to the set of account uri that resulted from that query
-    private final static Map<Uri, Set<Uri>> QUERY_URI_ACCOUNT_URIS_MAP = Maps.newHashMap();
+    private final Map<Uri, AccountCacheEntry> mAccountCache = Maps.newHashMap();
 
     private final Map<Uri, CursorLoader> mCursorLoaderMap = Maps.newHashMap();
 
@@ -127,11 +126,12 @@
 
         // Make a copy of the account cache
 
-        final Set<Account> accountList;
+        final Set<AccountCacheEntry> accountList;
         synchronized (mAccountCache) {
             accountList = ImmutableSet.copyOf(mAccountCache.values());
         }
-        for (Account account : accountList) {
+        for (AccountCacheEntry accountEntry : accountList) {
+            final Account account = accountEntry.mAccount;
             final MatrixCursor.RowBuilder builder = cursor.newRow();
 
             for (String column : resultProjection) {
@@ -240,19 +240,19 @@
         mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader);
     }
 
-    public static void addAccount(Account account) {
+    public static void addAccount(Account account, Uri accountsQueryUri) {
         final AccountCacheProvider provider = getInstance();
         if (provider == null) {
             throw new IllegalStateException("AccountCacheProvider not intialized");
         }
-        provider.addAccountImpl(account);
+        provider.addAccountImpl(account, accountsQueryUri);
     }
 
-    private void addAccountImpl(Account account) {
+    private void addAccountImpl(Account account, Uri accountsQueryUri) {
         synchronized (mAccountCache) {
             if (account != null) {
                 LogUtils.v(LOG_TAG, "adding account %s", account);
-                mAccountCache.put(account.uri, account);
+                mAccountCache.put(account.uri, new AccountCacheEntry(account, accountsQueryUri));
             }
         }
         // Explicitly calling this out of the synchronized block in case any of the observers get
@@ -302,11 +302,11 @@
         if (accountsStringSet != null) {
             for (String serializedAccount : accountsStringSet) {
                 try {
-                    final Account account = new Account(serializedAccount);
-                    addAccount(account);
+                    final AccountCacheEntry accountEntry = new AccountCacheEntry(serializedAccount);
+                    addAccount(accountEntry.mAccount, accountEntry.mAccountsQueryUri);
                 } catch (IllegalArgumentException e) {
                     // Unable to create account object, skip to next
-                    LogUtils.e(LOG_TAG,
+                    LogUtils.e(LOG_TAG, e,
                             "Unable to create account object from serialized string'%s'",
                             serializedAccount);
                 }
@@ -317,14 +317,14 @@
     private void cacheAccountList() {
         final SharedPreferences preference = getPreferences();
 
-        final Set<Account> accountList;
+        final Set<AccountCacheEntry> accountList;
         synchronized (mAccountCache) {
             accountList = ImmutableSet.copyOf(mAccountCache.values());
         }
 
         final Set<String> serializedAccounts = Sets.newHashSet();
-        for (Account account : accountList) {
-            serializedAccounts.add(account.serialize());
+        for (AccountCacheEntry accountEntry : accountList) {
+            serializedAccounts.add(accountEntry.serialize());
         }
 
         final SharedPreferences.Editor editor = getPreferences().edit();
@@ -340,8 +340,6 @@
         return mSharedPrefs;
     }
 
-
-
     @Override
     public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
         if (data == null) {
@@ -352,23 +350,26 @@
         LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount());
         final CursorLoader cursorLoader = (CursorLoader)loader;
         final Uri accountsQueryUri = cursorLoader.getUri();
-        // TODO(pwestbro):
-        // 1) Keep a cache of Cursors which would allow changes to be observered.
-        final Set<Uri> previousQueryUriMap = QUERY_URI_ACCOUNT_URIS_MAP.get(accountsQueryUri);
+
+        final Set<AccountCacheEntry> accountList;
+        synchronized (mAccountCache) {
+            accountList = ImmutableSet.copyOf(mAccountCache.values());
+        }
+
+        // Build a set of the account uris that had been associated with that query
+        final Set<Uri> previousQueryUriMap = Sets.newHashSet();
+        for (AccountCacheEntry entry : accountList) {
+            if (accountsQueryUri.equals(entry.mAccountsQueryUri)) {
+                previousQueryUriMap.add(entry.mAccount.uri);
+            }
+        }
 
         final Set<Uri> newQueryUriMap = Sets.newHashSet();
         while (data.moveToNext()) {
             final Account account = new Account(data);
             final Uri accountUri = account.uri;
             newQueryUriMap.add(accountUri);
-            addAccount(account);
-        }
-
-        // Save the new set, or remove the previous entry if it is empty
-        if (newQueryUriMap.size() > 0) {
-            QUERY_URI_ACCOUNT_URIS_MAP.put(accountsQueryUri, newQueryUriMap);
-        } else {
-            QUERY_URI_ACCOUNT_URIS_MAP.remove(accountsQueryUri);
+            addAccount(account, accountsQueryUri);
         }
 
         if (previousQueryUriMap != null) {
@@ -382,4 +383,50 @@
             }
         }
     }
+
+    /**
+     * Object that allows the Account Cache provider to associate the account with the content
+     * provider uri that originated that account.
+     */
+    private static class AccountCacheEntry {
+        final Account mAccount;
+        final Uri mAccountsQueryUri;
+
+        private static final String ACCOUNT_ENTRY_COMPONENT_SEPARATOR = "^**^";
+        private static final Pattern ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN =
+                Pattern.compile("\\^\\*\\*\\^");
+
+        private static final int NUMBER_MEMBERS = 2;
+
+        public AccountCacheEntry(Account account, Uri accountQueryUri) {
+            mAccount = account;
+            mAccountsQueryUri = accountQueryUri;
+        }
+
+        /**
+         * Return a serialized String for this AccountCacheEntry.
+         */
+        public synchronized String serialize() {
+            StringBuilder out = new StringBuilder();
+            out.append(mAccount.serialize()).append(ACCOUNT_ENTRY_COMPONENT_SEPARATOR);
+            final String accountQueryUri =
+                    mAccountsQueryUri != null ? mAccountsQueryUri.toString() : "";
+            out.append(accountQueryUri);
+            return out.toString();
+        }
+
+        public AccountCacheEntry(String serializedString) {
+            String[] cacheEntryMembers = TextUtils.split(serializedString,
+                    ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN);
+            if (cacheEntryMembers.length != NUMBER_MEMBERS) {
+                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
+                        + "Wrong number of members detected. "
+                        + cacheEntryMembers.length + " detected");
+            }
+            mAccount = new Account(cacheEntryMembers[0]);
+            mAccountsQueryUri = !TextUtils.isEmpty(cacheEntryMembers[1]) ?
+                    Uri.parse(cacheEntryMembers[1]) : null;
+        }
+
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/mail/providers/protos/mock/MockUiProvider.java b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
index d02f472..6a9603d 100644
--- a/src/com/android/mail/providers/protos/mock/MockUiProvider.java
+++ b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
@@ -54,6 +54,9 @@
 
     static final String BASE_URI_STRING = "content://" + AUTHORITY;
 
+    private static final Uri MOCK_ACCOUNTS_URI = Uri.parse("content://" + AUTHORITY + "/accounts");
+
+
     public static final int NUM_MOCK_ACCOUNTS = 2;
 
     // A map of query result for uris
@@ -393,7 +396,7 @@
         dest.writeParcelable((Uri) accountInfo.get(AccountColumns.RECENT_FOLDER_LIST_URI), 0);
         dest.setDataPosition(0);
         final Account account = new Account(dest);
-        AccountCacheProvider.addAccount(account);
+        AccountCacheProvider.addAccount(account, MOCK_ACCOUNTS_URI);
     }
 }