/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.bluetooth; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.app.ActivityManager; import android.bluetooth.BluetoothDevice; import android.car.ICarBluetoothUserService; import android.car.ICarPerUserService; import android.car.builtin.os.UserManagerHelper; import android.car.builtin.util.Slogf; import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.proto.ProtoOutputStream; import com.android.car.CarLog; import com.android.car.CarPerUserServiceHelper; import com.android.car.CarServiceBase; import com.android.car.R; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; import java.util.Objects; /** * CarBluetoothService - Maintains the current user's Bluetooth devices and profile connections. * * For each user, creates: * 1) A {@link BluetoothDeviceManager} object, responsible for maintaining a list of * the user's known devices. * 2) A {@link BluetoothProfileInhibitManager} object that will maintain a set of inhibited * profiles for each device, keeping a device from connecting on those profiles. This provides * an interface to request and release inhibits. * 3) A {@link BluetoothDeviceConnectionPolicy} object, representing a default implementation of * a policy based method of determining and requesting times to auto-connect devices. This * default is controllable through a resource overlay if one chooses to implement their own. * * Provides an interface for other programs to request auto connections. */ public class CarBluetoothService implements CarServiceBase { private static final String TAG = CarLog.tagFor(CarBluetoothService.class); private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); static final String THREAD_NAME = "CarBluetoothService"; private final Context mContext; // Each time CarPerUserService connects we need to get new Bluetooth profile proxies and refresh // all our internal objects to use them. When it disconnects we're to assume our proxies are // invalid. This lock protects all our internal objects. private final Object mPerUserLock = new Object(); // Default Bluetooth power policy, per user, enabled with an overlay private final boolean mUseDefaultPowerPolicy; @GuardedBy("mPerUserLock") private BluetoothPowerPolicy mBluetoothPowerPolicy; // The Bluetooth Device Manager, owns the priority connection list, updated on user // switch private BluetoothDeviceManager mDeviceManager; // Profile-Inhibit Manager that will temporarily inhibit connections on profiles, per user @GuardedBy("mPerUserLock") private BluetoothProfileInhibitManager mInhibitManager; // Default Bluetooth device connection policy, per user, enabled with an overlay private final boolean mUseDefaultConnectionPolicy; @GuardedBy("mPerUserLock") private BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicy; // Bluetooth Connection Retry Manager, updated on user switch @GuardedBy("mPerUserLock") private BluetoothConnectionRetryManager mConnectionRetryManager; // Listen for user switch events from the CarPerUserService @GuardedBy("mPerUserLock") private int mUserId; @GuardedBy("mPerUserLock") private ICarPerUserService mCarPerUserService; @GuardedBy("mPerUserLock") private ICarBluetoothUserService mCarBluetoothUserService; // Whether this service is already released. We should not create new handler thread if service // is already released. @GuardedBy("mPerUserLock") private boolean mReleased = false; private final CarPerUserServiceHelper mUserServiceHelper; private final CarPerUserServiceHelper.ServiceCallback mUserServiceCallback = new CarPerUserServiceHelper.ServiceCallback() { @Override public void onServiceConnected(ICarPerUserService carPerUserService) { if (DBG) { Slogf.d(TAG, "Connected to CarPerUserService"); } synchronized (mPerUserLock) { if (mReleased) { // We create handlerThread in initializeUserLocked. We must make sure we do not // create handler thread after release otherwise the newly created thread might // not be cleaned up properly. return; } // Explicitly clear out existing per-user objects since we can't rely on the // onServiceDisconnected and onPreUnbind calls to always be called before this destroyUserLocked(); mCarPerUserService = carPerUserService; // Create new objects with our new set of profile proxies initializeUserLocked(); } } @Override public void onPreUnbind() { if (DBG) { Slogf.d(TAG, "Before Unbinding from CarPerUserService"); } synchronized (mPerUserLock) { destroyUserLocked(); } } @Override public void onServiceDisconnected() { if (DBG) { Slogf.d(TAG, "Disconnected from CarPerUserService"); } synchronized (mPerUserLock) { destroyUserLocked(); } } }; /** * Create an instance of CarBluetoothService * * @param context - A Context object representing the context you want this service to run * @param userSwitchService - An instance of CarPerUserServiceHelper that we can bind a listener * to in order to receive user switch events */ public CarBluetoothService(Context context, CarPerUserServiceHelper userSwitchService) { mUserId = UserManagerHelper.USER_NULL; mContext = Objects.requireNonNull(context); mUserServiceHelper = userSwitchService; mUseDefaultConnectionPolicy = mContext.getResources().getBoolean( R.bool.useDefaultBluetoothConnectionPolicy); mUseDefaultPowerPolicy = mContext.getResources().getBoolean( R.bool.useDefaultBluetoothPowerPolicy); } /** * Complete all necessary initialization keeping this service from being running. * * Wait for the user service helper to report a user before initializing a user. */ @Override public void init() { if (DBG) { Slogf.d(TAG, "init()"); } synchronized (mPerUserLock) { mReleased = false; } mUserServiceHelper.registerServiceCallback(mUserServiceCallback); } /** * Release all resources required to run this service and stop running. * * Clean up the user context once we've detached from the user service helper, if any. */ @Override public void release() { if (DBG) { Slogf.d(TAG, "release()"); } mUserServiceHelper.unregisterServiceCallback(mUserServiceCallback); synchronized (mPerUserLock) { destroyUserLocked(); mReleased = true; } } /** * Initialize the user context using the current active user. * * Only call this following a known user switch once we've connected to the user service helper. */ @GuardedBy("mPerUserLock") private void initializeUserLocked() { if (DBG) { Slogf.d(TAG, "Initializing new user"); } mUserId = ActivityManager.getCurrentUser(); createBluetoothUserServiceLocked(); createBluetoothProfileInhibitManagerLocked(); // Determine if we need to begin the default power policy mBluetoothPowerPolicy = null; if (mUseDefaultPowerPolicy) { createBluetoothPowerPolicyLocked(); } createBluetoothConnectionRetryManagerLocked(); // Determine if we need to begin the default device connection policy and device manager mDeviceManager = null; mBluetoothDeviceConnectionPolicy = null; if (mUseDefaultConnectionPolicy) { createBluetoothDeviceManagerLocked(); createBluetoothDeviceConnectionPolicyLocked(); } if (DBG) { Slogf.d(TAG, "Switched to user %d", mUserId); } } /** * Destroy the current user context, defined by the set of profile proxies, profile device * managers, inhibit manager and the policy. */ @GuardedBy("mPerUserLock") private void destroyUserLocked() { if (DBG) { Slogf.d(TAG, "Destroying user %d", mUserId); } destroyBluetoothDeviceConnectionPolicyLocked(); destroyBluetoothConnectionRetryManagerLocked(); destroyBluetoothPowerPolicyLocked(); destroyBluetoothProfileInhibitManagerLocked(); destroyBluetoothDeviceManagerLocked(); destroyBluetoothUserServiceLocked(); mCarPerUserService = null; mUserId = UserManagerHelper.USER_NULL; } /** * Sets the Per User Car Bluetooth Service (ICarBluetoothService) from the CarPerUserService * which acts as a top level Service running in the current user context. * Also sets up the connection proxy objects required to communicate with the Bluetooth * Profile Services. */ @GuardedBy("mPerUserLock") private void createBluetoothUserServiceLocked() { if (mCarPerUserService != null) { try { mCarBluetoothUserService = mCarPerUserService.getBluetoothUserService(); mCarBluetoothUserService.setupBluetoothConnectionProxies(); } catch (RemoteException e) { Slogf.e(TAG, "Remote Service Exception on ServiceConnection Callback: %s", e); } catch (java.lang.NullPointerException e) { Slogf.e(TAG, "Initialization Failed: %s", e); } } else { if (DBG) { Slogf.d(TAG, "CarPerUserService not connected. Cannot get bluetooth user proxy objects"); } } } /** * Close out the Per User Car Bluetooth profile proxy connections and destroys the Car Bluetooth * User Service object. */ @GuardedBy("mPerUserLock") private void destroyBluetoothUserServiceLocked() { if (mCarBluetoothUserService == null) { return; } try { mCarBluetoothUserService.closeBluetoothConnectionProxies(); } catch (RemoteException e) { Slogf.e(TAG, "Remote Service Exception on ServiceConnection Callback: %s", e); } mCarBluetoothUserService = null; } /** * Clears out Profile Device Managers and re-creates them for the current user. */ @GuardedBy("mPerUserLock") private void createBluetoothDeviceManagerLocked() { if (DBG) { Slogf.d(TAG, "Creating device manager"); } if (mUserId == UserManagerHelper.USER_NULL) { if (DBG) { Slogf.d(TAG, "No foreground user, cannot create profile device managers"); } return; } mDeviceManager = BluetoothDeviceManager.create(mContext); mDeviceManager.start(); } /** * Stops and clears the entire set of Profile Device Managers. */ @GuardedBy("mPerUserLock") private void destroyBluetoothDeviceManagerLocked() { if (DBG) { Slogf.d(TAG, "Destroying device manager"); } if (mDeviceManager == null) return; mDeviceManager.stop(); mDeviceManager = null; } /** * Creates an instance of a BluetoothProfileInhibitManager under the current user */ @GuardedBy("mPerUserLock") private void createBluetoothProfileInhibitManagerLocked() { if (DBG) { Slogf.d(TAG, "Creating inhibit manager"); } if (mUserId == UserManagerHelper.USER_NULL) { if (DBG) { Slogf.d(TAG, "No foreground user, cannot create profile inhibit manager"); } return; } mInhibitManager = new BluetoothProfileInhibitManager(mContext, mUserId, mCarBluetoothUserService); mInhibitManager.start(); } /** * Destroys the current instance of a BluetoothProfileInhibitManager, if one exists */ @GuardedBy("mPerUserLock") private void destroyBluetoothProfileInhibitManagerLocked() { if (DBG) { Slogf.d(TAG, "Destroying inhibit manager"); } if (mInhibitManager == null) return; mInhibitManager.stop(); mInhibitManager = null; } /** * Creates an instance of {@link BluetoothConnectionRetryManager} for the current user. * Clears out any existing manager from previous user. */ @GuardedBy("mPerUserLock") private void createBluetoothConnectionRetryManagerLocked() { if (DBG) { Slogf.d(TAG, "Creating connection retry manager"); } if (mUserId == UserManagerHelper.USER_NULL) { if (DBG) { Slogf.d(TAG, "No foreground user, cannot create connection retry manager"); } return; } if (mConnectionRetryManager != null) { if (DBG) { Slogf.d(TAG, "Removing existing connection retry manager first"); } destroyBluetoothConnectionRetryManagerLocked(); } mConnectionRetryManager = BluetoothConnectionRetryManager.create(mContext); if (mConnectionRetryManager == null) { if (DBG) { Slogf.d(TAG, "Failed to create connection retry manager"); } return; } mConnectionRetryManager.init(); if (DBG) { Slogf.d(TAG, "Created connection retry manager"); } } /** * Releases and clears {@link BluetoothConnectionRetryManager}. */ @GuardedBy("mPerUserLock") private void destroyBluetoothConnectionRetryManagerLocked() { if (DBG) { Slogf.d(TAG, "Destroying connection retry manager"); } if (mConnectionRetryManager == null) return; mConnectionRetryManager.release(); mConnectionRetryManager = null; if (DBG) { Slogf.d(TAG, "Connection retry manager removed"); } } /** * Creates an instance of a BluetoothDeviceConnectionPolicy under the current user */ @GuardedBy("mPerUserLock") private void createBluetoothDeviceConnectionPolicyLocked() { if (DBG) { Slogf.d(TAG, "Creating device connection policy"); } if (mUserId == UserManagerHelper.USER_NULL) { if (DBG) { Slogf.d(TAG, "No foreground user, cannot create device connection policy"); } return; } mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext, mUserId, this); if (mBluetoothDeviceConnectionPolicy == null) { if (DBG) { Slogf.d(TAG, "Failed to create default Bluetooth device connection policy."); } return; } mBluetoothDeviceConnectionPolicy.init(); } /** * Destroys the current instance of a BluetoothDeviceConnectionPolicy, if one exists */ @GuardedBy("mPerUserLock") private void destroyBluetoothDeviceConnectionPolicyLocked() { if (DBG) { Slogf.d(TAG, "Destroying device connection policy"); } if (mBluetoothDeviceConnectionPolicy != null) { mBluetoothDeviceConnectionPolicy.release(); mBluetoothDeviceConnectionPolicy = null; } } /** * Creates an instance of a BluetoothDeviceConnectionPolicy under the current user */ @GuardedBy("mPerUserLock") private void createBluetoothPowerPolicyLocked() { if (DBG) { Slogf.d(TAG, "Creating power policy"); } if (mUserId == UserManagerHelper.USER_NULL) { if (DBG) { Slogf.d(TAG, "No foreground user, cannot create power policy"); } return; } mBluetoothPowerPolicy = BluetoothPowerPolicy.create(mContext, mUserId); if (mBluetoothPowerPolicy == null) { if (DBG) { Slogf.d(TAG, "Failed to create Bluetooth power policy."); } return; } mBluetoothPowerPolicy.init(); } /** * Destroys the current instance of a BluetoothDeviceConnectionPolicy, if one exists */ @GuardedBy("mPerUserLock") private void destroyBluetoothPowerPolicyLocked() { if (DBG) { Slogf.d(TAG, "Destroying power policy"); } if (mBluetoothPowerPolicy != null) { mBluetoothPowerPolicy.release(); mBluetoothPowerPolicy = null; } } /** * Determine if we are using the default device connection policy or not * * @return true if the default policy is active, false otherwise */ public boolean isUsingDefaultConnectionPolicy() { synchronized (mPerUserLock) { return mBluetoothDeviceConnectionPolicy != null; } } /** * Determine if we are using the default power policy or not * * @return true if the default policy is active, false otherwise */ public boolean isUsingDefaultPowerPolicy() { synchronized (mPerUserLock) { return mBluetoothPowerPolicy != null; } } /** * Initiate automatated connecting of devices based on the prioritized device lists for each * profile. */ public void connectDevices() { enforceBluetoothConnectPermission(); enforceBluetoothPrivilegedPermission(); enforceModifyPhoneStatePermission(); if (DBG) { Slogf.d(TAG, "Connect devices for each profile"); } synchronized (mPerUserLock) { if (mDeviceManager != null) { mDeviceManager.beginAutoConnecting(); } } } /** * Request to disconnect the given profile on the given device, and prevent it from reconnecting * until either the request is released, or the process owning the given token dies. * * @param device The device on which to inhibit a profile. * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit. * @param token A {@link IBinder} to be used as an identity for the request. If the process * owning the token dies, the request will automatically be released * @return True if the profile was successfully inhibited, false if an error occurred. */ public boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) { enforceBluetoothConnectPermission(); enforceBluetoothPrivilegedPermission(); if (DBG) { Slogf.d(TAG, "Request profile inhibit: profile %s, device %s", BluetoothUtils.getProfileName(profile), device.getAddress()); } synchronized (mPerUserLock) { if (mInhibitManager == null) return false; return mInhibitManager.requestProfileInhibit(device, profile, token); } } /** * Undo a previous call to {@link #requestProfileInhibit} with the same parameters, * and reconnect the profile if no other requests are active. * * @param device The device on which to release the inhibit request. * @param profile The profile on which to release the inhibit request. * @param token The token provided in the original call to * {@link #requestBluetoothProfileInhibit}. * @return True if the request was released, false if an error occurred. */ public boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) { enforceBluetoothConnectPermission(); enforceBluetoothPrivilegedPermission(); if (DBG) { Slogf.d(TAG, "Release profile inhibit: profile %s, device %s", BluetoothUtils.getProfileName(profile), device.getAddress()); } synchronized (mPerUserLock) { if (mInhibitManager == null) return false; return mInhibitManager.releaseProfileInhibit(device, profile, token); } } /** * Checks whether a request to disconnect the given profile on the given device has been made * and if the inhibit request is still active. * * @param device The device on which to verify the inhibit request. * @param profile The profile on which to verify the inhibit request. * @param token The token provided in the original call to * {@link #requestBluetoothProfileInhibit}. * @return True if inhibit was requested and is still active, false if an error occurred or * inactive. */ public boolean isProfileInhibited(BluetoothDevice device, int profile, IBinder token) { enforceBluetoothConnectPermission(); enforceBluetoothPrivilegedPermission(); if (DBG) { Slogf.d(TAG, "Check profile inhibit: profile %s, device %s", BluetoothUtils.getProfileName(profile), device.getAddress()); } synchronized (mPerUserLock) { if (mInhibitManager == null) return false; return mInhibitManager.isProfileInhibited(device, profile, token); } } /** * Triggers Bluetooth to start a BVRA session with the default HFP Client device. */ public boolean startBluetoothVoiceRecognition() { enforceBluetoothConnectPermission(); synchronized (mPerUserLock) { try { return mCarBluetoothUserService.startBluetoothVoiceRecognition(); } catch (RemoteException e) { Slogf.e(TAG, "Remote Service Exception on BVRA", e); } } return false; } /** * Make sure the caller has the Bluetooth Connect permission */ private void enforceBluetoothConnectPermission() { if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.BLUETOOTH_CONNECT)) { return; } throw new SecurityException("requires permission " + android.Manifest.permission.BLUETOOTH_CONNECT); } /** * Make sure the caller has the Bluetooth Privileged permission */ private void enforceBluetoothPrivilegedPermission() { if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.BLUETOOTH_PRIVILEGED)) { return; } throw new SecurityException("requires permission " + android.Manifest.permission.BLUETOOTH_PRIVILEGED); } /** * Make sure the caller has the Modify Phone State permission */ private void enforceModifyPhoneStatePermission() { if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_PHONE_STATE)) { return; } throw new SecurityException("requires permission " + android.Manifest.permission.MODIFY_PHONE_STATE); } /** * Print out the verbose debug status of this object */ @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { writer.printf("* %s *\n", TAG); mUserServiceHelper.dump(writer); synchronized (mPerUserLock) { writer.printf("User ID: %d\n", mUserId); writer.printf("User Proxies: %s\n", mCarBluetoothUserService != null ? "Yes" : "No"); writer.printf("Using default connection policy? %s\n", mUseDefaultConnectionPolicy ? "Yes" : "No"); writer.printf("Using default power policy? %s\n", mUseDefaultPowerPolicy ? "Yes" : "No"); // Device Connection Policy if (mBluetoothDeviceConnectionPolicy != null) { mBluetoothDeviceConnectionPolicy.dump(writer); } else { writer.printf("BluetoothDeviceConnectionPolicy: null\n"); } // Power Policy if (mBluetoothPowerPolicy != null) { mBluetoothPowerPolicy.dump(writer); } else { writer.printf("BluetoothPowerPolicy: null\n"); } // Device Manager status if (mDeviceManager != null) { mDeviceManager.dump(writer); } else { writer.printf("BluetoothDeviceManager: null\n"); } // Profile Inhibits if (mInhibitManager != null) { mInhibitManager.dump(writer); } else { writer.printf("BluetoothProfileInhibitManager: null\n"); } } } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) {} }