/* * 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.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothPbapClient; 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.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; 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.concurrent.ConcurrentHashMap; /** * Provides Bluetooth Phone Book Access Profile Client profile. * * @hide */ public class PbapClientService extends ProfileService { private static final boolean DBG = Utils.DBG; private static final boolean VDBG = 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 mPbapClientStateMachineMap = new ConcurrentHashMap<>(); private static PbapClientService sPbapClientService; private PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver(); private int mSdpHandle = -1; @Override public IProfileServiceBinder initBinder() { return new BluetoothPbapClientBinder(this); } @Override protected boolean start() { if (VDBG) { Log.v(TAG, "onStart"); } 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; } private PbapClientService getService() { if (!com.android.bluetooth.Utils.checkCaller()) { Log.w(TAG, "PbapClient call not allowed for non-active user"); return null; } if (mService != null && mService.isAvailable()) { return mService; } return null; } @Override public boolean connect(BluetoothDevice device) { PbapClientService service = getService(); 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) { PbapClientService service = getService(); if (service == null) { return false; } return service.disconnect(device); } @Override public List getConnectedDevices() { PbapClientService service = getService(); if (service == null) { return new ArrayList(0); } return service.getConnectedDevices(); } @Override public List getDevicesMatchingConnectionStates(int[] states) { PbapClientService service = getService(); if (service == null) { return new ArrayList(0); } return service.getDevicesMatchingConnectionStates(states); } @Override public int getConnectionState(BluetoothDevice device) { PbapClientService service = getService(); if (service == null) { return BluetoothProfile.STATE_DISCONNECTED; } return service.getConnectionState(device); } @Override public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { PbapClientService service = getService(); if (service == null) { return false; } return service.setConnectionPolicy(device, connectionPolicy); } @Override public int getConnectionPolicy(BluetoothDevice device) { PbapClientService service = getService(); 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; } 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 */ 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 getConnectedDevices() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); int[] desiredStates = {BluetoothProfile.STATE_CONNECTED}; return getDevicesMatchingConnectionStates(desiredStates); } private List getDevicesMatchingConnectionStates(int[] states) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); List deviceList = new ArrayList(0); for (Map.Entry 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"); } enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 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} * *

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 */ 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); } AdapterService.getAdapterService().getDatabase() .setProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT, connectionPolicy); 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. * *

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 */ public int getConnectionPolicy(BluetoothDevice device) { if (device == null) { throw new IllegalArgumentException("Null device"); } enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); return AdapterService.getAdapterService().getDatabase() .getProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT); } @Override public void dump(StringBuilder sb) { super.dump(sb); for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { stateMachine.dump(sb); } } }