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 17 package com.android.bluetooth.pbapclient; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.accounts.Account; 22 import android.accounts.AccountManager; 23 import android.bluetooth.BluetoothDevice; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.os.UserManager; 33 import android.util.Log; 34 35 import com.android.bluetooth.R; 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.annotations.VisibleForTesting; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Set; 44 45 /** 46 * This class abstracts away interactions and management of the AccountManager Account objects that 47 * we need to store contacts and call logs on Android. This object provides functions to get/create 48 * an account, as well as remove or cleanup accounts. 49 * 50 * <p>Most AccountManager functions we want to use require the caller (us) to have a signature match 51 * with the authenticator that owns the specified account. AccountManager knowing this is contingent 52 * on our AuthenticationService being started (Our PbapClientAccountAuthenticatorService, which owns 53 * our PbapClientAccountAuthenticator) and AccountManagerService being notified of it so it can 54 * update its cache. This happens asynchronously and can sometimes take as long as 30 seconds after 55 * stack startup to be available. This object also abstracts this issue away, handling the timing 56 * and notifying clients when accounts are ready. 57 * 58 * <p>Once the account list has been initialized, clients can begin making calls to add, remove and 59 * list accounts. 60 */ 61 class PbapClientAccountManager { 62 private static final String TAG = PbapClientAccountManager.class.getSimpleName(); 63 64 private final Context mContext; 65 private final AccountManager mAccountManager; 66 private final UserManager mUserManager; 67 private final String mAccountType; 68 private final AccountManagerReceiver mAccountManagerReceiver = new AccountManagerReceiver(); 69 70 private HandlerThread mHandlerThread = null; 71 private AccountHandler mAccountHandler = null; 72 73 private final Object mAccountLock = new Object(); 74 75 @GuardedBy("mAccountLock") 76 private final Set<Account> mAccounts = new HashSet<Account>(); 77 78 private boolean mIsUserReady = false; 79 private volatile boolean mAccountsInitialized = false; 80 81 // For sending events back to the object owner 82 private final Callback mCallback; 83 84 /** A Callback interface so clients can receive structured events from this account manager */ 85 interface Callback { 86 /** 87 * Receive account visibility updates 88 * 89 * @param oldAccounts The list of previously available accounts, or null if this is the 90 * first account update after initialization 91 * @param newAccounts The list of newly available accounts 92 */ onAccountsChanged(List<Account> oldAccounts, List<Account> newAccounts)93 void onAccountsChanged(List<Account> oldAccounts, List<Account> newAccounts); 94 } 95 PbapClientAccountManager(Context context, Callback callback)96 PbapClientAccountManager(Context context, Callback callback) { 97 this(context, null, callback); 98 } 99 100 @VisibleForTesting PbapClientAccountManager(Context context, HandlerThread handlerThread, Callback callback)101 PbapClientAccountManager(Context context, HandlerThread handlerThread, Callback callback) { 102 mContext = requireNonNull(context); 103 mAccountManager = mContext.getSystemService(AccountManager.class); 104 mUserManager = mContext.getSystemService(UserManager.class); 105 mAccountType = mContext.getResources().getString(R.string.pbap_client_account_type); 106 mHandlerThread = handlerThread; 107 mCallback = callback; 108 } 109 start()110 public void start() { 111 Log.d(TAG, "start()"); 112 113 mAccountsInitialized = false; 114 synchronized (mAccountLock) { 115 mAccounts.clear(); 116 } 117 118 // Allow injecting a TestLooper 119 if (mHandlerThread == null) { 120 mHandlerThread = new HandlerThread(TAG); 121 } 122 123 mHandlerThread.start(); 124 mAccountHandler = new AccountHandler(mHandlerThread.getLooper()); 125 126 IntentFilter filter = new IntentFilter(); 127 filter.addAction(Intent.ACTION_USER_UNLOCKED); 128 filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 129 mContext.registerReceiver(mAccountManagerReceiver, filter); 130 131 if (isUserUnlocked()) { 132 mAccountHandler.obtainMessage(AccountHandler.MSG_USER_UNLOCKED).sendToTarget(); 133 } 134 } 135 stop()136 public void stop() { 137 Log.d(TAG, "stop()"); 138 139 mContext.unregisterReceiver(mAccountManagerReceiver); 140 if (mAccountHandler != null) { 141 mAccountHandler.removeCallbacksAndMessages(null); 142 mAccountHandler = null; 143 } 144 145 if (mHandlerThread != null) { 146 mHandlerThread.quit(); 147 mHandlerThread = null; 148 } 149 150 mAccountsInitialized = false; 151 } 152 153 /** 154 * Determine if this object has completed initialization of the accounts list. 155 * 156 * <p>Initialization happens once the user is unlock and our account type is recognized by the 157 * AccountManager framework. 158 * 159 * @return True if the accounts list has been initialized, false otherwise. 160 */ isAccountTypeInitialized()161 public boolean isAccountTypeInitialized() { 162 return mAccountsInitialized; 163 } 164 165 /** 166 * Get a well-formed Pbap Client based Account object to add for a given remote device. 167 * 168 * <p>This account should be used when making storage calls. Be sure the account is added and 169 * exists before using it for storage calls. 170 * 171 * @param device The remote device you would like a PBAP Client account for 172 * @return an Account object corresponding to the given remote device 173 */ getAccountForDevice(BluetoothDevice device)174 public Account getAccountForDevice(BluetoothDevice device) { 175 if (device == null) { 176 throw new IllegalArgumentException("Null device"); 177 } 178 return new Account(device.getAddress(), mAccountType); 179 } 180 181 /** 182 * Get the list of available PBAP Client accounts 183 * 184 * @return A list of all available PBAP Client based accounts on this device 185 */ getAccounts()186 public List<Account> getAccounts() { 187 if (!mAccountsInitialized) { 188 Log.w(TAG, "getAccounts(): Not initialized"); 189 return Collections.emptyList(); 190 } 191 synchronized (mAccountLock) { 192 return Collections.unmodifiableList(new ArrayList<>(mAccounts)); 193 } 194 } 195 196 /** 197 * Request for an account to be added 198 * 199 * <p>Storage must be initialized before calls to this function will be successful 200 * 201 * @param account The account to add 202 * @return True if the account is successfully added, False otherwise 203 */ addAccount(Account account)204 public boolean addAccount(Account account) { 205 if (!mAccountsInitialized) { 206 Log.w(TAG, "addAccount(account=" + account + "): Cannot add account, not initialized"); 207 return false; 208 } 209 synchronized (mAccountLock) { 210 List<Account> oldAccounts = new ArrayList<>(mAccounts); 211 if (addAccountInternal(account)) { 212 notifyAccountsChanged(oldAccounts, new ArrayList<>(mAccounts)); 213 return true; 214 } 215 return false; 216 } 217 } 218 219 /** 220 * Request for an account to be removed 221 * 222 * <p>Storage must be initialized before calls to this function will be successful 223 * 224 * @param account The account to remove 225 * @return True if the account is successfully removed, False otherwise 226 */ removeAccount(Account account)227 public boolean removeAccount(Account account) { 228 if (!mAccountsInitialized) { 229 Log.w( 230 TAG, 231 "removeAccount(account=" 232 + account 233 + "): Cannot remove account, not initialized"); 234 return false; 235 } 236 synchronized (mAccountLock) { 237 List<Account> oldAccounts = new ArrayList<>(mAccounts); 238 if (removeAccountInternal(account)) { 239 notifyAccountsChanged(oldAccounts, new ArrayList<>(mAccounts)); 240 return true; 241 } 242 return false; 243 } 244 } 245 246 /** Receive user lifecycle events and forward them to the handler for processing */ 247 private class AccountManagerReceiver extends BroadcastReceiver { 248 @Override onReceive(Context context, Intent intent)249 public void onReceive(Context context, Intent intent) { 250 String action = intent.getAction(); 251 Log.v(TAG, "onReceive action=" + action); 252 if (action.equals(Intent.ACTION_USER_UNLOCKED)) { 253 mAccountHandler.obtainMessage(AccountHandler.MSG_USER_UNLOCKED).sendToTarget(); 254 } 255 } 256 } 257 258 /** 259 * A handler to serialize account events. This allows us to wait for our authentication service 260 * to be available until we interact with accounts, and then safely create and remove accounts 261 * as needed. 262 */ 263 private class AccountHandler extends Handler { 264 // There's an ~1-2 second latency between when our Authentication service is set as 265 // available to the system and when the Authentication/Account framework code will recognize 266 // it and allow us to alter accounts. In lieu of the Accounts team dealing with this race 267 // condition, we're going to periodically poll over 3 seconds until our accounts are 268 // visible, remove old accounts, and then notify device state machines that they can create 269 // accounts and download contacts. 270 // 271 // TODO(233361365): Remove this pattern when the framework solves their race condition 272 private static final int ACCOUNT_ADD_RETRY_MS = 1000; 273 274 public static final int MSG_USER_UNLOCKED = 0; 275 public static final int MSG_ACCOUNT_CHECK = 1; 276 AccountHandler(Looper looper)277 AccountHandler(Looper looper) { 278 super(looper); 279 } 280 281 @Override handleMessage(Message msg)282 public void handleMessage(Message msg) { 283 Log.v(TAG, "Process message=" + messageToString(msg.what)); 284 switch (msg.what) { 285 case MSG_USER_UNLOCKED: 286 handleUserUnlocked(); 287 break; 288 case MSG_ACCOUNT_CHECK: 289 handleAccountCheck(); 290 break; 291 default: 292 Log.e(TAG, "received an unknown message : " + msg.what); 293 } 294 } 295 handleUserUnlocked()296 private void handleUserUnlocked() { 297 if (mIsUserReady) { 298 Log.i(TAG, "Notified user unlocked, but we've already processed this event. Skip"); 299 return; 300 } 301 302 Log.i(TAG, "User is unlocked. Begin account check process"); 303 mIsUserReady = true; 304 this.obtainMessage(MSG_ACCOUNT_CHECK).sendToTarget(); 305 } 306 handleAccountCheck()307 private void handleAccountCheck() { 308 if (mAccountsInitialized) { 309 Log.w(TAG, "Accounts already initialized. Skipping"); 310 return; 311 } 312 313 if (isAccountAuthenticationServiceReady()) { 314 Log.d(TAG, "Account type ready to be interacted with. Initialize account list"); 315 316 Account[] availableAccounts = mAccountManager.getAccountsByType(mAccountType); 317 synchronized (mAccountLock) { 318 for (Account account : availableAccounts) { 319 Log.i(TAG, "Loaded saved account, account=" + account); 320 mAccounts.add(account); 321 } 322 323 mAccountsInitialized = true; 324 325 Log.d(TAG, "Accounts list initialized"); 326 notifyAccountsChanged(null, new ArrayList<>(mAccounts)); 327 } 328 } else { 329 Log.d(TAG, "Accounts not ready. Check again in " + ACCOUNT_ADD_RETRY_MS + "ms"); 330 sendMessageDelayed(obtainMessage(MSG_ACCOUNT_CHECK), ACCOUNT_ADD_RETRY_MS); 331 } 332 } 333 messageToString(int msg)334 private static String messageToString(int msg) { 335 switch (msg) { 336 case MSG_USER_UNLOCKED: 337 return "MSG_USER_UNLOCKED"; 338 case MSG_ACCOUNT_CHECK: 339 return "MSG_ACCOUNT_CHECK"; 340 default: 341 return "MSG_RESERVED_" + msg; 342 } 343 } 344 } 345 346 /** 347 * Determine if the user is unlocked 348 * 349 * <p>AccountManager functionality doesn't work until the user is unlocked. We need to hold our 350 * calls until we know the user is unlocked. 351 * 352 * @return True if the use it unlocked, False otherwise 353 */ isUserUnlocked()354 private boolean isUserUnlocked() { 355 return mUserManager.isUserUnlocked(); 356 } 357 358 /** 359 * Determine if we're able to interact with our own account type 360 * 361 * <p>We're able to interact with our account when our account service is up and the 362 * AccountManagerService has finished updating itself such that it also knows our service is 363 * ready. The AccountManager framework doesn't have a good way for us to know _exactly_ when 364 * this is, so the best we can do is try to interact with our account type and see if it works. 365 * 366 * <p>We use a fake device address and our account type here to see if our account is visible 367 * yet. 368 * 369 * <p>This function is used in conjunction with the handler and a polling scheme to see 370 * determine when we're finally ready. 371 * 372 * <p>Note: that this function uses the same restrictions as the other add and remove functions, 373 * but is *also* available to all system apps instead of throwing a runtime SecurityException. 374 * AccountManagerService makes an !isSystemUid check before throwing. 375 * 376 * @return True if our PBAP Client Account type is ready to use, False otherwise. 377 */ isAccountAuthenticationServiceReady()378 private boolean isAccountAuthenticationServiceReady() { 379 Account account = new Account("00:00:00:00:00:00", mAccountType); 380 int visibility = mAccountManager.getAccountVisibility(account, mContext.getPackageName()); 381 Log.d(TAG, "Checking visibility, visibility=" + visibility); 382 return visibility == AccountManager.VISIBILITY_VISIBLE 383 || visibility == AccountManager.VISIBILITY_USER_MANAGED_VISIBLE; 384 } 385 386 /** 387 * Explicitly add an account. Returns true is successful, false otherwise. 388 * 389 * <p>Any exceptions generated cause this function to fail silently. In particular, 390 * SecurityExceptions due to the fact that our authentication service isn't recognized by the 391 * AccountManager framework yet are dropped. Our handler is setup to make it so we shouldn't 392 * make these calls unless we know AccountManager knows of us though. 393 * 394 * @param account The account to add 395 * @return True on success, false otherwise 396 */ addAccountInternal(Account account)397 private boolean addAccountInternal(Account account) { 398 try { 399 synchronized (mAccountLock) { 400 if (mAccountManager.addAccountExplicitly(account, null, null)) { 401 mAccounts.add(account); 402 Log.i(TAG, "Added account=" + account); 403 return true; 404 } 405 Log.w(TAG, "Failed to add account=" + account); 406 return false; 407 } 408 } catch (Exception e) { 409 Log.w(TAG, "Exception while trying to add account=" + account, e); 410 return false; 411 } 412 } 413 414 /** 415 * Explicitly remove an account. Returns true is successful, false otherwise. 416 * 417 * <p>Any exceptions generated cause this function to fail silently. In particular, 418 * SecurityExceptions due to the fact that our authentication service isn't recognized by the 419 * AccountManager framework yet are dropped. Our handler is setup to make it so we shouldn't 420 * make these calls unless we know AccountManager knows of us though. 421 * 422 * @param account the account to explicitly remove 423 * @return True on success, false otherwise 424 */ removeAccountInternal(Account account)425 private boolean removeAccountInternal(Account account) { 426 try { 427 synchronized (mAccountLock) { 428 if (mAccountManager.removeAccountExplicitly(account)) { 429 mAccounts.remove(account); 430 Log.i(TAG, "Removed account=" + account); 431 return true; 432 } 433 Log.w(TAG, "Failed to remove account=" + account); 434 return false; 435 } 436 } catch (Exception e) { 437 Log.w(TAG, "Exception while trying to remove account=" + account, e); 438 return false; 439 } 440 } 441 442 /** 443 * Notify all client callbacks that the set of accounts has changed 444 * 445 * @param oldAccounts The previous list of accounts available, or null if this is the first 446 * update 447 * @param newAccounts The new list of accounts available 448 */ notifyAccountsChanged(List<Account> oldAccounts, List<Account> newAccounts)449 private void notifyAccountsChanged(List<Account> oldAccounts, List<Account> newAccounts) { 450 Log.v(TAG, "notifyAccountsChanged, old=" + oldAccounts + ", new=" + newAccounts); 451 if (mCallback != null) { 452 mCallback.onAccountsChanged(oldAccounts, newAccounts); 453 } 454 } 455 456 /** Get a debug dump of this class, containing the accounts on the device */ dump()457 public String dump() { 458 StringBuilder sb = new StringBuilder(); 459 sb.append(TAG).append(":\n"); 460 sb.append(" Account Type: ").append(mAccountType).append("\n"); 461 sb.append(" User Unlocked: ").append(isUserUnlocked()).append("\n"); 462 sb.append(" Account Type Ready: ") 463 .append(isAccountAuthenticationServiceReady()) 464 .append("\n"); 465 sb.append(" Accounts Initialized: ").append(mAccountsInitialized).append("\n"); 466 sb.append(" Accounts:\n"); 467 for (Account account : getAccounts()) { 468 sb.append(" ").append(account).append("\n"); 469 } 470 return sb.toString(); 471 } 472 } 473