• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.settings.bluetooth;
18 
19 import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
20 
21 import android.app.settings.SettingsEnums;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.Context;
24 import android.media.AudioDeviceAttributes;
25 import android.media.AudioDeviceInfo;
26 import android.media.AudioManager;
27 import android.media.Spatializer;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import androidx.annotation.Nullable;
32 import androidx.annotation.VisibleForTesting;
33 import androidx.preference.Preference;
34 import androidx.preference.PreferenceCategory;
35 import androidx.preference.PreferenceFragmentCompat;
36 import androidx.preference.PreferenceScreen;
37 import androidx.preference.SwitchPreferenceCompat;
38 import androidx.preference.TwoStatePreference;
39 
40 import com.android.settings.R;
41 import com.android.settings.overlay.FeatureFactory;
42 import com.android.settingslib.bluetooth.BluetoothUtils;
43 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
44 import com.android.settingslib.core.lifecycle.Lifecycle;
45 import com.android.settingslib.flags.Flags;
46 import com.android.settingslib.utils.ThreadUtils;
47 
48 import com.google.common.collect.ImmutableSet;
49 
50 import java.util.Set;
51 import java.util.concurrent.atomic.AtomicBoolean;
52 
53 /**
54  * The controller of the Spatial audio setting in the bluetooth detail settings.
55  */
56 public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsController
57         implements Preference.OnPreferenceClickListener {
58 
59     private static final String TAG = "BluetoothSpatialAudioController";
60     private static final String KEY_SPATIAL_AUDIO_GROUP = "spatial_audio_group";
61     private static final String KEY_SPATIAL_AUDIO = "spatial_audio";
62     private static final String KEY_HEAD_TRACKING = "head_tracking";
63 
64     private final AudioManager mAudioManager;
65     private final Spatializer mSpatializer;
66 
67     @VisibleForTesting
68     PreferenceCategory mProfilesContainer;
69     @VisibleForTesting @Nullable AudioDeviceAttributes mAudioDevice = null;
70 
71     AtomicBoolean mHasHeadTracker = new AtomicBoolean(false);
72     AtomicBoolean mInitialRefresh = new AtomicBoolean(true);
73 
74     public static final Set<Integer> SA_PROFILES =
75             ImmutableSet.of(
76                     BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID);
77 
BluetoothDetailsSpatialAudioController( Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle)78     public BluetoothDetailsSpatialAudioController(
79             Context context,
80             PreferenceFragmentCompat fragment,
81             CachedBluetoothDevice device,
82             Lifecycle lifecycle) {
83         super(context, fragment, device, lifecycle);
84         mAudioManager = context.getSystemService(AudioManager.class);
85         mSpatializer = FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider()
86                 .getSpatializer(context);
87     }
88 
89     @Override
isAvailable()90     public boolean isAvailable() {
91         return mSpatializer.getImmersiveAudioLevel() != SPATIALIZER_IMMERSIVE_LEVEL_NONE;
92     }
93 
94     @Override
onPreferenceClick(Preference preference)95     public boolean onPreferenceClick(Preference preference) {
96         TwoStatePreference switchPreference = (TwoStatePreference) preference;
97         String key = switchPreference.getKey();
98         if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
99             mMetricsFeatureProvider.action(
100                     mContext,
101                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TOGGLE_CLICKED,
102                     switchPreference.isChecked());
103             updateSpatializerEnabled(switchPreference.isChecked());
104             ThreadUtils.postOnBackgroundThread(
105                     () -> {
106                         mHasHeadTracker.set(
107                                 mAudioDevice != null && mSpatializer.hasHeadTracker(mAudioDevice));
108                         mContext.getMainExecutor()
109                                 .execute(() -> refreshSpatialAudioEnabled(switchPreference));
110                     });
111             return true;
112         } else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
113             mMetricsFeatureProvider.action(
114                     mContext,
115                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TOGGLE_CLICKED,
116                     switchPreference.isChecked());
117             updateSpatializerHeadTracking(switchPreference.isChecked());
118             return true;
119         } else {
120             Log.w(TAG, "invalid key name.");
121             return false;
122         }
123     }
124 
updateSpatializerEnabled(boolean enabled)125     private void updateSpatializerEnabled(boolean enabled)  {
126         if (mAudioDevice == null) {
127             Log.w(TAG, "cannot update spatializer enabled for null audio device.");
128             return;
129         }
130         if (enabled) {
131             mSpatializer.addCompatibleAudioDevice(mAudioDevice);
132         } else {
133             mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
134         }
135     }
136 
updateSpatializerHeadTracking(boolean enabled)137     private void updateSpatializerHeadTracking(boolean enabled)  {
138         if (mAudioDevice == null) {
139             Log.w(TAG, "cannot update spatializer head tracking for null audio device.");
140             return;
141         }
142         mSpatializer.setHeadTrackerEnabled(enabled, mAudioDevice);
143     }
144 
145     @Override
getPreferenceKey()146     public String getPreferenceKey() {
147         return KEY_SPATIAL_AUDIO_GROUP;
148     }
149 
150     @Override
init(PreferenceScreen screen)151     protected void init(PreferenceScreen screen) {
152         mProfilesContainer = screen.findPreference(getPreferenceKey());
153         if (com.android.settings.flags.Flags.enableBluetoothDeviceDetailsPolish()) {
154             mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
155         }
156         refresh();
157     }
158 
159     @Override
refresh()160     protected void refresh() {
161         if (Flags.enableDeterminingSpatialAudioAttributesByProfile()) {
162             getAvailableDeviceByProfileState();
163         } else {
164             if (mAudioDevice == null) {
165                 getAvailableDevice();
166             }
167         }
168         ThreadUtils.postOnBackgroundThread(
169                 () -> {
170                     mHasHeadTracker.set(
171                             mAudioDevice != null && mSpatializer.hasHeadTracker(mAudioDevice));
172                     mContext.getMainExecutor().execute(this::refreshUi);
173                 });
174     }
175 
refreshUi()176     private void refreshUi() {
177         TwoStatePreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
178         if (spatialAudioPref == null && mAudioDevice != null) {
179             spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
180             mProfilesContainer.addPreference(spatialAudioPref);
181         } else if (mAudioDevice == null || !mSpatializer.isAvailableForDevice(mAudioDevice)) {
182             if (spatialAudioPref != null) {
183                 mProfilesContainer.removePreference(spatialAudioPref);
184             }
185             final TwoStatePreference headTrackingPref =
186                     mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
187             if (headTrackingPref != null) {
188                 mProfilesContainer.removePreference(headTrackingPref);
189             }
190             mAudioDevice = null;
191             return;
192         }
193 
194         refreshSpatialAudioEnabled(spatialAudioPref);
195     }
196 
refreshSpatialAudioEnabled( TwoStatePreference spatialAudioPref)197     private void refreshSpatialAudioEnabled(
198             TwoStatePreference spatialAudioPref) {
199         boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
200         Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
201         spatialAudioPref.setChecked(isSpatialAudioOn);
202 
203         TwoStatePreference headTrackingPref = mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
204         if (headTrackingPref == null) {
205             headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
206             mProfilesContainer.addPreference(headTrackingPref);
207         }
208         refreshHeadTracking(spatialAudioPref, headTrackingPref);
209     }
210 
refreshHeadTracking(TwoStatePreference spatialAudioPref, TwoStatePreference headTrackingPref)211     private void refreshHeadTracking(TwoStatePreference spatialAudioPref,
212             TwoStatePreference headTrackingPref) {
213         boolean isHeadTrackingAvailable = spatialAudioPref.isChecked() && mHasHeadTracker.get();
214         Log.d(TAG, "refresh() has head tracker : " + mHasHeadTracker.get());
215         headTrackingPref.setVisible(isHeadTrackingAvailable);
216         if (isHeadTrackingAvailable) {
217             headTrackingPref.setChecked(mSpatializer.isHeadTrackerEnabled(mAudioDevice));
218         }
219 
220         if (mInitialRefresh.compareAndSet(true, false)) {
221             // Only triggered when shown for the first time
222             mMetricsFeatureProvider.action(
223                     mContext,
224                     SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TRIGGERED,
225                     spatialAudioPref.isChecked());
226             if (mHasHeadTracker.get()) {
227                 mMetricsFeatureProvider.action(
228                         mContext,
229                         SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TRIGGERED,
230                         headTrackingPref.isChecked());
231             }
232         }
233     }
234 
235     @VisibleForTesting
createSpatialAudioPreference(Context context)236     TwoStatePreference createSpatialAudioPreference(Context context) {
237         TwoStatePreference pref = new SwitchPreferenceCompat(context);
238         pref.setKey(KEY_SPATIAL_AUDIO);
239         pref.setTitle(context.getString(R.string.bluetooth_details_spatial_audio_title));
240         pref.setSummary(context.getString(R.string.bluetooth_details_spatial_audio_summary));
241         pref.setOnPreferenceClickListener(this);
242         return pref;
243     }
244 
245     @VisibleForTesting
createHeadTrackingPreference(Context context)246     TwoStatePreference createHeadTrackingPreference(Context context) {
247         TwoStatePreference pref = new SwitchPreferenceCompat(context);
248         pref.setKey(KEY_HEAD_TRACKING);
249         pref.setTitle(context.getString(R.string.bluetooth_details_head_tracking_title));
250         pref.setSummary(context.getString(R.string.bluetooth_details_head_tracking_summary));
251         pref.setOnPreferenceClickListener(this);
252         return pref;
253     }
254 
getAvailableDevice()255     private void getAvailableDevice() {
256         AudioDeviceAttributes a2dpDevice = new AudioDeviceAttributes(
257                 AudioDeviceAttributes.ROLE_OUTPUT,
258                 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
259                 mCachedDevice.getAddress());
260         AudioDeviceAttributes bleHeadsetDevice = new AudioDeviceAttributes(
261                 AudioDeviceAttributes.ROLE_OUTPUT,
262                 AudioDeviceInfo.TYPE_BLE_HEADSET,
263                 mCachedDevice.getAddress());
264         AudioDeviceAttributes bleSpeakerDevice = new AudioDeviceAttributes(
265                 AudioDeviceAttributes.ROLE_OUTPUT,
266                 AudioDeviceInfo.TYPE_BLE_SPEAKER,
267                 mCachedDevice.getAddress());
268         AudioDeviceAttributes bleBroadcastDevice = new AudioDeviceAttributes(
269                 AudioDeviceAttributes.ROLE_OUTPUT,
270                 AudioDeviceInfo.TYPE_BLE_BROADCAST,
271                 mCachedDevice.getAddress());
272         AudioDeviceAttributes hearingAidDevice = new AudioDeviceAttributes(
273                 AudioDeviceAttributes.ROLE_OUTPUT,
274                 AudioDeviceInfo.TYPE_HEARING_AID,
275                 mCachedDevice.getAddress());
276 
277         if (mSpatializer.isAvailableForDevice(bleHeadsetDevice)) {
278             mAudioDevice = bleHeadsetDevice;
279         } else if (mSpatializer.isAvailableForDevice(bleSpeakerDevice)) {
280             mAudioDevice = bleSpeakerDevice;
281         } else if (mSpatializer.isAvailableForDevice(bleBroadcastDevice)) {
282             mAudioDevice = bleBroadcastDevice;
283         } else if (mSpatializer.isAvailableForDevice(a2dpDevice)) {
284             mAudioDevice = a2dpDevice;
285         } else if (mSpatializer.isAvailableForDevice(hearingAidDevice)) {
286             mAudioDevice = hearingAidDevice;
287         } else {
288             mAudioDevice = null;
289         }
290 
291         Log.d(TAG, "getAvailableDevice() device : "
292                 + mCachedDevice.getDevice().getAnonymizedAddress()
293                 + ", is available : " + (mAudioDevice != null)
294                 + ", type : " + (mAudioDevice == null ? "no type" : mAudioDevice.getType()));
295     }
296 
getAvailableDeviceByProfileState()297     private void getAvailableDeviceByProfileState() {
298         Log.i(
299                 TAG,
300                 "getAvailableDevice() mCachedDevice: "
301                         + mCachedDevice
302                         + " profiles: "
303                         + mCachedDevice.getProfiles());
304 
305         AudioDeviceAttributes saDevice =
306                 BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
307                         mCachedDevice,
308                         mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress()));
309         if (saDevice != null && mSpatializer.isAvailableForDevice(saDevice)) {
310             mAudioDevice = saDevice;
311         } else {
312             mAudioDevice = null;
313         }
314 
315         Log.d(
316                 TAG,
317                 "getAvailableDevice() device : "
318                         + mCachedDevice.getDevice().getAnonymizedAddress()
319                         + ", is available : "
320                         + (mAudioDevice != null)
321                         + ", type : "
322                         + (mAudioDevice == null ? "no type" : mAudioDevice.getType()));
323     }
324 
325     @VisibleForTesting
setAvailableDevice(AudioDeviceAttributes audioDevice)326     void setAvailableDevice(AudioDeviceAttributes audioDevice) {
327         mAudioDevice = audioDevice;
328     }
329 }
330