1 /* 2 * Copyright (C) 2024 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.view.View.GONE; 20 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; 21 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; 22 import static android.view.View.VISIBLE; 23 24 import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER; 25 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; 26 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; 27 28 import android.bluetooth.BluetoothDevice; 29 import android.content.Context; 30 import android.view.View; 31 import android.widget.ImageView; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.preference.PreferenceGroup; 37 import androidx.preference.PreferenceViewHolder; 38 39 import com.android.settings.R; 40 import com.android.settings.overlay.FeatureFactory; 41 import com.android.settingslib.bluetooth.AmbientVolumeUi; 42 import com.android.settingslib.widget.SettingsThemeHelper; 43 import com.android.settingslib.widget.SliderPreference; 44 45 import com.google.common.collect.BiMap; 46 import com.google.common.collect.HashBiMap; 47 import com.google.common.primitives.Ints; 48 49 import java.util.Map; 50 51 /** 52 * A preference group of ambient volume controls. 53 * 54 * <p> It consists of a header with an expand icon and volume sliders for unified control and 55 * separated control for devices in the same set. Toggle the expand icon will make the UI switch 56 * between unified and separated control. 57 */ 58 public class AmbientVolumePreference extends PreferenceGroup implements AmbientVolumeUi { 59 60 private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0; 61 private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1; 62 63 private static final String METRIC_KEY_AMBIENT_SLIDER = "ambient_slider"; 64 private static final String METRIC_KEY_AMBIENT_MUTE = "ambient_mute"; 65 private static final String METRIC_KEY_AMBIENT_EXPAND = "ambient_expand"; 66 67 @Nullable 68 private AmbientVolumeUiListener mListener; 69 @Nullable 70 private View mExpandIcon; 71 @Nullable 72 private View mVolumeIconFrame; 73 @Nullable 74 private ImageView mVolumeIcon; 75 private boolean mExpandable = true; 76 private boolean mExpanded = false; 77 private boolean mMutable = false; 78 private boolean mMuted = false; 79 private final BiMap<Integer, SliderPreference> mSideToSliderMap = HashBiMap.create(); 80 private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; 81 82 private int mMetricsCategory; 83 84 private final OnPreferenceChangeListener mPreferenceChangeListener = 85 (slider, v) -> { 86 if (slider instanceof SliderPreference && v instanceof final Integer value) { 87 final Integer side = mSideToSliderMap.inverse().get(slider); 88 if (side != null) { 89 logMetrics(METRIC_KEY_AMBIENT_SLIDER, side); 90 if (mListener != null) { 91 mListener.onSliderValueChange(side, value); 92 } 93 } 94 return true; 95 } 96 return false; 97 }; 98 AmbientVolumePreference(@onNull Context context)99 public AmbientVolumePreference(@NonNull Context context) { 100 super(context, null); 101 int resId = SettingsThemeHelper.isExpressiveTheme(context) 102 ? R.layout.preference_ambient_volume_expressive 103 : R.layout.preference_ambient_volume; 104 setLayoutResource(resId); 105 setIcon(com.android.settingslib.R.drawable.ic_ambient_volume); 106 setTitle(R.string.bluetooth_ambient_volume_control); 107 setSelectable(false); 108 } 109 110 @Override onBindViewHolder(@onNull PreferenceViewHolder holder)111 public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { 112 super.onBindViewHolder(holder); 113 holder.setDividerAllowedAbove(false); 114 holder.setDividerAllowedBelow(false); 115 116 mVolumeIcon = holder.itemView.requireViewById(com.android.internal.R.id.icon); 117 mVolumeIcon.getDrawable().mutate().setTint(getContext().getColor( 118 com.android.internal.R.color.materialColorOnPrimaryContainer)); 119 mVolumeIconFrame = holder.itemView.requireViewById(R.id.icon_frame); 120 int volumeIconBackgroundResId = SettingsThemeHelper.isExpressiveTheme(getContext()) 121 ? R.drawable.ambient_icon_background_expressive 122 : R.drawable.ambient_icon_background; 123 mVolumeIconFrame.setBackgroundResource(volumeIconBackgroundResId); 124 mVolumeIconFrame.setOnClickListener(v -> { 125 if (!mMutable) { 126 return; 127 } 128 setMuted(!mMuted); 129 logMetrics(METRIC_KEY_AMBIENT_MUTE, mMuted ? 1 : 0); 130 if (mListener != null) { 131 mListener.onAmbientVolumeIconClick(); 132 } 133 }); 134 updateVolumeIcon(); 135 136 mExpandIcon = holder.itemView.requireViewById(R.id.expand_icon); 137 mExpandIcon.setOnClickListener(v -> { 138 setExpanded(!mExpanded); 139 logMetrics(METRIC_KEY_AMBIENT_EXPAND, mExpanded ? 1 : 0); 140 if (mListener != null) { 141 mListener.onExpandIconClick(); 142 } 143 }); 144 updateExpandIcon(); 145 } 146 147 @Override setExpandable(boolean expandable)148 public void setExpandable(boolean expandable) { 149 mExpandable = expandable; 150 if (!mExpandable) { 151 setExpanded(false); 152 } 153 updateExpandIcon(); 154 } 155 156 @Override isExpandable()157 public boolean isExpandable() { 158 return mExpandable; 159 } 160 161 @Override setExpanded(boolean expanded)162 public void setExpanded(boolean expanded) { 163 if (!mExpandable && expanded) { 164 return; 165 } 166 mExpanded = expanded; 167 updateExpandIcon(); 168 updateLayout(); 169 } 170 171 @Override isExpanded()172 public boolean isExpanded() { 173 return mExpanded; 174 } 175 176 @Override setMutable(boolean mutable)177 public void setMutable(boolean mutable) { 178 mMutable = mutable; 179 if (!mMutable) { 180 mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; 181 setMuted(false); 182 } 183 updateVolumeIcon(); 184 } 185 186 @Override isMutable()187 public boolean isMutable() { 188 return mMutable; 189 } 190 191 @Override setMuted(boolean muted)192 public void setMuted(boolean muted) { 193 if (!mMutable && muted) { 194 return; 195 } 196 mMuted = muted; 197 if (mMutable && mMuted) { 198 for (SliderPreference slider : mSideToSliderMap.values()) { 199 slider.setValue(slider.getMin()); 200 } 201 } 202 updateVolumeIcon(); 203 } 204 205 @Override isMuted()206 public boolean isMuted() { 207 return mMuted; 208 } 209 210 @Override setListener(@ullable AmbientVolumeUiListener listener)211 public void setListener(@Nullable AmbientVolumeUiListener listener) { 212 mListener = listener; 213 } 214 215 @Override setupSliders(@onNull Map<Integer, BluetoothDevice> sideToDeviceMap)216 public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) { 217 sideToDeviceMap.forEach((side, device) -> 218 createSlider(side, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + side)); 219 createSlider(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED); 220 221 if (!mSideToSliderMap.isEmpty()) { 222 for (int side : VALID_SIDES) { 223 final SliderPreference slider = mSideToSliderMap.get(side); 224 if (slider != null && findPreference(slider.getKey()) == null) { 225 addPreference(slider); 226 } 227 } 228 } 229 updateLayout(); 230 } 231 232 @Override setSliderEnabled(int side, boolean enabled)233 public void setSliderEnabled(int side, boolean enabled) { 234 SliderPreference slider = mSideToSliderMap.get(side); 235 if (slider != null && slider.isEnabled() != enabled) { 236 slider.setEnabled(enabled); 237 updateLayout(); 238 } 239 } 240 241 @Override setSliderValue(int side, int value)242 public void setSliderValue(int side, int value) { 243 SliderPreference slider = mSideToSliderMap.get(side); 244 if (slider != null && slider.getValue() != value) { 245 slider.setValue(value); 246 updateVolumeLevel(); 247 } 248 } 249 250 @Override setSliderRange(int side, int min, int max)251 public void setSliderRange(int side, int min, int max) { 252 SliderPreference slider = mSideToSliderMap.get(side); 253 if (slider != null) { 254 slider.setMin(min); 255 slider.setMax(max); 256 } 257 } 258 259 @Override updateLayout()260 public void updateLayout() { 261 mSideToSliderMap.forEach((side, slider) -> { 262 if (side == SIDE_UNIFIED) { 263 slider.setVisible(!mExpanded); 264 } else { 265 slider.setVisible(mExpanded); 266 } 267 if (!slider.isEnabled()) { 268 slider.setValue(slider.getMin()); 269 } 270 }); 271 updateVolumeLevel(); 272 } 273 274 /** Sets the metrics category. */ setMetricsCategory(int category)275 public void setMetricsCategory(int category) { 276 mMetricsCategory = category; 277 } 278 getMetricsCategory()279 private int getMetricsCategory() { 280 return mMetricsCategory; 281 } 282 updateVolumeLevel()283 private void updateVolumeLevel() { 284 int leftLevel, rightLevel; 285 if (mExpanded) { 286 leftLevel = getVolumeLevel(SIDE_LEFT); 287 rightLevel = getVolumeLevel(SIDE_RIGHT); 288 } else { 289 final int unifiedLevel = getVolumeLevel(SIDE_UNIFIED); 290 leftLevel = unifiedLevel; 291 rightLevel = unifiedLevel; 292 } 293 mVolumeLevel = Ints.constrainToRange(leftLevel * 5 + rightLevel, 294 AMBIENT_VOLUME_LEVEL_MIN, AMBIENT_VOLUME_LEVEL_MAX); 295 updateVolumeIcon(); 296 } 297 getVolumeLevel(int side)298 private int getVolumeLevel(int side) { 299 SliderPreference slider = mSideToSliderMap.get(side); 300 if (slider == null || !slider.isEnabled()) { 301 return 0; 302 } 303 final double min = slider.getMin(); 304 final double max = slider.getMax(); 305 final double levelGap = (max - min) / 4.0; 306 final int value = slider.getValue(); 307 return (int) Math.ceil((value - min) / levelGap); 308 } 309 updateExpandIcon()310 private void updateExpandIcon() { 311 if (mExpandIcon == null) { 312 return; 313 } 314 mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); 315 mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); 316 if (mExpandable) { 317 final int stringRes = mExpanded ? R.string.bluetooth_ambient_volume_control_collapse 318 : R.string.bluetooth_ambient_volume_control_expand; 319 mExpandIcon.setContentDescription(getContext().getString(stringRes)); 320 } else { 321 mExpandIcon.setContentDescription(null); 322 } 323 } 324 updateVolumeIcon()325 private void updateVolumeIcon() { 326 if (mVolumeIcon == null || mVolumeIconFrame == null) { 327 return; 328 } 329 mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); 330 if (mMutable) { 331 final int stringRes = mMuted ? R.string.bluetooth_ambient_volume_unmute 332 : R.string.bluetooth_ambient_volume_mute; 333 mVolumeIcon.setContentDescription(getContext().getString(stringRes)); 334 mVolumeIconFrame.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 335 } else { 336 mVolumeIcon.setContentDescription(null); 337 mVolumeIconFrame.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 338 } 339 } 340 createSlider(int side, int order)341 private void createSlider(int side, int order) { 342 if (mSideToSliderMap.containsKey(side)) { 343 return; 344 } 345 SliderPreference slider = new SliderPreference(getContext()); 346 slider.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side); 347 slider.setOrder(order); 348 slider.setOnPreferenceChangeListener(mPreferenceChangeListener); 349 if (side == SIDE_LEFT) { 350 slider.setTitle( 351 getContext().getString(R.string.bluetooth_ambient_volume_control_left)); 352 slider.setSliderContentDescription(getContext().getString( 353 R.string.bluetooth_ambient_volume_control_left_description)); 354 } else if (side == SIDE_RIGHT) { 355 slider.setTitle( 356 getContext().getString(R.string.bluetooth_ambient_volume_control_right)); 357 slider.setSliderContentDescription(getContext().getString( 358 R.string.bluetooth_ambient_volume_control_right_description)); 359 } else { 360 slider.setSliderContentDescription(getContext().getString( 361 R.string.bluetooth_ambient_volume_control_description)); 362 } 363 mSideToSliderMap.put(side, slider); 364 } 365 366 @VisibleForTesting getSliders()367 Map<Integer, SliderPreference> getSliders() { 368 return mSideToSliderMap; 369 } 370 logMetrics(String key, int value)371 private void logMetrics(String key, int value) { 372 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().changed( 373 getMetricsCategory(), key, value); 374 } 375 } 376