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);
+    }
+}