• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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