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