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.settings.bluetooth;
18 
19 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP;
20 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_HEARING_AIDS_PRESETS;
21 
22 import android.bluetooth.BluetoothCsipSetCoordinator;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothHapClient;
25 import android.bluetooth.BluetoothHapPresetInfo;
26 import android.content.Context;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.widget.Toast;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.ListPreference;
35 import androidx.preference.Preference;
36 import androidx.preference.PreferenceCategory;
37 import androidx.preference.PreferenceFragmentCompat;
38 import androidx.preference.PreferenceScreen;
39 
40 import com.android.settings.R;
41 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
42 import com.android.settingslib.bluetooth.HapClientProfile;
43 import com.android.settingslib.bluetooth.LocalBluetoothManager;
44 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
45 import com.android.settingslib.core.lifecycle.Lifecycle;
46 import com.android.settingslib.core.lifecycle.events.OnPause;
47 import com.android.settingslib.core.lifecycle.events.OnResume;
48 import com.android.settingslib.core.lifecycle.events.OnStart;
49 import com.android.settingslib.core.lifecycle.events.OnStop;
50 import com.android.settingslib.utils.ThreadUtils;
51 
52 import java.util.List;
53 
54 /**
55  * The controller of the hearing aid presets.
56  */
57 public class BluetoothDetailsHearingAidsPresetsController extends
58         BluetoothDetailsController implements Preference.OnPreferenceChangeListener,
59         BluetoothHapClient.Callback, LocalBluetoothProfileManager.ServiceListener,
60         OnStart, OnResume, OnPause, OnStop {
61 
62     private static final boolean DEBUG = true;
63     private static final String TAG = "BluetoothDetailsHearingAidsPresetsController";
64     static final String KEY_HEARING_AIDS_PRESETS = "hearing_aids_presets";
65 
66     private final LocalBluetoothProfileManager mProfileManager;
67     private final HapClientProfile mHapClientProfile;
68 
69     @Nullable
70     private ListPreference mPreference;
71 
BluetoothDetailsHearingAidsPresetsController(@onNull Context context, @NonNull PreferenceFragmentCompat fragment, @NonNull LocalBluetoothManager manager, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle)72     public BluetoothDetailsHearingAidsPresetsController(@NonNull Context context,
73             @NonNull PreferenceFragmentCompat fragment,
74             @NonNull LocalBluetoothManager manager,
75             @NonNull CachedBluetoothDevice device,
76             @NonNull Lifecycle lifecycle) {
77         super(context, fragment, device, lifecycle);
78         mProfileManager = manager.getProfileManager();
79         mHapClientProfile = mProfileManager.getHapClientProfile();
80     }
81 
82     @Override
onStart()83     public void onStart() {
84         if (mHapClientProfile != null && !mHapClientProfile.isProfileReady()) {
85             mProfileManager.addServiceListener(this);
86         }
87     }
88 
89     @Override
onResume()90     public void onResume() {
91         registerHapCallback();
92         super.onResume();
93     }
94 
95     @Override
onPause()96     public void onPause() {
97         unregisterHapCallback();
98         super.onPause();
99     }
100 
101     @Override
onStop()102     public void onStop() {
103         mProfileManager.removeServiceListener(this);
104     }
105 
106     @Override
onPreferenceChange(@onNull Preference preference, @Nullable Object newValue)107     public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) {
108         if (TextUtils.equals(preference.getKey(), getPreferenceKey())) {
109             if (newValue instanceof final String value
110                     && preference instanceof final ListPreference listPreference) {
111                 final int index = listPreference.findIndexOfValue(value);
112                 final String presetName = listPreference.getEntries()[index].toString();
113                 final int presetIndex = Integer.parseInt(value);
114                 listPreference.setSummary(presetName);
115                 if (DEBUG) {
116                     Log.d(TAG, "onPreferenceChange"
117                             + ", presetIndex: " + presetIndex
118                             + ", presetName: "  + presetName);
119                 }
120                 boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets(
121                         mCachedDevice.getDevice());
122                 int hapGroupId = mHapClientProfile.getHapGroup(mCachedDevice.getDevice());
123                 if (supportSynchronizedPresets) {
124                     if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
125                         selectPresetSynchronously(hapGroupId, presetIndex);
126                     } else {
127                         Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid.");
128                         selectPresetIndependently(presetIndex);
129                     }
130                 } else {
131                     selectPresetIndependently(presetIndex);
132                 }
133                 return true;
134             }
135         }
136         return false;
137     }
138 
139     @Nullable
140     @Override
getPreferenceKey()141     public String getPreferenceKey() {
142         return KEY_HEARING_AIDS_PRESETS;
143     }
144 
145     @Override
init(PreferenceScreen screen)146     protected void init(PreferenceScreen screen) {
147         PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP);
148         if (deviceControls != null) {
149             mPreference = createPresetPreference(deviceControls.getContext());
150             deviceControls.addPreference(mPreference);
151         }
152     }
153 
154     @Override
refresh()155     protected void refresh() {
156         if (!isAvailable() || mPreference == null) {
157             return;
158         }
159         mPreference.setEnabled(mCachedDevice.isConnectedHapClientDevice());
160 
161         loadAllPresetInfo();
162         if (mPreference.getEntries().length == 0) {
163             if (DEBUG) {
164                 Log.w(TAG, "Disable the preference since preset info size = 0");
165             }
166             mPreference.setEnabled(false);
167         } else {
168             int activePresetIndex = mHapClientProfile.getActivePresetIndex(
169                     mCachedDevice.getDevice());
170             if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) {
171                 mPreference.setValue(Integer.toString(activePresetIndex));
172                 mPreference.setSummary(mPreference.getEntry());
173             } else {
174                 mPreference.setSummary(null);
175             }
176         }
177     }
178 
179     @Override
isAvailable()180     public boolean isAvailable() {
181         if (mHapClientProfile == null) {
182             return false;
183         }
184         return mCachedDevice.getProfiles().stream().anyMatch(
185                 profile -> profile instanceof HapClientProfile);
186     }
187 
188     @Override
onPresetSelected(@onNull BluetoothDevice device, int presetIndex, int reason)189     public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) {
190         if (device.equals(mCachedDevice.getDevice())) {
191             if (DEBUG) {
192                 Log.d(TAG, "onPresetSelected, device: " + device.getAddress()
193                         + ", presetIndex: " + presetIndex + ", reason: " + reason);
194             }
195             mContext.getMainExecutor().execute(this::refresh);
196         }
197     }
198 
199     @Override
onPresetSelectionFailed(@onNull BluetoothDevice device, int reason)200     public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
201         if (device.equals(mCachedDevice.getDevice())) {
202             Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress()
203                     + ", reason: " + reason);
204             mContext.getMainExecutor().execute(() -> {
205                 refresh();
206                 showErrorToast();
207             });
208         }
209     }
210 
211     @Override
onPresetSelectionForGroupFailed(int hapGroupId, int reason)212     public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
213         if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) {
214             Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId
215                     + ", reason: " + reason);
216             // Try to set the preset independently if group operation failed
217             if (mPreference != null) {
218                 selectPresetIndependently(Integer.parseInt(mPreference.getValue()));
219             }
220         }
221     }
222 
223     @Override
onPresetInfoChanged(@onNull BluetoothDevice device, @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason)224     public void onPresetInfoChanged(@NonNull BluetoothDevice device,
225             @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
226         if (device.equals(mCachedDevice.getDevice())) {
227             if (DEBUG) {
228                 Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress()
229                         + ", reason: " + reason);
230                 for (BluetoothHapPresetInfo info: presetInfoList) {
231                     Log.d(TAG, "    preset " + info.getIndex() + ": " + info.getName());
232                 }
233             }
234             mContext.getMainExecutor().execute(this::refresh);
235         }
236     }
237 
238     @Override
onSetPresetNameFailed(@onNull BluetoothDevice device, int reason)239     public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
240         if (device.equals(mCachedDevice.getDevice())) {
241             Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress()
242                     + ", reason: " + reason);
243             mContext.getMainExecutor().execute(() -> {
244                 refresh();
245                 showErrorToast();
246             });
247         }
248     }
249 
250     @Override
onSetPresetNameForGroupFailed(int hapGroupId, int reason)251     public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
252         if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) {
253             Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId
254                     + ", reason: " + reason);
255             mContext.getMainExecutor().execute(() -> {
256                 refresh();
257                 showErrorToast();
258             });
259         }
260     }
261 
createPresetPreference(Context context)262     private ListPreference createPresetPreference(Context context) {
263         ListPreference preference = new ListPreference(context);
264         preference.setKey(KEY_HEARING_AIDS_PRESETS);
265         preference.setOrder(ORDER_HEARING_AIDS_PRESETS);
266         preference.setTitle(context.getString(R.string.bluetooth_hearing_aids_presets));
267         preference.setOnPreferenceChangeListener(this);
268         return preference;
269     }
270 
loadAllPresetInfo()271     private void loadAllPresetInfo() {
272         if (mPreference == null) {
273             return;
274         }
275         List<BluetoothHapPresetInfo> infoList = mHapClientProfile.getAllPresetInfo(
276                 mCachedDevice.getDevice());
277         CharSequence[] presetNames = new CharSequence[infoList.size()];
278         CharSequence[] presetIndexes = new CharSequence[infoList.size()];
279         for (int i = 0; i < infoList.size(); i++) {
280             presetNames[i] = infoList.get(i).getName();
281             presetIndexes[i] = Integer.toString(infoList.get(i).getIndex());
282         }
283         mPreference.setEntries(presetNames);
284         mPreference.setEntryValues(presetIndexes);
285     }
286 
287     @VisibleForTesting
288     @Nullable
getPreference()289     ListPreference getPreference() {
290         return mPreference;
291     }
292 
showErrorToast()293     void showErrorToast() {
294         Toast.makeText(mContext, R.string.bluetooth_hearing_aids_presets_error,
295                 Toast.LENGTH_SHORT).show();
296     }
297 
registerHapCallback()298     private void registerHapCallback() {
299         if (mHapClientProfile != null) {
300             try {
301                 mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
302             } catch (IllegalArgumentException e) {
303                 // The callback was already registered
304                 Log.w(TAG, "Cannot register callback: " + e.getMessage());
305             }
306 
307         }
308     }
309 
unregisterHapCallback()310     private void unregisterHapCallback() {
311         if (mHapClientProfile != null) {
312             try {
313                 mHapClientProfile.unregisterCallback(this);
314             } catch (IllegalArgumentException e) {
315                 // The callback was never registered or was already unregistered
316                 Log.w(TAG, "Cannot unregister callback: " + e.getMessage());
317             }
318         }
319     }
320 
321     @Override
onServiceConnected()322     public void onServiceConnected() {
323         if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) {
324             mProfileManager.removeServiceListener(this);
325             registerHapCallback();
326             refresh();
327         }
328     }
329 
330     @Override
onServiceDisconnected()331     public void onServiceDisconnected() {
332         // Do nothing
333     }
334 
selectPresetSynchronously(int groupId, int presetIndex)335     private void selectPresetSynchronously(int groupId, int presetIndex) {
336         if (mPreference == null) {
337             return;
338         }
339         if (DEBUG) {
340             Log.d(TAG, "selectPresetSynchronously"
341                     + ", presetIndex: " + presetIndex
342                     + ", groupId: "  + groupId
343                     + ", device: " + mCachedDevice.getAddress());
344         }
345         mHapClientProfile.selectPresetForGroup(groupId, presetIndex);
346     }
selectPresetIndependently(int presetIndex)347     private void selectPresetIndependently(int presetIndex) {
348         if (mPreference == null) {
349             return;
350         }
351         if (DEBUG) {
352             Log.d(TAG, "selectPresetIndependently"
353                     + ", presetIndex: " + presetIndex
354                     + ", device: " + mCachedDevice.getAddress());
355         }
356         mHapClientProfile.selectPreset(mCachedDevice.getDevice(), presetIndex);
357         final CachedBluetoothDevice subDevice = mCachedDevice.getSubDevice();
358         if (subDevice != null) {
359             if (DEBUG) {
360                 Log.d(TAG, "selectPreset for subDevice, device: " + subDevice);
361             }
362             mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex);
363         }
364         for (final CachedBluetoothDevice memberDevice :
365                 mCachedDevice.getMemberDevice()) {
366             if (DEBUG) {
367                 Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice);
368             }
369             mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex);
370         }
371     }
372 }
373