• 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 
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