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