Home | History | Annotate | Download | only in account
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.model.account;
     17 
     18 import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
     19 
     20 import android.accounts.AccountManager;
     21 import android.accounts.AuthenticatorDescription;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.SyncAdapterType;
     25 import android.provider.ContactsContract;
     26 import android.text.TextUtils;
     27 import android.util.Log;
     28 
     29 import com.android.contacts.util.DeviceLocalAccountTypeFactory;
     30 import com.android.contactsbind.ObjectFactory;
     31 import com.google.common.base.Objects;
     32 import com.google.common.collect.ImmutableList;
     33 import com.google.common.collect.ImmutableMap;
     34 
     35 import java.util.Collections;
     36 import java.util.HashSet;
     37 import java.util.List;
     38 import java.util.Map;
     39 import java.util.Set;
     40 import java.util.concurrent.ConcurrentHashMap;
     41 import java.util.concurrent.ConcurrentMap;
     42 
     43 /**
     44  * Provides access to {@link AccountType}s with contact data
     45  *
     46  * This class parses the contacts.xml for third-party accounts and caches the result.
     47  * This means that {@link AccountTypeProvider#getAccountTypes(String)}} should be called from a
     48  * background thread.
     49  */
     50 public class AccountTypeProvider {
     51     private static final String TAG = "AccountTypeProvider";
     52 
     53     private final Context mContext;
     54     private final DeviceLocalAccountTypeFactory mLocalAccountTypeFactory;
     55     private final ImmutableMap<String, AuthenticatorDescription> mAuthTypes;
     56 
     57     private final ConcurrentMap<String, List<AccountType>> mCache = new ConcurrentHashMap<>();
     58 
     59     public AccountTypeProvider(Context context) {
     60         this(context,
     61                 ObjectFactory.getDeviceLocalAccountTypeFactory(context),
     62                 ContentResolver.getSyncAdapterTypes(),
     63                 ((AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE))
     64                         .getAuthenticatorTypes());
     65     }
     66 
     67     public AccountTypeProvider(Context context, DeviceLocalAccountTypeFactory localTypeFactory,
     68             SyncAdapterType[] syncAdapterTypes,
     69             AuthenticatorDescription[] authenticatorDescriptions) {
     70         mContext = context;
     71         mLocalAccountTypeFactory = localTypeFactory;
     72 
     73         mAuthTypes = onlyContactSyncable(authenticatorDescriptions, syncAdapterTypes);
     74     }
     75 
     76     /**
     77      * Returns all account types associated with the provided type
     78      *
     79      * <p>There are many {@link AccountType}s for each accountType because {@AccountType} includes
     80      * a dataSet and accounts can declare extension packages in contacts.xml that provide additional
     81      * data sets for a particular type
     82      * </p>
     83      */
     84     public List<AccountType> getAccountTypes(String accountType) {
     85         // ConcurrentHashMap doesn't support null keys
     86         if (accountType == null) {
     87             AccountType type = mLocalAccountTypeFactory.getAccountType(accountType);
     88             // Just in case the DeviceLocalAccountTypeFactory doesn't handle the null type
     89             if (type == null) {
     90                 type = new FallbackAccountType(mContext);
     91             }
     92             return Collections.singletonList(type);
     93         }
     94 
     95         List<AccountType> types = mCache.get(accountType);
     96         if (types == null) {
     97             types = loadTypes(accountType);
     98             mCache.put(accountType, types);
     99         }
    100         return types;
    101     }
    102 
    103     public boolean hasTypeForAccount(AccountWithDataSet account) {
    104         return getTypeForAccount(account) != null;
    105     }
    106 
    107     public boolean hasTypeWithDataset(String type, String dataSet) {
    108         // getAccountTypes() never returns null
    109         final List<AccountType> accountTypes = getAccountTypes(type);
    110         for (AccountType accountType : accountTypes) {
    111             if (Objects.equal(accountType.dataSet, dataSet)) {
    112                 return true;
    113             }
    114         }
    115         return false;
    116     }
    117 
    118     /**
    119      * Returns the AccountType with the matching type and dataSet or null if no account with those
    120      * members exists
    121      */
    122     public AccountType getType(String type, String dataSet) {
    123         final List<AccountType> accountTypes = getAccountTypes(type);
    124         for (AccountType accountType : accountTypes) {
    125             if (Objects.equal(accountType.dataSet, dataSet)) {
    126                 return accountType;
    127             }
    128         }
    129         return null;
    130     }
    131 
    132     /**
    133      * Returns the AccountType for a particular account or null if no account type exists for the
    134      * account
    135      */
    136     public AccountType getTypeForAccount(AccountWithDataSet account) {
    137         return getType(account.type, account.dataSet);
    138     }
    139 
    140     public boolean shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
    141         Map<String, AuthenticatorDescription> contactsAuths = onlyContactSyncable(auths, syncTypes);
    142         if (!contactsAuths.keySet().equals(mAuthTypes.keySet())) {
    143             return true;
    144         }
    145         for (AuthenticatorDescription auth : contactsAuths.values()) {
    146             if (!deepEquals(mAuthTypes.get(auth.type), auth)) {
    147                 return true;
    148             }
    149         }
    150         return false;
    151     }
    152 
    153     public boolean supportsContactsSyncing(String accountType) {
    154         return mAuthTypes.containsKey(accountType);
    155     }
    156 
    157     private List<AccountType> loadTypes(String type) {
    158         final AuthenticatorDescription auth = mAuthTypes.get(type);
    159         if (auth == null) {
    160             if (Log.isLoggable(TAG, Log.DEBUG)) {
    161                 Log.d(TAG, "Null auth type for " + type);
    162             }
    163             return Collections.emptyList();
    164         }
    165 
    166         AccountType accountType;
    167         if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
    168             accountType = new GoogleAccountType(mContext, auth.packageName);
    169         } else if (ExchangeAccountType.isExchangeType(type)) {
    170             accountType = new ExchangeAccountType(mContext, auth.packageName, type);
    171         } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
    172                 auth.packageName)) {
    173             accountType = new SamsungAccountType(mContext, auth.packageName, type);
    174         } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
    175                 && isLocalAccountType(mLocalAccountTypeFactory, type)) {
    176             if (Log.isLoggable(TAG, Log.DEBUG)) {
    177                 Log.d(TAG, "Registering local account type=" + type
    178                         + ", packageName=" + auth.packageName);
    179             }
    180             accountType = mLocalAccountTypeFactory.getAccountType(type);
    181         } else {
    182             if (Log.isLoggable(TAG, Log.DEBUG)) {
    183                 Log.d(TAG, "Registering external account type=" + type
    184                         + ", packageName=" + auth.packageName);
    185             }
    186             accountType = new ExternalAccountType(mContext, auth.packageName, false);
    187         }
    188         if (!accountType.isInitialized()) {
    189             if (accountType.isEmbedded()) {
    190                 throw new IllegalStateException("Problem initializing embedded type "
    191                         + accountType.getClass().getCanonicalName());
    192             } else {
    193                 // Skip external account types that couldn't be initialized
    194                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    195                     Log.d(TAG, "Skipping external account type=" + type
    196                             + ", packageName=" + auth.packageName);
    197                 }
    198                 return Collections.emptyList();
    199             }
    200         }
    201 
    202         accountType.initializeFieldsFromAuthenticator(auth);
    203 
    204         final ImmutableList.Builder<AccountType> result = ImmutableList.builder();
    205         result.add(accountType);
    206 
    207         for (String extensionPackage : accountType.getExtensionPackageNames()) {
    208             final ExternalAccountType extensionType =
    209                     new ExternalAccountType(mContext, extensionPackage, true);
    210             if (!extensionType.isInitialized()) {
    211                 // Skip external account types that couldn't be initialized.
    212                 continue;
    213             }
    214             if (!extensionType.hasContactsMetadata()) {
    215                 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
    216                         + " it doesn't have the CONTACTS_STRUCTURE metadata");
    217                 continue;
    218             }
    219             if (TextUtils.isEmpty(extensionType.accountType)) {
    220                 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
    221                         + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
    222                         + " attribute");
    223                 continue;
    224             }
    225             if (!Objects.equal(extensionType.accountType, type)) {
    226                 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
    227                         + " the account type + " + extensionType.accountType +
    228                         " doesn't match expected type " + type);
    229                 continue;
    230             }
    231             if (Log.isLoggable(TAG, Log.DEBUG)) {
    232                 Log.d(TAG, "Registering extension package account type="
    233                         + accountType.accountType + ", dataSet=" + accountType.dataSet
    234                         + ", packageName=" + extensionPackage);
    235             }
    236 
    237             result.add(extensionType);
    238         }
    239         return result.build();
    240     }
    241 
    242     private static ImmutableMap<String, AuthenticatorDescription> onlyContactSyncable(
    243             AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
    244         final Set<String> mContactSyncableTypes = new HashSet<>();
    245         for (SyncAdapterType type : syncTypes) {
    246             if (type.authority.equals(ContactsContract.AUTHORITY)) {
    247                 mContactSyncableTypes.add(type.accountType);
    248             }
    249         }
    250 
    251         final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
    252                 ImmutableMap.builder();
    253         for (AuthenticatorDescription auth : auths) {
    254             if (mContactSyncableTypes.contains(auth.type)) {
    255                 builder.put(auth.type, auth);
    256             }
    257         }
    258         return builder.build();
    259     }
    260 
    261     /**
    262      * Compares all fields in auth1 and auth2
    263      *
    264      * <p>By default {@link AuthenticatorDescription#equals(Object)} only checks the type</p>
    265      */
    266     private boolean deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2) {
    267         return Objects.equal(auth1, auth2) &&
    268                 Objects.equal(auth1.packageName, auth2.packageName) &&
    269                 auth1.labelId == auth2.labelId &&
    270                 auth1.iconId == auth2.iconId &&
    271                 auth1.smallIconId == auth2.smallIconId &&
    272                 auth1.accountPreferencesId == auth2.accountPreferencesId &&
    273                 auth1.customTokens == auth2.customTokens;
    274     }
    275 
    276 }
    277