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 AccountTypeProvider(Context context)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 AccountTypeProvider(Context context, DeviceLocalAccountTypeFactory localTypeFactory, SyncAdapterType[] syncAdapterTypes, AuthenticatorDescription[] authenticatorDescriptions)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 */ getAccountTypes(String accountType)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 hasTypeForAccount(AccountWithDataSet account)103 public boolean hasTypeForAccount(AccountWithDataSet account) { 104 return getTypeForAccount(account) != null; 105 } 106 hasTypeWithDataset(String type, String dataSet)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 */ getType(String type, String dataSet)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 */ getTypeForAccount(AccountWithDataSet account)136 public AccountType getTypeForAccount(AccountWithDataSet account) { 137 return getType(account.type, account.dataSet); 138 } 139 shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes)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 supportsContactsSyncing(String accountType)153 public boolean supportsContactsSyncing(String accountType) { 154 return mAuthTypes.containsKey(accountType); 155 } 156 loadTypes(String type)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 onlyContactSyncable( AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes)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 */ deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2)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