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.systemui.accessibility.hearingaid; 18 19 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; 20 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; 21 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Context; 24 import android.util.AttributeSet; 25 import android.widget.ImageView; 26 import android.widget.LinearLayout; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.annotation.VisibleForTesting; 31 32 import com.android.settingslib.bluetooth.AmbientVolumeUi; 33 import com.android.systemui.res.R; 34 35 import com.google.common.collect.BiMap; 36 import com.google.common.collect.HashBiMap; 37 import com.google.common.primitives.Ints; 38 39 import java.util.Map; 40 41 /** 42 * A view of ambient volume controls. 43 * 44 * <p> It consists of a header with an expand icon and volume sliders for unified control and 45 * separated control for devices in the same set. Toggle the expand icon will make the UI switch 46 * between unified and separated control. 47 */ 48 public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi { 49 50 @Nullable 51 private AmbientVolumeUiListener mListener; 52 private ImageView mExpandIcon; 53 private ImageView mVolumeIcon; 54 private boolean mExpandable = true; 55 private boolean mExpanded = false; 56 private boolean mMutable = false; 57 private boolean mMuted = false; 58 private final BiMap<Integer, AmbientVolumeSlider> mSideToSliderMap = HashBiMap.create(); 59 private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; 60 61 private HearingDevicesUiEventLogger mUiEventLogger; 62 private int mLaunchSourceId; 63 64 private final AmbientVolumeSlider.OnChangeListener mSliderOnChangeListener = 65 (slider, value) -> { 66 final Integer side = mSideToSliderMap.inverse().get(slider); 67 if (side != null) { 68 if (mUiEventLogger != null) { 69 HearingDevicesUiEvent uiEvent = side == SIDE_UNIFIED 70 ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_UNIFIED 71 : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_SEPARATED; 72 mUiEventLogger.log(uiEvent, mLaunchSourceId); 73 } 74 if (mListener != null) { 75 mListener.onSliderValueChange(side, value); 76 } 77 } 78 }; 79 AmbientVolumeLayout(@ullable Context context)80 public AmbientVolumeLayout(@Nullable Context context) { 81 this(context, /* attrs= */ null); 82 } 83 AmbientVolumeLayout(@ullable Context context, @Nullable AttributeSet attrs)84 public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs) { 85 this(context, attrs, /* defStyleAttr= */ 0); 86 } 87 AmbientVolumeLayout(@ullable Context context, @Nullable AttributeSet attrs, int defStyleAttr)88 public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, 89 int defStyleAttr) { 90 this(context, attrs, defStyleAttr, /* defStyleRes= */ 0); 91 } 92 AmbientVolumeLayout(@ullable Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)93 public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, 94 int defStyleAttr, int defStyleRes) { 95 super(context, attrs, defStyleAttr, defStyleRes); 96 inflate(context, R.layout.hearing_device_ambient_volume_layout, /* root= */ this); 97 init(); 98 } 99 init()100 private void init() { 101 mVolumeIcon = requireViewById(R.id.ambient_volume_icon); 102 mVolumeIcon.setImageResource(com.android.settingslib.R.drawable.ic_ambient_volume); 103 mVolumeIcon.setOnClickListener(v -> { 104 if (!mMutable) { 105 return; 106 } 107 setMuted(!mMuted); 108 if (mUiEventLogger != null) { 109 HearingDevicesUiEvent uiEvent = mMuted 110 ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_MUTE 111 : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_UNMUTE; 112 mUiEventLogger.log(uiEvent, mLaunchSourceId); 113 } 114 if (mListener != null) { 115 mListener.onAmbientVolumeIconClick(); 116 } 117 }); 118 updateVolumeIcon(); 119 120 mExpandIcon = requireViewById(R.id.ambient_expand_icon); 121 mExpandIcon.setOnClickListener(v -> { 122 setExpanded(!mExpanded); 123 if (mUiEventLogger != null) { 124 HearingDevicesUiEvent uiEvent = mExpanded 125 ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS 126 : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS; 127 mUiEventLogger.log(uiEvent, mLaunchSourceId); 128 } 129 if (mListener != null) { 130 mListener.onExpandIconClick(); 131 } 132 }); 133 updateExpandIcon(); 134 } 135 136 @Override setVisible(boolean visible)137 public void setVisible(boolean visible) { 138 setVisibility(visible ? VISIBLE : GONE); 139 } 140 141 @Override setExpandable(boolean expandable)142 public void setExpandable(boolean expandable) { 143 mExpandable = expandable; 144 if (!mExpandable) { 145 setExpanded(false); 146 } 147 updateExpandIcon(); 148 } 149 150 @Override isExpandable()151 public boolean isExpandable() { 152 return mExpandable; 153 } 154 155 @Override setExpanded(boolean expanded)156 public void setExpanded(boolean expanded) { 157 if (!mExpandable && expanded) { 158 return; 159 } 160 mExpanded = expanded; 161 updateExpandIcon(); 162 updateLayout(); 163 } 164 165 @Override isExpanded()166 public boolean isExpanded() { 167 return mExpanded; 168 } 169 170 @Override setMutable(boolean mutable)171 public void setMutable(boolean mutable) { 172 mMutable = mutable; 173 if (!mMutable) { 174 mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; 175 setMuted(false); 176 } 177 updateVolumeIcon(); 178 } 179 180 @Override isMutable()181 public boolean isMutable() { 182 return mMutable; 183 } 184 185 @Override setMuted(boolean muted)186 public void setMuted(boolean muted) { 187 if (!mMutable && muted) { 188 return; 189 } 190 mMuted = muted; 191 if (mMutable && mMuted) { 192 for (AmbientVolumeSlider slider : mSideToSliderMap.values()) { 193 slider.setValue(slider.getMin()); 194 } 195 } 196 updateVolumeIcon(); 197 } 198 199 @Override isMuted()200 public boolean isMuted() { 201 return mMuted; 202 } 203 204 @Override setListener(@ullable AmbientVolumeUiListener listener)205 public void setListener(@Nullable AmbientVolumeUiListener listener) { 206 mListener = listener; 207 } 208 209 @Override setupSliders(@onNull Map<Integer, BluetoothDevice> sideToDeviceMap)210 public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) { 211 sideToDeviceMap.forEach((side, device) -> createSlider(side)); 212 createSlider(SIDE_UNIFIED); 213 214 LinearLayout controlContainer = requireViewById(R.id.ambient_control_container); 215 controlContainer.removeAllViews(); 216 if (!mSideToSliderMap.isEmpty()) { 217 for (int side : VALID_SIDES) { 218 final AmbientVolumeSlider slider = mSideToSliderMap.get(side); 219 if (slider != null) { 220 controlContainer.addView(slider); 221 } 222 } 223 } 224 updateLayout(); 225 } 226 227 @Override setSliderEnabled(int side, boolean enabled)228 public void setSliderEnabled(int side, boolean enabled) { 229 AmbientVolumeSlider slider = mSideToSliderMap.get(side); 230 if (slider != null && slider.isEnabled() != enabled) { 231 slider.setEnabled(enabled); 232 updateLayout(); 233 } 234 } 235 236 @Override setSliderValue(int side, int value)237 public void setSliderValue(int side, int value) { 238 AmbientVolumeSlider slider = mSideToSliderMap.get(side); 239 if (slider != null && slider.getValue() != value) { 240 slider.setValue(value); 241 updateVolumeLevel(); 242 } 243 } 244 245 @Override setSliderRange(int side, int min, int max)246 public void setSliderRange(int side, int min, int max) { 247 AmbientVolumeSlider slider = mSideToSliderMap.get(side); 248 if (slider != null) { 249 slider.setMin(min); 250 slider.setMax(max); 251 } 252 } 253 254 @Override updateLayout()255 public void updateLayout() { 256 mSideToSliderMap.forEach((side, slider) -> { 257 if (side == SIDE_UNIFIED) { 258 slider.setVisibility(mExpanded ? GONE : VISIBLE); 259 } else { 260 slider.setVisibility(mExpanded ? VISIBLE : GONE); 261 } 262 if (!slider.isEnabled()) { 263 slider.setValue(slider.getMin()); 264 } 265 }); 266 updateVolumeLevel(); 267 } 268 setUiEventLogger(HearingDevicesUiEventLogger uiEventLogger, int launchSourceId)269 void setUiEventLogger(HearingDevicesUiEventLogger uiEventLogger, int launchSourceId) { 270 mUiEventLogger = uiEventLogger; 271 mLaunchSourceId = launchSourceId; 272 } 273 updateVolumeLevel()274 private void updateVolumeLevel() { 275 int leftLevel, rightLevel; 276 if (mExpanded) { 277 leftLevel = getVolumeLevel(SIDE_LEFT); 278 rightLevel = getVolumeLevel(SIDE_RIGHT); 279 } else { 280 final int unifiedLevel = getVolumeLevel(SIDE_UNIFIED); 281 leftLevel = unifiedLevel; 282 rightLevel = unifiedLevel; 283 } 284 mVolumeLevel = Ints.constrainToRange(leftLevel * 5 + rightLevel, 285 AMBIENT_VOLUME_LEVEL_MIN, AMBIENT_VOLUME_LEVEL_MAX); 286 updateVolumeIcon(); 287 } 288 getVolumeLevel(int side)289 private int getVolumeLevel(int side) { 290 AmbientVolumeSlider slider = mSideToSliderMap.get(side); 291 if (slider == null || !slider.isEnabled()) { 292 return 0; 293 } 294 return slider.getVolumeLevel(); 295 } 296 updateExpandIcon()297 private void updateExpandIcon() { 298 mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); 299 mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); 300 if (mExpandable) { 301 final int stringRes = mExpanded ? R.string.hearing_devices_ambient_collapse_controls 302 : R.string.hearing_devices_ambient_expand_controls; 303 mExpandIcon.setContentDescription(mContext.getString(stringRes)); 304 } else { 305 mExpandIcon.setContentDescription(null); 306 } 307 } 308 updateVolumeIcon()309 private void updateVolumeIcon() { 310 mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); 311 if (mMutable) { 312 final int stringRes = mMuted ? R.string.hearing_devices_ambient_unmute 313 : R.string.hearing_devices_ambient_mute; 314 mVolumeIcon.setContentDescription(mContext.getString(stringRes)); 315 mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 316 } else { 317 mVolumeIcon.setContentDescription(null); 318 mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 319 } 320 } 321 createSlider(int side)322 private void createSlider(int side) { 323 if (mSideToSliderMap.containsKey(side)) { 324 return; 325 } 326 AmbientVolumeSlider slider = new AmbientVolumeSlider(mContext); 327 slider.addOnChangeListener(mSliderOnChangeListener); 328 if (side == SIDE_LEFT) { 329 slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_left)); 330 slider.setContentDescription( 331 mContext.getString(R.string.hearing_devices_ambient_control_left)); 332 slider.setSliderContentDescription( 333 mContext.getString(R.string.hearing_devices_ambient_control_left_description)); 334 } else if (side == SIDE_RIGHT) { 335 slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_right)); 336 slider.setContentDescription( 337 mContext.getString(R.string.hearing_devices_ambient_control_right)); 338 slider.setSliderContentDescription( 339 mContext.getString(R.string.hearing_devices_ambient_control_right_description)); 340 } else { 341 slider.setSliderContentDescription( 342 mContext.getString(R.string.hearing_devices_ambient_control_description)); 343 } 344 mSideToSliderMap.put(side, slider); 345 } 346 347 @VisibleForTesting getVolumeIcon()348 ImageView getVolumeIcon() { 349 return mVolumeIcon; 350 } 351 352 @VisibleForTesting getExpandIcon()353 ImageView getExpandIcon() { 354 return mExpandIcon; 355 } 356 357 @VisibleForTesting getSliders()358 Map<Integer, AmbientVolumeSlider> getSliders() { 359 return mSideToSliderMap; 360 } 361 } 362