Friend intent: Proper support for dataSet

- Introduce AccountTypeWithDataSet to encapsulate accountType + dataSet
  and use it instead of the "account type + '/' + dataset" string,
  for better type safety.

Bug 5162267

Change-Id: Id96aea69804bb1151b612838f3fdc24841e5f527
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index dbfe411..1107530 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -18,11 +18,13 @@
 
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountTypeWithDataSet;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.StreamItemEntry;
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.google.android.collect.Lists;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
 import android.content.ContentResolver;
@@ -781,20 +783,21 @@
          * TODO Exclude the ones with no raw contacts in the database.
          */
         private void loadInvitableAccountTypes(Result contactData) {
-            Map<String, AccountType> allInvitables =
+            Map<AccountTypeWithDataSet, AccountType> allInvitables =
                     AccountTypeManager.getInstance(getContext()).getInvitableAccountTypes();
             if (allInvitables.isEmpty()) {
                 return;
             }
 
-            HashMap<String, AccountType> result = new HashMap<String, AccountType>(allInvitables);
+            HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(allInvitables);
 
             // Remove the ones that already have a raw contact in the current contact
             for (Entity entity : contactData.getEntities()) {
-                final String type = entity.getEntityValues().getAsString(RawContacts.ACCOUNT_TYPE);
-                if (!TextUtils.isEmpty(type)) {
-                    result.remove(type);
-                }
+                final ContentValues values = entity.getEntityValues();
+                final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+                        values.getAsString(RawContacts.ACCOUNT_TYPE),
+                        values.getAsString(RawContacts.DATA_SET));
+                result.remove(type);
             }
 
             // Set to mInvitableAccountTypes
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 95216e6..8ee1aa1 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -51,8 +51,6 @@
 public abstract class AccountType {
     private static final String TAG = "AccountType";
 
-    private static final String ACCOUNT_TYPE_DATA_SET_DELIMITER = "/";
-
     /**
      * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
      */
@@ -150,21 +148,10 @@
     }
 
     /**
-     * Returns the account type with the data set (if any) appended after a delimiter.
-     * If the data set is null, this will simply return the account type.
+     * Returns {@link AccountTypeWithDataSet} for this type.
      */
-    public String getAccountTypeAndDataSet() {
-        return getAccountTypeAndDataSet(accountType, dataSet);
-    }
-
-    /**
-     * Utility method to concatenate the given account type with a data set with a delimiter.
-     * If the data set is null, this will simply return the account type.
-     */
-    public static String getAccountTypeAndDataSet(String accountType, String dataSet) {
-        return dataSet == null
-                ? accountType
-                : accountType + ACCOUNT_TYPE_DATA_SET_DELIMITER + dataSet;
+    public AccountTypeWithDataSet getAccountTypeAndDataSet() {
+        return AccountTypeWithDataSet.get(accountType, dataSet);
     }
 
     /**
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index b517c2c..6d52e18 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -88,10 +88,10 @@
     public abstract AccountType getAccountType(String accountType, String dataSet);
 
     /**
-     * @return Unmodifiable map from account type strings to {@link AccountType}s which support
-     * the "invite" feature and have one or more account.
+     * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
+     * which support the "invite" feature and have one or more account.
      */
-    public abstract Map<String, AccountType> getInvitableAccountTypes();
+    public abstract Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes();
 
     /**
      * Find the best {@link DataKind} matching the requested
@@ -114,9 +114,9 @@
 
     private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
     private List<AccountWithDataSet> mWritableAccounts = Lists.newArrayList();
-    private Map<String, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
-    private Map<String, AccountType> mInvitableAccountTypes = Collections.unmodifiableMap(
-            new HashMap<String, AccountType>());
+    private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
+    private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
+            Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
 
     private static final int MESSAGE_LOAD_DATA = 0;
     private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
@@ -266,7 +266,7 @@
         long startTime = SystemClock.currentThreadTimeMillis();
 
         // Account types, keyed off the account type and data set concatenation.
-        Map<String, AccountType> accountTypesByTypeAndDataSet = Maps.newHashMap();
+        Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = Maps.newHashMap();
 
         // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}.  Since there can
         // be multiple account types (with different data sets) for the same type of account, each
@@ -404,7 +404,7 @@
 
     // Bookkeeping method for tracking the known account types in the given maps.
     private void addAccountType(AccountType accountType,
-            Map<String, AccountType> accountTypesByTypeAndDataSet,
+            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
             Map<String, List<AccountType>> accountTypesByType) {
         accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
         List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
@@ -450,7 +450,7 @@
 
         // Try finding account type and kind matching request
         final AccountType type = mAccountTypesWithDataSets.get(
-                AccountType.getAccountTypeAndDataSet(accountType, dataSet));
+                AccountTypeWithDataSet.get(accountType, dataSet));
         if (type != null) {
             kind = type.getKindForMimetype(mimeType);
         }
@@ -475,13 +475,13 @@
         ensureAccountsLoaded();
         synchronized (this) {
             AccountType type = mAccountTypesWithDataSets.get(
-                    AccountType.getAccountTypeAndDataSet(accountType, dataSet));
+                    AccountTypeWithDataSet.get(accountType, dataSet));
             return type != null ? type : mFallbackAccountType;
         }
     }
 
     @Override
-    public Map<String, AccountType> getInvitableAccountTypes() {
+    public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
         return mInvitableAccountTypes;
     }
 
@@ -490,14 +490,13 @@
      * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
      */
     @VisibleForTesting
-    static Map<String, AccountType> findInvitableAccountTypes(Context context,
+    static Map<AccountTypeWithDataSet, AccountType> findInvitableAccountTypes(Context context,
             Collection<AccountWithDataSet> accounts,
-            Map<String, AccountType> accountTypesByTypeAndDataSet) {
-        HashMap<String, AccountType> result = Maps.newHashMap();
+            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
+        HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
         for (AccountWithDataSet account : accounts) {
-            String accountTypeWithDataSet = account.getAccountTypeWithDataSet();
-            AccountType type = accountTypesByTypeAndDataSet.get(
-                    account.getAccountTypeWithDataSet());
+            AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeAndWithDataSet();
+            AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
             if (type == null) continue; // just in case
             if (result.containsKey(accountTypeWithDataSet)) continue;
 
diff --git a/src/com/android/contacts/model/AccountTypeWithDataSet.java b/src/com/android/contacts/model/AccountTypeWithDataSet.java
new file mode 100644
index 0000000..d5cdbdd
--- /dev/null
+++ b/src/com/android/contacts/model/AccountTypeWithDataSet.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.model;
+
+import com.google.common.base.Objects;
+
+import android.text.TextUtils;
+
+
+/**
+ * Encapsulates an "account type" string and a "data set" string.
+ */
+public class AccountTypeWithDataSet {
+    /** account type will never be null. */
+    public final String accountType;
+
+    /** dataSet may be null, but never be "". */
+    public final String dataSet;
+
+    private AccountTypeWithDataSet(String accountType, String dataSet) {
+        if (accountType == null) throw new NullPointerException();
+
+        this.accountType = accountType;
+        this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet;
+    }
+
+    public static AccountTypeWithDataSet get(String accountType, String dataSet) {
+        return new AccountTypeWithDataSet(accountType, dataSet);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof AccountTypeWithDataSet)) return false;
+
+        AccountTypeWithDataSet other = (AccountTypeWithDataSet) o;
+        return Objects.equal(accountType, other.accountType)
+                && Objects.equal(dataSet, other.dataSet);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(accountType) ^ (dataSet == null ? 0 : Objects.hashCode(dataSet));
+    }
+
+    @Override
+    public String toString() {
+        return "[" + accountType + "/" + dataSet + "]";
+    }
+}
diff --git a/src/com/android/contacts/model/AccountWithDataSet.java b/src/com/android/contacts/model/AccountWithDataSet.java
index 1d97614..f607737 100644
--- a/src/com/android/contacts/model/AccountWithDataSet.java
+++ b/src/com/android/contacts/model/AccountWithDataSet.java
@@ -27,19 +27,22 @@
 public class AccountWithDataSet extends Account {
 
     public final String dataSet;
+    private final AccountTypeWithDataSet mAccountTypeWithDataSet;
 
     public AccountWithDataSet(String name, String type, String dataSet) {
         super(name, type);
         this.dataSet = dataSet;
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
     public AccountWithDataSet(Parcel in, String dataSet) {
         super(in);
         this.dataSet = dataSet;
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
-    public String getAccountTypeWithDataSet() {
-        return dataSet == null ? type : AccountType.getAccountTypeAndDataSet(type, dataSet);
+    public AccountTypeWithDataSet getAccountTypeAndWithDataSet() {
+        return mAccountTypeWithDataSet;
     }
 
     @Override
diff --git a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
index 27b3e90..81c270f 100644
--- a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
@@ -21,6 +21,7 @@
 
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import java.util.Collection;
 import java.util.HashMap;
@@ -33,25 +34,27 @@
  * adb shell am instrument -w -e class com.android.contacts.model.AccountTypeManagerTest \
        com.android.contacts.tests/android.test.InstrumentationTestRunner
  */
+@SmallTest
 public class AccountTypeManagerTest extends AndroidTestCase {
     public void testFindInvitableAccountTypes() {
         final Context c = getContext();
 
         // Define account types.
-        final AccountType typeA = new MockAccountType("typeA", null);
-        final AccountType typeB = new MockAccountType("typeB", null);
-        final AccountType typeC = new MockAccountType("typeC", "c");
-        final AccountType typeD = new MockAccountType("typeD", "d");
+        final AccountType typeA = new MockAccountType("type1", null, null);
+        final AccountType typeB = new MockAccountType("type1", "minus", null);
+        final AccountType typeC = new MockAccountType("type2", null, "c");
+        final AccountType typeD = new MockAccountType("type2", "minus", "d");
 
         // Define users
-        final AccountWithDataSet accountA1 = new AccountWithDataSet("a1", typeA.accountType, null);
-        final AccountWithDataSet accountC1 = new AccountWithDataSet("c1", typeC.accountType, null);
-        final AccountWithDataSet accountC2 = new AccountWithDataSet("c2", typeC.accountType, null);
-        final AccountWithDataSet accountD1 = new AccountWithDataSet("d1", typeD.accountType, null);
+        final AccountWithDataSet accountA1 = createAccountWithDataSet("a1", typeA);
+        final AccountWithDataSet accountC1 = createAccountWithDataSet("c1", typeC);
+        final AccountWithDataSet accountC2 = createAccountWithDataSet("c2", typeC);
+        final AccountWithDataSet accountD1 = createAccountWithDataSet("d1", typeD);
 
         // empty - empty
-        Map<String, AccountType> types = AccountTypeManagerImpl.findInvitableAccountTypes(c,
-                buildAccounts(), buildAccountTypes());
+        Map<AccountTypeWithDataSet, AccountType> types =
+                AccountTypeManagerImpl.findInvitableAccountTypes(c,
+                        buildAccounts(), buildAccountTypes());
         assertEquals(0, types.size());
         try {
             types.clear();
@@ -62,7 +65,7 @@
         // No invite support, no accounts
         verifyAccountTypes(
                 buildAccounts(),
-                buildAccountTypes(typeA)
+                buildAccountTypes(typeA, typeB)
                 /* empty */
                 );
 
@@ -115,18 +118,22 @@
 
         verifyAccountTypes(
                 buildAccounts(accountC1, accountA1, accountD1),
-                buildAccountTypes(typeD, typeA, typeC),
+                buildAccountTypes(typeD, typeA, typeC, typeB),
                 typeC, typeD
                 );
     }
 
+    private static AccountWithDataSet createAccountWithDataSet(String name, AccountType type) {
+        return new AccountWithDataSet(name, type.accountType, type.dataSet);
+    }
+
     /**
      * Array of {@link AccountType} -> {@link Map}
      */
-    private static Map<String, AccountType> buildAccountTypes(AccountType... types) {
-        final HashMap<String, AccountType> result = Maps.newHashMap();
+    private static Map<AccountTypeWithDataSet, AccountType> buildAccountTypes(AccountType... types) {
+        final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
         for (AccountType type : types) {
-            result.put(type.accountType, type);
+            result.put(type.getAccountTypeAndDataSet(), type);
         }
         return result;
     }
@@ -146,22 +153,27 @@
      * Executes {@link AccountTypeManagerImpl#findInvitableAccountTypes} and verifies the
      * result.
      */
-    private void verifyAccountTypes(Collection<AccountWithDataSet> accounts,
-            Map<String, AccountType> types, AccountType... expectedTypes) {
-        Map<String, AccountType> result = AccountTypeManagerImpl.findInvitableAccountTypes(
-                getContext(), accounts, types);
-        for (AccountType type : expectedTypes) {
-            if (!result.containsKey(type.getAccountTypeAndDataSet())) {
-                fail("Result doesn't contain type=" + type.getAccountTypeAndDataSet());
-            }
+    private void verifyAccountTypes(
+            Collection<AccountWithDataSet> accounts,
+            Map<AccountTypeWithDataSet, AccountType> types,
+            AccountType... expectedInvitableTypes
+            ) {
+        Map<AccountTypeWithDataSet, AccountType> result =
+                AccountTypeManagerImpl.findInvitableAccountTypes(getContext(), accounts, types);
+        for (AccountType type : expectedInvitableTypes) {
+            assertTrue("Result doesn't contain type=" + type.getAccountTypeAndDataSet(),
+                    result.containsKey(type.getAccountTypeAndDataSet()));
         }
+        final int numExcessTypes = result.size() - expectedInvitableTypes.length;
+        assertEquals("Result contains " + numExcessTypes + " excess type(s)", 0, numExcessTypes);
     }
 
     private static class MockAccountType extends AccountType {
         private final String mInviteContactActivityClassName;
 
-        public MockAccountType(String type, String inviteContactActivityClassName) {
+        public MockAccountType(String type, String dataSet, String inviteContactActivityClassName) {
             accountType = type;
+            this.dataSet = dataSet;
             mInviteContactActivityClassName = inviteContactActivityClassName;
         }
 
diff --git a/tests/src/com/android/contacts/model/AccountTypeTest.java b/tests/src/com/android/contacts/model/AccountTypeTest.java
index 986d840..3d80b52 100644
--- a/tests/src/com/android/contacts/model/AccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeTest.java
@@ -17,14 +17,10 @@
 package com.android.contacts.model;
 
 import com.android.contacts.tests.R;
-import com.google.common.collect.Lists;
 
 import android.content.Context;
 import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
-
-import java.util.ArrayList;
-import java.util.Collections;
+import android.test.suitebuilder.annotation.SmallTest;
 
 /**
  * Test case for {@link AccountType}.
@@ -32,6 +28,7 @@
  * adb shell am instrument -w -e class com.android.contacts.model.AccountTypeTest \
        com.android.contacts.tests/android.test.InstrumentationTestRunner
  */
+@SmallTest
 public class AccountTypeTest extends AndroidTestCase {
     public void testGetResourceText() {
         // In this test we use the test package itself as an external package.
diff --git a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
index e7ef496..eb8c059 100644
--- a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 /**
  * Test case for {@link ExternalAccountType}.
@@ -27,6 +28,7 @@
  * adb shell am instrument -w -e class com.android.contacts.model.ExternalAccountTypeTest \
        com.android.contacts.tests/android.test.InstrumentationTestRunner
  */
+@SmallTest
 public class ExternalAccountTypeTest extends AndroidTestCase {
 
     public void testResolveExternalResId() {
diff --git a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
index e60391e..2be662d 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
@@ -17,6 +17,7 @@
 
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountTypeWithDataSet;
 import com.android.contacts.model.AccountWithDataSet;
 import com.google.android.collect.Maps;
 
@@ -53,7 +54,7 @@
     }
 
     @Override
-    public Map<String, AccountType> getInvitableAccountTypes() {
+    public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
         return Maps.newHashMap(); // Always returns empty
     }
 }