• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.providers.contacts;
17 
18 import android.accounts.Account;
19 import android.content.ContentValues;
20 import android.net.Uri;
21 import android.provider.ContactsContract.RawContacts;
22 import android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState;
23 import android.provider.ContactsContract.SimAccount;
24 import android.text.TextUtils;
25 import android.util.Log;
26 
27 import java.util.List;
28 
29 public class AccountResolver {
30     public static final String UNABLE_TO_WRITE_TO_LOCAL_OR_SIM_EXCEPTION_MESSAGE =
31             "Cannot add contacts to local or SIM accounts when default account is set to cloud";
32     private static final String TAG = "AccountResolver";
33 
34     private final ContactsDatabaseHelper mDbHelper;
35     private final DefaultAccountManager mDefaultAccountManager;
36 
AccountResolver(ContactsDatabaseHelper dbHelper, DefaultAccountManager defaultAccountManager)37     public AccountResolver(ContactsDatabaseHelper dbHelper,
38             DefaultAccountManager defaultAccountManager) {
39         mDbHelper = dbHelper;
40         mDefaultAccountManager = defaultAccountManager;
41     }
42 
getLocalAccount()43     private static Account getLocalAccount() {
44         if (TextUtils.isEmpty(AccountWithDataSet.LOCAL.getAccountName())) {
45             // AccountWithDataSet.LOCAL's getAccountType() must be null as well, thus we return
46             // the NULL account.
47             return null;
48         } else {
49             // AccountWithDataSet.LOCAL's getAccountType() must not be null as well, thus we return
50             // the customized local account.
51             return new Account(AccountWithDataSet.LOCAL.getAccountName(),
52                     AccountWithDataSet.LOCAL.getAccountType());
53         }
54     }
55 
56     /**
57      * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
58      * in the URI or values (if any).
59      *
60      * @param uri                                     Current {@link Uri} being operated on.
61      * @param values                                  {@link ContentValues} to read and possibly
62      *                                                update.
63      * @param applyDefaultAccount                     Whether to look up default account during
64      *                                                account resolution.
65      * @param shouldValidateAccountForContactAddition Whether to validate the account accepts new
66      *                                                contacts.
67      */
resolveAccountWithDataSet(Uri uri, ContentValues values, boolean applyDefaultAccount, boolean shouldValidateAccountForContactAddition, boolean allowSimWriteOnCloudDcaBypassEnabled)68     public AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values,
69             boolean applyDefaultAccount, boolean shouldValidateAccountForContactAddition,
70             boolean allowSimWriteOnCloudDcaBypassEnabled) {
71         final Account[] accounts = resolveAccount(uri, values);
72         final Account account = applyDefaultAccount
73                 ? getAccountWithDefaultAccountApplied(accounts,
74                 shouldValidateAccountForContactAddition, allowSimWriteOnCloudDcaBypassEnabled)
75                 : getFirstAccountOrNull(accounts);
76 
77         AccountWithDataSet accountWithDataSet = null;
78         if (account != null) {
79             String dataSet = ContactsProvider2.getQueryParameter(uri, RawContacts.DATA_SET);
80             if (dataSet == null) {
81                 dataSet = values.getAsString(RawContacts.DATA_SET);
82             } else {
83                 values.put(RawContacts.DATA_SET, dataSet);
84             }
85             accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
86         }
87 
88         return accountWithDataSet;
89     }
90 
91     /**
92      * Resolves the account to be used, taking into consideration the default account settings.
93      *
94      * @param accounts 1-size array which contains specified account, or empty array if account is
95      *                 not specified.
96      * @return The resolved account, or null if it's the default device (aka "NULL") account.
97      * @throws IllegalArgumentException If there's an issue with the account resolution due to
98      *                                  default account incompatible account types.
99      */
getAccountWithDefaultAccountApplied(Account[] accounts, boolean shouldValidateAccountForContactAddition, boolean allowSimWriteOnCloudDcaBypassEnabled)100     private Account getAccountWithDefaultAccountApplied(Account[] accounts,
101             boolean shouldValidateAccountForContactAddition,
102             boolean allowSimWriteOnCloudDcaBypassEnabled)
103             throws IllegalArgumentException {
104         if (accounts.length == 0) {
105             DefaultAccountAndState defaultAccountAndState =
106                     mDefaultAccountManager.pullDefaultAccount();
107             if (defaultAccountAndState.getState()
108                     == DefaultAccountAndState.DEFAULT_ACCOUNT_STATE_NOT_SET
109                     || defaultAccountAndState.getState()
110                     == DefaultAccountAndState.DEFAULT_ACCOUNT_STATE_LOCAL) {
111                 return getLocalAccount();
112             } else {
113                 return defaultAccountAndState.getAccount();
114             }
115         } else {
116             validateAccountForContactAdditionInternal(accounts[0],
117                         shouldValidateAccountForContactAddition,
118                         allowSimWriteOnCloudDcaBypassEnabled);
119             return accounts[0];
120         }
121     }
122 
123     /**
124      * Checks if new contacts in specified account is accepted.
125      *
126      * <p>This method checks if contacts can be written to the given account based on the
127      * current default account settings. It throws an {@link IllegalArgumentException} if
128      * the contacts cannot be created in the given account .</p>
129      *
130      * @param accountName The name of the account to check.
131      * @param accountType The type of the account to check.
132      * @throws IllegalArgumentException if either of the following conditions are met:
133      *                                  <ul>
134      *                                      <li>Only one of <code>accountName</code> or
135      *                                      <code>accountType</code> is
136      *                                          specified.</li>
137      *                                      <li>The default account is set to cloud and the
138      *                                      specified account is a local
139      *                                          (device or SIM) account.</li>
140      *                                  </ul>
141      */
validateAccountForContactAddition(String accountName, String accountType, boolean shouldValidateAccountForContactAddition, boolean allowSimWriteOnCloudDcaBypassEnabled)142     public void validateAccountForContactAddition(String accountName, String accountType,
143             boolean shouldValidateAccountForContactAddition,
144             boolean allowSimWriteOnCloudDcaBypassEnabled) {
145         if (shouldValidateAccountForContactAddition) {
146             if (TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType)) {
147                 throw new IllegalArgumentException(
148                         "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE");
149             }
150         }
151 
152         if (TextUtils.isEmpty(accountName)) {
153             validateAccountForContactAdditionInternal(/*account=*/null,
154                     shouldValidateAccountForContactAddition,
155                     allowSimWriteOnCloudDcaBypassEnabled);
156         } else {
157             validateAccountForContactAdditionInternal(new Account(accountName, accountType),
158                     shouldValidateAccountForContactAddition,
159                     allowSimWriteOnCloudDcaBypassEnabled);
160         }
161     }
162 
validateAccountForContactAdditionInternal(Account account, boolean enforceCloudDefaultAccountRestriction, boolean allowSimWriteOnCloudDcaBypassEnabled)163     private void validateAccountForContactAdditionInternal(Account account,
164             boolean enforceCloudDefaultAccountRestriction,
165             boolean allowSimWriteOnCloudDcaBypassEnabled)
166             throws IllegalArgumentException {
167         DefaultAccountAndState defaultAccount = mDefaultAccountManager.pullDefaultAccount();
168 
169         if (defaultAccount.getState() == DefaultAccountAndState.DEFAULT_ACCOUNT_STATE_CLOUD) {
170             if (allowSimWriteOnCloudDcaBypassEnabled
171                     ? isDeviceAccount(account)
172                     : isDeviceOrSimAccount(account)) {
173                 if (enforceCloudDefaultAccountRestriction) {
174                     throw new IllegalArgumentException(
175                             UNABLE_TO_WRITE_TO_LOCAL_OR_SIM_EXCEPTION_MESSAGE);
176                 } else {
177                     Log.w(TAG,
178                             "Cloud default account: Local/SIM contact creation allowed (target "
179                                     + "SDK <36), but restricted in target SDK 36+. Avoid "
180                                     + "local/SIM writes in target SDK 36+.");
181                 }
182             }
183         }
184     }
185 
186     /**
187      * Gets the first account from the array, or null if the array is empty.
188      *
189      * @param accounts The array of accounts.
190      * @return The first account, or null if the array is empty.
191      */
getFirstAccountOrNull(Account[] accounts)192     private Account getFirstAccountOrNull(Account[] accounts) {
193         return accounts.length > 0 ? accounts[0] : null;
194     }
195 
196 
isDeviceOrSimAccount(Account account)197     private boolean isDeviceOrSimAccount(Account account) {
198         AccountWithDataSet accountWithDataSet = account == null
199                 ? new AccountWithDataSet(null, null, null)
200                 : new AccountWithDataSet(account.name, account.type, null);
201 
202         List<SimAccount> simAccounts = mDbHelper.getAllSimAccounts();
203         return accountWithDataSet.isLocalAccount() || accountWithDataSet.inSimAccounts(simAccounts);
204     }
205 
isDeviceAccount(Account account)206     private boolean isDeviceAccount(Account account) {
207         AccountWithDataSet accountWithDataSet = account == null
208                 ? new AccountWithDataSet(null, null, null)
209                 : new AccountWithDataSet(account.name, account.type, null);
210 
211         return accountWithDataSet.isLocalAccount();
212     }
213 
214     /**
215      * If account is non-null then store it in the values. If the account is
216      * already specified in the values then it must be consistent with the
217      * account, if it is non-null.
218      *
219      * @param uri    Current {@link Uri} being operated on.
220      * @param values {@link ContentValues} to read and possibly update.
221      * @return 1-size array which contains account specified by {@link Uri} and
222      * {@link ContentValues}, or empty array if account is not specified.
223      * @throws IllegalArgumentException when only one of
224      *                                  {@link RawContacts#ACCOUNT_NAME} or
225      *                                  {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
226      *                                  other undefined.
227      * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
228      *                                  and {@link RawContacts#ACCOUNT_TYPE} are inconsistent
229      *                                  between
230      *                                  the given {@link Uri} and {@link ContentValues}.
231      */
resolveAccount(Uri uri, ContentValues values)232     private Account[] resolveAccount(Uri uri, ContentValues values)
233             throws IllegalArgumentException {
234         String accountName = ContactsProvider2.getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
235         String accountType = ContactsProvider2.getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
236         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
237 
238         if (accountName == null && accountType == null
239                 && !values.containsKey(RawContacts.ACCOUNT_NAME)
240                 && !values.containsKey(RawContacts.ACCOUNT_TYPE)) {
241             // Account is not specified.
242             return new Account[0];
243         }
244 
245         String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
246         String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
247 
248         final boolean partialValues = TextUtils.isEmpty(valueAccountName)
249                 ^ TextUtils.isEmpty(valueAccountType);
250 
251         if (partialUri || partialValues) {
252             // Throw when either account is incomplete.
253             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
254                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
255         }
256 
257         // Accounts are valid by only checking one parameter, since we've
258         // already ruled out partial accounts.
259         final boolean validUri = !TextUtils.isEmpty(accountName);
260         final boolean validValues = !TextUtils.isEmpty(valueAccountName);
261 
262         if (validValues && validUri) {
263             // Check that accounts match when both present
264             final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
265                     && TextUtils.equals(accountType, valueAccountType);
266             if (!accountMatch) {
267                 throw new IllegalArgumentException(mDbHelper.exceptionMessage(
268                         "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
269             }
270         } else if (validUri) {
271             // Fill values from the URI when not present.
272             values.put(RawContacts.ACCOUNT_NAME, accountName);
273             values.put(RawContacts.ACCOUNT_TYPE, accountType);
274         } else if (validValues) {
275             accountName = valueAccountName;
276             accountType = valueAccountType;
277         } else {
278             return new Account[]{null};
279         }
280 
281         return new Account[]{new Account(accountName, accountType)};
282     }
283 }
284