• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.systemui.accessibility.hearingaid;
18 
19 import static java.util.Collections.emptyList;
20 
21 import android.bluetooth.BluetoothCsipSetCoordinator;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothHapClient;
24 import android.bluetooth.BluetoothHapPresetInfo;
25 import android.util.Log;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
31 import com.android.settingslib.bluetooth.HapClientProfile;
32 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
33 import com.android.settingslib.utils.ThreadUtils;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * The controller of handling hearing device preset with Bluetooth Hearing Access Profile(HAP).
40  */
41 public class HearingDevicesPresetsController implements
42         LocalBluetoothProfileManager.ServiceListener, BluetoothHapClient.Callback {
43 
44     private static final String TAG = "HearingDevicesPresetsController";
45     private static final boolean DEBUG = true;
46 
47     private final LocalBluetoothProfileManager mProfileManager;
48     private final HapClientProfile mHapClientProfile;
49     private final PresetCallback mPresetCallback;
50 
51     private CachedBluetoothDevice mDevice;
52     private List<BluetoothHapPresetInfo> mPresetInfos = new ArrayList<>();
53     private int mActivePresetIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
54     private int mSelectedPresetIndex;
55 
HearingDevicesPresetsController(@onNull LocalBluetoothProfileManager profileManager, @Nullable PresetCallback presetCallback)56     public HearingDevicesPresetsController(@NonNull LocalBluetoothProfileManager profileManager,
57             @Nullable PresetCallback presetCallback) {
58         mProfileManager = profileManager;
59         mHapClientProfile = mProfileManager.getHapClientProfile();
60         mPresetCallback = presetCallback;
61     }
62 
63     @Override
onServiceConnected()64     public void onServiceConnected() {
65         if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) {
66             mProfileManager.removeServiceListener(this);
67             registerHapCallback();
68             refreshPresetInfo();
69         }
70     }
71 
72     @Override
onServiceDisconnected()73     public void onServiceDisconnected() {
74         // Do nothing
75     }
76 
77     @Override
onPresetSelected(@onNull BluetoothDevice device, int presetIndex, int reason)78     public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) {
79         if (mDevice == null) {
80             return;
81         }
82         if (device.equals(mDevice.getDevice())) {
83             if (DEBUG) {
84                 Log.d(TAG, "onPresetSelected, device: " + device.getAddress()
85                         + ", presetIndex: " + presetIndex + ", reason: " + reason);
86             }
87             refreshPresetInfo();
88         }
89     }
90 
91     @Override
onPresetInfoChanged(@onNull BluetoothDevice device, @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason)92     public void onPresetInfoChanged(@NonNull BluetoothDevice device,
93             @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
94         if (mDevice == null) {
95             return;
96         }
97         if (device.equals(mDevice.getDevice())) {
98             if (DEBUG) {
99                 Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress()
100                         + ", reason: " + reason + ", infoList: " + presetInfoList);
101             }
102             refreshPresetInfo();
103         }
104     }
105 
106     @Override
onPresetSelectionFailed(@onNull BluetoothDevice device, int reason)107     public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
108         if (mDevice == null) {
109             return;
110         }
111         if (device.equals(mDevice.getDevice())) {
112             Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress()
113                     + ", reason: " + reason);
114             if (mPresetCallback != null) {
115                 mPresetCallback.onPresetCommandFailed(reason);
116             }
117         }
118     }
119 
120     @Override
onPresetSelectionForGroupFailed(int hapGroupId, int reason)121     public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
122         if (mDevice == null || mHapClientProfile == null) {
123             return;
124         }
125         if (hapGroupId == mHapClientProfile.getHapGroup(mDevice.getDevice())) {
126             Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId
127                     + ", reason: " + reason);
128             selectPresetIndependently(mSelectedPresetIndex);
129         }
130     }
131 
132     @Override
onSetPresetNameFailed(@onNull BluetoothDevice device, int reason)133     public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
134         if (mDevice == null) {
135             return;
136         }
137         if (device.equals(mDevice.getDevice())) {
138             Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress()
139                     + ", reason: " + reason);
140             if (mPresetCallback != null) {
141                 mPresetCallback.onPresetCommandFailed(reason);
142             }
143         }
144     }
145 
146     @Override
onSetPresetNameForGroupFailed(int hapGroupId, int reason)147     public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
148         if (mDevice == null || mHapClientProfile == null) {
149             return;
150         }
151         if (hapGroupId == mHapClientProfile.getHapGroup(mDevice.getDevice())) {
152             Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId
153                     + ", reason: " + reason);
154         }
155         if (mPresetCallback != null) {
156             mPresetCallback.onPresetCommandFailed(reason);
157         }
158     }
159 
160     /**
161      * Registers a callback to be notified about operation changed of {@link HapClientProfile}.
162      */
registerHapCallback()163     public void registerHapCallback() {
164         if (mHapClientProfile != null) {
165             if (!mHapClientProfile.isProfileReady()) {
166                 mProfileManager.addServiceListener(this);
167                 Log.w(TAG, "Profile is not ready yet, the callback will be registered once the "
168                         + "profile is ready.");
169                 return;
170             }
171             try {
172                 mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
173             } catch (IllegalArgumentException e) {
174                 // The callback was already registered
175                 Log.w(TAG, "Cannot register callback: " + e.getMessage());
176             }
177 
178         }
179     }
180 
181     /**
182      * Removes a previously-added {@link HapClientProfile} callback if exist.
183      */
unregisterHapCallback()184     public void unregisterHapCallback() {
185         mProfileManager.removeServiceListener(this);
186         if (mHapClientProfile != null) {
187             try {
188                 mHapClientProfile.unregisterCallback(this);
189             } catch (IllegalArgumentException e) {
190                 // The callback was never registered or was already unregistered
191                 Log.w(TAG, "Cannot unregister callback: " + e.getMessage());
192             }
193         }
194     }
195 
196     /**
197      * Sets the device for this controller to control the preset if it supports
198      * {@link HapClientProfile}, otherwise the device of this controller will be {@code null}.
199      *
200      * @param device the {@link CachedBluetoothDevice} set to the controller
201      */
setDevice(@ullable CachedBluetoothDevice device)202     public void setDevice(@Nullable CachedBluetoothDevice device) {
203         if (device != null && device.getProfiles().stream().anyMatch(
204                 profile -> profile instanceof HapClientProfile)) {
205             mDevice = device;
206         } else {
207             mDevice = null;
208         }
209         refreshPresetInfo();
210     }
211 
212     /**
213      * Refreshes the preset info of {@code mDevice}. If the preset info list or the active preset
214      * index is updated, the {@link PresetCallback#onPresetInfoUpdated(List, int)} will be called
215      * to notify the change.
216      *
217      * <b>Note:</b> If {@code mDevice} is null, the cached preset info and active preset index will
218      * be reset to empty list and {@code BluetoothHapClient.PRESET_INDEX_UNAVAILABLE} respectively.
219      */
refreshPresetInfo()220     public void refreshPresetInfo() {
221         List<BluetoothHapPresetInfo> updatedInfos = new ArrayList<>();
222         int updatedActiveIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
223         if (mHapClientProfile != null && mDevice != null) {
224             updatedInfos = mHapClientProfile.getAllPresetInfo(mDevice.getDevice()).stream().filter(
225                     BluetoothHapPresetInfo::isAvailable).toList();
226             updatedActiveIndex = mHapClientProfile.getActivePresetIndex(mDevice.getDevice());
227         }
228         final boolean infoUpdated = !mPresetInfos.equals(updatedInfos);
229         final boolean activeIndexUpdated = mActivePresetIndex != updatedActiveIndex;
230         mPresetInfos = updatedInfos;
231         mActivePresetIndex = updatedActiveIndex;
232         if (infoUpdated || activeIndexUpdated) {
233             if (mPresetCallback != null) {
234                 mPresetCallback.onPresetInfoUpdated(mPresetInfos, mActivePresetIndex);
235             }
236         }
237     }
238 
239     /**
240      * @return if the preset control is available. The preset control is available only
241      * when the {@code mDevice} supports HAP and the retrieved preset info list is not empty.
242      */
isPresetControlAvailable()243     public boolean isPresetControlAvailable() {
244         boolean deviceValid = mDevice != null && mDevice.isConnectedHapClientDevice();
245         boolean hasPreset = mPresetInfos != null && !mPresetInfos.isEmpty();
246         return deviceValid && hasPreset;
247     }
248 
249     /**
250      * @return a list of {@link BluetoothHapPresetInfo} retrieved from {@code mDevice}
251      */
getAllPresetInfo()252     public List<BluetoothHapPresetInfo> getAllPresetInfo() {
253         if (mDevice == null || mHapClientProfile == null) {
254             return emptyList();
255         }
256         return mPresetInfos;
257     }
258 
259     /**
260      * Gets the currently active preset of {@code mDevice}.
261      *
262      * @return active preset index
263      */
getActivePresetIndex()264     public int getActivePresetIndex() {
265         if (mDevice == null || mHapClientProfile == null) {
266             return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
267         }
268         return mActivePresetIndex;
269     }
270 
271     /**
272      * Selects the preset for {@code mDevice}. Performs individual or group operation according
273      * to whether the device supports synchronized presets feature or not.
274      *
275      * @param presetIndex an index of one of the available presets
276      */
selectPreset(int presetIndex)277     public void selectPreset(int presetIndex) {
278         if (mDevice == null || mHapClientProfile == null) {
279             return;
280         }
281         mSelectedPresetIndex = presetIndex;
282         boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets(
283                 mDevice.getDevice());
284         int hapGroupId = mHapClientProfile.getHapGroup(mDevice.getDevice());
285         if (supportSynchronizedPresets) {
286             if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
287                 selectPresetSynchronously(hapGroupId, presetIndex);
288             } else {
289                 Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid.");
290                 selectPresetIndependently(presetIndex);
291             }
292         } else {
293             selectPresetIndependently(presetIndex);
294         }
295     }
296 
selectPresetSynchronously(int groupId, int presetIndex)297     private void selectPresetSynchronously(int groupId, int presetIndex) {
298         if (mDevice == null || mHapClientProfile == null) {
299             return;
300         }
301         if (DEBUG) {
302             Log.d(TAG, "selectPresetSynchronously"
303                     + ", presetIndex: " + presetIndex
304                     + ", groupId: " + groupId
305                     + ", device: " + mDevice.getAddress());
306         }
307         mHapClientProfile.selectPresetForGroup(groupId, presetIndex);
308     }
309 
selectPresetIndependently(int presetIndex)310     private void selectPresetIndependently(int presetIndex) {
311         if (mDevice == null || mHapClientProfile == null) {
312             return;
313         }
314         if (DEBUG) {
315             Log.d(TAG, "selectPresetIndependently"
316                     + ", presetIndex: " + presetIndex
317                     + ", device: " + mDevice.getAddress());
318         }
319         mHapClientProfile.selectPreset(mDevice.getDevice(), presetIndex);
320         final CachedBluetoothDevice subDevice = mDevice.getSubDevice();
321         if (subDevice != null) {
322             if (DEBUG) {
323                 Log.d(TAG, "selectPreset for subDevice, device: " + subDevice);
324             }
325             mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex);
326         }
327         for (final CachedBluetoothDevice memberDevice : mDevice.getMemberDevice()) {
328             if (DEBUG) {
329                 Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice);
330             }
331             mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex);
332         }
333     }
334 
335     /**
336      * Interface to provide callbacks when preset command result from {@link HapClientProfile}
337      * changed.
338      */
339     public interface PresetCallback {
340         /**
341          * Called when preset info from {@link HapClientProfile} operation get updated.
342          *
343          * @param presetInfos all preset info of {@code mDevice}
344          * @param activePresetIndex currently active preset index of {@code mDevice}
345          */
onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex)346         void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex);
347 
348         /**
349          * Called when preset operation from {@link HapClientProfile} failed to handle.
350          *
351          * @param reason failure reason
352          */
onPresetCommandFailed(int reason)353         void onPresetCommandFailed(int reason);
354     }
355 }
356