• 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.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