Add experiment flag for device account detection.
Device account detection does DB queries that could potentially block the UI
thread. Just returning the "null" account as the only device account is fine
for now.
Test
* manually verify that "device" account still shows in editor and default
account chooser when no other accounts exist
* manually verify that "device" account still shows in nav drawer when another
non-google account is added
* manually verify that "device" account is hidden after Google Account is
added
Bug 32312206
Change-Id: I347e9dcf0aa6252c7b0be79fea7757b03569edf7
diff --git a/proguard.flags b/proguard.flags
index e50e640..e918c62 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -44,6 +44,7 @@
-keep class com.android.contacts.common.model.BuilderWrapper { *; }
-keep class com.android.contacts.common.model.Contact { *; }
-keep class com.android.contacts.common.model.ContactLoader { *; }
+-keep class com.android.contacts.common.model.Cp2DeviceLocalAccountLocator { *; }
-keep class com.android.contacts.common.model.CPOWrapper { *; }
-keep class com.android.contacts.common.model.dataitem.DataItem { *; }
-keep class com.android.contacts.common.model.dataitem.DataKind { *; }
diff --git a/src/com/android/contacts/common/Experiments.java b/src/com/android/contacts/common/Experiments.java
index e872694..dee95dd 100644
--- a/src/com/android/contacts/common/Experiments.java
+++ b/src/com/android/contacts/common/Experiments.java
@@ -53,6 +53,12 @@
public static final String DYNAMIC_SHORTCUTS = "Shortcuts__dynamic_shortcuts";
/**
+ * Experiment to enable device account detection using CP2 queries
+ */
+ public static final String OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED =
+ "OEM__cp2_device_account_detection_enabled";
+
+ /**
* Experiment to toggle contacts sync using the pull to refresh gesture.
*/
public static final String PULL_TO_REFRESH = "PullToRefresh__pull_to_refresh";
diff --git a/src/com/android/contacts/common/model/AccountTypeManager.java b/src/com/android/contacts/common/model/AccountTypeManager.java
index 4b0bcc1..15c9771 100644
--- a/src/com/android/contacts/common/model/AccountTypeManager.java
+++ b/src/com/android/contacts/common/model/AccountTypeManager.java
@@ -669,11 +669,10 @@
}
}
- final DeviceLocalAccountLocator deviceAccounts =
- new DeviceLocalAccountLocator(mContext.getContentResolver(),
- mDeviceLocalAccountTypeFactory,
- allAccounts);
- final List<AccountWithDataSet> localAccounts = deviceAccounts.getDeviceLocalAccounts();
+ final DeviceLocalAccountLocator deviceAccountLocator = DeviceLocalAccountLocator
+ .create(mContext, allAccounts);
+ final List<AccountWithDataSet> localAccounts = deviceAccountLocator
+ .getDeviceLocalAccounts();
allAccounts.addAll(localAccounts);
for (AccountWithDataSet localAccount : localAccounts) {
diff --git a/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocator.java b/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocator.java
new file mode 100644
index 0000000..64f7c03
--- /dev/null
+++ b/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocator.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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.common.model;
+
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Attempts to create accounts for "Device" contacts by querying
+ * CP2 for records with {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE} columns
+ * that do not exist for any account returned by {@link AccountManager#getAccounts()}
+ *
+ * This class should be used from a background thread since it does DB queries
+ */
+public class Cp2DeviceLocalAccountLocator extends DeviceLocalAccountLocator {
+
+ // Note this class is assuming ACCOUNT_NAME and ACCOUNT_TYPE have same values in
+ // RawContacts, Groups, and Settings. This assumption simplifies the code somewhat and it
+ // is true right now and unlikely to ever change.
+ @VisibleForTesting
+ static String[] PROJECTION = new String[] {
+ ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE,
+ ContactsContract.RawContacts.DATA_SET
+ };
+
+ private static final int COL_NAME = 0;
+ private static final int COL_TYPE = 1;
+ private static final int COL_DATA_SET = 2;
+
+ private final ContentResolver mResolver;
+ private final DeviceLocalAccountTypeFactory mAccountTypeFactory;
+
+ private final String mSelection;
+ private final String[] mSelectionArgs;
+
+ public Cp2DeviceLocalAccountLocator(ContentResolver contentResolver,
+ DeviceLocalAccountTypeFactory factory,
+ List<AccountWithDataSet> knownAccounts) {
+ mResolver = contentResolver;
+ mAccountTypeFactory = factory;
+
+ final Set<String> knownAccountTypes = new HashSet<>();
+ for (AccountWithDataSet account : knownAccounts) {
+ knownAccountTypes.add(account.type);
+ }
+ mSelection = getSelection(knownAccountTypes);
+ mSelectionArgs = getSelectionArgs(knownAccountTypes);
+ }
+
+ @Override
+ public List<AccountWithDataSet> getDeviceLocalAccounts() {
+
+ final Set<AccountWithDataSet> localAccounts = new HashSet<>();
+
+ // Many device accounts have default groups associated with them.
+ addAccountsFromQuery(ContactsContract.Groups.CONTENT_URI, localAccounts);
+ addAccountsFromQuery(ContactsContract.Settings.CONTENT_URI, localAccounts);
+ addAccountsFromQuery(ContactsContract.RawContacts.CONTENT_URI, localAccounts);
+
+ return new ArrayList<>(localAccounts);
+ }
+
+ private void addAccountsFromQuery(Uri uri, Set<AccountWithDataSet> accounts) {
+ final Cursor cursor = mResolver.query(uri, PROJECTION, mSelection, mSelectionArgs, null);
+
+ if (cursor == null) return;
+
+ try {
+ addAccountsFromCursor(cursor, accounts);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void addAccountsFromCursor(Cursor cursor, Set<AccountWithDataSet> accounts) {
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(COL_NAME);
+ final String type = cursor.getString(COL_TYPE);
+ final String dataSet = cursor.getString(COL_DATA_SET);
+
+ if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
+ mAccountTypeFactory, type)) {
+ accounts.add(new AccountWithDataSet(name, type, dataSet));
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public String getSelection() {
+ return mSelection;
+ }
+
+ @VisibleForTesting
+ public String[] getSelectionArgs() {
+ return mSelectionArgs;
+ }
+
+ private static String getSelection(Set<String> knownAccountTypes) {
+ final StringBuilder sb = new StringBuilder()
+ .append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" IS NULL");
+ if (knownAccountTypes.isEmpty()) {
+ return sb.toString();
+ }
+ sb.append(" OR ").append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" NOT IN (");
+ for (String ignored : knownAccountTypes) {
+ sb.append("?,");
+ }
+ // Remove trailing ','
+ sb.deleteCharAt(sb.length() - 1).append(')');
+ return sb.toString();
+ }
+
+ private static String[] getSelectionArgs(Set<String> knownAccountTypes) {
+ if (knownAccountTypes.isEmpty()) return null;
+
+ return knownAccountTypes.toArray(new String[knownAccountTypes.size()]);
+ }
+}
diff --git a/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java b/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
index 8c45c40..17f5f5e 100644
--- a/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
@@ -15,128 +15,41 @@
*/
package com.android.contacts.common.model;
-import android.accounts.AccountManager;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.support.annotation.VisibleForTesting;
+import android.content.Context;
+import com.android.contacts.common.Experiments;
import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
+import com.android.contactsbind.ObjectFactory;
+import com.android.contactsbind.experiments.Flags;
-import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collections;
import java.util.List;
-import java.util.Set;
/**
- * DeviceLocalAccountLocator attempts to create accounts for "Device" contacts by querying
- * CP2 for records with {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE} columns
- * that do not exist for any account returned by {@link AccountManager#getAccounts()}
- *
- * This class should be used from a background thread since it does DB queries
+ * Attempts to detect accounts for device contacts
*/
-public class DeviceLocalAccountLocator {
+public abstract class DeviceLocalAccountLocator {
- // Note this class is assuming ACCOUNT_NAME and ACCOUNT_TYPE have same values in
- // RawContacts, Groups, and Settings. This assumption simplifies the code somewhat and it
- // is true right now and unlikely to ever change.
- @VisibleForTesting
- static String[] PROJECTION = new String[] {
- ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE,
- ContactsContract.RawContacts.DATA_SET
+ /**
+ * Returns a list of device local accounts
+ */
+ public abstract List<AccountWithDataSet> getDeviceLocalAccounts();
+
+ // This works on Nexus and AOSP because the local device account is the null account but most
+ // OEMs have a special account name and type for their device account.
+ public static final DeviceLocalAccountLocator NULL_ONLY = new DeviceLocalAccountLocator() {
+ @Override
+ public List<AccountWithDataSet> getDeviceLocalAccounts() {
+ return Collections.singletonList(AccountWithDataSet.getNullAccount());
+ }
};
- private static final int COL_NAME = 0;
- private static final int COL_TYPE = 1;
- private static final int COL_DATA_SET = 2;
-
- private final ContentResolver mResolver;
- private final DeviceLocalAccountTypeFactory mAccountTypeFactory;
-
- private final String mSelection;
- private final String[] mSelectionArgs;
-
- public DeviceLocalAccountLocator(ContentResolver contentResolver,
- DeviceLocalAccountTypeFactory factory,
+ public static DeviceLocalAccountLocator create(Context context,
List<AccountWithDataSet> knownAccounts) {
- mResolver = contentResolver;
- mAccountTypeFactory = factory;
-
- final Set<String> knownAccountTypes = new HashSet<>();
- for (AccountWithDataSet account : knownAccounts) {
- knownAccountTypes.add(account.type);
+ if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
+ return new Cp2DeviceLocalAccountLocator(context.getContentResolver(),
+ ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownAccounts);
}
- mSelection = getSelection(knownAccountTypes);
- mSelectionArgs = getSelectionArgs(knownAccountTypes);
- }
-
- public List<AccountWithDataSet> getDeviceLocalAccounts() {
-
- final Set<AccountWithDataSet> localAccounts = new HashSet<>();
-
- // Many device accounts have default groups associated with them.
- addAccountsFromQuery(ContactsContract.Groups.CONTENT_URI, localAccounts);
- addAccountsFromQuery(ContactsContract.Settings.CONTENT_URI, localAccounts);
- addAccountsFromQuery(ContactsContract.RawContacts.CONTENT_URI, localAccounts);
-
- return new ArrayList<>(localAccounts);
- }
-
- private void addAccountsFromQuery(Uri uri, Set<AccountWithDataSet> accounts) {
- final Cursor cursor = mResolver.query(uri, PROJECTION, mSelection, mSelectionArgs, null);
-
- if (cursor == null) return;
-
- try {
- addAccountsFromCursor(cursor, accounts);
- } finally {
- cursor.close();
- }
- }
-
- private void addAccountsFromCursor(Cursor cursor, Set<AccountWithDataSet> accounts) {
- while (cursor.moveToNext()) {
- final String name = cursor.getString(COL_NAME);
- final String type = cursor.getString(COL_TYPE);
- final String dataSet = cursor.getString(COL_DATA_SET);
-
- if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
- mAccountTypeFactory, type)) {
- accounts.add(new AccountWithDataSet(name, type, dataSet));
- }
- }
- }
-
- @VisibleForTesting
- public String getSelection() {
- return mSelection;
- }
-
- @VisibleForTesting
- public String[] getSelectionArgs() {
- return mSelectionArgs;
- }
-
- private static String getSelection(Set<String> knownAccountTypes) {
- final StringBuilder sb = new StringBuilder()
- .append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" IS NULL");
- if (knownAccountTypes.isEmpty()) {
- return sb.toString();
- }
- sb.append(" OR ").append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" NOT IN (");
- for (String ignored : knownAccountTypes) {
- sb.append("?,");
- }
- // Remove trailing ','
- sb.deleteCharAt(sb.length() - 1).append(')');
- return sb.toString();
- }
-
- private static String[] getSelectionArgs(Set<String> knownAccountTypes) {
- if (knownAccountTypes.isEmpty()) return null;
-
- return knownAccountTypes.toArray(new String[knownAccountTypes.size()]);
+ return NULL_ONLY;
}
}
diff --git a/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java b/tests/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocatorTests.java
similarity index 94%
rename from tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java
rename to tests/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocatorTests.java
index e8c4e2f..192eb44 100644
--- a/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java
+++ b/tests/src/com/android/contacts/common/model/Cp2DeviceLocalAccountLocatorTests.java
@@ -40,12 +40,12 @@
import java.util.Map;
@SmallTest
-public class DeviceLocalAccountLocatorTests extends AndroidTestCase {
+public class Cp2DeviceLocalAccountLocatorTests extends AndroidTestCase {
// Basic smoke test that just checks that it doesn't throw when loading from CP2. We don't
// care what CP2 actually contains for this.
public void testShouldNotCrash() {
- final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
+ final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
getContext().getContentResolver(),
new DeviceLocalAccountTypeFactory.Default(getContext()),
Collections.<AccountWithDataSet>emptyList());
@@ -80,7 +80,7 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
+ final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
createStubResolverWithContentQueryResult(queryResult(
"user", "com.example",
"user", "com.example",
@@ -98,7 +98,7 @@
}
public void test_getDeviceLocalAccounts_doesNotContainItemsForKnownAccounts() {
- final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
+ final Cp2DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
getContext().getContentResolver(), new FakeDeviceAccountTypeFactory(),
Arrays.asList(new AccountWithDataSet("user", "com.example", null),
new AccountWithDataSet("user1", "com.example", null),
@@ -116,7 +116,7 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
+ final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
createContentResolverWithProvider(new FakeContactsProvider()
.withQueryResult(ContactsContract.Settings.CONTENT_URI, queryResult(
"phone_account", "vnd.sec.contact.phone",
@@ -130,7 +130,7 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
+ final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
createContentResolverWithProvider(new FakeContactsProvider()
.withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
"phone_account", "vnd.sec.contact.phone",
@@ -142,7 +142,7 @@
private DeviceLocalAccountLocator createWithQueryResult(
Cursor cursor) {
- final DeviceLocalAccountLocator locator = new DeviceLocalAccountLocator(
+ final DeviceLocalAccountLocator locator = new Cp2DeviceLocalAccountLocator(
createStubResolverWithContentQueryResult(cursor),
new DeviceLocalAccountTypeFactory.Default(getContext()),
Collections.<AccountWithDataSet>emptyList());