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