Hearing Aid Service Stub
This is the Hearing Aid service in Java layer.
Bug: 64038649
Test: compilation; toggling the Bluetooth on/off.
Change-Id: Ib32f140d09dd33a9f49ecb91adb8e3cfd5eb3e86
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 73a4154..ae36f1e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -382,6 +382,14 @@
<action android:name="android.bluetooth.IBluetoothPbapClient" />
</intent-filter>
</service>
+ <service
+ android:process="@string/process"
+ android:name = ".hearingaid.HearingAidService"
+ android:enabled="@bool/profile_supported_hearing_aid">
+ <intent-filter>
+ <action android:name="android.bluetooth.IBluetoothHearingAid" />
+ </intent-filter>
+ </service>
<!-- Authenticator for PBAP account. -->
<service
android:process="@string/process"
diff --git a/res/values/config.xml b/res/values/config.xml
index 3595e56..1e5a03c 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -31,6 +31,7 @@
<bool name="profile_supported_pbapclient">false</bool>
<bool name="profile_supported_mapmce">false</bool>
<bool name="profile_supported_hid_device">true</bool>
+ <bool name="profile_supported_hearing_aid">true</bool>
<!-- If true, we will require location to be enabled on the device to
fire Bluetooth LE scan result callbacks in addition to having one
diff --git a/src/com/android/bluetooth/btservice/Config.java b/src/com/android/bluetooth/btservice/Config.java
index 927c929..4f027b3 100644
--- a/src/com/android/bluetooth/btservice/Config.java
+++ b/src/com/android/bluetooth/btservice/Config.java
@@ -29,6 +29,7 @@
import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
import com.android.bluetooth.gatt.GattService;
import com.android.bluetooth.hdp.HealthService;
+import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.hfpclient.HeadsetClientService;
import com.android.bluetooth.hid.HidDeviceService;
@@ -94,7 +95,9 @@
new ProfileConfig(BluetoothOppService.class, R.bool.profile_supported_opp,
(1 << BluetoothProfile.OPP)),
new ProfileConfig(BluetoothPbapService.class, R.bool.profile_supported_pbap,
- (1 << BluetoothProfile.PBAP))
+ (1 << BluetoothProfile.PBAP)),
+ new ProfileConfig(HearingAidService.class, R.bool.profile_supported_hearing_aid,
+ (1 << BluetoothProfile.HEARING_AID))
};
private static Class[] sSupportedProfiles = new Class[0];
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidService.java b/src/com/android/bluetooth/hearingaid/HearingAidService.java
new file mode 100644
index 0000000..cf495f7
--- /dev/null
+++ b/src/com/android/bluetooth/hearingaid/HearingAidService.java
@@ -0,0 +1,515 @@
+/*
+ * 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.BluetoothHearingAid;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.IBluetoothHearingAid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.HandlerThread;
+import android.os.ParcelUuid;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ProfileService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application.
+ * @hide
+ */
+public class HearingAidService extends ProfileService {
+ private static final boolean DBG = false;
+ private static final String TAG = "HearingAidService";
+
+ private static HearingAidService sHearingAidService;
+
+ private BluetoothAdapter mAdapter;
+ private AdapterService mAdapterService;
+ private HandlerThread mStateMachinesThread;
+
+ private BluetoothDevice mActiveDevice;
+
+ private final Map<BluetoothDevice, Integer> mDeviceMap = new HashMap<>();
+
+ private BroadcastReceiver mBondStateChangedReceiver;
+ private BroadcastReceiver mConnectionStateChangedReceiver;
+
+ @Override
+ protected IProfileServiceBinder initBinder() {
+ return new BluetoothHearingAidBinder(this);
+ }
+
+ @Override
+ protected void create() {
+ if (DBG) {
+ Log.d(TAG, "create()");
+ }
+ }
+
+ @Override
+ protected boolean start() {
+ if (DBG) {
+ Log.d(TAG, "start()");
+ }
+ if (sHearingAidService != null) {
+ throw new IllegalStateException("start() called twice");
+ }
+
+ // Get BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager.
+ // None of them can be null.
+ mAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter(),
+ "BluetoothAdapter cannot be null when HearingAidService starts");
+ mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
+ "AdapterService cannot be null when HearingAidService starts");
+ // TODO: Add native interface
+
+ // Start handler thread for state machines
+ // TODO: Clear state machines
+ mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
+ mStateMachinesThread.start();
+
+ // Initialize native interface
+ // TODO: Init native interface
+
+ // Setup broadcast receivers
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ mBondStateChangedReceiver = new BondStateChangedReceiver();
+ registerReceiver(mBondStateChangedReceiver, filter);
+ filter = new IntentFilter();
+ filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
+ mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
+ registerReceiver(mConnectionStateChangedReceiver, filter);
+
+ // Mark service as started
+ setHearingAidService(this);
+
+ // Clear active device
+ setActiveDevice(null);
+
+ return true;
+ }
+
+ @Override
+ protected boolean stop() {
+ if (DBG) {
+ Log.d(TAG, "stop()");
+ }
+ if (sHearingAidService == null) {
+ Log.w(TAG, "stop() called before start()");
+ return true;
+ }
+
+ // Clear active device
+ setActiveDevice(null);
+
+ // Mark service as stopped
+ setHearingAidService(null);
+
+ // Unregister broadcast receivers
+ unregisterReceiver(mBondStateChangedReceiver);
+ mBondStateChangedReceiver = null;
+ unregisterReceiver(mConnectionStateChangedReceiver);
+ mConnectionStateChangedReceiver = null;
+
+ // Cleanup native interface
+ // TODO: Cleanup native interface
+
+ // Destroy state machines and stop handler thread
+ // TODO: Implement me: destroy state machine
+ if (mStateMachinesThread != null) {
+ mStateMachinesThread.quitSafely();
+ mStateMachinesThread = null;
+ }
+
+ // Clear BluetoothAdapter, AdapterService, HearingAidNativeInterface
+ // TODO: Set native interface to null
+ mAdapterService = null;
+ mAdapter = null;
+
+ return true;
+ }
+
+ @Override
+ protected void cleanup() {
+ if (DBG) {
+ Log.d(TAG, "cleanup()");
+ }
+ }
+
+ /**
+ * Get the HearingAidService instance
+ * @return HearingAidService instance
+ */
+ public static synchronized HearingAidService getHearingAidService() {
+ if (sHearingAidService == null) {
+ Log.w(TAG, "getHearingAidService(): service is NULL");
+ return null;
+ }
+
+ if (!sHearingAidService.isAvailable()) {
+ Log.w(TAG, "getHearingAidService(): service is not available");
+ return null;
+ }
+ return sHearingAidService;
+ }
+
+ private static synchronized void setHearingAidService(HearingAidService instance) {
+ if (DBG) {
+ Log.d(TAG, "setHearingAidService(): set to: " + instance);
+ }
+ sHearingAidService = instance;
+ }
+
+ boolean connect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+ if (DBG) {
+ Log.d(TAG, "connect(): " + device);
+ }
+
+ if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
+ return false;
+ }
+ ParcelUuid[] featureUuids = device.getUuids();
+ if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
+ Log.e(TAG, "Cannot connect to " + device + " : Remote does not have HearingAid UUID");
+ return false;
+ }
+
+ // TODO: Implement me
+ return false;
+ }
+
+ boolean disconnect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+ if (DBG) {
+ Log.d(TAG, "disconnect(): " + device);
+ }
+
+ // TODO: Implement me
+ return false;
+ }
+
+ List<BluetoothDevice> getConnectedDevices() {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ // TODO: Implement me
+ return new ArrayList<>();
+ }
+
+ /**
+ * Check whether can connect to a peer device.
+ * The check considers a number of factors during the evaluation.
+ *
+ * @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");
+ }
+
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ // TODO: Implement me
+ return new ArrayList<>();
+ }
+
+ /**
+ * Get the list of devices that have state machines.
+ *
+ * @return the list of devices that have state machines
+ */
+ @VisibleForTesting
+ List<BluetoothDevice> getDevices() {
+ // TODO: Implement me
+ return new ArrayList<>();
+ }
+
+ int getConnectionState(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ // TODO: Implement me
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ /**
+ * Set the active device.
+ *
+ * @param device the active device
+ * @return true on success, otherwise false
+ */
+ public synchronized boolean setActiveDevice(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+ // TODO: Implement me
+ return false;
+ }
+
+ /**
+ * Get the active device.
+ *
+ * @return the active device or null if no device is active
+ */
+ public synchronized BluetoothDevice getActiveDevice() {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ throw new IllegalStateException("Implement me");
+ }
+
+ private synchronized boolean isActiveDevice(BluetoothDevice device) {
+ throw new IllegalStateException("Implement me");
+ }
+
+ /**
+ * Set the priority of the Hearing Aid profile.
+ *
+ * @param device the remote device
+ * @param priority the priority of the profile
+ * @return true on success, otherwise false
+ */
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
+ Settings.Global.putInt(getContentResolver(),
+ Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()), priority);
+ if (DBG) {
+ Log.d(TAG, "Saved priority " + device + " = " + priority);
+ }
+ 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(),
+ Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ return priority;
+ }
+
+ private void broadcastActiveDevice(BluetoothDevice device) {
+ if (DBG) {
+ Log.d(TAG, "broadcastActiveDevice(" + device + ")");
+ }
+
+ Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+ }
+
+ // Remove state machine if the bonding for a device is removed
+ private class BondStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ 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
+ }
+ }
+
+ /**
+ * Process a change in the bonding state for a device.
+ *
+ * @param device the device whose bonding state has changed
+ * @param bondState the new bond state for the device. Possible values are:
+ * {@link BluetoothDevice#BOND_NONE},
+ * {@link BluetoothDevice#BOND_BONDING},
+ * {@link BluetoothDevice#BOND_BONDED}.
+ */
+ @VisibleForTesting
+ void bondStateChanged(BluetoothDevice device, int bondState) {
+ if (DBG) {
+ Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
+ }
+ // Remove state machine if the bonding for a device is removed
+ if (bondState != BluetoothDevice.BOND_NONE) {
+ return;
+ }
+ // TODO: Implement me
+ }
+
+ private synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
+ int toState) {
+ // TODO: Implement me
+ }
+
+ private class ConnectionStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+ int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ connectionStateChanged(device, fromState, toState);
+ }
+ }
+
+ /**
+ * Binder object: must be a static class or memory leak may occur
+ */
+ @VisibleForTesting
+ static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub
+ implements IProfileServiceBinder {
+ private HearingAidService mService;
+
+ private HearingAidService getService() {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "HearingAid call not allowed for non-active user");
+ return null;
+ }
+
+ if (mService != null && mService.isAvailable()) {
+ return mService;
+ }
+ return null;
+ }
+
+ BluetoothHearingAidBinder(HearingAidService svc) {
+ mService = svc;
+ }
+
+ @Override
+ public void cleanup() {
+ mService = null;
+ }
+
+ @Override
+ public boolean connect(BluetoothDevice device) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.connect(device);
+ }
+
+ @Override
+ public boolean disconnect(BluetoothDevice device) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.disconnect(device);
+ }
+
+ @Override
+ public List<BluetoothDevice> getConnectedDevices() {
+ HearingAidService service = getService();
+ if (service == null) {
+ return new ArrayList<>(0);
+ }
+ return service.getConnectedDevices();
+ }
+
+ @Override
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return new ArrayList<>(0);
+ }
+ return service.getDevicesMatchingConnectionStates(states);
+ }
+
+ @Override
+ public int getConnectionState(BluetoothDevice device) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ return service.getConnectionState(device);
+ }
+
+ @Override
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.setPriority(device, priority);
+ }
+
+ @Override
+ public int getPriority(BluetoothDevice device) {
+ HearingAidService service = getService();
+ if (service == null) {
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ return service.getPriority(device);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ }
+
+ @Override
+ public void adjustVolume(int direction) {
+ }
+
+ @Override
+ public int getVolume() {
+ return 0;
+ }
+
+ @Override
+ public long getHiSyncId(BluetoothDevice device) {
+ return 0;
+ }
+
+ @Override
+ public int getDeviceSide(BluetoothDevice device) {
+ return 0;
+ }
+
+ @Override
+ public int getDeviceMode(BluetoothDevice device) {
+ return 0;
+ }
+ }
+
+ @Override
+ public void dump(StringBuilder sb) {
+ super.dump(sb);
+ ProfileService.println(sb, "mActiveDevice: " + mActiveDevice);
+ }
+}