Show device "account" in nav drawer menu
If there are contacts with a "null" account name and type (indicating that they
are device local contacts) then an item is included in the account section of
the nav drawer for "Device" contacts.
Bug 28637652
Bug 28637715
Change-Id: I975d20c8ab2bb14b9a9441e585d13237f7c09cb2
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..98b865d
--- /dev/null
+++ b/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.contacts.common.list.ContactListFilter;
+
+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) {
+ 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()]);
+
+ 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) {
+ }
+
+ 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);
+ }
}