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 || mLocalAccountTypeFactory.classifyAccount(accountType) 87 == DeviceLocalAccountTypeFactory.TYPE_SIM) { 88 AccountType type = mLocalAccountTypeFactory.getAccountType(accountType); 89 // Just in case the DeviceLocalAccountTypeFactory doesn't handle the null type 90 if (type == null) { 91 type = new FallbackAccountType(mContext); 92 } 93 return Collections.singletonList(type); 94 } 95 96 List<AccountType> types = mCache.get(accountType); 97 if (types == null) { 98 types = loadTypes(accountType); 99 mCache.put(accountType, types); 100 } 101 return types; 102 } 103 hasTypeForAccount(AccountWithDataSet account)104 public boolean hasTypeForAccount(AccountWithDataSet account) { 105 return getTypeForAccount(account) != null; 106 } 107 hasTypeWithDataset(String type, String dataSet)108 public boolean hasTypeWithDataset(String type, String dataSet) { 109 // getAccountTypes() never returns null 110 final List<AccountType> accountTypes = getAccountTypes(type); 111 for (AccountType accountType : accountTypes) { 112 if (Objects.equal(accountType.dataSet, dataSet)) { 113 return true; 114 } 115 } 116 return false; 117 } 118 119 /** 120 * Returns the AccountType with the matching type and dataSet or null if no account with those 121 * members exists 122 */ getType(String type, String dataSet)123 public AccountType getType(String type, String dataSet) { 124 final List<AccountType> accountTypes = getAccountTypes(type); 125 for (AccountType accountType : accountTypes) { 126 if (Objects.equal(accountType.dataSet, dataSet)) { 127 return accountType; 128 } 129 } 130 return null; 131 } 132 133 /** 134 * Returns the AccountType for a particular account or null if no account type exists for the 135 * account 136 */ getTypeForAccount(AccountWithDataSet account)137 public AccountType getTypeForAccount(AccountWithDataSet account) { 138 return getType(account.type, account.dataSet); 139 } 140 shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes)141 public boolean shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) { 142 Map<String, AuthenticatorDescription> contactsAuths = onlyContactSyncable(auths, syncTypes); 143 if (!contactsAuths.keySet().equals(mAuthTypes.keySet())) { 144 return true; 145 } 146 for (AuthenticatorDescription auth : contactsAuths.values()) { 147 if (!deepEquals(mAuthTypes.get(auth.type), auth)) { 148 return true; 149 } 150 } 151 return false; 152 } 153 supportsContactsSyncing(String accountType)154 public boolean supportsContactsSyncing(String accountType) { 155 return mAuthTypes.containsKey(accountType); 156 } 157 loadTypes(String type)158 private List<AccountType> loadTypes(String type) { 159 final AuthenticatorDescription auth = mAuthTypes.get(type); 160 if (auth == null) { 161 if (Log.isLoggable(TAG, Log.DEBUG)) { 162 Log.d(TAG, "Null auth type for " + type); 163 } 164 return Collections.emptyList(); 165 } 166 167 AccountType accountType; 168 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { 169 accountType = new GoogleAccountType(mContext, auth.packageName); 170 } else if (ExchangeAccountType.isExchangeType(type)) { 171 accountType = new ExchangeAccountType(mContext, auth.packageName, type); 172 } else if (SamsungAccountType.isSamsungAccountType(mContext, type, 173 auth.packageName)) { 174 accountType = new SamsungAccountType(mContext, auth.packageName, type); 175 } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName) 176 && isLocalAccountType(mLocalAccountTypeFactory, type)) { 177 if (Log.isLoggable(TAG, Log.DEBUG)) { 178 Log.d(TAG, "Registering local account type=" + type 179 + ", packageName=" + auth.packageName); 180 } 181 accountType = mLocalAccountTypeFactory.getAccountType(type); 182 } else { 183 if (Log.isLoggable(TAG, Log.DEBUG)) { 184 Log.d(TAG, "Registering external account type=" + type 185 + ", packageName=" + auth.packageName); 186 } 187 accountType = new ExternalAccountType(mContext, auth.packageName, false); 188 } 189 if (!accountType.isInitialized()) { 190 if (accountType.isEmbedded()) { 191 throw new IllegalStateException("Problem initializing embedded type " 192 + accountType.getClass().getCanonicalName()); 193 } else { 194 // Skip external account types that couldn't be initialized 195 if (Log.isLoggable(TAG, Log.DEBUG)) { 196 Log.d(TAG, "Skipping external account type=" + type 197 + ", packageName=" + auth.packageName); 198 } 199 return Collections.emptyList(); 200 } 201 } 202 203 accountType.initializeFieldsFromAuthenticator(auth); 204 205 final ImmutableList.Builder<AccountType> result = ImmutableList.builder(); 206 result.add(accountType); 207 208 for (String extensionPackage : accountType.getExtensionPackageNames()) { 209 final ExternalAccountType extensionType = 210 new ExternalAccountType(mContext, extensionPackage, true); 211 if (!extensionType.isInitialized()) { 212 // Skip external account types that couldn't be initialized. 213 continue; 214 } 215 if (!extensionType.hasContactsMetadata()) { 216 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 217 + " it doesn't have the CONTACTS_STRUCTURE metadata"); 218 continue; 219 } 220 if (TextUtils.isEmpty(extensionType.accountType)) { 221 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 222 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" 223 + " attribute"); 224 continue; 225 } 226 if (!Objects.equal(extensionType.accountType, type)) { 227 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 228 + " the account type + " + extensionType.accountType + 229 " doesn't match expected type " + type); 230 continue; 231 } 232 if (Log.isLoggable(TAG, Log.DEBUG)) { 233 Log.d(TAG, "Registering extension package account type=" 234 + accountType.accountType + ", dataSet=" + accountType.dataSet 235 + ", packageName=" + extensionPackage); 236 } 237 238 result.add(extensionType); 239 } 240 return result.build(); 241 } 242 onlyContactSyncable( AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes)243 private static ImmutableMap<String, AuthenticatorDescription> onlyContactSyncable( 244 AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) { 245 final Set<String> mContactSyncableTypes = new HashSet<>(); 246 for (SyncAdapterType type : syncTypes) { 247 if (type.authority.equals(ContactsContract.AUTHORITY)) { 248 mContactSyncableTypes.add(type.accountType); 249 } 250 } 251 252 final ImmutableMap.Builder<String, AuthenticatorDescription> builder = 253 ImmutableMap.builder(); 254 for (AuthenticatorDescription auth : auths) { 255 if (mContactSyncableTypes.contains(auth.type)) { 256 builder.put(auth.type, auth); 257 } 258 } 259 return builder.build(); 260 } 261 262 /** 263 * Compares all fields in auth1 and auth2 264 * 265 * <p>By default {@link AuthenticatorDescription#equals(Object)} only checks the type</p> 266 */ deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2)267 private boolean deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2) { 268 return Objects.equal(auth1, auth2) && 269 Objects.equal(auth1.packageName, auth2.packageName) && 270 auth1.labelId == auth2.labelId && 271 auth1.iconId == auth2.iconId && 272 auth1.smallIconId == auth2.smallIconId && 273 auth1.accountPreferencesId == auth2.accountPreferencesId && 274 auth1.customTokens == auth2.customTokens; 275 } 276 277 } 278