blob: 5e64a7d14f90d5ddc214f573e3b79f4be6aa0db3 [file] [log] [blame]
Marcus Hagerott04be88c2016-12-07 09:55:03 -08001/*
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 */
16package com.android.contacts.model.account;
17
18import android.accounts.AccountManager;
19import android.accounts.AuthenticatorDescription;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.SyncAdapterType;
23import android.provider.ContactsContract;
24import android.support.v4.util.ArraySet;
25import android.text.TextUtils;
26import android.util.Log;
27
28import com.android.contacts.util.DeviceLocalAccountTypeFactory;
29import com.android.contactsbind.ObjectFactory;
30import com.google.common.base.Objects;
31import com.google.common.collect.ImmutableList;
32import com.google.common.collect.ImmutableMap;
33
34import java.util.Collections;
35import java.util.List;
Marcus Hagerott2a4673a2016-12-15 11:49:47 -080036import java.util.Map;
Marcus Hagerott04be88c2016-12-07 09:55:03 -080037import java.util.Set;
38import java.util.concurrent.ConcurrentHashMap;
39import java.util.concurrent.ConcurrentMap;
40
41import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
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 */
50public 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
Marcus Hagerott2a4673a2016-12-15 11:49:47 -080073 mAuthTypes = onlyContactSyncable(authenticatorDescriptions, syncAdapterTypes);
Marcus Hagerott04be88c2016-12-07 09:55:03 -080074 }
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) {
Marcus Hagerottc2093f32016-12-12 10:18:12 -080085 // 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
Marcus Hagerott04be88c2016-12-07 09:55:03 -080095 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
Marcus Hagerott2a4673a2016-12-15 11:49:47 -0800140 public boolean shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
141 Map<String, AuthenticatorDescription> contactsAuths = onlyContactSyncable(auths, syncTypes);
142 if (!contactsAuths.keySet().equals(mAuthTypes.keySet())) {
143 return false;
144 }
145 for (AuthenticatorDescription auth : contactsAuths.values()) {
146 if (!deepEquals(mAuthTypes.get(auth.type), auth)) {
147 return false;
148 }
149 }
150 return true;
151 }
152
Marcus Hagerott04be88c2016-12-07 09:55:03 -0800153 private List<AccountType> loadTypes(String type) {
154 final AuthenticatorDescription auth = mAuthTypes.get(type);
155 if (auth == null) {
156 return Collections.emptyList();
157 }
158
159 AccountType accountType;
160 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
161 accountType = new GoogleAccountType(mContext, auth.packageName);
162 } else if (ExchangeAccountType.isExchangeType(type)) {
163 accountType = new ExchangeAccountType(mContext, auth.packageName, type);
164 } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
165 auth.packageName)) {
166 accountType = new SamsungAccountType(mContext, auth.packageName, type);
167 } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
168 && isLocalAccountType(mLocalAccountTypeFactory, type)) {
169 accountType = mLocalAccountTypeFactory.getAccountType(type);
170 } else {
171 Log.d(TAG, "Registering external account type=" + type
172 + ", packageName=" + auth.packageName);
173 accountType = new ExternalAccountType(mContext, auth.packageName, false);
174 }
175 if (!accountType.isInitialized()) {
176 if (accountType.isEmbedded()) {
177 throw new IllegalStateException("Problem initializing embedded type "
178 + accountType.getClass().getCanonicalName());
179 } else {
180 // Skip external account types that couldn't be initialized
181 return Collections.emptyList();
182 }
183 }
184
185 accountType.initializeFieldsFromAuthenticator(auth);
186
187 final ImmutableList.Builder<AccountType> result = ImmutableList.builder();
188 result.add(accountType);
189
190 for (String extensionPackage : accountType.getExtensionPackageNames()) {
191 final ExternalAccountType extensionType =
192 new ExternalAccountType(mContext, extensionPackage, true);
193 if (!extensionType.isInitialized()) {
194 // Skip external account types that couldn't be initialized.
195 continue;
196 }
197 if (!extensionType.hasContactsMetadata()) {
198 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
199 + " it doesn't have the CONTACTS_STRUCTURE metadata");
200 continue;
201 }
202 if (TextUtils.isEmpty(extensionType.accountType)) {
203 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
204 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
205 + " attribute");
206 continue;
207 }
208 if (Objects.equal(extensionType.accountType, type)) {
209 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
210 + " the account type + " + extensionType.accountType +
211 " doesn't match expected type " + type);
212 continue;
213 }
214 Log.d(TAG, "Registering extension package account type="
215 + accountType.accountType + ", dataSet=" + accountType.dataSet
216 + ", packageName=" + extensionPackage);
217
218 result.add(extensionType);
219 }
220 return result.build();
221 }
222
Marcus Hagerott2a4673a2016-12-15 11:49:47 -0800223 private static ImmutableMap<String, AuthenticatorDescription> onlyContactSyncable(
224 AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
225 final Set<String> mContactSyncableTypes = new ArraySet<>();
226 for (SyncAdapterType type : syncTypes) {
227 if (type.authority.equals(ContactsContract.AUTHORITY)) {
228 mContactSyncableTypes.add(type.accountType);
229 }
230 }
231
232 final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
233 ImmutableMap.builder();
234 for (AuthenticatorDescription auth : auths) {
235 if (mContactSyncableTypes.contains(auth.type)) {
236 builder.put(auth.type, auth);
237 }
238 }
239 return builder.build();
240 }
241
242 /**
243 * Compares all fields in auth1 and auth2
244 *
245 * <p>By default {@link AuthenticatorDescription#equals(Object)} only checks the type</p>
246 */
247 private boolean deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2) {
248 return Objects.equal(auth1, auth2) &&
249 Objects.equal(auth1.packageName, auth2.packageName) &&
250 auth1.labelId == auth2.labelId &&
251 auth1.iconId == auth2.iconId &&
252 auth1.smallIconId == auth2.smallIconId &&
253 auth1.accountPreferencesId == auth2.accountPreferencesId &&
254 auth1.customTokens == auth2.customTokens;
255 }
256
Marcus Hagerott04be88c2016-12-07 09:55:03 -0800257}