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

Commit 26cc0f13 authored by Hansong Zhang's avatar Hansong Zhang
Browse files

Hearing Aid State Machine without native interface

This is the state machine in Java layer. The native interface is not
hooked up yet.
It cannot handle native connect/disconnect event.

Bug: 64038649
Test: compilation and instrumentation test
Change-Id: I494af5b7cdfecde536fb034f2ebe5f889f888b32
(cherry picked from commit b1c893b36f1db91f93f5712d9ca6a1e1b37df73f)
parent bf823af4
Loading
Loading
Loading
Loading
+187 −34
Original line number Original line Diff line number Diff line
@@ -41,6 +41,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Map;
import java.util.Objects;
import java.util.Objects;
import java.util.Set;


/**
/**
 * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application.
 * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application.
@@ -58,8 +59,13 @@ public class HearingAidService extends ProfileService {


    private BluetoothDevice mActiveDevice;
    private BluetoothDevice mActiveDevice;


    private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines =
            new HashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceMap = new HashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceMap = new HashMap<>();


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

    private BroadcastReceiver mBondStateChangedReceiver;
    private BroadcastReceiver mBondStateChangedReceiver;
    private BroadcastReceiver mConnectionStateChangedReceiver;
    private BroadcastReceiver mConnectionStateChangedReceiver;


@@ -93,7 +99,7 @@ public class HearingAidService extends ProfileService {
        // TODO: Add native interface
        // TODO: Add native interface


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


@@ -145,7 +151,14 @@ public class HearingAidService extends ProfileService {
        // TODO: Cleanup native interface
        // TODO: Cleanup native interface


        // Destroy state machines and stop handler thread
        // Destroy state machines and stop handler thread
        // TODO: Implement me: destroy state machine
        synchronized (mStateMachines) {
            for (HearingAidStateMachine sm : mStateMachines.values()) {
                sm.doQuit();
                sm.cleanup();
            }
            mStateMachines.clear();
        }

        if (mStateMachinesThread != null) {
        if (mStateMachinesThread != null) {
            mStateMachinesThread.quitSafely();
            mStateMachinesThread.quitSafely();
            mStateMachinesThread = null;
            mStateMachinesThread = null;
@@ -205,9 +218,16 @@ public class HearingAidService extends ProfileService {
            return false;
            return false;
        }
        }


        // TODO: Implement me
        synchronized (mStateMachines) {
            HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
            if (smConnect == null) {
                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
                return false;
                return false;
            }
            }
            smConnect.sendMessage(HearingAidStateMachine.CONNECT);
            return true;
        }
    }


    boolean disconnect(BluetoothDevice device) {
    boolean disconnect(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
@@ -215,14 +235,34 @@ public class HearingAidService extends ProfileService {
            Log.d(TAG, "disconnect(): " + device);
            Log.d(TAG, "disconnect(): " + device);
        }
        }


        // TODO: Implement me
        int hiSyncId = mDeviceMap.get(device);
        return false;
        for (BluetoothDevice storedDevice : mDeviceMap.keySet()) {
            if (mDeviceMap.get(storedDevice) != hiSyncId) {
                continue;
            }
            synchronized (mStateMachines) {
                HearingAidStateMachine sm = mStateMachines.get(device);
                if (sm == null) {
                    Log.e(TAG, "Ignored disconnect request for " + device + " : no state machine");
                    continue;
                }
                sm.sendMessage(HearingAidStateMachine.DISCONNECT);
            }
        }
        return true;
    }
    }


    List<BluetoothDevice> getConnectedDevices() {
    List<BluetoothDevice> getConnectedDevices() {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        // TODO: Implement me
        synchronized (mStateMachines) {
        return new ArrayList<>();
            List<BluetoothDevice> devices = new ArrayList<>();
            for (HearingAidStateMachine sm : mStateMachines.values()) {
                if (sm.isConnected()) {
                    devices.add(sm.getDevice());
                }
            }
            return devices;
        }
    }
    }


    /**
    /**
@@ -232,14 +272,54 @@ public class HearingAidService extends ProfileService {
     * @param device the peer device to connect to
     * @param device the peer device to connect to
     * @return true if connection is allowed, otherwise false
     * @return true if connection is allowed, otherwise false
     */
     */
    boolean okToConnect(BluetoothDevice device) {
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
        throw new IllegalStateException("Implement me");
    public boolean okToConnect(BluetoothDevice device) {
        // Check if this is an incoming connection in Quiet mode.
        if (mAdapterService.isQuietModeEnabled()) {
            Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
            return false;
        }
        // Check priority and accept or reject the connection
        int priority = getPriority(device);
        int bondState = mAdapterService.getBondState(device);
        // Allow the connection only if the device is bonded or bonding.
        if ((priority == BluetoothProfile.PRIORITY_UNDEFINED)
                && (bondState == BluetoothDevice.BOND_NONE)) {
            Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority
                    + " bondState=" + bondState);
            return false;
        }
        if (priority <= BluetoothProfile.PRIORITY_OFF) {
            Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority);
            return false;
        }
        return true;
    }
    }


    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        // TODO: Implement me
        synchronized (mStateMachines) {
        return new ArrayList<>();
            List<BluetoothDevice> devices = new ArrayList<>();
            Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();

            for (BluetoothDevice device : bondedDevices) {
                ParcelUuid[] featureUuids = device.getUuids();
                if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
                    continue;
                }
                int connectionState = BluetoothProfile.STATE_DISCONNECTED;
                HearingAidStateMachine sm = mStateMachines.get(device);
                if (sm != null) {
                    connectionState = sm.getConnectionState();
                }
                for (int state : states) {
                    if (connectionState == state) {
                        devices.add(device);
                    }
                }
            }
            return devices;
        }
    }
    }


    /**
    /**
@@ -249,15 +329,25 @@ public class HearingAidService extends ProfileService {
     */
     */
    @VisibleForTesting
    @VisibleForTesting
    List<BluetoothDevice> getDevices() {
    List<BluetoothDevice> getDevices() {
        // TODO: Implement me
        List<BluetoothDevice> devices = new ArrayList<>();
        return new ArrayList<>();
        synchronized (mStateMachines) {
            for (HearingAidStateMachine sm : mStateMachines.values()) {
                devices.add(sm.getDevice());
            }
            return devices;
        }
    }
    }


    int getConnectionState(BluetoothDevice device) {
    int getConnectionState(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        // TODO: Implement me
        synchronized (mStateMachines) {
            HearingAidStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
                return BluetoothProfile.STATE_DISCONNECTED;
                return BluetoothProfile.STATE_DISCONNECTED;
            }
            }
            return sm.getConnectionState();
        }
    }


    /**
    /**
     * Set the active device.
     * Set the active device.
@@ -267,7 +357,7 @@ public class HearingAidService extends ProfileService {
     */
     */
    public synchronized boolean setActiveDevice(BluetoothDevice device) {
    public synchronized boolean setActiveDevice(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
        // TODO: Implement me

        return false;
        return false;
    }
    }


@@ -278,11 +368,15 @@ public class HearingAidService extends ProfileService {
     */
     */
    public synchronized BluetoothDevice getActiveDevice() {
    public synchronized BluetoothDevice getActiveDevice() {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        throw new IllegalStateException("Implement me");
        synchronized (mStateMachines) {
            return mActiveDevice;
        }
    }
    }


    private synchronized boolean isActiveDevice(BluetoothDevice device) {
    private synchronized boolean isActiveDevice(BluetoothDevice device) {
        throw new IllegalStateException("Implement me");
        synchronized (mStateMachines) {
            return (device != null) && Objects.equals(device, mActiveDevice);
        }
    }
    }


    /**
    /**
@@ -301,12 +395,7 @@ public class HearingAidService extends ProfileService {
        }
        }
        return true;
        return true;
    }
    }
    /**

     * Get the priority of the Hearing Aid profile.
     *
     * @param device the remote device
     * @return the profile priority
     */
    public int getPriority(BluetoothDevice device) {
    public int getPriority(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
        int priority = Settings.Global.getInt(getContentResolver(),
        int priority = Settings.Global.getInt(getContentResolver(),
@@ -315,6 +404,31 @@ public class HearingAidService extends ProfileService {
        return priority;
        return priority;
    }
    }


    private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
        if (device == null) {
            Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
            return null;
        }
        synchronized (mStateMachines) {
            HearingAidStateMachine sm = mStateMachines.get(device);
            if (sm != null) {
                return sm;
            }
            // Limit the maximum number of state machines to avoid DoS attack
            if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) {
                Log.e(TAG, "Maximum number of HearingAid state machines reached: "
                        + MAX_HEARING_AID_STATE_MACHINES);
                return null;
            }
            if (DBG) {
                Log.d(TAG, "Creating a new state machine for " + device);
            }
            sm = HearingAidStateMachine.make(device, this, mStateMachinesThread.getLooper());
            mStateMachines.put(device, sm);
            return sm;
        }
    }

    private void broadcastActiveDevice(BluetoothDevice device) {
    private void broadcastActiveDevice(BluetoothDevice device) {
        if (DBG) {
        if (DBG) {
            Log.d(TAG, "broadcastActiveDevice(" + device + ")");
            Log.d(TAG, "broadcastActiveDevice(" + device + ")");
@@ -337,13 +451,8 @@ public class HearingAidService extends ProfileService {
            int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
            int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
                                           BluetoothDevice.ERROR);
                                           BluetoothDevice.ERROR);
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if (DBG) {
            Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
                Log.d(TAG, "Bond state changed for device: " + device + " state: " + state);
            bondStateChanged(device, state);
            }
            if (state != BluetoothDevice.BOND_NONE) {
                return;
            }
            // TODO: Implement me
        }
        }
    }
    }


@@ -365,12 +474,45 @@ public class HearingAidService extends ProfileService {
        if (bondState != BluetoothDevice.BOND_NONE) {
        if (bondState != BluetoothDevice.BOND_NONE) {
            return;
            return;
        }
        }
        // TODO: Implement me
        synchronized (mStateMachines) {
            HearingAidStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
                return;
            }
            if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
                return;
            }
            removeStateMachine(device);
        }
    }

    private void removeStateMachine(BluetoothDevice device) {
        synchronized (mStateMachines) {
            HearingAidStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
                Log.w(TAG, "removeStateMachine: device " + device
                        + " does not have a state machine");
                return;
            }
            Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
            sm.doQuit();
            sm.cleanup();
            mStateMachines.remove(device);
        }
    }
    }


    private synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
    private synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
                                                     int toState) {
                                                     int toState) {
        // TODO: Implement me
        if ((device == null) || (fromState == toState)) {
            return;
        }
        // Check if the device is disconnected - if unbond, remove the state machine
        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
            int bondState = mAdapterService.getBondState(device);
            if (bondState == BluetoothDevice.BOND_NONE) {
                removeStateMachine(device);
            }
        }
    }
    }


    private class ConnectionStateChangedReceiver extends BroadcastReceiver {
    private class ConnectionStateChangedReceiver extends BroadcastReceiver {
@@ -406,6 +548,14 @@ public class HearingAidService extends ProfileService {
            return null;
            return null;
        }
        }


        @VisibleForTesting
        HearingAidService getServiceForTesting() {
            if (mService != null && mService.isAvailable()) {
                return mService;
            }
            return null;
        }

        BluetoothHearingAidBinder(HearingAidService svc) {
        BluetoothHearingAidBinder(HearingAidService svc) {
            mService = svc;
            mService = svc;
        }
        }
@@ -511,5 +661,8 @@ public class HearingAidService extends ProfileService {
    public void dump(StringBuilder sb) {
    public void dump(StringBuilder sb) {
        super.dump(sb);
        super.dump(sb);
        ProfileService.println(sb, "mActiveDevice: " + mActiveDevice);
        ProfileService.println(sb, "mActiveDevice: " + mActiveDevice);
        for (HearingAidStateMachine sm : mStateMachines.values()) {
            sm.dump(sb);
        }
    }
    }
}
}
+377 −0

File added.

Preview size limit exceeded, changes collapsed.

+97 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright 2018 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.hearingaid;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.HandlerThread;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;

import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;

import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class HearingAidStateMachineTest {
    private BluetoothAdapter mAdapter;
    private Context mTargetContext;
    private HandlerThread mHandlerThread;
    private HearingAidStateMachine mHearingAidStateMachine;
    private BluetoothDevice mTestDevice;
    private static final int TIMEOUT_MS = 1000;

    @Mock private AdapterService mAdapterService;
    @Mock private HearingAidService mHearingAidService;

    @Before
    public void setUp() throws Exception {
        mTargetContext = InstrumentationRegistry.getTargetContext();
        Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
                mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
        // Set up mocks and test assets
        MockitoAnnotations.initMocks(this);
        TestUtils.setAdapterService(mAdapterService);

        mAdapter = BluetoothAdapter.getDefaultAdapter();

        // Get a device for testing
        mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");

        // Set up thread and looper
        mHandlerThread = new HandlerThread("HearingAidStateMachineTestHandlerThread");
        mHandlerThread.start();
        mHearingAidStateMachine = new HearingAidStateMachine(mTestDevice, mHearingAidService,
                mHandlerThread.getLooper());
        // Override the timeout value to speed up the test
        mHearingAidStateMachine.sConnectTimeoutMs = 1000;     // 1s
        mHearingAidStateMachine.start();
    }

    @After
    public void tearDown() throws Exception {
        if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid)) {
            return;
        }
        mHearingAidStateMachine.doQuit();
        mHandlerThread.quit();
        TestUtils.clearAdapterService(mAdapterService);
    }

    /**
     * Test that default state is disconnected
     */
    @Test
    public void testDefaultDisconnectedState() {
        Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
                mHearingAidStateMachine.getConnectionState());
    }

}