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

Commit c9f9f02a authored by Hansong Zhang's avatar Hansong Zhang Committed by Pavlin Radoslavov
Browse files

Revert "Hearing Aid State Machine without native interface"

This reverts commit 3e1b61a6f8397046cb2a65e20ba251bfdf343444.

Reason for revert: Service shutdown not working
Bug: 74511352
Bug: 69623109
Test: Manual
Change-Id: I0a1a4d98104b7d7c0dac85464236b61cbb5a44a8

(cherry picked from commit 59d340a991edea214c63a3e657255e6cad572583)
parent e02f912c
Loading
Loading
Loading
Loading
+60 −188
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.bluetooth.hearingaid;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
@@ -41,7 +42,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

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

    private BluetoothDevice mActiveDevice;

    private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines =
            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 mConnectionStateChangedReceiver;

@@ -98,13 +93,12 @@ public class HearingAidService extends ProfileService {
        // TODO: Add native interface

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


        // Step 3: Initialize native interface
        // TODO: Implement me
        // TODO: Init native interface

        // Step 4: Setup broadcast receivers
        IntentFilter filter = new IntentFilter();
@@ -148,16 +142,10 @@ public class HearingAidService extends ProfileService {
        mConnectionStateChangedReceiver = null;

        // Step 3: Cleanup native interface
        // TODO: Implement me
        // TODO: Cleanup native interface

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

        if (mStateMachinesThread != null) {
            mStateMachinesThread.quitSafely();
@@ -165,7 +153,7 @@ public class HearingAidService extends ProfileService {
        }

        // Step 1: Clear BluetoothAdapter, AdapterService, HearingAidNativeInterface
        // TODO: Add native interface
        // TODO: Set native interface to null
        mAdapterService = null;
        mAdapter = null;

@@ -218,16 +206,9 @@ public class HearingAidService extends ProfileService {
            return false;
        }

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

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

        int hiSyncId = mDeviceMap.get(device);
        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;
        // TODO: Implement me
        return false;
    }

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

    /**
@@ -272,54 +233,14 @@ public class HearingAidService extends ProfileService {
     * @param device the peer device to connect to
     * @return true if connection is allowed, otherwise false
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    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;
    boolean okToConnect(BluetoothDevice device) {
        throw new IllegalStateException("Implement me");
    }

    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        synchronized (mStateMachines) {
            List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
            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 i = 0; i < states.length; i++) {
                    if (connectionState == states[i]) {
                        devices.add(device);
                    }
                }
            }
            return devices;
        }
        // TODO: Implement me
        return new ArrayList<>();
    }

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

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

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

        // TODO: Implement me
        return false;
    }

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

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

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

    /**
     * Get the priority of the Hearing Aid profile.
     *
     * @param device the remote device
     * @return the profile priority
     */
    public int getPriority(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
        int priority = Settings.Global.getInt(getContentResolver(),
@@ -404,29 +316,28 @@ public class HearingAidService extends ProfileService {
        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;
    synchronized boolean isHearingAidPlaying(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        if (DBG) {
            Log.d(TAG, "isHearingAidPlaying(" + device + ")");
        }
            // 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;
        throw new IllegalStateException("Implement me");
    }

    /**
     * Gets the current codec status (configuration and capability).
     *
     * @param device the remote Bluetooth device. If null, use the currect
     * active HearingAid Bluetooth device.
     * @return the current codec status
     * @hide
     */
    public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        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;
            Log.d(TAG, "getCodecStatus(" + device + ")");
        }
        throw new IllegalStateException("Implement me");
    }

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

@@ -474,45 +390,12 @@ public class HearingAidService extends ProfileService {
        if (bondState != BluetoothDevice.BOND_NONE) {
            return;
        }
        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);
        }
        // TODO: Implement me
    }

    private synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
                                                     int toState) {
        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);
            }
        }
        // TODO: Implement me
    }

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

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

        BluetoothHearingAidBinder(HearingAidService svc) {
            mService = svc;
        }
@@ -628,12 +503,12 @@ public class HearingAidService extends ProfileService {

        @Override
        public void setVolume(int volume) {
            // Android sends value in scale 0 to 25, hearing aid accept -128 to 0
            volume = ((volume * 512) / 100) - 128;
            // TODO: Implement me
        }

        @Override
        public void adjustVolume(int direction) {
            // TODO: Implement me
        }

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

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −97
Original line number 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;    // 1s

    @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());
    }

}