1 /*
2  * Copyright (C) 2022 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.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
21 
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.bluetooth.BluetoothAdapter;
25 import android.bluetooth.BluetoothClass;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothHapClient;
28 import android.bluetooth.BluetoothManager;
29 import android.bluetooth.BluetoothProfile;
30 import android.content.Context;
31 import android.util.Log;
32 
33 import com.android.settingslib.R;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.util.ArrayList;
38 import java.util.List;
39 
40 /**
41  * HapClientProfile handles the Bluetooth HAP service client role.
42  */
43 public class HapClientProfile implements LocalBluetoothProfile {
44     @Retention(RetentionPolicy.SOURCE)
45     @IntDef(flag = true, value = {
46             HearingAidType.TYPE_INVALID,
47             HearingAidType.TYPE_BINAURAL,
48             HearingAidType.TYPE_MONAURAL,
49             HearingAidType.TYPE_BANDED,
50             HearingAidType.TYPE_RFU
51     })
52 
53     /** Hearing aid type definition for HAP Client. */
54     public @interface HearingAidType {
55         int TYPE_INVALID = -1;
56         int TYPE_BINAURAL = BluetoothHapClient.TYPE_BINAURAL;
57         int TYPE_MONAURAL = BluetoothHapClient.TYPE_MONAURAL;
58         int TYPE_BANDED = BluetoothHapClient.TYPE_BANDED;
59         int TYPE_RFU = BluetoothHapClient.TYPE_RFU;
60     }
61 
62     static final String NAME = "HapClient";
63     private static final String TAG = "HapClientProfile";
64 
65     // Order of this profile in device profiles list
66     private static final int ORDINAL = 1;
67 
68     private final BluetoothAdapter mBluetoothAdapter;
69     private final CachedBluetoothDeviceManager mDeviceManager;
70     private final LocalBluetoothProfileManager mProfileManager;
71     private BluetoothHapClient mService;
72     private boolean mIsProfileReady;
73 
74     // These callbacks run on the main thread.
75     private final class HapClientServiceListener implements BluetoothProfile.ServiceListener {
76 
77         @Override
onServiceConnected(int profile, BluetoothProfile proxy)78         public void onServiceConnected(int profile, BluetoothProfile proxy) {
79             mService = (BluetoothHapClient) proxy;
80             // We just bound to the service, so refresh the UI for any connected HapClient devices.
81             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
82             while (!deviceList.isEmpty()) {
83                 BluetoothDevice nextDevice = deviceList.remove(0);
84                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
85                 // Adds a new device into mDeviceManager if it does not exist
86                 if (device == null) {
87                     Log.w(TAG, "HapClient profile found new device: " + nextDevice);
88                     device = mDeviceManager.addDevice(nextDevice);
89                 }
90                 device.onProfileStateChanged(
91                         HapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
92                 device.refresh();
93             }
94 
95             mIsProfileReady = true;
96             mProfileManager.callServiceConnectedListeners();
97         }
98 
99         @Override
onServiceDisconnected(int profile)100         public void onServiceDisconnected(int profile) {
101             mIsProfileReady = false;
102             mProfileManager.callServiceDisconnectedListeners();
103         }
104     }
105 
HapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)106     HapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
107             LocalBluetoothProfileManager profileManager) {
108         mDeviceManager = deviceManager;
109         mProfileManager = profileManager;
110         BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
111         if (bluetoothManager != null) {
112             mBluetoothAdapter = bluetoothManager.getAdapter();
113             mBluetoothAdapter.getProfileProxy(context, new HapClientServiceListener(),
114                     BluetoothProfile.HAP_CLIENT);
115         } else {
116             mBluetoothAdapter = null;
117         }
118     }
119 
120     /**
121      * Get hearing aid devices matching connection states{
122      * {@code BluetoothProfile.STATE_CONNECTED},
123      * {@code BluetoothProfile.STATE_CONNECTING},
124      * {@code BluetoothProfile.STATE_DISCONNECTING}}
125      *
126      * @return Matching device list
127      */
getConnectedDevices()128     public List<BluetoothDevice> getConnectedDevices() {
129         return getDevicesByStates(new int[] {
130                 BluetoothProfile.STATE_CONNECTED,
131                 BluetoothProfile.STATE_CONNECTING,
132                 BluetoothProfile.STATE_DISCONNECTING});
133     }
134 
135     /**
136      * Get hearing aid devices matching connection states{
137      * {@code BluetoothProfile.STATE_DISCONNECTED},
138      * {@code BluetoothProfile.STATE_CONNECTED},
139      * {@code BluetoothProfile.STATE_CONNECTING},
140      * {@code BluetoothProfile.STATE_DISCONNECTING}}
141      *
142      * @return Matching device list
143      */
getConnectableDevices()144     public List<BluetoothDevice> getConnectableDevices() {
145         return getDevicesByStates(new int[] {
146                 BluetoothProfile.STATE_DISCONNECTED,
147                 BluetoothProfile.STATE_CONNECTED,
148                 BluetoothProfile.STATE_CONNECTING,
149                 BluetoothProfile.STATE_DISCONNECTING});
150     }
151 
getDevicesByStates(int[] states)152     private List<BluetoothDevice> getDevicesByStates(int[] states) {
153         if (mService == null) {
154             return new ArrayList<>(0);
155         }
156         return mService.getDevicesMatchingConnectionStates(states);
157     }
158 
159     /**
160      * Gets the hearing aid type of the device.
161      *
162      * @param device is the device for which we want to get the hearing aid type
163      * @return hearing aid type
164      */
165     @HearingAidType
getHearingAidType(@onNull BluetoothDevice device)166     public int getHearingAidType(@NonNull BluetoothDevice device) {
167         if (mService == null) {
168             return HearingAidType.TYPE_INVALID;
169         }
170         return mService.getHearingAidType(device);
171     }
172 
173     /**
174      * Gets if this device supports synchronized presets or not
175      *
176      * @param device is the device for which we want to know if supports synchronized presets
177      * @return {@code true} if the device supports synchronized presets
178      */
supportsSynchronizedPresets(@onNull BluetoothDevice device)179     public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
180         if (mService == null) {
181             return false;
182         }
183         return mService.supportsSynchronizedPresets(device);
184     }
185 
186     /**
187      * Gets if this device supports independent presets or not
188      *
189      * @param device is the device for which we want to know if supports independent presets
190      * @return {@code true} if the device supports independent presets
191      */
supportsIndependentPresets(@onNull BluetoothDevice device)192     public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
193         if (mService == null) {
194             return false;
195         }
196         return mService.supportsIndependentPresets(device);
197     }
198 
199     /**
200      * Gets if this device supports dynamic presets or not
201      *
202      * @param device is the device for which we want to know if supports dynamic presets
203      * @return {@code true} if the device supports dynamic presets
204      */
supportsDynamicPresets(@onNull BluetoothDevice device)205     public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
206         if (mService == null) {
207             return false;
208         }
209         return mService.supportsDynamicPresets(device);
210     }
211 
212     /**
213      * Gets if this device supports writable presets or not
214      *
215      * @param device is the device for which we want to know if supports writable presets
216      * @return {@code true} if the device supports writable presets
217      */
supportsWritablePresets(@onNull BluetoothDevice device)218     public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
219         if (mService == null) {
220             return false;
221         }
222         return mService.supportsWritablePresets(device);
223     }
224 
225     @Override
accessProfileEnabled()226     public boolean accessProfileEnabled() {
227         return false;
228     }
229 
230     @Override
isAutoConnectable()231     public boolean isAutoConnectable() {
232         return true;
233     }
234 
235     @Override
getConnectionStatus(BluetoothDevice device)236     public int getConnectionStatus(BluetoothDevice device) {
237         if (mService == null) {
238             return BluetoothProfile.STATE_DISCONNECTED;
239         }
240         return mService.getConnectionState(device);
241     }
242 
243     @Override
isEnabled(BluetoothDevice device)244     public boolean isEnabled(BluetoothDevice device) {
245         if (mService == null || device == null) {
246             return false;
247         }
248         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
249     }
250 
251     @Override
getConnectionPolicy(BluetoothDevice device)252     public int getConnectionPolicy(BluetoothDevice device) {
253         if (mService == null || device == null) {
254             return CONNECTION_POLICY_FORBIDDEN;
255         }
256         return mService.getConnectionPolicy(device);
257     }
258 
259     @Override
setEnabled(BluetoothDevice device, boolean enabled)260     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
261         boolean isEnabled = false;
262         if (mService == null || device == null) {
263             return false;
264         }
265         if (enabled) {
266             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
267                 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
268             }
269         } else {
270             isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
271         }
272 
273         return isEnabled;
274     }
275 
276     @Override
isProfileReady()277     public boolean isProfileReady() {
278         return mIsProfileReady;
279     }
280 
281     @Override
getProfileId()282     public int getProfileId() {
283         return BluetoothProfile.HAP_CLIENT;
284     }
285 
286     @Override
getOrdinal()287     public int getOrdinal() {
288         return ORDINAL;
289     }
290 
291     @Override
getNameResource(BluetoothDevice device)292     public int getNameResource(BluetoothDevice device) {
293         return R.string.bluetooth_profile_hearing_aid;
294     }
295 
296     @Override
getSummaryResourceForDevice(BluetoothDevice device)297     public int getSummaryResourceForDevice(BluetoothDevice device) {
298         int state = getConnectionStatus(device);
299         switch (state) {
300             case BluetoothProfile.STATE_DISCONNECTED:
301                 return R.string.bluetooth_hearing_aid_profile_summary_use_for;
302 
303             case BluetoothProfile.STATE_CONNECTED:
304                 return R.string.bluetooth_hearing_aid_profile_summary_connected;
305 
306             default:
307                 return BluetoothUtils.getConnectionStateSummary(state);
308         }
309     }
310 
311     @Override
getDrawableResource(BluetoothClass btClass)312     public int getDrawableResource(BluetoothClass btClass) {
313         return com.android.internal.R.drawable.ic_bt_hearing_aid;
314     }
315 
316     /**
317      * Gets the name of this class
318      *
319      * @return the name of this class
320      */
toString()321     public String toString() {
322         return NAME;
323     }
324 
finalize()325     protected void finalize() {
326         Log.d(TAG, "finalize()");
327         if (mService != null) {
328             try {
329                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, mService);
330                 mService = null;
331             } catch (Throwable t) {
332                 Log.w(TAG, "Error cleaning up HAP Client proxy", t);
333             }
334         }
335     }
336 }
337