blob: 887978f9dfa44b58ed7b9af03deff394c86977f1 [file] [log] [blame]
/*
* Copyright (c) 2016 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.bluetooth.pbapclient;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothPbapClient;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.provider.CallLog;
import android.sysprop.BluetoothProperties;
import android.util.Log;
import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.hfpclient.HfpClientConnectionService;
import com.android.bluetooth.sdp.SdpManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Provides Bluetooth Phone Book Access Profile Client profile.
*
* @hide
*/
public class PbapClientService extends ProfileService {
private static final String TAG = "PbapClientService";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
private static final String SERVICE_NAME = "Phonebook Access PCE";
/**
* The component names for the owned authenticator service
*/
private static final String AUTHENTICATOR_SERVICE =
AuthenticationService.class.getCanonicalName();
// MAXIMUM_DEVICES set to 10 to prevent an excessive number of simultaneous devices.
private static final int MAXIMUM_DEVICES = 10;
@VisibleForTesting
Map<BluetoothDevice, PbapClientStateMachine> mPbapClientStateMachineMap =
new ConcurrentHashMap<>();
private static PbapClientService sPbapClientService;
@VisibleForTesting
PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver();
private int mSdpHandle = -1;
private DatabaseManager mDatabaseManager;
/**
* There's an ~1-2 second latency between when our Authentication service is set as available to
* the system and when the Authentication/Account framework code will recognize it and allow us
* to alter accounts. In lieu of the Accounts team dealing with this race condition, we're going
* to periodically poll over 3 seconds until our accounts are visible, remove old accounts, and
* then notify device state machines that they can create accounts and download contacts.
*/
// TODO(233361365): Remove this pattern when the framework solves their race condition
private static final int ACCOUNT_VISIBILITY_CHECK_MS = 500;
private static final int ACCOUNT_VISIBILITY_CHECK_TRIES_MAX = 6;
private int mAccountVisibilityCheckTries = 0;
private final Handler mAuthServiceHandler = new Handler();
private final Runnable mCheckAuthService = new Runnable() {
@Override
public void run() {
// If our accounts are finally visible to use, clean up old ones and tell devices they
// can issue downloads if they're ready. Otherwise, wait and try again.
if (isAuthenticationServiceReady()) {
Log.i(TAG, "Service ready! Clean up old accounts and try contacts downloads");
removeUncleanAccounts();
for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
stateMachine.tryDownloadIfConnected();
}
} else if (mAccountVisibilityCheckTries < ACCOUNT_VISIBILITY_CHECK_TRIES_MAX) {
mAccountVisibilityCheckTries += 1;
Log.w(TAG, "AccountManager hasn't registered our service yet. Retry "
+ mAccountVisibilityCheckTries + "/" + ACCOUNT_VISIBILITY_CHECK_TRIES_MAX);
mAuthServiceHandler.postDelayed(this, ACCOUNT_VISIBILITY_CHECK_MS);
} else {
Log.e(TAG, "Failed to register Authenication Service and get account visibility");
}
}
};
public static boolean isEnabled() {
return BluetoothProperties.isProfilePbapClientEnabled().orElse(false);
}
@Override
public IProfileServiceBinder initBinder() {
return new BluetoothPbapClientBinder(this);
}
@Override
protected boolean start() {
if (VDBG) {
Log.v(TAG, "onStart");
}
mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
"DatabaseManager cannot be null when PbapClientService starts");
setComponentAvailable(AUTHENTICATOR_SERVICE, true);
IntentFilter filter = new IntentFilter();
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
// delay initial download until after the user is unlocked to add an account.
filter.addAction(Intent.ACTION_USER_UNLOCKED);
// To remove call logs when PBAP was never connected while calls were made,
// we also listen for HFP to become disconnected.
filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
try {
registerReceiver(mPbapBroadcastReceiver, filter);
} catch (Exception e) {
Log.w(TAG, "Unable to register pbapclient receiver", e);
}
initializeAuthenticationService();
registerSdpRecord();
setPbapClientService(this);
return true;
}
@Override
protected boolean stop() {
setPbapClientService(null);
cleanUpSdpRecord();
try {
unregisterReceiver(mPbapBroadcastReceiver);
} catch (Exception e) {
Log.w(TAG, "Unable to unregister pbapclient receiver", e);
}
for (PbapClientStateMachine pbapClientStateMachine : mPbapClientStateMachineMap.values()) {
pbapClientStateMachine.doQuit();
}
mPbapClientStateMachineMap.clear();
cleanupAuthenicationService();
setComponentAvailable(AUTHENTICATOR_SERVICE, false);
return true;
}
void cleanupDevice(BluetoothDevice device) {
if (DBG) Log.d(TAG, "Cleanup device: " + device);
synchronized (mPbapClientStateMachineMap) {
PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
if (pbapClientStateMachine != null) {
mPbapClientStateMachineMap.remove(device);
}
}
}
/**
* Periodically check if the account framework has recognized our service and will allow us to
* interact with our accounts. Notify state machines once our service is ready so we can trigger
* account downloads.
*/
private void initializeAuthenticationService() {
mAuthServiceHandler.postDelayed(mCheckAuthService, ACCOUNT_VISIBILITY_CHECK_MS);
}
private void cleanupAuthenicationService() {
mAuthServiceHandler.removeCallbacks(mCheckAuthService);
removeUncleanAccounts();
}
/**
* Determine if our account type is visible to us yet. If it is, then our service is ready and
* our account type is ready to use.
*
* Make a placeholder device account and determine our visibility relative to it. Note that this
* function uses the same restrictions are the other add and remove functions, but is *also*
* available to all system apps instead of throwing a runtime SecurityException.
*/
protected boolean isAuthenticationServiceReady() {
Account account = new Account("00:00:00:00:00:00", getString(R.string.pbap_account_type));
AccountManager accountManager = AccountManager.get(this);
int visibility = accountManager.getAccountVisibility(account, getPackageName());
if (DBG) {
Log.d(TAG, "Checking visibility, visibility=" + visibility);
}
return visibility == AccountManager.VISIBILITY_VISIBLE
|| visibility == AccountManager.VISIBILITY_USER_MANAGED_VISIBLE;
}
private void removeUncleanAccounts() {
if (!isAuthenticationServiceReady()) {
Log.w(TAG, "Can't remove accounts. AccountManager hasn't registered our service yet.");
return;
}
// Find all accounts that match the type "pbap" and delete them.
AccountManager accountManager = AccountManager.get(this);
Account[] accounts =
accountManager.getAccountsByType(getString(R.string.pbap_account_type));
if (VDBG) Log.v(TAG, "Found " + accounts.length + " unclean accounts");
for (Account acc : accounts) {
Log.w(TAG, "Deleting " + acc);
try {
getContentResolver().delete(CallLog.Calls.CONTENT_URI,
CallLog.Calls.PHONE_ACCOUNT_ID + "=?", new String[]{acc.name});
} catch (IllegalArgumentException e) {
Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
}
// The device ID is the name of the account.
accountManager.removeAccountExplicitly(acc);
}
}
private void removeHfpCallLog(String accountName, Context context) {
if (DBG) Log.d(TAG, "Removing call logs from " + accountName);
// Delete call logs belonging to accountName==BD_ADDR that also match
// component name "hfpclient".
ComponentName componentName = new ComponentName(context, HfpClientConnectionService.class);
String selectionFilter = CallLog.Calls.PHONE_ACCOUNT_ID + "=? AND "
+ CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=?";
String[] selectionArgs = new String[]{accountName, componentName.flattenToString()};
try {
BluetoothMethodProxy.getInstance().contentResolverDelete(getContentResolver(),
CallLog.Calls.CONTENT_URI, selectionFilter, selectionArgs);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
}
}
private void registerSdpRecord() {
SdpManager sdpManager = SdpManager.getDefaultManager();
if (sdpManager == null) {
Log.e(TAG, "SdpManager is null");
return;
}
mSdpHandle = sdpManager.createPbapPceRecord(SERVICE_NAME,
PbapClientConnectionHandler.PBAP_V1_2);
}
private void cleanUpSdpRecord() {
if (mSdpHandle < 0) {
Log.e(TAG, "cleanUpSdpRecord, SDP record never created");
return;
}
int sdpHandle = mSdpHandle;
mSdpHandle = -1;
SdpManager sdpManager = SdpManager.getDefaultManager();
if (sdpManager == null) {
Log.e(TAG, "cleanUpSdpRecord failed, sdpManager is null, sdpHandle=" + sdpHandle);
return;
}
Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle);
if (!sdpManager.removeSdpRecord(sdpHandle)) {
Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle);
}
}
@VisibleForTesting
class PbapBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DBG) Log.v(TAG, "onReceive" + action);
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int transport =
intent.getIntExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.ERROR);
Log.i(TAG, "Received ACL disconnection event, device=" + device.toString()
+ ", transport=" + transport);
if (transport != BluetoothDevice.TRANSPORT_BREDR) {
return;
}
if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
disconnect(device);
}
} else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
stateMachine.tryDownloadIfConnected();
}
} else if (action.equals(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED)) {
// PbapClientConnectionHandler has code to remove calllogs when PBAP disconnects.
// However, if PBAP was never connected/enabled in the first place, and calls are
// made over HFP, these calllogs will not be removed when the device disconnects.
// This code ensures callogs are still removed in this case.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
if (DBG) {
Log.d(TAG, "Received intent to disconnect HFP with " + device);
}
// HFP client stores entries in calllog.db by BD_ADDR and component name
removeHfpCallLog(device.getAddress(), context);
}
}
}
}
/**
* Handler for incoming service calls
*/
@VisibleForTesting
static class BluetoothPbapClientBinder extends IBluetoothPbapClient.Stub
implements IProfileServiceBinder {
private PbapClientService mService;
BluetoothPbapClientBinder(PbapClientService svc) {
mService = svc;
}
@Override
public void cleanup() {
mService = null;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
private PbapClientService getService(AttributionSource source) {
if (Utils.isInstrumentationTestMode()) {
return mService;
}
if (!Utils.checkServiceAvailable(mService, TAG)
|| !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG)
|| !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) {
return null;
}
return mService;
}
@Override
public void connect(BluetoothDevice device, AttributionSource source,
SynchronousResultReceiver receiver) {
if (DBG) Log.d(TAG, "PbapClient Binder connect ");
try {
PbapClientService service = getService(source);
boolean defaultValue = false;
if (service != null) {
defaultValue = service.connect(device);
} else {
Log.e(TAG, "PbapClient Binder connect no service");
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void disconnect(BluetoothDevice device, AttributionSource source,
SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
boolean defaultValue = false;
if (service != null) {
defaultValue = service.disconnect(device);
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void getConnectedDevices(AttributionSource source,
SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(0);
if (service != null) {
defaultValue = service.getConnectedDevices();
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void getDevicesMatchingConnectionStates(int[] states,
AttributionSource source, SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(0);
if (service != null) {
defaultValue = service.getDevicesMatchingConnectionStates(states);
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void getConnectionState(BluetoothDevice device, AttributionSource source,
SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
if (service != null) {
defaultValue = service.getConnectionState(device);
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
AttributionSource source, SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
boolean defaultValue = false;
if (service != null) {
defaultValue = service.setConnectionPolicy(device, connectionPolicy);
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
@Override
public void getConnectionPolicy(BluetoothDevice device, AttributionSource source,
SynchronousResultReceiver receiver) {
try {
PbapClientService service = getService(source);
int defaultValue = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
if (service != null) {
defaultValue = service.getConnectionPolicy(device);
}
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
}
// API methods
public static synchronized PbapClientService getPbapClientService() {
if (sPbapClientService == null) {
Log.w(TAG, "getPbapClientService(): service is null");
return null;
}
if (!sPbapClientService.isAvailable()) {
Log.w(TAG, "getPbapClientService(): service is not available");
return null;
}
return sPbapClientService;
}
@VisibleForTesting
static synchronized void setPbapClientService(PbapClientService instance) {
if (VDBG) {
Log.v(TAG, "setPbapClientService(): set to: " + instance);
}
sPbapClientService = instance;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean connect(BluetoothDevice device) {
if (device == null) {
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
if (DBG) Log.d(TAG, "Received request to ConnectPBAPPhonebook " + device.getAddress());
if (getConnectionPolicy(device) <= BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return false;
}
synchronized (mPbapClientStateMachineMap) {
PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
if (pbapClientStateMachine == null
&& mPbapClientStateMachineMap.size() < MAXIMUM_DEVICES) {
pbapClientStateMachine = new PbapClientStateMachine(this, device);
pbapClientStateMachine.start();
mPbapClientStateMachineMap.put(device, pbapClientStateMachine);
return true;
} else {
Log.w(TAG, "Received connect request while already connecting/connected.");
return false;
}
}
}
/**
* Disconnects the pbap client profile from the passed in device
*
* @param device is the device with which we will disconnect the pbap client profile
* @return true if we disconnected the pbap client profile, false otherwise
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean disconnect(BluetoothDevice device) {
if (device == null) {
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
if (pbapClientStateMachine != null) {
pbapClientStateMachine.disconnect(device);
return true;
} else {
Log.w(TAG, "disconnect() called on unconnected device.");
return false;
}
}
public List<BluetoothDevice> getConnectedDevices() {
int[] desiredStates = {BluetoothProfile.STATE_CONNECTED};
return getDevicesMatchingConnectionStates(desiredStates);
}
@VisibleForTesting
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(0);
for (Map.Entry<BluetoothDevice, PbapClientStateMachine> stateMachineEntry :
mPbapClientStateMachineMap
.entrySet()) {
int currentDeviceState = stateMachineEntry.getValue().getConnectionState();
for (int state : states) {
if (currentDeviceState == state) {
deviceList.add(stateMachineEntry.getKey());
break;
}
}
}
return deviceList;
}
/**
* Get the current connection state of the profile
*
* @param device is the remote bluetooth device
* @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
* {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
* {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
* {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
*/
public int getConnectionState(BluetoothDevice device) {
if (device == null) {
throw new IllegalArgumentException("Null device");
}
PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
if (pbapClientStateMachine == null) {
return BluetoothProfile.STATE_DISCONNECTED;
} else {
return pbapClientStateMachine.getConnectionState(device);
}
}
/**
* Set connection policy of the profile and connects it if connectionPolicy is
* {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
* {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
*
* <p> The device should already be paired.
* Connection policy can be one of:
* {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
* {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
* {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
*
* @param device Paired bluetooth device
* @param connectionPolicy is the connection policy to set to for this profile
* @return true if connectionPolicy is set, false on error
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
if (device == null) {
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
if (DBG) {
Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
}
if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT,
connectionPolicy)) {
return false;
}
if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
connect(device);
} else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
disconnect(device);
}
return true;
}
/**
* Get the connection policy of the profile.
*
* <p> The connection policy can be any of:
* {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
* {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
* {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
*
* @param device Bluetooth device
* @return connection policy of the device
* @hide
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public int getConnectionPolicy(BluetoothDevice device) {
if (device == null) {
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
return mDatabaseManager
.getProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT);
}
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
ProfileService.println(sb, "isAuthServiceReady: " + isAuthenticationServiceReady());
for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
stateMachine.dump(sb);
}
}
}