1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.bluetooth;
18 
19 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
20 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL;
21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
22 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
23 
24 import android.bluetooth.BluetoothAdapter;
25 import android.bluetooth.BluetoothClass;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothHearingAid;
28 import android.bluetooth.BluetoothProfile;
29 import android.content.Context;
30 import android.util.Log;
31 
32 import androidx.annotation.IntDef;
33 import androidx.annotation.NonNull;
34 
35 import com.android.settingslib.R;
36 import com.android.settingslib.Utils;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 public class HearingAidProfile implements LocalBluetoothProfile {
44     @Retention(RetentionPolicy.SOURCE)
45     @IntDef({
46             DeviceSide.SIDE_INVALID,
47             DeviceSide.SIDE_LEFT,
48             DeviceSide.SIDE_RIGHT
49     })
50 
51     /** Side definition for hearing aids. See {@link BluetoothHearingAid}. */
52     public @interface DeviceSide {
53         int SIDE_INVALID = -1;
54         int SIDE_LEFT = 0;
55         int SIDE_RIGHT = 1;
56     }
57 
58     @Retention(RetentionPolicy.SOURCE)
59     @IntDef({
60             DeviceMode.MODE_INVALID,
61             DeviceMode.MODE_MONAURAL,
62             DeviceMode.MODE_BINAURAL
63     })
64 
65     /** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */
66     public @interface DeviceMode {
67         int MODE_INVALID = -1;
68         int MODE_MONAURAL = 0;
69         int MODE_BINAURAL = 1;
70     }
71 
72     private static final String TAG = "HearingAidProfile";
73     private static boolean V = true;
74 
75     private Context mContext;
76 
77     private BluetoothHearingAid mService;
78     private boolean mIsProfileReady;
79 
80     private final CachedBluetoothDeviceManager mDeviceManager;
81 
82     static final String NAME = "HearingAid";
83     private final LocalBluetoothProfileManager mProfileManager;
84     private final BluetoothAdapter mBluetoothAdapter;
85 
86     // Order of this profile in device profiles list
87     private static final int ORDINAL = 1;
88 
89     // These callbacks run on the main thread.
90     private final class HearingAidServiceListener
91             implements BluetoothProfile.ServiceListener {
92 
onServiceConnected(int profile, BluetoothProfile proxy)93         public void onServiceConnected(int profile, BluetoothProfile proxy) {
94             mService = (BluetoothHearingAid) proxy;
95             // We just bound to the service, so refresh the UI for any connected HearingAid devices.
96             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
97             while (!deviceList.isEmpty()) {
98                 BluetoothDevice nextDevice = deviceList.remove(0);
99                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
100                 // we may add a new device here, but generally this should not happen
101                 if (device == null) {
102                     if (V) {
103                         Log.d(TAG, "HearingAidProfile found new device: " + nextDevice);
104                     }
105                     device = mDeviceManager.addDevice(nextDevice);
106                 }
107                 device.onProfileStateChanged(HearingAidProfile.this,
108                         BluetoothProfile.STATE_CONNECTED);
109                 device.refresh();
110             }
111 
112             // Check current list of CachedDevices to see if any are Hearing Aid devices.
113             mDeviceManager.updateHearingAidsDevices();
114             mIsProfileReady = true;
115             mProfileManager.callServiceConnectedListeners();
116         }
117 
onServiceDisconnected(int profile)118         public void onServiceDisconnected(int profile) {
119             mIsProfileReady = false;
120             mProfileManager.callServiceDisconnectedListeners();
121         }
122     }
123 
isProfileReady()124     public boolean isProfileReady() {
125         return mIsProfileReady;
126     }
127 
128     @Override
getProfileId()129     public int getProfileId() {
130         return BluetoothProfile.HEARING_AID;
131     }
132 
HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)133     HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager,
134             LocalBluetoothProfileManager profileManager) {
135         mContext = context;
136         mDeviceManager = deviceManager;
137         mProfileManager = profileManager;
138         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
139         mBluetoothAdapter.getProfileProxy(context,
140                 new HearingAidServiceListener(), BluetoothProfile.HEARING_AID);
141     }
142 
accessProfileEnabled()143     public boolean accessProfileEnabled() {
144         return false;
145     }
146 
isAutoConnectable()147     public boolean isAutoConnectable() {
148         return true;
149     }
150 
151     /**
152      * Get Hearing Aid devices matching connection states{
153      * @code BluetoothProfile.STATE_CONNECTED,
154      * @code BluetoothProfile.STATE_CONNECTING,
155      * @code BluetoothProfile.STATE_DISCONNECTING}
156      *
157      * @return Matching device list
158      */
getConnectedDevices()159     public List<BluetoothDevice> getConnectedDevices() {
160         return getDevicesByStates(new int[] {
161                 BluetoothProfile.STATE_CONNECTED,
162                 BluetoothProfile.STATE_CONNECTING,
163                 BluetoothProfile.STATE_DISCONNECTING});
164     }
165 
166     /**
167      * Get Hearing Aid devices matching connection states{
168      * @code BluetoothProfile.STATE_DISCONNECTED,
169      * @code BluetoothProfile.STATE_CONNECTED,
170      * @code BluetoothProfile.STATE_CONNECTING,
171      * @code BluetoothProfile.STATE_DISCONNECTING}
172      *
173      * @return Matching device list
174      */
getConnectableDevices()175     public List<BluetoothDevice> getConnectableDevices() {
176         return getDevicesByStates(new int[] {
177                 BluetoothProfile.STATE_DISCONNECTED,
178                 BluetoothProfile.STATE_CONNECTED,
179                 BluetoothProfile.STATE_CONNECTING,
180                 BluetoothProfile.STATE_DISCONNECTING});
181     }
182 
getDevicesByStates(int[] states)183     private List<BluetoothDevice> getDevicesByStates(int[] states) {
184         if (mService == null) {
185             return new ArrayList<BluetoothDevice>(0);
186         }
187         return mService.getDevicesMatchingConnectionStates(states);
188     }
189 
getConnectionStatus(BluetoothDevice device)190     public int getConnectionStatus(BluetoothDevice device) {
191         if (mService == null) {
192             return BluetoothProfile.STATE_DISCONNECTED;
193         }
194         return mService.getConnectionState(device);
195     }
196 
setActiveDevice(BluetoothDevice device)197     public boolean setActiveDevice(BluetoothDevice device) {
198         if (mBluetoothAdapter == null) {
199             return false;
200         }
201         int profiles = Utils.isAudioModeOngoingCall(mContext)
202                 ? ACTIVE_DEVICE_PHONE_CALL
203                 : ACTIVE_DEVICE_AUDIO;
204         return device == null
205                 ? mBluetoothAdapter.removeActiveDevice(profiles)
206                 : mBluetoothAdapter.setActiveDevice(device, profiles);
207     }
208 
getActiveDevices()209     public List<BluetoothDevice> getActiveDevices() {
210         if (mBluetoothAdapter == null) {
211             return new ArrayList<>();
212         }
213         return mBluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
214     }
215 
216     @Override
isEnabled(BluetoothDevice device)217     public boolean isEnabled(BluetoothDevice device) {
218         if (mService == null || device == null) {
219             return false;
220         }
221         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
222     }
223 
224     @Override
getConnectionPolicy(BluetoothDevice device)225     public int getConnectionPolicy(BluetoothDevice device) {
226         if (mService == null || device == null) {
227             return CONNECTION_POLICY_FORBIDDEN;
228         }
229         return mService.getConnectionPolicy(device);
230     }
231 
232     @Override
setEnabled(BluetoothDevice device, boolean enabled)233     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
234         boolean isEnabled = false;
235         if (mService == null || device == null) {
236             return false;
237         }
238         if (enabled) {
239             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
240                 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
241             }
242         } else {
243             isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
244         }
245 
246         return isEnabled;
247     }
248 
249     /**
250      * Tells remote device to set an absolute volume.
251      *
252      * @param volume Absolute volume to be set on remote
253      */
setVolume(int volume)254     public void setVolume(int volume) {
255         if (mService == null) {
256             return;
257         }
258         mService.setVolume(volume);
259     }
260 
261     /**
262      * Gets the HiSyncId (unique hearing aid device identifier) of the device.
263      *
264      * @param device Bluetooth device
265      * @return the HiSyncId of the device
266      */
getHiSyncId(BluetoothDevice device)267     public long getHiSyncId(BluetoothDevice device) {
268         if (mService == null || device == null) {
269             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
270         }
271         return mService.getHiSyncId(device);
272     }
273 
274     /**
275      * Gets the side of the device.
276      *
277      * @param device Bluetooth device.
278      * @return side of the device. See {@link DeviceSide}.
279      */
280     @DeviceSide
getDeviceSide(@onNull BluetoothDevice device)281     public int getDeviceSide(@NonNull BluetoothDevice device) {
282         final int defaultValue = DeviceSide.SIDE_INVALID;
283         if (mService == null) {
284             Log.w(TAG, "Proxy not attached to HearingAidService");
285             return defaultValue;
286         }
287 
288         return mService.getDeviceSide(device);
289     }
290 
291     /**
292      * Gets the mode of the device.
293      *
294      * @param device Bluetooth device
295      * @return mode of the device. See {@link DeviceMode}.
296      */
297     @DeviceMode
getDeviceMode(@onNull BluetoothDevice device)298     public int getDeviceMode(@NonNull BluetoothDevice device) {
299         final int defaultValue = DeviceMode.MODE_INVALID;
300         if (mService == null) {
301             Log.w(TAG, "Proxy not attached to HearingAidService");
302             return defaultValue;
303         }
304 
305         return mService.getDeviceMode(device);
306     }
307 
toString()308     public String toString() {
309         return NAME;
310     }
311 
getOrdinal()312     public int getOrdinal() {
313         return ORDINAL;
314     }
315 
getNameResource(BluetoothDevice device)316     public int getNameResource(BluetoothDevice device) {
317         return R.string.bluetooth_profile_hearing_aid;
318     }
319 
getSummaryResourceForDevice(BluetoothDevice device)320     public int getSummaryResourceForDevice(BluetoothDevice device) {
321         int state = getConnectionStatus(device);
322         switch (state) {
323             case BluetoothProfile.STATE_DISCONNECTED:
324                 return R.string.bluetooth_hearing_aid_profile_summary_use_for;
325 
326             case BluetoothProfile.STATE_CONNECTED:
327                 return R.string.bluetooth_hearing_aid_profile_summary_connected;
328 
329             default:
330                 return BluetoothUtils.getConnectionStateSummary(state);
331         }
332     }
333 
getDrawableResource(BluetoothClass btClass)334     public int getDrawableResource(BluetoothClass btClass) {
335         return com.android.internal.R.drawable.ic_bt_hearing_aid;
336     }
337 
finalize()338     protected void finalize() {
339         Log.d(TAG, "finalize()");
340         if (mService != null) {
341             try {
342                 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEARING_AID,
343                                                                        mService);
344                 mService = null;
345             }catch (Throwable t) {
346                 Log.w(TAG, "Error cleaning up Hearing Aid proxy", t);
347             }
348         }
349     }
350 }
351