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

Commit 6258f843 authored by Hansong Zhang's avatar Hansong Zhang Committed by android-build-merger
Browse files

Merge "Hearing Aid State Machine without native interface"

am: 1b581d64

Change-Id: I85c73f6437baa1bf875bec278fed2cdee197cece
parents 265cf1ce 1b581d64
Loading
Loading
Loading
Loading
+188 −60
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
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;
@@ -42,6 +41,7 @@ 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,8 +58,13 @@ 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;

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

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


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

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

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

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

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

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

@@ -206,9 +218,16 @@ public class HearingAidService extends ProfileService {
            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;
            }
            smConnect.sendMessage(HearingAidStateMachine.CONNECT);
            return true;
        }
    }

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

        // TODO: Implement me
        return false;
        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;
    }

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

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

    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        // TODO: Implement me
        return new ArrayList<>();
        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;
        }
    }

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

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

    /**
     * Set the active device.
@@ -268,7 +357,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;
    }

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

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

    /**
@@ -302,12 +395,7 @@ 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(),
@@ -316,28 +404,29 @@ public class HearingAidService extends ProfileService {
        return priority;
    }

    synchronized boolean isHearingAidPlaying(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        if (DBG) {
            Log.d(TAG, "isHearingAidPlaying(" + device + ")");
    private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
        if (device == null) {
            Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
            return null;
        }
        throw new IllegalStateException("Implement me");
        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;
            }

    /**
     * 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, "getCodecStatus(" + device + ")");
                Log.d(TAG, "Creating a new state machine for " + device);
            }
            sm = HearingAidStateMachine.make(device, this, mStateMachinesThread.getLooper());
            mStateMachines.put(device, sm);
            return sm;
        }
        throw new IllegalStateException("Implement me");
    }

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

@@ -390,12 +474,45 @@ public class HearingAidService extends ProfileService {
        if (bondState != BluetoothDevice.BOND_NONE) {
            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,
                                                     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 {
@@ -429,6 +546,14 @@ public class HearingAidService extends ProfileService {
            return null;
        }

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

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

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

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

        @Override
@@ -536,5 +661,8 @@ 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);
        }
    }
}
+377 −0

File added.

Preview size limit exceeded, changes collapsed.

+97 −0
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());
    }

}