1 /* 2 * Copyright (C) 2023 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.connecteddevice.audiosharing; 18 19 import android.app.settings.SettingsEnums; 20 import android.bluetooth.BluetoothCsipSetCoordinator; 21 import android.bluetooth.BluetoothDevice; 22 import android.content.Context; 23 import android.media.AudioManager; 24 import android.util.Log; 25 import android.widget.SeekBar; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import com.android.settings.R; 31 import com.android.settings.bluetooth.Utils; 32 import com.android.settings.overlay.FeatureFactory; 33 import com.android.settings.widget.SeekBarPreference; 34 import com.android.settingslib.bluetooth.BluetoothUtils; 35 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 36 import com.android.settingslib.bluetooth.LocalBluetoothManager; 37 import com.android.settingslib.bluetooth.VolumeControlProfile; 38 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 39 import com.android.settingslib.utils.ThreadUtils; 40 41 public class AudioSharingDeviceVolumePreference extends SeekBarPreference { 42 private static final String TAG = "AudioSharingVolPref"; 43 44 public static final int MIN_VOLUME = 0; 45 public static final int MAX_VOLUME = 255; 46 47 private final Context mContext; 48 private final CachedBluetoothDevice mCachedDevice; 49 @Nullable private final LocalBluetoothManager mBtManager; 50 @Nullable protected SeekBar mSeekBar; 51 private Boolean mTrackingTouch = false; 52 private MetricsFeatureProvider mMetricsFeatureProvider = 53 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 54 AudioSharingDeviceVolumePreference( Context context, @NonNull CachedBluetoothDevice device)55 public AudioSharingDeviceVolumePreference( 56 Context context, @NonNull CachedBluetoothDevice device) { 57 super(context); 58 setLayoutResource(R.layout.preference_volume_slider); 59 mContext = context; 60 mCachedDevice = device; 61 mBtManager = Utils.getLocalBtManager(mContext); 62 } 63 64 @NonNull getCachedDevice()65 public CachedBluetoothDevice getCachedDevice() { 66 return mCachedDevice; 67 } 68 69 /** 70 * Initialize {@link AudioSharingDeviceVolumePreference}. 71 * 72 * <p>Need to be called after creating the preference. 73 */ initialize()74 public void initialize() { 75 setMax(MAX_VOLUME); 76 setMin(MIN_VOLUME); 77 } 78 79 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)80 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 81 super.onProgressChanged(seekBar, progress, fromUser); 82 // When user use talk back swipe up/down or use Switch Access to change the volume bar 83 // progress, there is no onStopTrackingTouch triggered. So we need to check this scenario 84 // and update the device volume here. 85 if (fromUser && !mTrackingTouch) { 86 Log.d(TAG, "onProgressChanged from user and not in touch, handleProgressChange."); 87 handleProgressChange(progress); 88 } 89 } 90 91 @Override onStartTrackingTouch(SeekBar seekBar)92 public void onStartTrackingTouch(SeekBar seekBar) { 93 mTrackingTouch = true; 94 super.onStartTrackingTouch(seekBar); 95 } 96 97 @Override onStopTrackingTouch(SeekBar seekBar)98 public void onStopTrackingTouch(SeekBar seekBar) { 99 mTrackingTouch = false; 100 super.onStopTrackingTouch(seekBar); 101 // When user touch the volume bar to change volume, we only update the device volume when 102 // user stop touching the bar. 103 Log.d(TAG, "onStopTrackingTouch, handleProgressChange."); 104 handleProgressChange(seekBar.getProgress()); 105 } 106 107 @Override equals(@ullable Object o)108 public boolean equals(@Nullable Object o) { 109 if ((o == null) || !(o instanceof AudioSharingDeviceVolumePreference)) { 110 return false; 111 } 112 return mCachedDevice.equals( 113 ((AudioSharingDeviceVolumePreference) o).mCachedDevice); 114 } 115 116 @Override hashCode()117 public int hashCode() { 118 return mCachedDevice.hashCode(); 119 } 120 121 @Override 122 @NonNull toString()123 public String toString() { 124 StringBuilder builder = new StringBuilder("Preference{"); 125 builder.append("preference=").append(super.toString()); 126 if (mCachedDevice.getDevice() != null) { 127 builder.append(", device=").append(mCachedDevice.getDevice().getAnonymizedAddress()); 128 } 129 builder.append("}"); 130 return builder.toString(); 131 } 132 onPreferenceAttributesChanged()133 void onPreferenceAttributesChanged() { 134 var unused = ThreadUtils.postOnBackgroundThread(() -> { 135 String name = mCachedDevice.getName(); 136 AudioSharingUtils.postOnMainThread(mContext, () -> setTitle(name)); 137 }); 138 } 139 handleProgressChange(int progress)140 private void handleProgressChange(int progress) { 141 var unused = 142 ThreadUtils.postOnBackgroundThread( 143 () -> { 144 int groupId = BluetoothUtils.getGroupId(mCachedDevice); 145 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID 146 && groupId 147 == BluetoothUtils.getPrimaryGroupIdForBroadcast( 148 mContext.getContentResolver(), mBtManager)) { 149 // Set media stream volume for primary buds, audio manager will 150 // update all buds volume in the audio sharing. 151 setAudioManagerStreamVolume(progress); 152 } else { 153 // Set buds volume for other buds. 154 setDeviceVolume(mCachedDevice.getDevice(), progress); 155 } 156 }); 157 } 158 setDeviceVolume(@ullable BluetoothDevice device, int progress)159 private void setDeviceVolume(@Nullable BluetoothDevice device, int progress) { 160 if (device == null) { 161 Log.d(TAG, "Skip set device volume, device is null"); 162 return; 163 } 164 VolumeControlProfile vc = mBtManager == null ? null 165 : mBtManager.getProfileManager().getVolumeControlProfile(); 166 if (vc != null) { 167 vc.setDeviceVolume(device, progress, /* isGroupOp= */ true); 168 mMetricsFeatureProvider.action( 169 mContext, 170 SettingsEnums.ACTION_AUDIO_SHARING_CHANGE_MEDIA_DEVICE_VOLUME, 171 /* isPrimary= */ false); 172 Log.d( 173 TAG, 174 "set device volume, device = " 175 + device.getAnonymizedAddress() 176 + " volume = " 177 + progress); 178 } 179 } 180 setAudioManagerStreamVolume(int progress)181 private void setAudioManagerStreamVolume(int progress) { 182 int seekbarRange = 183 AudioSharingDeviceVolumePreference.MAX_VOLUME 184 - AudioSharingDeviceVolumePreference.MIN_VOLUME; 185 try { 186 AudioManager audioManager = mContext.getSystemService(AudioManager.class); 187 int streamVolumeRange = 188 audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) 189 - audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC); 190 int volume = Math.round((float) progress * streamVolumeRange / seekbarRange); 191 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 192 mMetricsFeatureProvider.action( 193 mContext, 194 SettingsEnums.ACTION_AUDIO_SHARING_CHANGE_MEDIA_DEVICE_VOLUME, 195 /* isPrimary= */ true); 196 Log.d(TAG, "set music stream volume, volume = " + progress); 197 } catch (RuntimeException e) { 198 Log.e(TAG, "Fail to setAudioManagerStreamVolumeForFallbackDevice, error = " + e); 199 } 200 } 201 } 202