Merge changes Ib4c2a049,I1ec162e0,Iaf0b73b9,Ie6545785,I9ab87cde into ub-contactsdialer-i-dev
* changes:
Make account filters for nav drawer update
Replace AccountTypeManager usages in PeopleActivity
Include device local accounts in AccountTypeManager
Load accounts in background for SIM import
Query for accounts on-demand in AccountTypeManager
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index 5f4d181..8d8e2db 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -18,13 +18,15 @@
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
-import android.content.AsyncTaskLoader;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IntentFilter;
import android.content.Loader;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
+import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.util.ArrayMap;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ContentLoadingProgressBar;
@@ -47,6 +49,11 @@
import com.android.contacts.model.SimContact;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.preference.ContactsPreferences;
+import com.android.contacts.util.concurrent.ContactsExecutors;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Arrays;
@@ -54,6 +61,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Callable;
/**
* Dialog that presents a list of contacts from a SIM card that can be imported into a selected
@@ -77,6 +85,8 @@
private ListView mListView;
private View mImportButton;
+ private Bundle mSavedInstanceState;
+
private final Map<AccountWithDataSet, long[]> mPerAccountCheckedIds = new ArrayMap<>();
private int mSubscriptionId;
@@ -85,6 +95,7 @@
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mSavedInstanceState = savedInstanceState;
mPreferences = new ContactsPreferences(getContext());
mAccountTypeManager = AccountTypeManager.getInstance(getActivity());
mAdapter = new SimContactAdapter(getActivity());
@@ -128,7 +139,6 @@
}
});
mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount());
- restoreAdapterSelectedStates(savedInstanceState);
mListView = (ListView) view.findViewById(R.id.list);
mListView.setOnScrollListener(this);
@@ -224,7 +234,7 @@
}
@Override
- public SimContactLoader onCreateLoader(int id, Bundle args) {
+ public Loader<LoaderResult> onCreateLoader(int id, Bundle args) {
return new SimContactLoader(getContext(), mSubscriptionId);
}
@@ -235,6 +245,8 @@
if (data == null) {
return;
}
+ mAccountHeaderPresenter.setAccounts(data.accounts);
+ restoreAdapterSelectedStates(data.accounts);
mAdapter.setData(data);
mListView.setEmptyView(getView().findViewById(R.id.empty_message));
@@ -246,17 +258,17 @@
public void onLoaderReset(Loader<LoaderResult> loader) {
}
- private void restoreAdapterSelectedStates(Bundle savedInstanceState) {
- if (savedInstanceState == null) {
+ private void restoreAdapterSelectedStates(List<AccountWithDataSet> accounts) {
+ if (mSavedInstanceState == null) {
return;
}
- final List<AccountWithDataSet> accounts = mAccountTypeManager.getAccounts(true);
for (AccountWithDataSet account : accounts) {
- final long[] selections = savedInstanceState.getLongArray(
+ final long[] selections = mSavedInstanceState.getLongArray(
account.stringify() + KEY_SUFFIX_SELECTED_IDS);
mPerAccountCheckedIds.put(account, selections);
}
+ mSavedInstanceState = null;
}
private void saveAdapterSelectedStates(Bundle outState) {
@@ -429,34 +441,44 @@
}
- private static class SimContactLoader extends AsyncTaskLoader<LoaderResult> {
+ private static class SimContactLoader extends ListenableFutureLoader<LoaderResult> {
private SimContactDao mDao;
+ private AccountTypeManager mAccountTypeManager;
private final int mSubscriptionId;
- LoaderResult mResult;
+
+ private BroadcastReceiver mReceiver;
public SimContactLoader(Context context, int subscriptionId) {
super(context);
mDao = SimContactDao.create(context);
+ mAccountTypeManager = AccountTypeManager.getInstance(getContext());
mSubscriptionId = subscriptionId;
}
@Override
- protected void onStartLoading() {
- if (mResult != null) {
- deliverResult(mResult);
- } else {
- forceLoad();
- }
+ protected ListenableFuture<LoaderResult> loadData() {
+ final ListenableFuture<List<Object>> future = Futures.<Object>allAsList(
+ mAccountTypeManager
+ .filterAccountsByTypeAsync(AccountTypeManager.writableFilter()),
+ ContactsExecutors.getSimReadExecutor().<Object>submit(
+ new Callable<Object>() {
+ @Override
+ public LoaderResult call() throws Exception {
+ return loadFromSim();
+ }
+ }));
+ return Futures.transform(future, new Function<List<Object>, LoaderResult>() {
+ @Override
+ public LoaderResult apply(List<Object> input) {
+ final List<AccountWithDataSet> accounts = (List<AccountWithDataSet>) input.get(0);
+ final LoaderResult simLoadResult = (LoaderResult) input.get(1);
+ simLoadResult.accounts = accounts;
+ return simLoadResult;
+ }
+ });
}
- @Override
- public void deliverResult(LoaderResult result) {
- mResult = result;
- super.deliverResult(result);
- }
-
- @Override
- public LoaderResult loadInBackground() {
+ private LoaderResult loadFromSim() {
final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId);
LoaderResult result = new LoaderResult();
if (sim == null) {
@@ -470,13 +492,24 @@
}
@Override
- protected void onReset() {
- mResult = null;
+ protected void onStartLoading() {
+ super.onStartLoading();
+ if (mReceiver == null) {
+ mReceiver = new ForceLoadReceiver();
+ LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
+ new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+ }
}
+ @Override
+ protected void onReset() {
+ super.onReset();
+ LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
+ }
}
public static class LoaderResult {
+ public List<AccountWithDataSet> accounts;
public ArrayList<SimContact> contacts;
public Map<AccountWithDataSet, Set<SimContact>> accountsMap;
}
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 113121b..426dcd7 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -68,6 +68,7 @@
import com.android.contacts.logging.Logger;
import com.android.contacts.logging.ScreenEvent.ScreenType;
import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.util.AccountFilterUtil;
import com.android.contacts.util.Constants;
@@ -77,6 +78,7 @@
import com.android.contactsbind.FeatureHighlightHelper;
import com.android.contactsbind.ObjectFactory;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -100,6 +102,7 @@
private ContactsIntentResolver mIntentResolver;
private ContactsRequest mRequest;
+ private AccountTypeManager mAccountTypeManager;
private FloatingActionButtonController mFloatingActionButtonController;
private View mFloatingActionButtonContainer;
@@ -163,17 +166,18 @@
return;
}
- final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(this)
- .getAccounts(/* contactsWritableOnly */ true);
- final List<Account> syncableAccounts = filter.getSyncableAccounts(accounts);
- // If one of the accounts is active or pending, use spinning circle to indicate one of
- // the syncs is in progress.
- if (syncableAccounts != null && syncableAccounts.size() > 0) {
- for (Account account: syncableAccounts) {
- if (SyncUtil.isSyncStatusPendingOrActive(account)) {
- return;
- }
- }
+ final List<AccountWithDataSet> accounts;
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT &&
+ filter.isGoogleAccountType()) {
+ accounts = Collections.singletonList(new AccountWithDataSet(filter.accountName,
+ filter.accountType, null));
+ } else if (filter.shouldShowSyncState()) {
+ accounts = mAccountTypeManager.getWritableGoogleAccounts();
+ } else {
+ accounts = Collections.emptyList();
+ }
+ if (SyncUtil.isAnySyncing(accounts)) {
+ return;
}
swipeRefreshLayout.setRefreshing(false);
}
@@ -222,6 +226,7 @@
Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
}
super.onCreate(savedState);
+ mAccountTypeManager = AccountTypeManager.getInstance(this);
if (RequestPermissionsActivity.startPermissionActivity(this)) {
return;
@@ -573,21 +578,9 @@
private boolean shouldShowList() {
return mProviderStatus != null
- && ((mProviderStatus.equals(ProviderStatus.STATUS_EMPTY) && hasNonLocalAccount())
- || mProviderStatus.equals(ProviderStatus.STATUS_NORMAL));
- }
-
- // Returns true if there are real accounts (not "local" account) in the list of accounts.
- private boolean hasNonLocalAccount() {
- final List<AccountWithDataSet> allAccounts =
- AccountTypeManager.getInstance(this).getAccounts(/* contactWritableOnly */ false);
- if (allAccounts == null || allAccounts.size() == 0) {
- return false;
- }
- if (allAccounts.size() > 1) {
- return true;
- }
- return !allAccounts.get(0).isNullAccount();
+ && ((mProviderStatus.equals(ProviderStatus.STATUS_EMPTY)
+ && mAccountTypeManager.hasNonLocalAccount())
+ || mProviderStatus.equals(ProviderStatus.STATUS_NORMAL));
}
private void invalidateOptionsMenuIfNeeded() {
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
index c94dcd4..0185419 100644
--- a/src/com/android/contacts/editor/AccountHeaderPresenter.java
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -58,6 +58,7 @@
private final Context mContext;
private AccountDisplayInfoFactory mAccountDisplayInfoFactory;
+ private List<AccountWithDataSet> mAccounts;
private AccountWithDataSet mCurrentAccount;
// Account header
@@ -82,8 +83,6 @@
mAccountHeaderName = (TextView) container.findViewById(R.id.account_name);
mAccountHeaderIcon = (ImageView) container.findViewById(R.id.account_type_icon);
mAccountHeaderExpanderIcon = (ImageView) container.findViewById(R.id.account_expander_icon);
-
- mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(mContext);
}
public void setObserver(Observer observer) {
@@ -101,6 +100,17 @@
updateDisplayedAccount();
}
+ public void setAccounts(List<AccountWithDataSet> accounts) {
+ mAccounts = accounts;
+ mAccountDisplayInfoFactory = new AccountDisplayInfoFactory(mContext, accounts);
+ // If the current account was removed just switch to the next one in the list.
+ if (mCurrentAccount != null && !mAccounts.contains(mCurrentAccount)) {
+ mCurrentAccount = mAccounts.isEmpty() ? null : accounts.get(0);
+ mObserver.onChange(this);
+ }
+ updateDisplayedAccount();
+ }
+
public AccountWithDataSet getCurrentAccount() {
return mCurrentAccount;
}
@@ -120,17 +130,14 @@
private void updateDisplayedAccount() {
mAccountHeaderContainer.setVisibility(View.GONE);
if (mCurrentAccount == null) return;
+ if (mAccounts == null) return;
final AccountDisplayInfo account =
mAccountDisplayInfoFactory.getAccountDisplayInfo(mCurrentAccount);
final String accountLabel = getAccountLabel(account);
- // Either the account header or selector should be shown, not both.
- final List<AccountWithDataSet> accounts =
- AccountTypeManager.getInstance(mContext).getAccounts(true);
-
- if (accounts.size() > 1) {
+ if (mAccounts.size() > 1) {
addAccountSelector(accountLabel);
} else {
addAccountHeader(accountLabel);
@@ -174,9 +181,7 @@
private void showPopup() {
final ListPopupWindow popup = new ListPopupWindow(mContext);
final AccountsListAdapter adapter =
- new AccountsListAdapter(mContext,
- AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
- mCurrentAccount);
+ new AccountsListAdapter(mContext, mAccounts, mCurrentAccount);
popup.setWidth(mAccountHeaderContainer.getWidth());
popup.setAnchorView(mAccountHeaderContainer);
popup.setAdapter(adapter);
diff --git a/src/com/android/contacts/list/ContactListFilter.java b/src/com/android/contacts/list/ContactListFilter.java
index 850683c..4245be4 100644
--- a/src/com/android/contacts/list/ContactListFilter.java
+++ b/src/com/android/contacts/list/ContactListFilter.java
@@ -420,6 +420,13 @@
return false;
}
+ public boolean shouldShowSyncState() {
+ return (isGoogleAccountType() && filterType == ContactListFilter.FILTER_TYPE_ACCOUNT)
+ || filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
+ || filterType == ContactListFilter.FILTER_TYPE_CUSTOM
+ || filterType == ContactListFilter.FILTER_TYPE_DEFAULT;
+ }
+
/**
* Returns the Google accounts (see {@link #isGoogleAccountType) for this ContactListFilter.
*/
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 24957d7..d87b444 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -18,7 +18,6 @@
import android.accounts.Account;
import android.accounts.AccountManager;
-import android.accounts.AuthenticatorDescription;
import android.accounts.OnAccountsUpdateListener;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -26,57 +25,48 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.SyncAdapterType;
import android.content.SyncStatusObserver;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
import android.provider.ContactsContract;
import android.support.v4.content.ContextCompat;
+import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
-import android.util.TimingLogger;
import com.android.contacts.Experiments;
import com.android.contacts.R;
import com.android.contacts.list.ContactListFilterController;
+import com.android.contacts.model.account.AccountComparator;
import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountTypeProvider;
import com.android.contacts.model.account.AccountTypeWithDataSet;
import com.android.contacts.model.account.AccountWithDataSet;
-import com.android.contacts.model.account.ExchangeAccountType;
-import com.android.contacts.model.account.ExternalAccountType;
import com.android.contacts.model.account.FallbackAccountType;
import com.android.contacts.model.account.GoogleAccountType;
-import com.android.contacts.model.account.SamsungAccountType;
import com.android.contacts.model.dataitem.DataKind;
-import com.android.contacts.util.Constants;
-import com.android.contacts.util.DeviceLocalAccountTypeFactory;
-import com.android.contactsbind.ObjectFactory;
+import com.android.contacts.util.concurrent.ContactsExecutors;
import com.android.contactsbind.experiments.Flags;
-import com.google.common.base.Objects;
+import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
import javax.annotation.Nullable;
-import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
-
/**
* Singleton holder for all parsed {@link AccountType} available on the
* system, typically filled through {@link PackageManager} queries.
@@ -87,6 +77,9 @@
private static final Object mInitializationLock = new Object();
private static AccountTypeManager mAccountTypeManager;
+ public static final String BROADCAST_ACCOUNTS_CHANGED = AccountTypeManager.class.getName() +
+ ".AccountsChanged";
+
/**
* Requests the singleton instance of {@link AccountTypeManager} with data bound from
* the available authenticators. This method can safely be called from the UI thread.
@@ -100,8 +93,7 @@
synchronized (mInitializationLock) {
if (mAccountTypeManager == null) {
context = context.getApplicationContext();
- mAccountTypeManager = new AccountTypeManagerImpl(context,
- ObjectFactory.getDeviceLocalAccountTypeFactory(context));
+ mAccountTypeManager = new AccountTypeManagerImpl(context);
}
}
return mAccountTypeManager;
@@ -121,6 +113,7 @@
}
private static final AccountTypeManager EMPTY = new AccountTypeManager() {
+
@Override
public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
return Collections.emptyList();
@@ -132,6 +125,17 @@
}
@Override
+ public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+ return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+ }
+
+ @Override
+ public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+ Predicate<AccountType> type) {
+ return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+ }
+
+ @Override
public List<AccountWithDataSet> getGroupWritableAccounts() {
return Collections.emptyList();
}
@@ -157,6 +161,14 @@
public abstract List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter);
/**
+ * Loads accounts in background and returns future that will complete with list of all accounts
+ */
+ public abstract ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync();
+
+ public abstract ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+ Predicate<AccountType> type);
+
+ /**
* Returns the list of accounts that are group writable.
*/
public abstract List<AccountWithDataSet> getGroupWritableAccounts();
@@ -166,6 +178,39 @@
*/
public abstract Account getDefaultGoogleAccount();
+ /**
+ * Returns the Google Accounts.
+ *
+ * <p>This method exists in addition to filterAccountsByTypeAsync because it should be safe
+ * to call synchronously.
+ * </p>
+ */
+ public List<AccountWithDataSet> getWritableGoogleAccounts() {
+ // This implementation may block and should be overridden by the Impl class
+ return Futures.getUnchecked(filterAccountsByTypeAsync(new Predicate<AccountType>() {
+ @Override
+ public boolean apply(@Nullable AccountType input) {
+ return input.areContactsWritable() &&
+ GoogleAccountType.ACCOUNT_TYPE.equals(input.accountType);
+
+ }
+ }));
+ }
+
+ /**
+ * Returns true if there are real accounts (not "local" account) in the list of accounts.
+ */
+ public boolean hasNonLocalAccount() {
+ final List<AccountWithDataSet> allAccounts = getAccounts(/* contactWritableOnly */ false);
+ if (allAccounts == null || allAccounts.size() == 0) {
+ return false;
+ }
+ if (allAccounts.size() > 1) {
+ return true;
+ }
+ return !allAccounts.get(0).isNullAccount();
+ }
+
static Account getDefaultGoogleAccount(AccountManager accountManager,
SharedPreferences prefs, String defaultAccountKey) {
// Get all the google accounts on the device
@@ -258,53 +303,33 @@
}
};
}
-}
-class AccountComparator implements Comparator<AccountWithDataSet> {
- private AccountWithDataSet mDefaultAccount;
-
- public AccountComparator(AccountWithDataSet defaultAccount) {
- mDefaultAccount = defaultAccount;
+ public static Predicate<AccountWithDataSet> adaptTypeFilter(
+ final Predicate<AccountType> typeFilter, final AccountTypeProvider provider) {
+ return new Predicate<AccountWithDataSet>() {
+ @Override
+ public boolean apply(@Nullable AccountWithDataSet input) {
+ return typeFilter.apply(provider.getTypeForAccount(input));
+ }
+ };
}
- @Override
- public int compare(AccountWithDataSet a, AccountWithDataSet b) {
- if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
- && Objects.equal(a.dataSet, b.dataSet)) {
- return 0;
- } else if (b.name == null || b.type == null) {
- return -1;
- } else if (a.name == null || a.type == null) {
- return 1;
- } else if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
- return -1;
- } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
- return 1;
- } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
- return -1;
- } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
- return 1;
- } else {
- int diff = a.name.compareToIgnoreCase(b.name);
- if (diff != 0) {
- return diff;
+ public static Predicate<AccountType> writableFilter() {
+ return new Predicate<AccountType>() {
+ @Override
+ public boolean apply(@Nullable AccountType account) {
+ return account.areContactsWritable();
}
- diff = a.type.compareToIgnoreCase(b.type);
- if (diff != 0) {
- return diff;
- }
-
- // Accounts without data sets get sorted before those that have them.
- if (a.dataSet != null) {
- return b.dataSet == null ? 1 : a.dataSet.compareToIgnoreCase(b.dataSet);
- } else {
- return -1;
- }
- }
+ };
}
- private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
- return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
+ public static Predicate<AccountType> groupWritableFilter() {
+ return new Predicate<AccountType>() {
+ @Override
+ public boolean apply(@Nullable AccountType account) {
+ return account.isGroupMembershipEditable();
+ }
+ };
}
}
@@ -313,68 +338,49 @@
private Context mContext;
private AccountManager mAccountManager;
- private DeviceLocalAccountTypeFactory mDeviceLocalAccountTypeFactory;
+ private DeviceLocalAccountLocator mLocalAccountLocator;
+ private AccountTypeProvider mTypeProvider;
+ private ListeningExecutorService mExecutor;
+ private Executor mMainThreadExecutor;
private AccountType mFallbackAccountType;
- private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
- private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
- private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
- private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
+ private ListenableFuture<List<AccountWithDataSet>> mLocalAccountsFuture;
+ private ListenableFuture<AccountTypeProvider> mAccountTypesFuture;
- private static final int MESSAGE_LOAD_DATA = 0;
- private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+ private FutureCallback<Object> mAccountsUpdateCallback = new FutureCallback<Object>() {
+ @Override
+ public void onSuccess(@Nullable Object result) {
+ onAccountsUpdatedInternal();
+ }
- private HandlerThread mListenerThread;
- private Handler mListenerHandler;
+ @Override
+ public void onFailure(Throwable t) {
+ }
+ };
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private final Runnable mCheckFilterValidityRunnable = new Runnable () {
- @Override
- public void run() {
- ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
- }
- };
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-
@Override
public void onReceive(Context context, Intent intent) {
- Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
- mListenerHandler.sendMessage(msg);
+ reloadAccountTypes();
}
-
};
- /* A latch that ensures that asynchronous initialization completes before data is used */
- private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
-
/**
* Internal constructor that only performs initial parsing.
*/
- public AccountTypeManagerImpl(Context context,
- DeviceLocalAccountTypeFactory deviceLocalAccountTypeFactory) {
+ public AccountTypeManagerImpl(Context context) {
mContext = context;
+ mLocalAccountLocator = DeviceLocalAccountLocator.create(context);
+ mTypeProvider = new AccountTypeProvider(context);
mFallbackAccountType = new FallbackAccountType(context);
- mDeviceLocalAccountTypeFactory = deviceLocalAccountTypeFactory;
mAccountManager = AccountManager.get(mContext);
- mListenerThread = new HandlerThread("AccountChangeListener");
- mListenerThread.start();
- mListenerHandler = new Handler(mListenerThread.getLooper()) {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MESSAGE_LOAD_DATA:
- loadAccountsInBackground();
- break;
- case MESSAGE_PROCESS_BROADCAST_INTENT:
- processBroadcastIntent((Intent) msg.obj);
- break;
- }
- }
- };
+ mExecutor = ContactsExecutors.getDefaultThreadPoolExecutor();
+ mMainThreadExecutor = ContactsExecutors.newHandlerExecutor(mMainThreadHandler);
// Request updates when packages or accounts change
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
@@ -392,17 +398,16 @@
filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
mContext.registerReceiver(mBroadcastReceiver, filter);
- mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+ mAccountManager.addOnAccountsUpdatedListener(this, mMainThreadHandler, false);
ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
-
if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
// Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
// if a new device contact is added.
mContext.getContentResolver().registerContentObserver(
ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
- new ContentObserver(mListenerHandler) {
+ new ContentObserver(mMainThreadHandler) {
@Override
public boolean deliverSelfNotifications() {
return true;
@@ -410,294 +415,73 @@
@Override
public void onChange(boolean selfChange) {
- mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ reloadLocalAccounts();
}
@Override
public void onChange(boolean selfChange, Uri uri) {
- mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ reloadLocalAccounts();
}
});
}
-
- mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ loadAccountTypes();
}
@Override
public void onStatusChanged(int which) {
- mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
- }
-
- public void processBroadcastIntent(Intent intent) {
- mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ reloadAccountTypes();
}
/* This notification will arrive on the background thread */
public void onAccountsUpdated(Account[] accounts) {
- // Refresh to catch any changed accounts
- loadAccountsInBackground();
+ onAccountsUpdatedInternal();
}
- /**
- * Returns instantly if accounts and account types have already been loaded.
- * Otherwise waits for the background thread to complete the loading.
- */
- void ensureAccountsLoaded() {
- CountDownLatch latch = mInitializationLatch;
- if (latch == null) {
- return;
- }
+ private void onAccountsUpdatedInternal() {
+ ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+ LocalBroadcastManager.getInstance(mContext).sendBroadcast(
+ new Intent(BROADCAST_ACCOUNTS_CHANGED));
+ }
- while (true) {
- try {
- latch.await();
- return;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
+ private synchronized void startLoadingIfNeeded() {
+ if (mTypeProvider == null && mAccountTypesFuture == null) {
+ reloadAccountTypes();
+ }
+ if (mLocalAccountsFuture == null) {
+ reloadLocalAccounts();
}
}
- /**
- * Loads account list and corresponding account types (potentially with data sets). Always
- * called on a background thread.
- */
- protected void loadAccountsInBackground() {
- if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
- Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
- }
- TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
- final long startTime = SystemClock.currentThreadTimeMillis();
- final long startTimeWall = SystemClock.elapsedRealtime();
+ private void loadAccountTypes() {
+ mTypeProvider = new AccountTypeProvider(mContext);
- // Account types, keyed off the account type and data set concatenation.
- final 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
- // type string may have multiple AccountType entries.
- final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
-
- final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
- final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
- final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
- final Set<String> extensionPackages = Sets.newHashSet();
-
- final AccountManager am = mAccountManager;
-
- final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
- final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
-
- // First process sync adapters to find any that provide contact data.
- for (SyncAdapterType sync : syncs) {
- if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
- // Skip sync adapters that don't provide contact data.
- continue;
+ mAccountTypesFuture = mExecutor.submit(new Callable<AccountTypeProvider>() {
+ @Override
+ public AccountTypeProvider call() throws Exception {
+ // This will request the AccountType for each Account
+ getAccountsFromProvider(mTypeProvider);
+ return mTypeProvider;
}
-
- // Look for the formatting details provided by each sync
- // adapter, using the authenticator to find general resources.
- final String type = sync.accountType;
- final AuthenticatorDescription auth = findAuthenticator(auths, type);
- if (auth == null) {
- Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
- continue;
- }
-
- AccountType accountType;
- if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
- accountType = new GoogleAccountType(mContext, auth.packageName);
- } else if (ExchangeAccountType.isExchangeType(type)) {
- accountType = new ExchangeAccountType(mContext, auth.packageName, type);
- } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
- auth.packageName)) {
- accountType = new SamsungAccountType(mContext, auth.packageName, type);
- } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
- && isLocalAccountType(mDeviceLocalAccountTypeFactory, type)) {
- // This will be loaded by the DeviceLocalAccountLocator so don't try to create an
- // ExternalAccountType for it.
- continue;
- } else {
- Log.d(TAG, "Registering external account type=" + type
- + ", packageName=" + auth.packageName);
- accountType = new ExternalAccountType(mContext, auth.packageName, false);
- }
- if (!accountType.isInitialized()) {
- if (accountType.isEmbedded()) {
- throw new IllegalStateException("Problem initializing embedded type "
- + accountType.getClass().getCanonicalName());
- } else {
- // Skip external account types that couldn't be initialized.
- continue;
- }
- }
-
- accountType.initializeFieldsFromAuthenticator(auth);
-
- addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
-
- // Check to see if the account type knows of any other non-sync-adapter packages
- // that may provide other data sets of contact data.
- extensionPackages.addAll(accountType.getExtensionPackageNames());
- }
-
- // If any extension packages were specified, process them as well.
- if (!extensionPackages.isEmpty()) {
- Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
- for (String extensionPackage : extensionPackages) {
- ExternalAccountType accountType =
- new ExternalAccountType(mContext, extensionPackage, true);
- if (!accountType.isInitialized()) {
- // Skip external account types that couldn't be initialized.
- continue;
- }
- if (!accountType.hasContactsMetadata()) {
- Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
- + " it doesn't have the CONTACTS_STRUCTURE metadata");
- continue;
- }
- if (TextUtils.isEmpty(accountType.accountType)) {
- Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
- + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
- + " attribute");
- continue;
- }
- Log.d(TAG, "Registering extension package account type="
- + accountType.accountType + ", dataSet=" + accountType.dataSet
- + ", packageName=" + extensionPackage);
-
- addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
- }
- }
- timings.addSplit("Loaded account types");
-
- boolean foundWritableGoogleAccount = false;
- // Map in accounts to associate the account names with each account type entry.
- Account[] accounts = mAccountManager.getAccounts();
- for (Account account : accounts) {
- boolean syncable =
- ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
-
- if (syncable || GoogleAccountType.ACCOUNT_TYPE.equals(account.type)) {
- List<AccountType> accountTypes = accountTypesByType.get(account.type);
- if (accountTypes != null) {
- // Add an account-with-data-set entry for each account type that is
- // authenticated by this account.
- for (AccountType accountType : accountTypes) {
- AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
- account.name, account.type, accountType.dataSet);
- allAccounts.add(accountWithDataSet);
- if (accountType.areContactsWritable()) {
- contactWritableAccounts.add(accountWithDataSet);
- if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
- && accountWithDataSet.dataSet == null) {
- foundWritableGoogleAccount = true;
- }
-
- if (accountType.isGroupMembershipEditable()) {
- groupWritableAccounts.add(accountWithDataSet);
- }
- }
- }
- }
- }
- }
-
- final DeviceLocalAccountLocator deviceAccountLocator = DeviceLocalAccountLocator
- .create(mContext, allAccounts);
- final List<AccountWithDataSet> localAccounts = deviceAccountLocator
- .getDeviceLocalAccounts();
- allAccounts.addAll(localAccounts);
-
- for (AccountWithDataSet localAccount : localAccounts) {
- // Prefer a known type if it exists. This covers the case that a local account has an
- // authenticator with a valid contacts.xml
- AccountType localAccountType = accountTypesByTypeAndDataSet.get(
- localAccount.getAccountTypeWithDataSet());
- if (localAccountType == null) {
- localAccountType = mDeviceLocalAccountTypeFactory.getAccountType(localAccount.type);
- }
- accountTypesByTypeAndDataSet.put(localAccount.getAccountTypeWithDataSet(),
- localAccountType);
-
- // Skip the null account if there is a Google account available. This is done because
- // the Google account's sync adapter will automatically move accounts in the "null"
- // account. Hence, it would be confusing to still show it as an available writable
- // account since contacts that were saved to it would magically change accounts when the
- // sync adapter runs.
- if (foundWritableGoogleAccount && localAccount.type == null) {
- continue;
- }
- if (localAccountType.areContactsWritable()) {
- contactWritableAccounts.add(localAccount);
-
- if (localAccountType.isGroupMembershipEditable()) {
- groupWritableAccounts.add(localAccount);
- }
- }
- }
-
- final AccountComparator accountComparator = new AccountComparator(null);
- Collections.sort(allAccounts, accountComparator);
- Collections.sort(contactWritableAccounts, accountComparator);
- Collections.sort(groupWritableAccounts, accountComparator);
-
- timings.addSplit("Loaded accounts");
-
- synchronized (this) {
- mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
- mAccounts = allAccounts;
- mContactWritableAccounts = contactWritableAccounts;
- mGroupWritableAccounts = groupWritableAccounts;
- }
-
- timings.dumpToLog();
- final long endTimeWall = SystemClock.elapsedRealtime();
- final long endTime = SystemClock.currentThreadTimeMillis();
-
- Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
- + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) "
- + (endTime - startTime) + "ms(cpu)");
-
- if (mInitializationLatch != null) {
- mInitializationLatch.countDown();
- mInitializationLatch = null;
- }
- if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
- Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
- }
-
- // Check filter validity since filter may become obsolete after account update. It must be
- // done from UI thread.
- mMainThreadHandler.post(mCheckFilterValidityRunnable);
+ });
}
- // Bookkeeping method for tracking the known account types in the given maps.
- private void addAccountType(AccountType accountType,
- Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
- Map<String, List<AccountType>> accountTypesByType) {
- accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
- List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
- if (accountsForType == null) {
- accountsForType = Lists.newArrayList();
- }
- accountsForType.add(accountType);
- accountTypesByType.put(accountType.accountType, accountsForType);
+ private synchronized void reloadAccountTypes() {
+ loadAccountTypes();
+ Futures.addCallback(mAccountTypesFuture, mAccountsUpdateCallback, mMainThreadExecutor);
}
- /**
- * Find a specific {@link AuthenticatorDescription} in the provided list
- * that matches the given account type.
- */
- protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
- String accountType) {
- for (AuthenticatorDescription auth : auths) {
- if (accountType.equals(auth.type)) {
- return auth;
+ private synchronized void loadLocalAccounts() {
+ mLocalAccountsFuture = mExecutor.submit(new Callable<List<AccountWithDataSet>>() {
+ @Override
+ public List<AccountWithDataSet> call() throws Exception {
+ return mLocalAccountLocator.getDeviceLocalAccounts();
}
- }
- return null;
+ });
+ }
+
+ private void reloadLocalAccounts() {
+ loadLocalAccounts();
+ Futures.addCallback(mLocalAccountsFuture, mAccountsUpdateCallback, mMainThreadExecutor);
}
/**
@@ -706,21 +490,101 @@
*/
@Override
public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
- ensureAccountsLoaded();
- return Lists.newArrayList(contactWritableOnly ? mContactWritableAccounts : mAccounts);
+ final Predicate<AccountType> filter = contactWritableOnly ?
+ writableFilter() : Predicates.<AccountType>alwaysTrue();
+ // TODO: Shouldn't have a synchronous version for getting all accounts
+ return Futures.getUnchecked(filterAccountsByTypeAsync(filter));
}
@Override
public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
- return new ArrayList<>(Collections2.filter(mAccounts, filter));
+ // TODO: Shouldn't have a synchronous version for getting all accounts
+ return Futures.getUnchecked(filterAccountsAsync(filter));
}
+ @Override
+ public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+ startLoadingIfNeeded();
+ return filterAccountsAsync(Predicates.<AccountWithDataSet>alwaysTrue());
+ }
+
+ @Override
+ public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+ final Predicate<AccountType> typeFilter) {
+ // Ensure that mTypeProvider is initialized so that the reference will be the same
+ // here as in the call to filterAccountsAsync
+ startLoadingIfNeeded();
+ return filterAccountsAsync(adaptTypeFilter(typeFilter, mTypeProvider));
+ }
+
+ private ListenableFuture<List<AccountWithDataSet>> filterAccountsAsync(
+ final Predicate<AccountWithDataSet> filter) {
+ startLoadingIfNeeded();
+ final ListenableFuture<List<AccountWithDataSet>> accountsFromTypes =
+ Futures.transform(Futures.nonCancellationPropagating(mAccountTypesFuture),
+ new Function<AccountTypeProvider, List<AccountWithDataSet>>() {
+ @Override
+ public List<AccountWithDataSet> apply(AccountTypeProvider provider) {
+ return getAccountsFromProvider(provider);
+ }
+ });
+
+ final ListenableFuture<List<List<AccountWithDataSet>>> all =
+ Futures.successfulAsList(accountsFromTypes, mLocalAccountsFuture);
+
+ return Futures.transform(all, new Function<List<List<AccountWithDataSet>>,
+ List<AccountWithDataSet>>() {
+ @Nullable
+ @Override
+ public List<AccountWithDataSet> apply(@Nullable List<List<AccountWithDataSet>> input) {
+ // The first result list is from the account types. Check if there is a Google
+ // account in this list and if there is exclude the null account
+ final Predicate<AccountWithDataSet> appliedFilter =
+ hasWritableGoogleAccount(input.get(0)) ?
+ Predicates.and(nonNullAccountFilter(), filter) :
+ filter;
+ List<AccountWithDataSet> result = new ArrayList<>();
+ for (List<AccountWithDataSet> list : input) {
+ if (list != null) {
+ result.addAll(Collections2.filter(list, appliedFilter));
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+ private List<AccountWithDataSet> getAccountsFromProvider(AccountTypeProvider cache) {
+ final List<AccountWithDataSet> result = new ArrayList<>();
+ final Account[] accounts = mAccountManager.getAccounts();
+ for (Account account : accounts) {
+ final List<AccountType> types = cache.getAccountTypes(account.type);
+ for (AccountType type : types) {
+ result.add(new AccountWithDataSet(account.name, account.type, type.dataSet));
+ }
+ }
+ return result;
+ }
+
+ private boolean hasWritableGoogleAccount(List<AccountWithDataSet> accounts) {
+ if (accounts == null) {
+ return false;
+ }
+ AccountType type;
+ for (AccountWithDataSet account : accounts) {
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
/**
* Return the list of all known, group writable {@link AccountWithDataSet}'s.
*/
public List<AccountWithDataSet> getGroupWritableAccounts() {
- ensureAccountsLoaded();
- return Lists.newArrayList(mGroupWritableAccounts);
+ return Futures.getUnchecked(filterAccountsByTypeAsync(groupWritableFilter()));
}
/**
@@ -729,12 +593,29 @@
*/
@Override
public Account getDefaultGoogleAccount() {
- final AccountManager accountManager = AccountManager.get(mContext);
final SharedPreferences sharedPreferences =
mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE);
final String defaultAccountKey =
mContext.getResources().getString(R.string.contact_editor_default_account_key);
- return getDefaultGoogleAccount(accountManager, sharedPreferences, defaultAccountKey);
+ return getDefaultGoogleAccount(mAccountManager, sharedPreferences, defaultAccountKey);
+ }
+
+ @Override
+ public List<AccountWithDataSet> getWritableGoogleAccounts() {
+ final Account[] googleAccounts =
+ mAccountManager.getAccountsByType(GoogleAccountType.ACCOUNT_TYPE);
+ final List<AccountWithDataSet> result = new ArrayList<>();
+ for (Account account : googleAccounts) {
+ // Accounts with a dataSet (e.g. Google plus accounts) are not writable.
+ result.add(new AccountWithDataSet(account.name, account.type, null));
+ }
+ return result;
+ }
+
+ @Override
+ public boolean hasNonLocalAccount() {
+ final Account[] accounts = mAccountManager.getAccounts();
+ return accounts != null && accounts.length > 0;
}
/**
@@ -744,7 +625,6 @@
*/
@Override
public DataKind getKindOrFallback(AccountType type, String mimeType) {
- ensureAccountsLoaded();
DataKind kind = null;
// Try finding account type and kind matching request
@@ -771,10 +651,7 @@
*/
@Override
public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
- ensureAccountsLoaded();
- synchronized (this) {
- AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
- return type != null ? type : mFallbackAccountType;
- }
+ return mTypeProvider.getType(
+ accountTypeWithDataSet.accountType, accountTypeWithDataSet.dataSet);
}
}
diff --git a/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java b/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
index afd7917..307577d 100644
--- a/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
@@ -60,14 +60,10 @@
public Cp2DeviceLocalAccountLocator(ContentResolver contentResolver,
DeviceLocalAccountTypeFactory factory,
- List<AccountWithDataSet> knownAccounts) {
+ Set<String> knownAccountTypes) {
mResolver = contentResolver;
mAccountTypeFactory = factory;
- final Set<String> knownAccountTypes = new HashSet<>();
- for (AccountWithDataSet account : knownAccounts) {
- knownAccountTypes.add(account.type);
- }
mSelection = getSelection(knownAccountTypes);
mSelectionArgs = getSelectionArgs(knownAccountTypes);
}
diff --git a/src/com/android/contacts/model/DeviceLocalAccountLocator.java b/src/com/android/contacts/model/DeviceLocalAccountLocator.java
index 7fb9ef8..0d34057 100644
--- a/src/com/android/contacts/model/DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/model/DeviceLocalAccountLocator.java
@@ -15,6 +15,8 @@
*/
package com.android.contacts.model;
+import android.accounts.Account;
+import android.accounts.AccountManager;
import android.content.Context;
import com.android.contacts.Experiments;
@@ -23,7 +25,9 @@
import com.android.contactsbind.experiments.Flags;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Attempts to detect accounts for device contacts
@@ -45,10 +49,24 @@
};
public static DeviceLocalAccountLocator create(Context context,
- List<AccountWithDataSet> knownAccounts) {
+ Set<String> knownAccountTypes) {
if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
return new Cp2DeviceLocalAccountLocator(context.getContentResolver(),
- ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownAccounts);
+ ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownAccountTypes);
+ }
+ return NULL_ONLY;
+ }
+
+ public static DeviceLocalAccountLocator create(Context context) {
+ final AccountManager accountManager =
+ (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+ final Set<String> knownTypes = new HashSet<>();
+ for (Account account : accountManager.getAccounts()) {
+ knownTypes.add(account.type);
+ }
+ if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
+ return new Cp2DeviceLocalAccountLocator(context.getContentResolver(),
+ ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownTypes);
}
return NULL_ONLY;
}
diff --git a/src/com/android/contacts/model/account/AccountComparator.java b/src/com/android/contacts/model/account/AccountComparator.java
new file mode 100644
index 0000000..70ccf49
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountComparator.java
@@ -0,0 +1,71 @@
+/*
+ * 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.model.account;
+
+import com.google.common.base.Objects;
+
+import java.util.Comparator;
+
+/**
+ * Orders accounts for display such that the default account is first
+ */
+public class AccountComparator implements Comparator<AccountWithDataSet> {
+ private AccountWithDataSet mDefaultAccount;
+
+ public AccountComparator(AccountWithDataSet defaultAccount) {
+ mDefaultAccount = defaultAccount;
+ }
+
+ @Override
+ public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+ if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
+ && Objects.equal(a.dataSet, b.dataSet)) {
+ return 0;
+ } else if (b.name == null || b.type == null) {
+ return -1;
+ } else if (a.name == null || a.type == null) {
+ return 1;
+ } else if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
+ return -1;
+ } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
+ return 1;
+ } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
+ return -1;
+ } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
+ return 1;
+ } else {
+ int diff = a.name.compareToIgnoreCase(b.name);
+ if (diff != 0) {
+ return diff;
+ }
+ diff = a.type.compareToIgnoreCase(b.type);
+ if (diff != 0) {
+ return diff;
+ }
+
+ // Accounts without data sets get sorted before those that have them.
+ if (a.dataSet != null) {
+ return b.dataSet == null ? 1 : a.dataSet.compareToIgnoreCase(b.dataSet);
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
+ return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
+ }
+}
diff --git a/src/com/android/contacts/model/account/AccountTypeProvider.java b/src/com/android/contacts/model/account/AccountTypeProvider.java
new file mode 100644
index 0000000..474b3b4
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountTypeProvider.java
@@ -0,0 +1,223 @@
+/*
+ * 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.model.account;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.provider.ContactsContract;
+import android.support.v4.util.ArraySet;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.util.DeviceLocalAccountTypeFactory;
+import com.android.contactsbind.ObjectFactory;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
+
+/**
+ * Provides access to {@link AccountType}s with contact data
+ *
+ * This class parses the contacts.xml for third-party accounts and caches the result.
+ * This means that {@link AccountTypeProvider#getAccountTypes(String)}} should be called from a
+ * background thread.
+ */
+public class AccountTypeProvider {
+ private static final String TAG = "AccountTypeProvider";
+
+ private final Context mContext;
+ private final DeviceLocalAccountTypeFactory mLocalAccountTypeFactory;
+ private final ImmutableMap<String, AuthenticatorDescription> mAuthTypes;
+
+ private final ConcurrentMap<String, List<AccountType>> mCache = new ConcurrentHashMap<>();
+
+ public AccountTypeProvider(Context context) {
+ this(context,
+ ObjectFactory.getDeviceLocalAccountTypeFactory(context),
+ ContentResolver.getSyncAdapterTypes(),
+ ((AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE))
+ .getAuthenticatorTypes());
+ }
+
+ public AccountTypeProvider(Context context, DeviceLocalAccountTypeFactory localTypeFactory,
+ SyncAdapterType[] syncAdapterTypes,
+ AuthenticatorDescription[] authenticatorDescriptions) {
+ mContext = context;
+ mLocalAccountTypeFactory = localTypeFactory;
+
+ final Set<String> mContactSyncableTypes = new ArraySet<>();
+ for (SyncAdapterType type : syncAdapterTypes) {
+ if (type.authority.equals(ContactsContract.AUTHORITY)) {
+ mContactSyncableTypes.add(type.accountType);
+ }
+ }
+
+ final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
+ ImmutableMap.builder();
+ for (AuthenticatorDescription auth : authenticatorDescriptions) {
+ if (mContactSyncableTypes.contains(auth.type)) {
+ builder.put(auth.type, auth);
+ }
+ }
+ mAuthTypes = builder.build();
+ }
+
+ /**
+ * Returns all account types associated with the provided type
+ *
+ * <p>There are many {@link AccountType}s for each accountType because {@AccountType} includes
+ * a dataSet and accounts can declare extension packages in contacts.xml that provide additional
+ * data sets for a particular type
+ * </p>
+ */
+ public List<AccountType> getAccountTypes(String accountType) {
+ // ConcurrentHashMap doesn't support null keys
+ if (accountType == null) {
+ AccountType type = mLocalAccountTypeFactory.getAccountType(accountType);
+ // Just in case the DeviceLocalAccountTypeFactory doesn't handle the null type
+ if (type == null) {
+ type = new FallbackAccountType(mContext);
+ }
+ return Collections.singletonList(type);
+ }
+
+ List<AccountType> types = mCache.get(accountType);
+ if (types == null) {
+ types = loadTypes(accountType);
+ mCache.put(accountType, types);
+ }
+ return types;
+ }
+
+ public boolean hasTypeForAccount(AccountWithDataSet account) {
+ return getTypeForAccount(account) != null;
+ }
+
+ public boolean hasTypeWithDataset(String type, String dataSet) {
+ // getAccountTypes() never returns null
+ final List<AccountType> accountTypes = getAccountTypes(type);
+ for (AccountType accountType : accountTypes) {
+ if (Objects.equal(accountType.dataSet, dataSet)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the AccountType with the matching type and dataSet or null if no account with those
+ * members exists
+ */
+ public AccountType getType(String type, String dataSet) {
+ final List<AccountType> accountTypes = getAccountTypes(type);
+ for (AccountType accountType : accountTypes) {
+ if (Objects.equal(accountType.dataSet, dataSet)) {
+ return accountType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the AccountType for a particular account or null if no account type exists for the
+ * account
+ */
+ public AccountType getTypeForAccount(AccountWithDataSet account) {
+ return getType(account.type, account.dataSet);
+ }
+
+ private List<AccountType> loadTypes(String type) {
+ final AuthenticatorDescription auth = mAuthTypes.get(type);
+ if (auth == null) {
+ return Collections.emptyList();
+ }
+
+ AccountType accountType;
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
+ accountType = new GoogleAccountType(mContext, auth.packageName);
+ } else if (ExchangeAccountType.isExchangeType(type)) {
+ accountType = new ExchangeAccountType(mContext, auth.packageName, type);
+ } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
+ auth.packageName)) {
+ accountType = new SamsungAccountType(mContext, auth.packageName, type);
+ } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
+ && isLocalAccountType(mLocalAccountTypeFactory, type)) {
+ accountType = mLocalAccountTypeFactory.getAccountType(type);
+ } else {
+ Log.d(TAG, "Registering external account type=" + type
+ + ", packageName=" + auth.packageName);
+ accountType = new ExternalAccountType(mContext, auth.packageName, false);
+ }
+ if (!accountType.isInitialized()) {
+ if (accountType.isEmbedded()) {
+ throw new IllegalStateException("Problem initializing embedded type "
+ + accountType.getClass().getCanonicalName());
+ } else {
+ // Skip external account types that couldn't be initialized
+ return Collections.emptyList();
+ }
+ }
+
+ accountType.initializeFieldsFromAuthenticator(auth);
+
+ final ImmutableList.Builder<AccountType> result = ImmutableList.builder();
+ result.add(accountType);
+
+ for (String extensionPackage : accountType.getExtensionPackageNames()) {
+ final ExternalAccountType extensionType =
+ new ExternalAccountType(mContext, extensionPackage, true);
+ if (!extensionType.isInitialized()) {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ if (!extensionType.hasContactsMetadata()) {
+ Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+ + " it doesn't have the CONTACTS_STRUCTURE metadata");
+ continue;
+ }
+ if (TextUtils.isEmpty(extensionType.accountType)) {
+ Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+ + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+ + " attribute");
+ continue;
+ }
+ if (Objects.equal(extensionType.accountType, type)) {
+ Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+ + " the account type + " + extensionType.accountType +
+ " doesn't match expected type " + type);
+ continue;
+ }
+ Log.d(TAG, "Registering extension package account type="
+ + accountType.accountType + ", dataSet=" + accountType.dataSet
+ + ", packageName=" + extensionPackage);
+
+ result.add(extensionType);
+ }
+ return result.build();
+ }
+
+}
diff --git a/src/com/android/contacts/util/AccountFilterUtil.java b/src/com/android/contacts/util/AccountFilterUtil.java
index 3a6d49e..9eb8e7b 100644
--- a/src/com/android/contacts/util/AccountFilterUtil.java
+++ b/src/com/android/contacts/util/AccountFilterUtil.java
@@ -21,11 +21,14 @@
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.AsyncTaskLoader;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
+import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
@@ -36,18 +39,27 @@
import com.android.contacts.list.ContactListFilter;
import com.android.contacts.list.ContactListFilterController;
import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.Contact;
import com.android.contacts.model.account.AccountDisplayInfo;
import com.android.contacts.model.account.AccountDisplayInfoFactory;
import com.android.contacts.model.account.AccountType;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.preference.ContactsPreferences;
+import com.android.contacts.util.concurrent.ContactsExecutors;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
import com.android.contactsbind.ObjectFactory;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
+import javax.annotation.Nullable;
+
/**
* Utility class for account filter manipulation.
*/
@@ -98,65 +110,78 @@
/**
* Loads a list of contact list filters
*/
- public static class FilterLoader extends AsyncTaskLoader<List<ContactListFilter>> {
- private Context mContext;
+ public static class FilterLoader extends ListenableFutureLoader<List<ContactListFilter>> {
+ private AccountTypeManager mAccountTypeManager;
private DeviceLocalAccountTypeFactory mDeviceLocalFactory;
+ private LocalBroadcastManager mLocalBroadcastManager;
+ private BroadcastReceiver mReceiver;
public FilterLoader(Context context) {
super(context);
- mContext = context;
+ mAccountTypeManager = AccountTypeManager.getInstance(context);
mDeviceLocalFactory = ObjectFactory.getDeviceLocalAccountTypeFactory(context);
- }
-
- @Override
- public List<ContactListFilter> loadInBackground() {
- return loadAccountFilters(mContext, mDeviceLocalFactory);
+ mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
}
@Override
protected void onStartLoading() {
- forceLoad();
- }
-
- @Override
- protected void onStopLoading() {
- cancelLoad();
+ super.onStartLoading();
+ if (mReceiver == null) {
+ mReceiver = new ForceLoadReceiver();
+ mLocalBroadcastManager.registerReceiver(mReceiver,
+ new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+ }
}
@Override
protected void onReset() {
- onStopLoading();
- }
- }
-
- private static List<ContactListFilter> loadAccountFilters(Context context,
- DeviceLocalAccountTypeFactory deviceAccountTypeFactory) {
- final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
- final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
- final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(true);
- AccountTypeManager.sortAccounts(getDefaultAccount(context), accounts);
-
- for (AccountWithDataSet account : accounts) {
- final AccountType accountType =
- accountTypeManager.getAccountType(account.type, account.dataSet);
- if ((accountType.isExtension() || DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
- deviceAccountTypeFactory, account.type)) && !account.hasData(context)) {
- // Hide extensions and device accounts with no raw_contacts.
- continue;
- }
- final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
- if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
- deviceAccountTypeFactory, account.type)) {
- accountFilters.add(ContactListFilter.createDeviceContactsFilter(icon, account));
- } else {
- accountFilters.add(ContactListFilter.createAccountFilter(
- account.type, account.name, account.dataSet, icon));
+ super.onReset();
+ if (mReceiver != null) {
+ mLocalBroadcastManager.unregisterReceiver(mReceiver);
}
}
- final ArrayList<ContactListFilter> result = Lists.newArrayList();
- result.addAll(accountFilters);
- return result;
+ @Override
+ protected ListenableFuture<List<ContactListFilter>> loadData() {
+ return Futures.transform(mAccountTypeManager.filterAccountsByTypeAsync(
+ AccountTypeManager.writableFilter()),
+ new Function<List<AccountWithDataSet>, List<ContactListFilter>>() {
+ @Override
+ public List<ContactListFilter> apply(List<AccountWithDataSet> input) {
+ return getFiltersForAccounts(input);
+ }
+ }, ContactsExecutors.getDefaultThreadPoolExecutor());
+ }
+
+ private List<ContactListFilter> getFiltersForAccounts(List<AccountWithDataSet> accounts) {
+ final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
+ AccountTypeManager.sortAccounts(getDefaultAccount(getContext()), accounts);
+
+ for (AccountWithDataSet account : accounts) {
+ final AccountType accountType =
+ mAccountTypeManager.getAccountType(account.type, account.dataSet);
+ if ((accountType.isExtension() ||
+ DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
+ mDeviceLocalFactory, account.type)) &&
+ !account.hasData(getContext())) {
+ // Hide extensions and device accounts with no raw_contacts.
+ continue;
+ }
+ final Drawable icon = accountType != null ?
+ accountType.getDisplayIcon(getContext()) : null;
+ if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
+ mDeviceLocalFactory, account.type)) {
+ accountFilters.add(ContactListFilter.createDeviceContactsFilter(icon, account));
+ } else {
+ accountFilters.add(ContactListFilter.createAccountFilter(
+ account.type, account.name, account.dataSet, icon));
+ }
+ }
+
+ final ArrayList<ContactListFilter> result = Lists.newArrayList();
+ result.addAll(accountFilters);
+ return result;
+ }
}
private static AccountWithDataSet getDefaultAccount(Context context) {
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index 256123e..94a7c29 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -25,9 +25,11 @@
import android.widget.TextView;
import com.android.contacts.R;
+import com.android.contacts.list.ContactListFilter;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.account.AccountDisplayInfo;
import com.android.contacts.model.account.AccountDisplayInfoFactory;
+import com.android.contacts.model.account.AccountType;
import com.android.contacts.model.account.AccountWithDataSet;
import java.util.ArrayList;
@@ -39,30 +41,53 @@
public final class AccountsListAdapter extends BaseAdapter {
private final LayoutInflater mInflater;
private final List<AccountDisplayInfo> mAccountDisplayInfoList;
+ private final List<AccountWithDataSet> mAccounts;
private final Context mContext;
private int mCustomLayout = -1;
- /**
- * Filters that affect the list of accounts that is displayed by this adapter.
- */
public enum AccountListFilter {
- ALL_ACCOUNTS, // All read-only and writable accounts
- ACCOUNTS_CONTACT_WRITABLE, // Only where the account type is contact writable
- ACCOUNTS_GROUP_WRITABLE // Only accounts where the account type is group writable
+ ALL_ACCOUNTS {
+ @Override
+ public List<AccountWithDataSet> getAccounts(Context context) {
+ return AccountTypeManager.getInstance(context).getAccounts(false);
+ }
+ },
+ ACCOUNTS_CONTACT_WRITABLE {
+ @Override
+ public List<AccountWithDataSet> getAccounts(Context context) {
+ return AccountTypeManager.getInstance(context).getAccounts(true);
+ }
+ },
+ ACCOUNTS_GROUP_WRITABLE {
+ @Override
+ public List<AccountWithDataSet> getAccounts(Context context) {
+ return AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+ }
+ };
+
+ public abstract List<AccountWithDataSet> getAccounts(Context context);
}
- public AccountsListAdapter(Context context, AccountListFilter accountListFilter) {
- this(context, accountListFilter, null);
+ public AccountsListAdapter(Context context, AccountListFilter filter) {
+ this(context, filter.getAccounts(context), null);
+ }
+
+ public AccountsListAdapter(Context context, AccountListFilter filter,
+ AccountWithDataSet currentAccount) {
+ this(context, filter.getAccounts(context), currentAccount);
+ }
+
+ public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts) {
+ this(context, accounts, null);
}
/**
* @param currentAccount the Account currently selected by the user, which should come
* first in the list. Can be null.
*/
- public AccountsListAdapter(Context context, AccountListFilter accountListFilter,
+ public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts,
AccountWithDataSet currentAccount) {
mContext = context;
- final List<AccountWithDataSet> accounts = getAccounts(accountListFilter);
if (currentAccount != null
&& !accounts.isEmpty()
&& !accounts.get(0).equals(currentAccount)
@@ -77,15 +102,8 @@
mAccountDisplayInfoList.add(factory.getAccountDisplayInfo(account));
}
mInflater = LayoutInflater.from(context);
- }
- private List<AccountWithDataSet> getAccounts(AccountListFilter accountListFilter) {
- final AccountTypeManager typeManager = AccountTypeManager.getInstance(mContext);
- if (accountListFilter == AccountListFilter.ACCOUNTS_GROUP_WRITABLE) {
- return new ArrayList<AccountWithDataSet>(typeManager.getGroupWritableAccounts());
- }
- return new ArrayList<AccountWithDataSet>(typeManager.getAccounts(
- accountListFilter == AccountListFilter.ACCOUNTS_CONTACT_WRITABLE));
+ mAccounts = accounts;
}
public void setCustomLayout(int customLayout) {
diff --git a/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java b/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
index 3e61555..59ee7e5 100644
--- a/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
+++ b/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
@@ -15,8 +15,6 @@
*/
package com.android.contacts.util;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
import android.content.Context;
import android.support.annotation.IntDef;
@@ -25,6 +23,8 @@
import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
/**
* Reports whether a value from RawContacts.ACCOUNT_TYPE should be considered a "Device"
* account
diff --git a/src/com/android/contacts/util/SyncUtil.java b/src/com/android/contacts/util/SyncUtil.java
index 0d6c494..ce10937 100644
--- a/src/com/android/contacts/util/SyncUtil.java
+++ b/src/com/android/contacts/util/SyncUtil.java
@@ -22,8 +22,11 @@
import android.net.NetworkInfo;
import android.provider.ContactsContract;
+import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.model.account.GoogleAccountType;
+import java.util.List;
+
/**
* Utilities related to sync.
*/
@@ -46,6 +49,19 @@
}
/**
+ * Returns true {@link ContentResolver#isSyncPending(Account, String)} or
+ * {@link ContentResolver#isSyncActive(Account, String)} is true for any account in accounts
+ */
+ public static final boolean isAnySyncing(List<AccountWithDataSet> accounts) {
+ for (AccountWithDataSet accountWithDataSet : accounts) {
+ if (isSyncStatusPendingOrActive(accountWithDataSet.getAccountOrNull())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Returns true if the given Google account is not syncable.
*/
public static final boolean isUnsyncableGoogleAccount(Account account) {
diff --git a/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
new file mode 100644
index 0000000..f7edb64
--- /dev/null
+++ b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
@@ -0,0 +1,118 @@
+/*
+ * 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.util.concurrent;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+
+/**
+ * Wraps a ListenableFuture for integration with {@link android.app.LoaderManager}
+ *
+ * <p>Using a loader ensures that the result is delivered while the receiving component (activity
+ * or fragment) is resumed and also prevents leaking references these components
+ * </p>
+ */
+public abstract class ListenableFutureLoader<D> extends Loader<D> {
+ private static final String TAG = "FutureLoader";
+
+ private ListenableFuture<D> mFuture;
+ private D mLoadedData;
+ private Executor mUiExecutor;
+
+ /**
+ * Stores away the application context associated with context.
+ * Since Loaders can be used across multiple activities it's dangerous to
+ * store the context directly; always use {@link #getContext()} to retrieve
+ * the Loader's Context, don't use the constructor argument directly.
+ * The Context returned by {@link #getContext} is safe to use across
+ * Activity instances.
+ *
+ * @param context used to retrieve the application context.
+ */
+ public ListenableFutureLoader(Context context) {
+ super(context);
+ mUiExecutor = ContactsExecutors.newUiThreadExecutor();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mLoadedData != null) {
+ deliverResult(mLoadedData);
+ }
+ if (mFuture == null) {
+ takeContentChanged();
+ forceLoad();
+ } else if (takeContentChanged()) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onForceLoad() {
+ mFuture = loadData();
+ Futures.addCallback(mFuture, new FutureCallback<D>() {
+ @Override
+ public void onSuccess(D result) {
+ mLoadedData = result;
+ deliverResult(mLoadedData);
+ commitContentChanged();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof CancellationException) {
+ Log.i(TAG, "Loading cancelled", t);
+ rollbackContentChanged();
+ } else {
+ Log.e(TAG, "Failed to load accounts", t);
+ }
+ }
+ }, mUiExecutor);
+ }
+
+ @Override
+ protected void onStopLoading() {
+ if (mFuture != null) {
+ mFuture.cancel(false);
+ mFuture = null;
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ mFuture = null;
+ mLoadedData = null;
+ }
+
+ protected abstract ListenableFuture<D> loadData();
+
+ public class ForceLoadReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onContentChanged();
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java b/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
index e6e67bf..4e62126 100644
--- a/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
+++ b/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
@@ -24,6 +24,7 @@
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.Nullable;
+import android.support.v4.util.ArraySet;
import android.test.AndroidTestCase;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.SmallTest;
@@ -48,7 +49,7 @@
final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
getContext().getContentResolver(),
new DeviceLocalAccountTypeFactory.Default(getContext()),
- Collections.<AccountWithDataSet>emptyList());
+ Collections.<String>emptySet());
sut.getDeviceLocalAccounts();
// We didn't throw so it passed
}
@@ -80,31 +81,29 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
- createStubResolverWithContentQueryResult(queryResult(
- "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"
- )), stubFactory,
- Collections.<AccountWithDataSet>emptyList());
+ final DeviceLocalAccountLocator sut = createLocator(queryResult(
+ "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"
+ ), stubFactory);
+
assertEquals(4, sut.getDeviceLocalAccounts().size());
}
- public void test_getDeviceLocalAccounts_doesNotContainItemsForKnownAccounts() {
+ public void test_getDeviceLocalAccounts_doesNotContainItemsForKnownAccountTypes() {
final Cp2DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
getContext().getContentResolver(), new FakeDeviceAccountTypeFactory(),
- Arrays.asList(new AccountWithDataSet("user", "com.example", null),
- new AccountWithDataSet("user1", "com.example", null),
- new AccountWithDataSet("user", "com.example.1", null)));
+ new ArraySet<>(Arrays.asList("com.example", "com.example.1")));
- assertTrue("Selection should filter known accounts", sut.getSelection().contains("NOT IN (?,?)"));
+ assertTrue("Selection should filter known accounts",
+ sut.getSelection().contains("NOT IN (?,?)"));
final List<String> args = Arrays.asList(sut.getSelectionArgs());
assertEquals(2, args.size());
@@ -116,12 +115,11 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
- createContentResolverWithProvider(new FakeContactsProvider()
- .withQueryResult(ContactsContract.Settings.CONTENT_URI, queryResult(
- "phone_account", "vnd.sec.contact.phone",
- "sim_account", "vnd.sec.contact.sim"
- ))), stubFactory, Collections.<AccountWithDataSet>emptyList());
+ final DeviceLocalAccountLocator sut = createLocator(new FakeContactsProvider()
+ .withQueryResult(ContactsContract.Settings.CONTENT_URI, queryResult(
+ "phone_account", "vnd.sec.contact.phone",
+ "sim_account", "vnd.sec.contact.sim"
+ )), stubFactory);
assertEquals(2, sut.getDeviceLocalAccounts().size());
}
@@ -130,22 +128,34 @@
final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
.withDeviceTypes(null, "vnd.sec.contact.phone")
.withSimTypes("vnd.sec.contact.sim");
- final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
- createContentResolverWithProvider(new FakeContactsProvider()
- .withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
- "phone_account", "vnd.sec.contact.phone",
- "sim_account", "vnd.sec.contact.sim"
- ))), stubFactory, Collections.<AccountWithDataSet>emptyList());
+ final DeviceLocalAccountLocator sut = createLocator(new FakeContactsProvider()
+ .withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
+ "phone_account", "vnd.sec.contact.phone",
+ "sim_account", "vnd.sec.contact.sim"
+ )), stubFactory);
assertEquals(2, sut.getDeviceLocalAccounts().size());
}
private DeviceLocalAccountLocator createWithQueryResult(
Cursor cursor) {
+ return createLocator(cursor, new DeviceLocalAccountTypeFactory.Default(mContext));
+ }
+
+ private DeviceLocalAccountLocator createLocator(ContentProvider contactsProvider,
+ DeviceLocalAccountTypeFactory localAccountTypeFactory) {
+ final DeviceLocalAccountLocator locator = new Cp2DeviceLocalAccountLocator(
+ createContentResolverWithProvider(contactsProvider),
+ localAccountTypeFactory, Collections.<String>emptySet());
+ return locator;
+ }
+
+ private DeviceLocalAccountLocator createLocator(Cursor cursor,
+ DeviceLocalAccountTypeFactory localAccountTypeFactory) {
final DeviceLocalAccountLocator locator = new Cp2DeviceLocalAccountLocator(
createStubResolverWithContentQueryResult(cursor),
- new DeviceLocalAccountTypeFactory.Default(getContext()),
- Collections.<AccountWithDataSet>emptyList());
+ localAccountTypeFactory,
+ Collections.<String>emptySet());
return locator;
}
@@ -155,7 +165,6 @@
return resolver;
}
-
private ContentResolver createStubResolverWithContentQueryResult(Cursor cursor) {
final MockContentResolver resolver = new MockContentResolver();
resolver.addProvider(ContactsContract.AUTHORITY, new FakeContactsProvider()
diff --git a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
index a69c877..e1c370a 100644
--- a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
@@ -22,16 +22,15 @@
import com.android.contacts.model.account.AccountTypeWithDataSet;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.model.account.BaseAccountType;
-
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import java.util.Arrays;
import java.util.List;
-import java.util.Map;
/**
* A mock {@link AccountTypeManager} class.
@@ -76,6 +75,17 @@
}
@Override
+ public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+ return Futures.immediateFuture(Arrays.asList(mAccounts));
+ }
+
+ @Override
+ public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+ Predicate<AccountType> type) {
+ return Futures.immediateFuture(Arrays.asList(mAccounts));
+ }
+
+ @Override
public List<AccountWithDataSet> getGroupWritableAccounts() {
return Arrays.asList(mAccounts);
}