Loading src/com/android/bluetooth/hearingaid/HearingAidService.java +188 −60 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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(); Loading @@ -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; Loading Loading @@ -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"); Loading @@ -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; } } /** Loading @@ -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; } } /** Loading @@ -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. Loading @@ -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; } Loading @@ -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); } } /** Loading @@ -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(), Loading @@ -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) { Loading @@ -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); } } Loading @@ -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 { Loading Loading @@ -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; } Loading Loading @@ -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 Loading Loading @@ -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); } } } src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java 0 → 100644 +377 −0 File added.Preview size limit exceeded, changes collapsed. Show changes tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java 0 → 100644 +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()); } } Loading
src/com/android/bluetooth/hearingaid/HearingAidService.java +188 −60 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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(); Loading @@ -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; Loading Loading @@ -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"); Loading @@ -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; } } /** Loading @@ -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; } } /** Loading @@ -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. Loading @@ -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; } Loading @@ -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); } } /** Loading @@ -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(), Loading @@ -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) { Loading @@ -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); } } Loading @@ -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 { Loading Loading @@ -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; } Loading Loading @@ -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 Loading Loading @@ -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); } } }
src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java 0 → 100644 +377 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java 0 → 100644 +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()); } }