| /* |
| * 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.provider.CallLog; |
| import android.util.Log; |
| |
| 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.connserv.HfpClientConnectionService; |
| import com.android.bluetooth.sdp.SdpManager; |
| |
| 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 boolean DBG = com.android.bluetooth.pbapclient.Utils.DBG; |
| private static final boolean VDBG = com.android.bluetooth.pbapclient.Utils.VDBG; |
| |
| private static final String TAG = "PbapClientService"; |
| private static final String SERVICE_NAME = "Phonebook Access PCE"; |
| // MAXIMUM_DEVICES set to 10 to prevent an excessive number of simultaneous devices. |
| private static final int MAXIMUM_DEVICES = 10; |
| private Map<BluetoothDevice, PbapClientStateMachine> mPbapClientStateMachineMap = |
| new ConcurrentHashMap<>(); |
| private static PbapClientService sPbapClientService; |
| private PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver(); |
| private int mSdpHandle = -1; |
| |
| private DatabaseManager mDatabaseManager; |
| |
| @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"); |
| |
| IntentFilter filter = new IntentFilter(); |
| 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); |
| } |
| |
| removeUncleanAccounts(); |
| 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(); |
| } |
| removeUncleanAccounts(); |
| 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); |
| } |
| } |
| } |
| |
| private void removeUncleanAccounts() { |
| // 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 { |
| getContentResolver().delete(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); |
| } |
| } |
| |
| |
| private 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); |
| if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) { |
| disconnect(device); |
| } |
| } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) { |
| for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { |
| stateMachine.resumeDownload(); |
| } |
| } 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 |
| */ |
| private 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.checkCallerIsSystemOrActiveUser(TAG) |
| || !Utils.checkServiceAvailable(mService, TAG) |
| || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { |
| return null; |
| } |
| return mService; |
| } |
| |
| @Override |
| public boolean connect(BluetoothDevice device, AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (DBG) { |
| Log.d(TAG, "PbapClient Binder connect "); |
| } |
| if (service == null) { |
| Log.e(TAG, "PbapClient Binder connect no service"); |
| return false; |
| } |
| return service.connect(device); |
| } |
| |
| @Override |
| public boolean disconnect(BluetoothDevice device, AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return false; |
| } |
| return service.disconnect(device); |
| } |
| |
| @Override |
| public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return new ArrayList<BluetoothDevice>(0); |
| } |
| return service.getConnectedDevices(); |
| } |
| |
| @Override |
| public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states, |
| AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return new ArrayList<BluetoothDevice>(0); |
| } |
| return service.getDevicesMatchingConnectionStates(states); |
| } |
| |
| @Override |
| public int getConnectionState(BluetoothDevice device, AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return BluetoothProfile.STATE_DISCONNECTED; |
| } |
| return service.getConnectionState(device); |
| } |
| |
| @Override |
| public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy, |
| AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return false; |
| } |
| return service.setConnectionPolicy(device, connectionPolicy); |
| } |
| |
| @Override |
| public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { |
| PbapClientService service = getService(source); |
| if (service == null) { |
| return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; |
| } |
| return service.getConnectionPolicy(device); |
| } |
| |
| |
| } |
| |
| // 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; |
| } |
| |
| private 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); |
| } |
| |
| private 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); |
| for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { |
| stateMachine.dump(sb); |
| } |
| } |
| } |