• 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.content.Context;
22 import android.media.AudioDeviceAttributes;
23 import android.media.AudioDeviceInfo;
24 import android.media.AudioManager;
25 import android.media.Spatializer;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import androidx.annotation.VisibleForTesting;
30 import androidx.preference.Preference;
31 import androidx.preference.PreferenceCategory;
32 import androidx.preference.PreferenceFragmentCompat;
33 import androidx.preference.PreferenceScreen;
34 import androidx.preference.SwitchPreference;
35 
36 import com.android.settings.R;
37 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
38 import com.android.settingslib.core.lifecycle.Lifecycle;
39 
40 /**
41  * The controller of the Spatial audio setting in the bluetooth detail settings.
42  */
43 public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsController
44         implements Preference.OnPreferenceClickListener {
45 
46     private static final String TAG = "BluetoothSpatialAudioController";
47     private static final String KEY_SPATIAL_AUDIO_GROUP = "spatial_audio_group";
48     private static final String KEY_SPATIAL_AUDIO = "spatial_audio";
49     private static final String KEY_HEAD_TRACKING = "head_tracking";
50 
51     private final Spatializer mSpatializer;
52 
53     @VisibleForTesting
54     PreferenceCategory mProfilesContainer;
55     @VisibleForTesting
56     AudioDeviceAttributes mAudioDevice = null;
57 
BluetoothDetailsSpatialAudioController( Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle)58     public BluetoothDetailsSpatialAudioController(
59             Context context,
60             PreferenceFragmentCompat fragment,
61             CachedBluetoothDevice device,
62             Lifecycle lifecycle) {
63         super(context, fragment, device, lifecycle);
64         AudioManager audioManager = context.getSystemService(AudioManager.class);
65         mSpatializer = audioManager.getSpatializer();
66     }
67 
68     @Override
isAvailable()69     public boolean isAvailable() {
70         return mSpatializer.getImmersiveAudioLevel() != SPATIALIZER_IMMERSIVE_LEVEL_NONE;
71     }
72 
73     @Override
onPreferenceClick(Preference preference)74     public boolean onPreferenceClick(Preference preference) {
75         SwitchPreference switchPreference = (SwitchPreference) preference;
76         String key = switchPreference.getKey();
77         if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
78             updateSpatializerEnabled(switchPreference.isChecked());
79             refreshSpatialAudioEnabled(switchPreference);
80             return true;
81         } else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
82             updateSpatializerHeadTracking(switchPreference.isChecked());
83             return true;
84         } else {
85             Log.w(TAG, "invalid key name.");
86             return false;
87         }
88     }
89 
updateSpatializerEnabled(boolean enabled)90     private void updateSpatializerEnabled(boolean enabled)  {
91         if (mAudioDevice == null) {
92             Log.w(TAG, "cannot update spatializer enabled for null audio device.");
93             return;
94         }
95         if (enabled) {
96             mSpatializer.addCompatibleAudioDevice(mAudioDevice);
97         } else {
98             mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
99         }
100     }
101 
updateSpatializerHeadTracking(boolean enabled)102     private void updateSpatializerHeadTracking(boolean enabled)  {
103         if (mAudioDevice == null) {
104             Log.w(TAG, "cannot update spatializer head tracking for null audio device.");
105             return;
106         }
107         mSpatializer.setHeadTrackerEnabled(enabled, mAudioDevice);
108     }
109 
110     @Override
getPreferenceKey()111     public String getPreferenceKey() {
112         return KEY_SPATIAL_AUDIO_GROUP;
113     }
114 
115     @Override
init(PreferenceScreen screen)116     protected void init(PreferenceScreen screen) {
117         mProfilesContainer = screen.findPreference(getPreferenceKey());
118         refresh();
119     }
120 
121     @Override
refresh()122     protected void refresh() {
123         if (mAudioDevice == null) {
124             getAvailableDevice();
125         }
126 
127         SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
128         if (spatialAudioPref == null && mAudioDevice != null) {
129             spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
130             mProfilesContainer.addPreference(spatialAudioPref);
131         } else if (mAudioDevice == null || !mSpatializer.isAvailableForDevice(mAudioDevice)) {
132             if (spatialAudioPref != null) {
133                 mProfilesContainer.removePreference(spatialAudioPref);
134             }
135             final SwitchPreference headTrackingPref =
136                     mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
137             if (headTrackingPref != null) {
138                 mProfilesContainer.removePreference(headTrackingPref);
139             }
140             mAudioDevice = null;
141             return;
142         }
143 
144         refreshSpatialAudioEnabled(spatialAudioPref);
145     }
146 
refreshSpatialAudioEnabled(SwitchPreference spatialAudioPref)147     private void refreshSpatialAudioEnabled(SwitchPreference spatialAudioPref) {
148         boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
149         Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
150         spatialAudioPref.setChecked(isSpatialAudioOn);
151 
152         SwitchPreference headTrackingPref = mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
153         if (headTrackingPref == null) {
154             headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
155             mProfilesContainer.addPreference(headTrackingPref);
156         }
157         refreshHeadTracking(spatialAudioPref, headTrackingPref);
158     }
159 
refreshHeadTracking(SwitchPreference spatialAudioPref, SwitchPreference headTrackingPref)160     private void refreshHeadTracking(SwitchPreference spatialAudioPref,
161                                      SwitchPreference headTrackingPref) {
162         boolean isHeadTrackingAvailable =
163                 spatialAudioPref.isChecked() && mSpatializer.hasHeadTracker(mAudioDevice);
164         Log.d(TAG, "refresh() has head tracker : " + mSpatializer.hasHeadTracker(mAudioDevice));
165         headTrackingPref.setVisible(isHeadTrackingAvailable);
166         if (isHeadTrackingAvailable) {
167             headTrackingPref.setChecked(mSpatializer.isHeadTrackerEnabled(mAudioDevice));
168         }
169     }
170 
171     @VisibleForTesting
createSpatialAudioPreference(Context context)172     SwitchPreference createSpatialAudioPreference(Context context) {
173         SwitchPreference pref = new SwitchPreference(context);
174         pref.setKey(KEY_SPATIAL_AUDIO);
175         pref.setTitle(context.getString(R.string.bluetooth_details_spatial_audio_title));
176         pref.setSummary(context.getString(R.string.bluetooth_details_spatial_audio_summary));
177         pref.setOnPreferenceClickListener(this);
178         return pref;
179     }
180 
181     @VisibleForTesting
createHeadTrackingPreference(Context context)182     SwitchPreference createHeadTrackingPreference(Context context) {
183         SwitchPreference pref = new SwitchPreference(context);
184         pref.setKey(KEY_HEAD_TRACKING);
185         pref.setTitle(context.getString(R.string.bluetooth_details_head_tracking_title));
186         pref.setSummary(context.getString(R.string.bluetooth_details_head_tracking_summary));
187         pref.setOnPreferenceClickListener(this);
188         return pref;
189     }
190 
getAvailableDevice()191     private void getAvailableDevice() {
192         AudioDeviceAttributes a2dpDevice = new AudioDeviceAttributes(
193                 AudioDeviceAttributes.ROLE_OUTPUT,
194                 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
195                 mCachedDevice.getAddress());
196         AudioDeviceAttributes bleHeadsetDevice = new AudioDeviceAttributes(
197                 AudioDeviceAttributes.ROLE_OUTPUT,
198                 AudioDeviceInfo.TYPE_BLE_HEADSET,
199                 mCachedDevice.getAddress());
200         AudioDeviceAttributes bleSpeakerDevice = new AudioDeviceAttributes(
201                 AudioDeviceAttributes.ROLE_OUTPUT,
202                 AudioDeviceInfo.TYPE_BLE_SPEAKER,
203                 mCachedDevice.getAddress());
204         AudioDeviceAttributes bleBroadcastDevice = new AudioDeviceAttributes(
205                 AudioDeviceAttributes.ROLE_OUTPUT,
206                 AudioDeviceInfo.TYPE_BLE_BROADCAST,
207                 mCachedDevice.getAddress());
208         AudioDeviceAttributes hearingAidDevice = new AudioDeviceAttributes(
209                 AudioDeviceAttributes.ROLE_OUTPUT,
210                 AudioDeviceInfo.TYPE_HEARING_AID,
211                 mCachedDevice.getAddress());
212 
213         if (mSpatializer.isAvailableForDevice(bleHeadsetDevice)) {
214             mAudioDevice = bleHeadsetDevice;
215         } else if (mSpatializer.isAvailableForDevice(bleSpeakerDevice)) {
216             mAudioDevice = bleSpeakerDevice;
217         } else if (mSpatializer.isAvailableForDevice(bleBroadcastDevice)) {
218             mAudioDevice = bleBroadcastDevice;
219         } else if (mSpatializer.isAvailableForDevice(a2dpDevice)) {
220             mAudioDevice = a2dpDevice;
221         } else if (mSpatializer.isAvailableForDevice(hearingAidDevice)) {
222             mAudioDevice = hearingAidDevice;
223         } else {
224             mAudioDevice = null;
225         }
226 
227         Log.d(TAG, "getAvailableDevice() device : "
228                 + mCachedDevice.getDevice().getAnonymizedAddress()
229                 + ", is available : " + (mAudioDevice != null)
230                 + ", type : " + (mAudioDevice == null ? "no type" : mAudioDevice.getType()));
231     }
232 
233     @VisibleForTesting
setAvailableDevice(AudioDeviceAttributes audioDevice)234     void setAvailableDevice(AudioDeviceAttributes audioDevice) {
235         mAudioDevice = audioDevice;
236     }
237 }
238