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