Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 83aeaf27 authored by Sal Savage's avatar Sal Savage
Browse files

Create PbapClientBinder

This change factors out Binder logic into its own testible place

Flag: EXEMPT, mechanical refactor. No logic change
Bug: 365626536
Test: atest PbapClientBinderTest.java
Test: atest PbapClientServiceTest.java
Change-Id: Ia2e8906e6ea4c6c75b0bad2db3e95f6d14fb2407
parent a593d6ca
Loading
Loading
Loading
Loading
+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;

import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothPbapClient;
import android.content.AttributionSource;
import android.util.Log;

import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Handler for incoming service calls destined for PBAP Client */
public class PbapClientBinder extends IBluetoothPbapClient.Stub implements IProfileServiceBinder {
    private static final String TAG = PbapClientBinder.class.getSimpleName();

    private PbapClientService mService;

    PbapClientBinder(PbapClientService service) {
        mService = service;
    }

    @Override
    public void cleanup() {
        mService = null;
    }

    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    private PbapClientService getService(AttributionSource source) {
        // Cache mService because it can change while getService is called
        PbapClientService service = mService;

        if (Utils.isInstrumentationTestMode()) {
            return service;
        }

        if (!Utils.checkServiceAvailable(service, TAG)) {
            Log.w(TAG, "getService() failed, service not available");
            return null;
        }

        if (!Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)
                || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
            Log.w(TAG, "getService() failed, rejected due to permissions");
            return null;
        }

        service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);

        return service;
    }

    @Override
    public boolean connect(BluetoothDevice device, AttributionSource source) {
        Log.d(TAG, "connect(device=" + device + ")");
        PbapClientService service = getService(source);
        if (service == null) {
            return false;
        }
        return service.connect(device);
    }

    @Override
    public boolean disconnect(BluetoothDevice device, AttributionSource source) {
        Log.d(TAG, "disconnect(device=" + device + ")");
        PbapClientService service = getService(source);
        if (service == null) {
            return false;
        }
        return service.disconnect(device);
    }

    @Override
    public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
        Log.d(TAG, "getConnectedDevices()");
        PbapClientService service = getService(source);
        if (service == null) {
            return Collections.emptyList();
        }
        return service.getConnectedDevices();
    }

    @Override
    public List<BluetoothDevice> getDevicesMatchingConnectionStates(
            int[] states, AttributionSource source) {
        Log.d(TAG, "getDevicesMatchingConnectionStates(states=" + Arrays.toString(states) + ")");
        PbapClientService service = getService(source);
        if (service == null) {
            return Collections.emptyList();
        }
        return service.getDevicesMatchingConnectionStates(states);
    }

    @Override
    public int getConnectionState(BluetoothDevice device, AttributionSource source) {
        Log.d(TAG, "getConnectionState(device=" + device + ")");
        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) {
        Log.d(TAG, "setConnectionPolicy(device=" + device + ", policy=" + connectionPolicy + ")");

        PbapClientService service = getService(source);
        if (service == null) {
            return false;
        }
        return service.setConnectionPolicy(device, connectionPolicy);
    }

    @Override
    public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
        Log.d(TAG, "getConnectionPolicy(device=" + device + ")");
        PbapClientService service = getService(source);
        if (service == null) {
            return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
        }
        return service.getConnectionPolicy(device);
    }
}
+96 −161
Original line number Diff line number Diff line
@@ -16,17 +16,11 @@

package com.android.bluetooth.pbapclient;

import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothPbapClient;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -42,7 +36,6 @@ 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;
@@ -51,7 +44,6 @@ import com.android.bluetooth.sdp.SdpManagerNativeInterface;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -138,7 +130,7 @@ public class PbapClientService extends ProfileService {

    @Override
    public IProfileServiceBinder initBinder() {
        return new BluetoothPbapClientBinder(this);
        return new PbapClientBinder(this);
    }

    @Override
@@ -331,27 +323,6 @@ public class PbapClientService extends ProfileService {
        }
    }

    public void aclDisconnected(BluetoothDevice device, int transport) {
        mHandler.post(() -> handleAclDisconnected(device, transport));
    }

    private void handleAclDisconnected(BluetoothDevice device, int transport) {
        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);
        }
    }

    /**
     * Ensure that after HFP disconnects, we remove call logs. This addresses the situation when
     * PBAP was never connected while calls were made. Ideally {@link PbapClientConnectionHandler}
@@ -367,118 +338,90 @@ public class PbapClientService extends ProfileService {
        }
    }

    /** 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(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
        private PbapClientService getService(AttributionSource source) {
            // Cache mService because it can change while getService is called
            PbapClientService service = mService;

            if (Utils.isInstrumentationTestMode()) {
                return service;
            }

            if (!Utils.checkServiceAvailable(service, TAG)
                    || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)
                    || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
                return null;
            }

            service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);

            return service;
        }

        @Override
        public boolean connect(BluetoothDevice device, AttributionSource source) {
            Log.d(TAG, "PbapClient Binder connect");

            PbapClientService service = getService(source);
            if (service == null) {
                Log.e(TAG, "PbapClient Binder connect no service");
                return false;
            }

            return service.connect(device);
        }

    /**
     * Get debug information about this PbapClientService instance
     *
     * @param sb The StringBuilder instance to add our debug dump info to
     */
    @Override
        public boolean disconnect(BluetoothDevice device, AttributionSource source) {
            PbapClientService service = getService(source);
            if (service == null) {
                return false;
    public void dump(StringBuilder sb) {
        super.dump(sb);
        ProfileService.println(sb, "isAuthServiceReady: " + isAuthenticationServiceReady());
        for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
            stateMachine.dump(sb);
        }

            return service.disconnect(device);
    }

        @Override
        public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
            PbapClientService service = getService(source);
            if (service == null) {
                return Collections.emptyList();
            }
    // *********************************************************************************************
    // * Events from AdapterService
    // *********************************************************************************************

            return service.getConnectedDevices();
    /**
     * Get notified of incoming ACL disconnections
     *
     * <p>OBEX client's are supposed to be in control of the connection lifecycle, and servers are
     * not supposed to disconnect OBEX sessions. Despite this, its normal/possible the remote device
     * to tear down connections at lower levels than OBEX, mainly the L2CAP/RFCOMM links or the ACL.
     * The OBEX framework isn't setup to be notified of these disconnections, so we must listen for
     * them separately and clean up the device connection and, if necessary, data when this happens.
     *
     * @param device The device that had the ACL disconnect
     * @param transport The transport the device disconnected on
     */
    public void aclDisconnected(BluetoothDevice device, int transport) {
        mHandler.post(() -> handleAclDisconnected(device, transport));
    }

        @Override
        public List<BluetoothDevice> getDevicesMatchingConnectionStates(
                int[] states, AttributionSource source) {
            PbapClientService service = getService(source);
            if (service == null) {
                return Collections.emptyList();
            }
    private void handleAclDisconnected(BluetoothDevice device, int transport) {
        Log.i(
                TAG,
                "Received ACL disconnection event, device="
                        + device.toString()
                        + ", transport="
                        + transport);

            return service.getDevicesMatchingConnectionStates(states);
        if (transport != BluetoothDevice.TRANSPORT_BREDR) {
            return;
        }

        @Override
        public int getConnectionState(BluetoothDevice device, AttributionSource source) {
            PbapClientService service = getService(source);
            if (service == null) {
                return BluetoothProfile.STATE_DISCONNECTED;
        if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
            disconnect(device);
        }

            return service.getConnectionState(device);
    }

        @Override
        public boolean setConnectionPolicy(
                BluetoothDevice device, int connectionPolicy, AttributionSource source) {
            PbapClientService service = getService(source);
            if (service == null) {
                return false;
    /**
     * Get notified of incoming SDP records
     *
     * <p>This function looks for PBAP Server records coming from remote devices, and forwards them
     * to the appropriate device's state machine instance for processing. SDP records are used to
     * determine which L2CAP/RFCOMM psm/channel to connect on, as well as which phonebooks to expect
     */
    public void receiveSdpSearchRecord(
            BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) {
        PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device);
        if (stateMachine == null) {
            Log.e(TAG, "No Statemachine found for the device=" + device.toString());
            return;
        }

            return service.setConnectionPolicy(device, connectionPolicy);
        Log.v(
                TAG,
                "Received SDP record for UUID="
                        + uuid.toString()
                        + " (expected UUID="
                        + BluetoothUuid.PBAP_PSE.toString()
                        + ")");
        if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
            stateMachine
                    .obtainMessage(PbapClientStateMachine.MSG_SDP_COMPLETE, record)
                    .sendToTarget();
        }

        @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
    // *********************************************************************************************

    // API methods
    /** Get the singleton instance of PbapClientService, if one exists */
    public static synchronized PbapClientService getPbapClientService() {
        if (sPbapClientService == null) {
            Log.w(TAG, "getPbapClientService(): service is null");
@@ -491,12 +434,23 @@ public class PbapClientService extends ProfileService {
        return sPbapClientService;
    }

    /**
     * Set the singleton instance of PbapClientService
     *
     * <p>This function is meant to be used by tests only.
     */
    @VisibleForTesting
    static synchronized void setPbapClientService(PbapClientService instance) {
        Log.v(TAG, "setPbapClientService(): set to: " + instance);
        sPbapClientService = instance;
    }

    /**
     * Requests a connection to the given device's PBAP Server
     *
     * @param device is the device with which we will connect to
     * @return true if we successfully begin the connection process, false otherwise
     */
    public boolean connect(BluetoothDevice device) {
        if (device == null) {
            throw new IllegalArgumentException("Null device");
@@ -540,13 +494,24 @@ public class PbapClientService extends ProfileService {
        }
    }

    /**
     * Get the list of PBAP Server devices this PBAP Client device is connected to
     *
     * @return The list of connected PBAP Server devices
     */
    public List<BluetoothDevice> getConnectedDevices() {
        int[] desiredStates = {BluetoothProfile.STATE_CONNECTED};
        return getDevicesMatchingConnectionStates(desiredStates);
    }

    @VisibleForTesting
    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
    /**
     * Get the list of PBAP Server devices this PBAP Client device know about, who are in a given
     * state.
     *
     * @param states The array of BluutoothProfile states you want to match on
     * @return The list of connected PBAP Server devices
     */
    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(0);
        for (Map.Entry<BluetoothDevice, PbapClientStateMachine> stateMachineEntry :
                mPbapClientStateMachineMap.entrySet()) {
@@ -561,27 +526,6 @@ public class PbapClientService extends ProfileService {
        return deviceList;
    }

    public void receiveSdpSearchRecord(
            BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) {
        PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device);
        if (stateMachine == null) {
            Log.e(TAG, "No Statemachine found for the device=" + device.toString());
            return;
        }
        Log.v(
                TAG,
                "Received SDP record for UUID="
                        + uuid.toString()
                        + " (expected UUID="
                        + BluetoothUuid.PBAP_PSE.toString()
                        + ")");
        if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
            stateMachine
                    .obtainMessage(PbapClientStateMachine.MSG_SDP_COMPLETE, record)
                    .sendToTarget();
        }
    }

    /**
     * Get the current connection state of the profile
     *
@@ -651,13 +595,4 @@ public class PbapClientService extends ProfileService {
        }
        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);
        }
    }
}
+186 −0

File added.

Preview size limit exceeded, changes collapsed.

+0 −98
Original line number Diff line number Diff line
@@ -221,104 +221,6 @@ public class PbapClientServiceTest {
        assertThat(mService.mPbapClientStateMachineMap).doesNotContainKey(mRemoteDevice);
    }

    @Test
    public void getConnectedDevices() {
        int connectionState = BluetoothProfile.STATE_CONNECTED;
        PbapClientStateMachine sm = mock(PbapClientStateMachine.class);
        mService.mPbapClientStateMachineMap.put(mRemoteDevice, sm);
        when(sm.getConnectionState()).thenReturn(connectionState);

        assertThat(mService.getConnectedDevices()).contains(mRemoteDevice);
    }

    @Test
    public void binder_connect_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.connect(mRemoteDevice, null);

        verify(mockService).connect(mRemoteDevice);
    }

    @Test
    public void binder_disconnect_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.disconnect(mRemoteDevice, null);

        verify(mockService).disconnect(mRemoteDevice);
    }

    @Test
    public void binder_getConnectedDevices_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.getConnectedDevices(null);

        verify(mockService).getConnectedDevices();
    }

    @Test
    public void binder_getDevicesMatchingConnectionStates_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        int[] states = new int[] {BluetoothProfile.STATE_CONNECTED};
        binder.getDevicesMatchingConnectionStates(states, null);

        verify(mockService).getDevicesMatchingConnectionStates(states);
    }

    @Test
    public void binder_getConnectionState_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.getConnectionState(mRemoteDevice, null);

        verify(mockService).getConnectionState(mRemoteDevice);
    }

    @Test
    public void binder_setConnectionPolicy_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        int connectionPolicy = BluetoothProfile.CONNECTION_POLICY_ALLOWED;
        binder.setConnectionPolicy(mRemoteDevice, connectionPolicy, null);

        verify(mockService).setConnectionPolicy(mRemoteDevice, connectionPolicy);
    }

    @Test
    public void binder_getConnectionPolicy_callsServiceMethod() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.getConnectionPolicy(mRemoteDevice, null);

        verify(mockService).getConnectionPolicy(mRemoteDevice);
    }

    @Test
    public void binder_cleanUp_doesNotCrash() {
        PbapClientService mockService = mock(PbapClientService.class);
        PbapClientService.BluetoothPbapClientBinder binder =
                new PbapClientService.BluetoothPbapClientBinder(mockService);

        binder.cleanup();
    }

    @Test
    public void broadcastReceiver_withActionAclDisconnectedLeTransport_doesNotCallDisconnect() {
        int connectionState = BluetoothProfile.STATE_CONNECTED;