blob: 5e64a7d14f90d5ddc214f573e3b79f4be6aa0db3 [file] [log] [blame]
/*
* 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.Map;
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;
mAuthTypes = onlyContactSyncable(authenticatorDescriptions, syncAdapterTypes);
}
/**
* 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);
}
public boolean shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
Map<String, AuthenticatorDescription> contactsAuths = onlyContactSyncable(auths, syncTypes);
if (!contactsAuths.keySet().equals(mAuthTypes.keySet())) {
return false;
}
for (AuthenticatorDescription auth : contactsAuths.values()) {
if (!deepEquals(mAuthTypes.get(auth.type), auth)) {
return false;
}
}
return true;
}
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();
}
private static ImmutableMap<String, AuthenticatorDescription> onlyContactSyncable(
AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
final Set<String> mContactSyncableTypes = new ArraySet<>();
for (SyncAdapterType type : syncTypes) {
if (type.authority.equals(ContactsContract.AUTHORITY)) {
mContactSyncableTypes.add(type.accountType);
}
}
final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
ImmutableMap.builder();
for (AuthenticatorDescription auth : auths) {
if (mContactSyncableTypes.contains(auth.type)) {
builder.put(auth.type, auth);
}
}
return builder.build();
}
/**
* Compares all fields in auth1 and auth2
*
* <p>By default {@link AuthenticatorDescription#equals(Object)} only checks the type</p>
*/
private boolean deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2) {
return Objects.equal(auth1, auth2) &&
Objects.equal(auth1.packageName, auth2.packageName) &&
auth1.labelId == auth2.labelId &&
auth1.iconId == auth2.iconId &&
auth1.smallIconId == auth2.smallIconId &&
auth1.accountPreferencesId == auth2.accountPreferencesId &&
auth1.customTokens == auth2.customTokens;
}
}