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

Commit c4aa78db authored by William Escande's avatar William Escande
Browse files

HearingAid profile: rm impossible null handler

Use final & injected dependency
Update test to TestLooper & truth
Split test to actionable smaller test

Fix: 368527278
Bug: 368527278
Test: atest BluetoothInstrumentationTest:HearingAidServiceTest
Test: atest BluetoothInstrumentationTest:HearingAidStateMachineTest
Flag: Exempt refactor
Change-Id: Id5bf8f6d8daf8057b313905e1a13cb993667f2f3
parent 9dfd223e
Loading
Loading
Loading
Loading
+65 −103
Original line number Original line Diff line number Diff line
@@ -18,6 +18,11 @@ package com.android.bluetooth.hearingaid;


import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;

import static java.util.Objects.requireNonNull;


import android.annotation.RequiresPermission;
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevice;
@@ -27,7 +32,6 @@ import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothHearingAid;
import android.bluetooth.IBluetoothHearingAid;
import android.content.AttributionSource;
import android.content.AttributionSource;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioDeviceInfo;
@@ -62,38 +66,63 @@ import java.util.concurrent.ConcurrentHashMap;


/** Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. */
/** Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. */
public class HearingAidService extends ProfileService {
public class HearingAidService extends ProfileService {
    private static final String TAG = "HearingAidService";
    private static final String TAG = HearingAidService.class.getSimpleName();


    // Timeout for state machine thread join, to prevent potential ANR.
    private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000;
    private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000;


    // Upper limit of all HearingAid devices: Bonded or Connected
    // Upper limit of all HearingAid devices: Bonded or Connected
    private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
    private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
    private static HearingAidService sHearingAidService;


    private AdapterService mAdapterService;
    private static HearingAidService sHearingAidService;
    private DatabaseManager mDatabaseManager;
    private HandlerThread mStateMachinesThread;
    private BluetoothDevice mActiveDevice;


    @VisibleForTesting HearingAidNativeInterface mHearingAidNativeInterface;
    private final AdapterService mAdapterService;
    @VisibleForTesting AudioManager mAudioManager;
    private final DatabaseManager mDatabaseManager;
    private final HearingAidNativeInterface mNativeInterface;
    private final AudioManager mAudioManager;
    private final HandlerThread mStateMachinesThread;
    private final Looper mStateMachinesLooper;
    private final Handler mHandler;


    private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = new HashMap<>();
    private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = new HashMap<>();
    private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>();
    private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>();
    private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>();
    private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>();
    private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;

    private Handler mHandler = new Handler(Looper.getMainLooper());
    private final AudioManagerOnAudioDevicesAddedCallback mAudioManagerOnAudioDevicesAddedCallback =
    private final AudioManagerOnAudioDevicesAddedCallback mAudioManagerOnAudioDevicesAddedCallback =
            new AudioManagerOnAudioDevicesAddedCallback();
            new AudioManagerOnAudioDevicesAddedCallback();
    private final AudioManagerOnAudioDevicesRemovedCallback
    private final AudioManagerOnAudioDevicesRemovedCallback
            mAudioManagerOnAudioDevicesRemovedCallback =
            mAudioManagerOnAudioDevicesRemovedCallback =
                    new AudioManagerOnAudioDevicesRemovedCallback();
                    new AudioManagerOnAudioDevicesRemovedCallback();


    public HearingAidService(Context ctx) {
    private BluetoothDevice mActiveDevice;
        super(ctx);
    private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;

    public HearingAidService(AdapterService adapterService) {
        this(adapterService, null, HearingAidNativeInterface.getInstance());
    }

    @VisibleForTesting
    HearingAidService(
            AdapterService adapterService,
            Looper looper,
            HearingAidNativeInterface nativeInterface) {
        super(requireNonNull(adapterService));
        mAdapterService = adapterService;
        mDatabaseManager = requireNonNull(mAdapterService.getDatabase());
        if (looper == null) {
            mHandler = new Handler(requireNonNull(Looper.getMainLooper()));
            mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
            mStateMachinesThread.start();
            mStateMachinesLooper = mStateMachinesThread.getLooper();
        } else {
            mHandler = new Handler(looper);
            mStateMachinesThread = null;
            mStateMachinesLooper = looper;
        }
        mNativeInterface = requireNonNull(nativeInterface);
        mAudioManager = requireNonNull(getSystemService(AudioManager.class));

        setHearingAidService(this);
        mNativeInterface.init();
    }
    }


    public static boolean isEnabled() {
    public static boolean isEnabled() {
@@ -105,56 +134,11 @@ public class HearingAidService extends ProfileService {
        return new BluetoothHearingAidBinder(this);
        return new BluetoothHearingAidBinder(this);
    }
    }


    @Override
    public void start() {
        Log.d(TAG, "start()");
        if (sHearingAidService != null) {
            throw new IllegalStateException("start() called twice");
        }

        mAdapterService =
                Objects.requireNonNull(
                        AdapterService.getAdapterService(),
                        "AdapterService cannot be null when HearingAidService starts");
        mHearingAidNativeInterface =
                Objects.requireNonNull(
                        HearingAidNativeInterface.getInstance(),
                        "HearingAidNativeInterface cannot be null when HearingAidService starts");
        mDatabaseManager =
                Objects.requireNonNull(
                        mAdapterService.getDatabase(),
                        "DatabaseManager cannot be null when HearingAidService starts");
        mAudioManager = getSystemService(AudioManager.class);
        Objects.requireNonNull(
                mAudioManager, "AudioManager cannot be null when HearingAidService starts");

        // Start handler thread for state machines
        mStateMachines.clear();
        mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
        mStateMachinesThread.start();

        // Clear HiSyncId map, capabilities map and HiSyncId Connected map
        mDeviceHiSyncIdMap.clear();
        mDeviceCapabilitiesMap.clear();
        mHiSyncIdConnectedMap.clear();

        // Mark service as started
        setHearingAidService(this);

        // Initialize native interface
        mHearingAidNativeInterface.init();
    }

    @Override
    @Override
    public void stop() {
    public void stop() {
        Log.d(TAG, "stop()");
        Log.d(TAG, "stop()");
        if (sHearingAidService == null) {
            Log.w(TAG, "stop() called before start()");
            return;
        }
        // Cleanup native interface
        // Cleanup native interface
        mHearingAidNativeInterface.cleanup();
        mNativeInterface.cleanup();
        mHearingAidNativeInterface = null;


        // Mark service as stopped
        // Mark service as stopped
        setHearingAidService(null);
        setHearingAidService(null);
@@ -163,7 +147,6 @@ public class HearingAidService extends ProfileService {
        synchronized (mStateMachines) {
        synchronized (mStateMachines) {
            for (HearingAidStateMachine sm : mStateMachines.values()) {
            for (HearingAidStateMachine sm : mStateMachines.values()) {
                sm.doQuit();
                sm.doQuit();
                sm.cleanup();
            }
            }
            mStateMachines.clear();
            mStateMachines.clear();
        }
        }
@@ -177,29 +160,15 @@ public class HearingAidService extends ProfileService {
            try {
            try {
                mStateMachinesThread.quitSafely();
                mStateMachinesThread.quitSafely();
                mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
                mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
                mStateMachinesThread = null;
            } catch (InterruptedException e) {
            } catch (InterruptedException e) {
                // Do not rethrow as we are shutting down anyway
                // Do not rethrow as we are shutting down anyway
            }
            }
        }
        }


        if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
        mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }


        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesAddedCallback);
        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesAddedCallback);
        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesRemovedCallback);
        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesRemovedCallback);

        // Clear AdapterService, HearingAidNativeInterface
        mAudioManager = null;
        mHearingAidNativeInterface = null;
        mAdapterService = null;
    }

    @Override
    public void cleanup() {
        Log.d(TAG, "cleanup()");
    }
    }


    /**
    /**
@@ -263,7 +232,7 @@ public class HearingAidService extends ProfileService {
            if (smConnect == null) {
            if (smConnect == null) {
                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
            }
            }
            smConnect.sendMessage(HearingAidStateMachine.CONNECT);
            smConnect.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT);
        }
        }


        for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
        for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
@@ -279,7 +248,7 @@ public class HearingAidService extends ProfileService {
                        Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
                        Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
                        continue;
                        continue;
                    }
                    }
                    sm.sendMessage(HearingAidStateMachine.CONNECT);
                    sm.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT);
                }
                }
                if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
                if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
                        && !device.equals(storedDevice)) {
                        && !device.equals(storedDevice)) {
@@ -316,7 +285,7 @@ public class HearingAidService extends ProfileService {
                                "Ignored disconnect request for " + device + " : no state machine");
                                "Ignored disconnect request for " + device + " : no state machine");
                        continue;
                        continue;
                    }
                    }
                    sm.sendMessage(HearingAidStateMachine.DISCONNECT);
                    sm.sendMessage(HearingAidStateMachine.MESSAGE_DISCONNECT);
                }
                }
                if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
                if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
                        && !device.equals(storedDevice)) {
                        && !device.equals(storedDevice)) {
@@ -402,7 +371,7 @@ public class HearingAidService extends ProfileService {
                if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) {
                if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) {
                    continue;
                    continue;
                }
                }
                int connectionState = BluetoothProfile.STATE_DISCONNECTED;
                int connectionState = STATE_DISCONNECTED;
                HearingAidStateMachine sm = mStateMachines.get(device);
                HearingAidStateMachine sm = mStateMachines.get(device);
                if (sm != null) {
                if (sm != null) {
                    connectionState = sm.getConnectionState();
                    connectionState = sm.getConnectionState();
@@ -457,7 +426,7 @@ public class HearingAidService extends ProfileService {
        synchronized (mStateMachines) {
        synchronized (mStateMachines) {
            HearingAidStateMachine sm = mStateMachines.get(device);
            HearingAidStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
            if (sm == null) {
                return BluetoothProfile.STATE_DISCONNECTED;
                return STATE_DISCONNECTED;
            }
            }
            return sm.getConnectionState();
            return sm.getConnectionState();
        }
        }
@@ -507,7 +476,7 @@ public class HearingAidService extends ProfileService {
    }
    }


    void setVolume(int volume) {
    void setVolume(int volume) {
        mHearingAidNativeInterface.setVolume(volume);
        mNativeInterface.setVolume(volume);
    }
    }


    public long getHiSyncId(BluetoothDevice device) {
    public long getHiSyncId(BluetoothDevice device) {
@@ -567,7 +536,7 @@ public class HearingAidService extends ProfileService {
                return true;
                return true;
            }
            }


            if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
            if (getConnectionState(device) != STATE_CONNECTED) {
                Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected");
                Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected");
                return false;
                return false;
            }
            }
@@ -597,7 +566,7 @@ public class HearingAidService extends ProfileService {
                return activeDevices;
                return activeDevices;
            }
            }
            for (BluetoothDevice device : mDeviceHiSyncIdMap.keySet()) {
            for (BluetoothDevice device : mDeviceHiSyncIdMap.keySet()) {
                if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
                if (getConnectionState(device) != STATE_CONNECTED) {
                    continue;
                    continue;
                }
                }
                if (mDeviceHiSyncIdMap.get(device) == mActiveDeviceHiSyncId) {
                if (mDeviceHiSyncIdMap.get(device) == mActiveDeviceHiSyncId) {
@@ -639,8 +608,8 @@ public class HearingAidService extends ProfileService {
            if (sm == null) {
            if (sm == null) {
                if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
                if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
                    switch (stackEvent.valueInt1) {
                    switch (stackEvent.valueInt1) {
                        case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
                        case STATE_CONNECTED:
                        case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
                        case STATE_CONNECTING:
                            sm = getOrCreateStateMachine(device);
                            sm = getOrCreateStateMachine(device);
                            break;
                            break;
                        default:
                        default:
@@ -652,7 +621,7 @@ public class HearingAidService extends ProfileService {
                Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
                Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
                return;
                return;
            }
            }
            sm.sendMessage(HearingAidStateMachine.STACK_EVENT, stackEvent);
            sm.sendMessage(HearingAidStateMachine.MESSAGE_STACK_EVENT, stackEvent);
        }
        }
    }
    }


@@ -721,12 +690,8 @@ public class HearingAidService extends ProfileService {
                return null;
                return null;
            }
            }
            Log.d(TAG, "Creating a new state machine for " + device);
            Log.d(TAG, "Creating a new state machine for " + device);
            sm =
            sm = new HearingAidStateMachine(this, device, mNativeInterface, mStateMachinesLooper);
                    HearingAidStateMachine.make(
            sm.start();
                            device,
                            this,
                            mHearingAidNativeInterface,
                            mStateMachinesThread.getLooper());
            mStateMachines.put(device, sm);
            mStateMachines.put(device, sm);
            return sm;
            return sm;
        }
        }
@@ -747,9 +712,7 @@ public class HearingAidService extends ProfileService {
        }
        }


        // Note: This is just a safety check for handling illegal call - setActiveDevice(null).
        // Note: This is just a safety check for handling illegal call - setActiveDevice(null).
        if (device == null
        if (device == null && stopAudio && getConnectionState(mActiveDevice) == STATE_CONNECTED) {
                && stopAudio
                && getConnectionState(mActiveDevice) == BluetoothProfile.STATE_CONNECTED) {
            Log.e(
            Log.e(
                    TAG,
                    TAG,
                    "Illegal arguments: stopAudio should be false when the active hearing aid "
                    "Illegal arguments: stopAudio should be false when the active hearing aid "
@@ -816,7 +779,7 @@ public class HearingAidService extends ProfileService {
            if (sm == null) {
            if (sm == null) {
                return;
                return;
            }
            }
            if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
            if (sm.getConnectionState() != STATE_DISCONNECTED) {
                Log.i(TAG, "Disconnecting device because it was unbonded.");
                Log.i(TAG, "Disconnecting device because it was unbonded.");
                disconnect(device);
                disconnect(device);
                return;
                return;
@@ -836,7 +799,6 @@ public class HearingAidService extends ProfileService {
            }
            }
            Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
            Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
            sm.doQuit();
            sm.doQuit();
            sm.cleanup();
            mStateMachines.remove(device);
            mStateMachines.remove(device);
        }
        }
    }
    }
@@ -863,7 +825,7 @@ public class HearingAidService extends ProfileService {
                            + toState);
                            + toState);
            return;
            return;
        }
        }
        if (toState == BluetoothProfile.STATE_CONNECTED) {
        if (toState == STATE_CONNECTED) {
            long myHiSyncId = getHiSyncId(device);
            long myHiSyncId = getHiSyncId(device);
            if (myHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
            if (myHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
                    || getConnectedPeerDevices(myHiSyncId).size() == 1) {
                    || getConnectedPeerDevices(myHiSyncId).size() == 1) {
@@ -876,13 +838,13 @@ public class HearingAidService extends ProfileService {
                mHiSyncIdConnectedMap.put(myHiSyncId, true);
                mHiSyncIdConnectedMap.put(myHiSyncId, true);
            }
            }
        }
        }
        if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) {
        if (fromState == STATE_CONNECTED && getConnectedDevices().isEmpty()) {
            long myHiSyncId = getHiSyncId(device);
            long myHiSyncId = getHiSyncId(device);
            mHiSyncIdConnectedMap.put(myHiSyncId, false);
            mHiSyncIdConnectedMap.put(myHiSyncId, false);
            // ActiveDeviceManager will call removeActiveDevice().
            // ActiveDeviceManager will call removeActiveDevice().
        }
        }
        // Check if the device is disconnected - if unbond, remove the state machine
        // Check if the device is disconnected - if unbond, remove the state machine
        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
        if (toState == STATE_DISCONNECTED) {
            int bondState = mAdapterService.getBondState(device);
            int bondState = mAdapterService.getBondState(device);
            if (bondState == BluetoothDevice.BOND_NONE) {
            if (bondState == BluetoothDevice.BOND_NONE) {
                Log.d(TAG, device + " is unbond. Remove state machine");
                Log.d(TAG, device + " is unbond. Remove state machine");
@@ -980,7 +942,7 @@ public class HearingAidService extends ProfileService {
        public int getConnectionState(BluetoothDevice device, AttributionSource source) {
        public int getConnectionState(BluetoothDevice device, AttributionSource source) {
            HearingAidService service = getService(source);
            HearingAidService service = getService(source);
            if (service == null) {
            if (service == null) {
                return BluetoothProfile.STATE_DISCONNECTED;
                return STATE_DISCONNECTED;
            }
            }


            return service.getConnectionState(device);
            return service.getConnectionState(device);
+1 −8
Original line number Original line Diff line number Diff line
@@ -22,19 +22,12 @@ import android.bluetooth.BluetoothDevice;
 * Stack event sent via a callback from JNI to Java, or generated internally by the Hearing Aid
 * Stack event sent via a callback from JNI to Java, or generated internally by the Hearing Aid
 * State Machine.
 * State Machine.
 */
 */
public class HearingAidStackEvent {
class HearingAidStackEvent {
    // Event types for STACK_EVENT message (coming from native)
    // Event types for STACK_EVENT message (coming from native)
    private static final int EVENT_TYPE_NONE = 0;
    private static final int EVENT_TYPE_NONE = 0;
    public static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
    public static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
    public static final int EVENT_TYPE_DEVICE_AVAILABLE = 2;
    public static final int EVENT_TYPE_DEVICE_AVAILABLE = 2;


    // Do not modify without updating the HAL bt_hearing_aid.h files.
    // Match up with enum class ConnectionState of bt_hearing_aid.h.
    static final int CONNECTION_STATE_DISCONNECTED = 0;
    static final int CONNECTION_STATE_CONNECTING = 1;
    static final int CONNECTION_STATE_CONNECTED = 2;
    static final int CONNECTION_STATE_DISCONNECTING = 3;

    public int type;
    public int type;
    public BluetoothDevice device;
    public BluetoothDevice device;
    public int valueInt1;
    public int valueInt1;
+159 −216

File changed.

Preview size limit exceeded, changes collapsed.

+324 −883

File changed.

Preview size limit exceeded, changes collapsed.

+118 −199

File changed.

Preview size limit exceeded, changes collapsed.

Loading