Merge "Remove FeatureHighlight lib from AOSP contacts." into ub-contactsdialer-g-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bd36560..7d7579d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.contacts"
- android:versionCode="10512"
- android:versionName="1.5.12">
+ android:versionCode="20000"
+ android:versionName="2.0.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="25" />
diff --git a/proguard.flags b/proguard.flags
index f1e609b..d9dad57 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -19,6 +19,7 @@
-keep class com.android.contacts.common.database.NoNullCursorAsyncQueryHandler { *; }
-keep class com.android.contacts.common.format.FormatUtils { *; }
-keep class com.android.contacts.common.format.TextHighlighter { *; }
+-keep class com.android.contacts.common.list.ContactListFilter { *; }
-keep class com.android.contacts.common.list.ContactListItemView { *; }
-keep class com.android.contacts.common.list.ContactsSectionIndexer { *; }
-keep class com.android.contacts.common.location.CountryDetector { *; }
@@ -66,6 +67,11 @@
-keep class com.android.contacts.common.util.NameConverter { *; }
-keep class com.android.contacts.common.util.SearchUtil { *; }
-keep class com.android.contacts.common.util.SearchUtil$* { *; }
+-keep class com.android.contacts.common.util.DeviceAccountFilter { *; }
+-keep class com.android.contacts.common.util.DeviceAccountFilter$* { *; }
+-keep class com.android.contacts.common.util.DeviceAccountPresentationValues { *; }
+-keep class com.android.contacts.common.util.DeviceAccountPresentationValues$* { *; }
+-keep class com.android.contacts.common.util.DeviceLocalContactsFilterProvider { *; }
-keep class com.android.contacts.ContactsApplication { *; }
-keep class com.android.contacts.ContactSaveService { *; }
-keep class com.android.contacts.ContactSaveService$* { *; }
@@ -86,11 +92,9 @@
-keep class com.google.common.collect.Multimap { *; }
-keep class com.google.common.collect.Sets { *; }
-# Any class or method annotated with NeededForTesting or NeededForReflection.
--keep @com.android.contacts.common.testing.NeededForTesting class *
+# Any class or method annotated with NeededForReflection.
-keep @com.android.contacts.test.NeededForReflection class *
-keepclassmembers class * {
-@com.android.contacts.common.testing.NeededForTesting *;
@com.android.contacts.test.NeededForReflection *;
}
diff --git a/res/drawable/ic_sim_card_black_24dp.xml b/res/drawable/ic_sim_card_black_24dp.xml
new file mode 100644
index 0000000..40ee84f
--- /dev/null
+++ b/res/drawable/ic_sim_card_black_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Material design SIM card icon https://design.google.com/icons/#ic_sim_card -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.99,4c0,-1.1 -0.89,-2 -1.99,-2h-8L4,8v12c0,1.1 0.9,2 2,2h12.01c1.1,0 1.99,-0.9 1.99,-2l-0.01,-16zM9,19L7,19v-2h2v2zM17,19h-2v-2h2v2zM9,15L7,15v-4h2v4zM13,19h-2v-4h2v4zM13,13h-2v-2h2v2zM17,15h-2v-4h2v4z"/>
+</vector>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 37ea0ba..355a4e8 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -187,13 +187,8 @@
<color name="letter_tile_font_color">#ffffff</color>
- <!-- Background color of action bars. Ensure this stays in sync with packages/Telephony
- actionbar_background_color. -->
- <color name="actionbar_background_color">#0fc6dc</color>
<!-- Color for icons in the actionbar -->
<color name="actionbar_icon_color">#ffffff</color>
- <!-- Darker version of the actionbar color. Used for the status bar and navigation bar colors. -->
- <color name="actionbar_background_color_dark">#008aa1</color>
<color name="tab_ripple_color">@color/tab_accent_color</color>
<color name="tab_accent_color">#ffffff</color>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c4971f1..14207d2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -943,11 +943,6 @@
The user can click on the action to rollback the modification-->
<string name="undo">Undo</string>
- <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
- <string name="toast_text_copied">Text copied</string>
- <!-- Option displayed in context menu to copy long pressed item to clipboard [CHAR LIMIT=64] -->
- <string name="copy_text">Copy to clipboard</string>
-
<!-- Action string for calling a custom phone number -->
<string name="call_custom">Call
<xliff:g id="custom_label" example="business">%s</xliff:g>
@@ -1070,9 +1065,6 @@
<!-- Used to display as default status when the contact is busy or Do not disturb for chat [CHAR LIMIT=19] -->
<string name="status_busy">Busy</string>
- <!-- Directory partition name (also exists in contacts) -->
- <string name="contactsList">Contacts</string>
-
<!-- The name of the invisible local contact directory -->
<string name="local_invisible_directory">Other</string>
@@ -1085,23 +1077,9 @@
<!-- The label in section header in the contact list for a local contacts [CHAR LIMIT=128] -->
<string name="local_search_label">All contacts</string>
- <!-- Title shown in the search result activity of contacts app while searching. [CHAR LIMIT=20]
- (also in contacts) -->
- <string name="search_results_searching">Searching\u2026</string>
-
<!-- Displayed at the top of search results indicating that more contacts were found than shown [CHAR LIMIT=64] -->
<string name="foundTooManyContacts">More than <xliff:g id="count">%d</xliff:g> found.</string>
- <!-- Displayed at the top of the contacts showing the zero total number of contacts found when "Only contacts with phones" not selected. [CHAR LIMIT=30]
- (also in contacts) -->
- <string name="listFoundAllContactsZero">No contacts</string>
-
- <!-- Displayed at the top of the contacts showing the total number of contacts found when typing search query -->
- <plurals name="searchFoundContacts">
- <item quantity="one">1 found</item>
- <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
- </plurals>
-
<!-- String describing the text for photo of a contact in a contacts list.
Note: AccessibilityServices use this attribute to announce what the view represents.
@@ -1137,6 +1115,11 @@
and will not be synced. [CHAR LIMIT=20] -->
<string name="account_phone">Device</string>
+ <!-- Title for data source when creating or editing a contact that is stored on the
+ devices SIM card. This contact will only exist on the phone and will not be synced.
+ [CHAR LIMIT=20] -->
+ <string name="account_sim">SIM</string>
+
<!-- Header that expands to list all name types when editing a structured name of a contact
[CHAR LIMIT=20] -->
<string name="nameLabelsGroup">Name</string>
@@ -1589,9 +1572,6 @@
<!-- Label of the "About" setting -->
<string name="setting_about">About Contacts</string>
- <!-- Title of the settings activity [CHAR LIMIT=64] -->
- <string name="activity_title_settings">Settings</string>
-
<!-- Action that shares visible contacts -->
<string name="share_visible_contacts">Share visible contacts</string>
@@ -1707,7 +1687,7 @@
<string name="call_with_a_note">Call with a note</string>
<!-- Hint text shown in the call subject dialog. [CHAR LIMIT=255] -->
- <string name="call_subject_hint">Type a note to send with call ...</string>
+ <string name="call_subject_hint">Type a note to send with call…</string>
<!-- Button used to start a new call with the user entered subject. [CHAR LIMIT=32] -->
<string name="send_and_call_button">SEND & CALL</string>
diff --git a/src-bind/com/android/contactsbind/ObjectFactory.java b/src-bind/com/android/contactsbind/ObjectFactory.java
index 1a5b346..f6dc0c7 100644
--- a/src-bind/com/android/contactsbind/ObjectFactory.java
+++ b/src-bind/com/android/contactsbind/ObjectFactory.java
@@ -15,6 +15,8 @@
import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.preference.PreferenceManager;
+import com.android.contacts.common.util.DeviceAccountFilter;
+import com.android.contacts.common.util.DeviceAccountPresentationValues;
import android.content.Context;
@@ -28,4 +30,12 @@
}
public static PreferenceManager getPreferenceManager(Context context) { return null; }
+
+ public static DeviceAccountPresentationValues createDeviceAccountPresentationValues(Context context) {
+ return new DeviceAccountPresentationValues.Default(context);
+ }
+
+ public static DeviceAccountFilter getDeviceAccountFilter(Context context) {
+ return DeviceAccountFilter.ONLY_NULL;
+ }
}
diff --git a/src/com/android/contacts/ContactsDrawerActivity.java b/src/com/android/contacts/ContactsDrawerActivity.java
index bea1411..f976807 100644
--- a/src/com/android/contacts/ContactsDrawerActivity.java
+++ b/src/com/android/contacts/ContactsDrawerActivity.java
@@ -59,6 +59,7 @@
import com.android.contacts.common.preference.ContactsPreferenceActivity;
import com.android.contacts.common.util.AccountFilterUtil;
import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
+import com.android.contacts.common.util.DeviceAccountPresentationValues;
import com.android.contacts.common.util.ImplicitIntentsUtil;
import com.android.contacts.common.util.ViewUtil;
import com.android.contacts.editor.ContactEditorFragment;
@@ -74,6 +75,7 @@
import com.android.contacts.util.SharedPreferenceUtil;
import com.android.contactsbind.Assistants;
import com.android.contactsbind.HelpUtils;
+import com.android.contactsbind.ObjectFactory;
import java.util.HashMap;
import java.util.Iterator;
@@ -171,6 +173,7 @@
// The account the new group will be created under.
private AccountWithDataSet mNewGroupAccount;
+ private DeviceAccountPresentationValues mDeviceAccountPresentationValues;
private int mPositionOfLastGroup;
@@ -183,6 +186,8 @@
super.setContentView(R.layout.contacts_drawer_activity);
+ mDeviceAccountPresentationValues = ObjectFactory.createDeviceAccountPresentationValues(this);
+
// Set up the action bar.
mToolbar = getView(R.id.toolbar);
setSupportActionBar(mToolbar);
@@ -468,12 +473,12 @@
int positionOfLastFilter = mPositionOfLastGroup + GAP_BETWEEN_TWO_MENU_GROUPS;
+ mDeviceAccountPresentationValues.setFilters(accountFilterItems);
+
for (int i = 0; i < accountFilterItems.size(); i++) {
positionOfLastFilter++;
final ContactListFilter filter = accountFilterItems.get(i);
- final String menuName =
- filter.filterType == ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS
- ? getString(R.string.account_phone) : filter.accountName;
+ final CharSequence menuName = mDeviceAccountPresentationValues.getLabel(i);
final MenuItem menuItem = subMenu.add(R.id.nav_filters_items, Menu.NONE,
positionOfLastFilter, menuName);
mFilterMenuMap.put(filter, menuItem);
@@ -497,7 +502,7 @@
return true;
}
});
- menuItem.setIcon(filter.icon);
+ menuItem.setIcon(mDeviceAccountPresentationValues.getIcon(i));
// Get rid of the default menu item overlay and show original account icons.
menuItem.getIcon().setColorFilter(Color.TRANSPARENT, PorterDuff.Mode.SRC_ATOP);
// Create a dummy action view to attach extra hidden content description to the menuItem
diff --git a/src/com/android/contacts/common/list/ContactListFilter.java b/src/com/android/contacts/common/list/ContactListFilter.java
index 6d60a82..e99c374 100644
--- a/src/com/android/contacts/common/list/ContactListFilter.java
+++ b/src/com/android/contacts/common/list/ContactListFilter.java
@@ -181,6 +181,8 @@
int code = filterType;
if (accountType != null) {
code = code * 31 + accountType.hashCode();
+ }
+ if (accountName != null) {
code = code * 31 + accountName.hashCode();
}
if (dataSet != null) {
diff --git a/src/com/android/contacts/common/list/ContactListFilterController.java b/src/com/android/contacts/common/list/ContactListFilterController.java
index 48d36ed..4d3d6ad 100644
--- a/src/com/android/contacts/common/list/ContactListFilterController.java
+++ b/src/com/android/contacts/common/list/ContactListFilterController.java
@@ -184,12 +184,6 @@
ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), true, notifyListeners);
}
break;
- case ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS:
- if (!localAccountExists()) {
- setContactListFilter(ContactListFilter.createFilterWithType(
- ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), true, notifyListeners);
- }
- break;
}
}
@@ -202,13 +196,4 @@
mFilter.accountName, mFilter.accountType, mFilter.dataSet);
return accountTypeManager.contains(filterAccount, /* contactWritableOnly */ false);
}
-
- /**
- * @return true if the local account still exists.
- */
- private boolean localAccountExists() {
- final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
- final AccountWithDataSet localAccount = AccountWithDataSet.getLocalAccount();
- return accountTypeManager.contains(localAccount, /* contactWritableOnly */ false);
- }
}
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
index 666de8c..43cca1a 100644
--- a/src/com/android/contacts/common/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -245,7 +245,18 @@
break;
}
case ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS: {
- selection.append(AccountWithDataSet.LOCAL_ACCOUNT_SELECTION);
+ if (filter.accountType != null) {
+ selection.append(ContactsContract.RawContacts.ACCOUNT_TYPE)
+ .append("=?");
+ selectionArgs.add(filter.accountType);
+ if (filter.accountName != null) {
+ selection.append(" AND ").append(ContactsContract.RawContacts.ACCOUNT_NAME)
+ .append(("=?"));
+ selectionArgs.add(filter.accountName);
+ }
+ } else {
+ selection.append(AccountWithDataSet.LOCAL_ACCOUNT_SELECTION);
+ }
break;
}
}
diff --git a/src/com/android/contacts/common/model/AccountTypeManager.java b/src/com/android/contacts/common/model/AccountTypeManager.java
index aaf1476..35a7a3a 100644
--- a/src/com/android/contacts/common/model/AccountTypeManager.java
+++ b/src/com/android/contacts/common/model/AccountTypeManager.java
@@ -53,6 +53,8 @@
import com.android.contacts.common.model.account.SamsungAccountType;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.DeviceAccountFilter;
+import com.android.contactsbind.ObjectFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
@@ -63,6 +65,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -87,7 +90,8 @@
synchronized (mInitializationLock) {
if (mAccountTypeManager == null) {
context = context.getApplicationContext();
- mAccountTypeManager = new AccountTypeManagerImpl(context);
+ mAccountTypeManager = new AccountTypeManagerImpl(context,
+ ObjectFactory.getDeviceAccountFilter(context));
}
}
return mAccountTypeManager;
@@ -247,6 +251,7 @@
private Context mContext;
private AccountManager mAccountManager;
+ private DeviceAccountFilter mDeviceAccountFilter;
private AccountType mFallbackAccountType;
@@ -301,9 +306,10 @@
/**
* Internal constructor that only performs initial parsing.
*/
- public AccountTypeManagerImpl(Context context) {
+ public AccountTypeManagerImpl(Context context, DeviceAccountFilter deviceAccountFilter) {
mContext = context;
mFallbackAccountType = new FallbackAccountType(context);
+ mDeviceAccountFilter = deviceAccountFilter;
mAccountManager = AccountManager.get(mContext);
@@ -437,6 +443,8 @@
} else if (SamsungAccountType.isSamsungAccountType(mContext, type,
auth.packageName)) {
accountType = new SamsungAccountType(mContext, auth.packageName, type);
+ } else if (mDeviceAccountFilter.isDeviceAccountType(type)) {
+ accountType = new FallbackAccountType(mContext);
} else {
Log.d(TAG, "Registering external account type=" + type
+ ", packageName=" + auth.packageName);
@@ -452,9 +460,13 @@
}
}
- accountType.accountType = auth.type;
- accountType.titleRes = auth.labelId;
- accountType.iconRes = auth.iconId;
+ // TODO: this is a hack. For FallbackAccountType we want to use a default icon and
+ // label instead of what is pulled out of the authenticator
+ if (!(accountType instanceof FallbackAccountType)) {
+ accountType.accountType = auth.type;
+ accountType.titleRes = auth.labelId;
+ accountType.iconRes = auth.iconId;
+ }
addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
diff --git a/src/com/android/contacts/common/util/AccountFilterUtil.java b/src/com/android/contacts/common/util/AccountFilterUtil.java
index 76975a6..2d59981 100644
--- a/src/com/android/contacts/common/util/AccountFilterUtil.java
+++ b/src/com/android/contacts/common/util/AccountFilterUtil.java
@@ -33,7 +33,6 @@
import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountWithDataSet;
-
import com.google.common.collect.Lists;
import java.util.ArrayList;
diff --git a/src/com/android/contacts/common/util/DeviceAccountFilter.java b/src/com/android/contacts/common/util/DeviceAccountFilter.java
new file mode 100644
index 0000000..9dc98a5
--- /dev/null
+++ b/src/com/android/contacts/common/util/DeviceAccountFilter.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+/**
+ * Reports whether a value from RawContacts.ACCOUNT_TYPE should be considered a "Device"
+ * account
+ */
+public interface DeviceAccountFilter {
+ boolean isDeviceAccountType(String accountType);
+
+ public static DeviceAccountFilter ONLY_NULL = new DeviceAccountFilter() {
+ @Override
+ public boolean isDeviceAccountType(String accountType) {
+ return accountType == null;
+ }
+ };
+}
diff --git a/src/com/android/contacts/common/util/DeviceAccountPresentationValues.java b/src/com/android/contacts/common/util/DeviceAccountPresentationValues.java
new file mode 100644
index 0000000..dab81ed
--- /dev/null
+++ b/src/com/android/contacts/common/util/DeviceAccountPresentationValues.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import com.android.contacts.common.list.ContactListFilter;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Supplies the label and icon that should be used for device accounts in the Nav Drawer.
+ *
+ * This operates on the list of filters to allow the implementation to choose better resources
+ * in the case that there are multiple device accounts in the filter list.
+ */
+public interface DeviceAccountPresentationValues {
+ void setFilters(List<ContactListFilter> filters);
+
+ CharSequence getLabel(int index);
+
+ Drawable getIcon(int index);
+
+ /**
+ * The default implementation only returns a label and icon for a device filter that as null
+ * values for the accountType and accountName
+ */
+ class Default implements DeviceAccountPresentationValues {
+ private final Context mContext;
+
+ private List<ContactListFilter> mFilters = null;
+
+ public Default(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public CharSequence getLabel(int index) {
+ assertFiltersInitialized();
+
+ final ContactListFilter filter = mFilters.get(index);
+ if (filter.filterType != ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) {
+ return filter.accountName;
+ }
+ return filter.accountName != null ? filter.accountName :
+ mContext.getString(com.android.contacts.common.R.string.account_phone);
+ }
+
+ @Override
+ public Drawable getIcon(int index) {
+ assertFiltersInitialized();
+
+ final ContactListFilter filter = mFilters.get(index);
+ if (filter.filterType != ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) {
+ return filter.icon;
+ }
+ return mContext.getDrawable(com.android.contacts.common.R.drawable.ic_device);
+ }
+
+ @Override
+ public void setFilters(List<ContactListFilter> filters) {
+ if (filters == null) {
+ mFilters = Collections.emptyList();
+ } else {
+ mFilters = filters;
+ }
+ }
+
+ private void assertFiltersInitialized() {
+ if (mFilters == null) {
+ throw new IllegalStateException("setFilters must be called first.");
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java b/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
new file mode 100644
index 0000000..1d06a43
--- /dev/null
+++ b/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
@@ -0,0 +1,173 @@
+/*
+ * 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.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.LoaderManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.support.annotation.Keep;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.test.NeededForReflection;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Get filters for device local accounts. These are "accounts" that have contacts associated
+ * with them but are not returned by AccountManager. Any other account will be displayed
+ * automatically so we don't worry about it.
+ */
+public class DeviceLocalContactsFilterProvider
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ public static String[] PROJECTION = new String[] {
+ ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE
+ };
+
+ private static final int COL_NAME = 0;
+ private static final int COL_TYPE = 1;
+
+ private final Context mContext;
+ private final DeviceAccountFilter mAccountTypeFilter;
+
+ private String[] mKnownAccountTypes;
+
+ private List<ContactListFilter> mDeviceFilters = Collections.emptyList();
+
+ public DeviceLocalContactsFilterProvider(Context context,
+ DeviceAccountFilter accountTypeFilter) {
+ mContext = context;
+ mAccountTypeFilter = accountTypeFilter;
+ }
+
+ private ContactListFilter createFilterForAccount(Account account) {
+ return new ContactListFilter(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
+ account.type, account.name, null, null);
+ }
+
+ public List<ContactListFilter> getListFilters() {
+ return mDeviceFilters;
+ }
+
+ @Override
+ public CursorLoader onCreateLoader(int i, Bundle bundle) {
+ if (mKnownAccountTypes == null) {
+ initKnownAccountTypes();
+ }
+ return new CursorLoader(mContext, getUri(), PROJECTION, getSelection(),
+ getSelectionArgs(), null);
+ }
+
+
+ private List<ContactListFilter> createFiltersFromResults(Cursor cursor) {
+ final Set<Account> accounts = new HashSet<>();
+ boolean hasNullType = false;
+
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(COL_NAME);
+ final String type = cursor.getString(COL_TYPE);
+ // The case where where only one of the columns is null isn't handled specifically.
+ if (mAccountTypeFilter.isDeviceAccountType(type)) {
+ if (name != null && type != null) {
+ accounts.add(new Account(name, type));
+ } else {
+ hasNullType = true;
+ }
+ }
+ }
+
+ final List<ContactListFilter> result = new ArrayList<>(accounts.size());
+ if (hasNullType) {
+ result.add(new ContactListFilter(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
+ null, null, null, null));
+ }
+ for (Account account : accounts) {
+ result.add(createFilterForAccount(account));
+ }
+ return result;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ if (cursor == null) return;
+ mDeviceFilters = createFiltersFromResults(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @Keep
+ @VisibleForTesting
+ public void setKnownAccountTypes(String... accountTypes) {
+ mKnownAccountTypes = accountTypes;
+ }
+
+ private void initKnownAccountTypes() {
+ final AccountManager accountManager = (AccountManager) mContext
+ .getSystemService(Context.ACCOUNT_SERVICE);
+ final Set<String> knownTypes = new HashSet<>();
+ final Account[] accounts = accountManager.getAccounts();
+ for (Account account : accounts) {
+ if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0) {
+ knownTypes.add(account.type);
+ }
+ }
+ mKnownAccountTypes = knownTypes.toArray(new String[knownTypes.size()]);
+ }
+
+ private Uri getUri() {
+ final Uri.Builder builder = ContactsContract.RawContacts.CONTENT_URI.buildUpon();
+ if (mKnownAccountTypes == null || mKnownAccountTypes.length == 0) {
+ builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1");
+ }
+ return builder.build();
+ }
+
+ private String getSelection() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(ContactsContract.RawContacts.DELETED).append(" =0 AND (")
+ .append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" IS NULL");
+ if (mKnownAccountTypes == null || mKnownAccountTypes.length == 0) {
+ return sb.append(')').toString();
+ }
+ sb.append(" OR ").append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" NOT IN (");
+ for (String ignored : mKnownAccountTypes) {
+ sb.append("?,");
+ }
+ // Remove trailing ','
+ sb.deleteCharAt(sb.length() - 1).append(')').append(')');
+
+ return sb.toString();
+ }
+
+ private String[] getSelectionArgs() {
+ return mKnownAccountTypes;
+ }
+}
diff --git a/src/com/android/contacts/interactions/AccountFiltersFragment.java b/src/com/android/contacts/interactions/AccountFiltersFragment.java
index 7836c19..b2b21f7 100644
--- a/src/com/android/contacts/interactions/AccountFiltersFragment.java
+++ b/src/com/android/contacts/interactions/AccountFiltersFragment.java
@@ -19,11 +19,16 @@
import android.app.Fragment;
import android.app.LoaderManager;
import android.content.Loader;
+import android.database.Cursor;
import android.os.Bundle;
import com.android.contacts.common.list.ContactListFilter;
import com.android.contacts.common.util.AccountFilterUtil;
+import com.android.contacts.common.util.DeviceLocalContactsFilterProvider;
+import com.android.contactsbind.ObjectFactory;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -32,6 +37,7 @@
public class AccountFiltersFragment extends Fragment {
private static final int LOADER_FILTERS = 1;
+ private static final int LOADER_DEVICE_LOCAL_CONTACTS = 3;
/**
* Callbacks for hosts of the {@link AccountFiltersFragment}.
@@ -44,6 +50,8 @@
void onFiltersLoaded(List<ContactListFilter> accountFilterItems);
}
+ private LoaderManager.LoaderCallbacks<Cursor> mDeviceLocalLoaderListener;
+
private final LoaderManager.LoaderCallbacks<List<ContactListFilter>> mFiltersLoaderListener =
new LoaderManager.LoaderCallbacks<List<ContactListFilter>> () {
@Override
@@ -54,24 +62,56 @@
@Override
public void onLoadFinished(
Loader<List<ContactListFilter>> loader, List<ContactListFilter> data) {
- if (mListener != null) {
- mListener.onFiltersLoaded(data);
+ if (data == null) {
+ mLoadedFilters = Collections.emptyList();
+ } else {
+ mLoadedFilters = data;
}
+ notifyWithCurrentFilters();
}
public void onLoaderReset(Loader<List<ContactListFilter>> loader) {
}
};
+
+ private List<ContactListFilter> mLoadedFilters = null;
+ private List<ContactListFilter> mDeviceLocalFilters = null;
private AccountFiltersListener mListener;
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mDeviceLocalLoaderListener = new DeviceLocalContactsFilterProvider(getActivity(),
+ ObjectFactory.getDeviceAccountFilter(getActivity())) {
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ super.onLoadFinished(loader, data);
+ mDeviceLocalFilters = getListFilters();
+ notifyWithCurrentFilters();
+ }
+ };
+ }
+
+ @Override
public void onStart() {
getLoaderManager().initLoader(LOADER_FILTERS, null, mFiltersLoaderListener);
+ getLoaderManager().initLoader(LOADER_DEVICE_LOCAL_CONTACTS, null,
+ mDeviceLocalLoaderListener);
+
super.onStart();
}
public void setListener(AccountFiltersListener listener) {
mListener = listener;
}
+
+ private void notifyWithCurrentFilters() {
+ if (mListener == null || mLoadedFilters == null || mDeviceLocalFilters == null) return;
+
+ final List<ContactListFilter> result = new ArrayList<>(mLoadedFilters);
+ result.addAll(mDeviceLocalFilters);
+ mListener.onFiltersLoaded(result);
+ }
}
diff --git a/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java b/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java
new file mode 100644
index 0000000..d776ab8
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java
@@ -0,0 +1,221 @@
+/*
+ * 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.util;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.Nullable;
+import android.test.LoaderTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.mockito.Mockito.when;
+
+@SmallTest
+public class DeviceLocalContactsFilterProviderTests extends LoaderTestCase {
+
+ // 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 DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+ getContext(), DeviceAccountFilter.ONLY_NULL);
+ final CursorLoader loader = sut.onCreateLoader(0, null);
+ getLoaderResultSynchronously(loader);
+ // We didn't throw so it passed
+ }
+
+ public void testCreatesNoFiltersIfNoRawContactsHaveDeviceAccountType() {
+ final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+ DeviceAccountFilter.ONLY_NULL, queryResult(
+ "user", "com.example",
+ "user", "com.example",
+ "user", "com.example"));
+ sut.setKnownAccountTypes("com.example");
+
+ doLoad(sut);
+
+ assertEquals(0, sut.getListFilters().size());
+ }
+
+ public void testCreatesOneFilterForDeviceAccount() {
+ final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+ DeviceAccountFilter.ONLY_NULL, queryResult(
+ "user", "com.example",
+ "user", "com.example",
+ null, null,
+ "user", "com.example",
+ null, null));
+ sut.setKnownAccountTypes("com.example");
+
+ doLoad(sut);
+
+ assertEquals(1, sut.getListFilters().size());
+ assertEquals(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
+ sut.getListFilters().get(0).filterType);
+ }
+
+ public void testCreatesOneFilterForEachDeviceAccount() {
+ final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+ filterAllowing(null, "vnd.sec.contact.phone", "vnd.sec.contact.sim"), queryResult(
+ "sim_account", "vnd.sec.contact.sim",
+ "user", "com.example",
+ "user", "com.example",
+ "phone_account", "vnd.sec.contact.phone",
+ null, null,
+ "phone_account", "vnd.sec.contact.phone",
+ "user", "com.example",
+ null, null,
+ "sim_account", "vnd.sec.contact.sim",
+ "sim_account_2", "vnd.sec.contact.sim"
+ ));
+ sut.setKnownAccountTypes("com.example");
+
+ doLoad(sut);
+
+ assertEquals(4, sut.getListFilters().size());
+ }
+
+ public void testFilterIsUpdatedWhenLoaderReloads() {
+ final FakeContactsProvider provider = new FakeContactsProvider();
+ final DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+ createStubContextWithContactsProvider(provider), DeviceAccountFilter.ONLY_NULL);
+ sut.setKnownAccountTypes("com.example");
+
+ provider.setNextQueryResult(queryResult(
+ null, null,
+ "user", "com.example",
+ "user", "com.example"
+ ));
+ doLoad(sut);
+
+ assertFalse(sut.getListFilters().isEmpty());
+
+ provider.setNextQueryResult(queryResult(
+ "user", "com.example",
+ "user", "com.example"
+ ));
+ doLoad(sut);
+
+ assertTrue(sut.getListFilters().isEmpty());
+ }
+
+ public void testDoesNotCreateFiltersForKnownAccounts() {
+ final DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+ getContext(), DeviceAccountFilter.ONLY_NULL);
+ sut.setKnownAccountTypes("com.example", "maybe_syncable_device_account_type");
+
+ final CursorLoader loader = sut.onCreateLoader(0, null);
+
+ // The filtering is done at the DB level rather than in the code so just verify that
+ // selection is about right.
+ assertTrue("Loader selection is wrong", loader.getSelection().contains("NOT IN (?,?)"));
+ assertEquals("com.example", loader.getSelectionArgs()[0]);
+ assertEquals("maybe_syncable_device_account_type", loader.getSelectionArgs()[1]);
+ }
+
+ private void doLoad(DeviceLocalContactsFilterProvider loaderCallbacks) {
+ final CursorLoader loader = loaderCallbacks.onCreateLoader(0, null);
+ final Cursor cursor = getLoaderResultSynchronously(loader);
+ loaderCallbacks.onLoadFinished(loader, cursor);
+ }
+
+ private DeviceLocalContactsFilterProvider createWithFilterAndLoaderResult(
+ DeviceAccountFilter filter, Cursor cursor) {
+ final DeviceLocalContactsFilterProvider result = new DeviceLocalContactsFilterProvider(
+ createStubContextWithContentQueryResult(cursor), filter);
+ return result;
+ }
+
+ private Context createStubContextWithContentQueryResult(final Cursor cursor) {
+ return createStubContextWithContactsProvider(new FakeContactsProvider(cursor));
+ }
+
+ private Context createStubContextWithContactsProvider(ContentProvider contactsProvider) {
+ final MockContentResolver resolver = new MockContentResolver();
+ resolver.addProvider(ContactsContract.AUTHORITY, contactsProvider);
+
+ final Context context = Mockito.mock(MockContext.class);
+ when(context.getContentResolver()).thenReturn(resolver);
+
+ // The loader pulls out the application context instead of usign the context directly
+ when(context.getApplicationContext()).thenReturn(context);
+
+ return context;
+ }
+
+ private Cursor queryResult(String... typeNamePairs) {
+ final MatrixCursor cursor = new MatrixCursor(new String[]
+ { RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE });
+ for (int i = 0; i < typeNamePairs.length; i += 2) {
+ cursor.newRow().add(typeNamePairs[i]).add(typeNamePairs[i+1]);
+ }
+ return cursor;
+ }
+
+ private DeviceAccountFilter filterAllowing(String... accountTypes) {
+ final Set<String> allowed = new HashSet<>(Arrays.asList(accountTypes));
+ return new DeviceAccountFilter() {
+ @Override
+ public boolean isDeviceAccountType(String accountType) {
+ return allowed.contains(accountType);
+ }
+ };
+ }
+
+ private static class FakeContactsProvider extends MockContentProvider {
+ public Cursor mNextQueryResult;
+
+ public FakeContactsProvider() {}
+
+ public FakeContactsProvider(Cursor result) {
+ mNextQueryResult = result;
+ }
+
+ public void setNextQueryResult(Cursor result) {
+ mNextQueryResult = result;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return query(uri, projection, selection, selectionArgs, sortOrder, null);
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder, CancellationSignal cancellationSignal) {
+ return mNextQueryResult;
+ }
+ }
+}