blob: a61b2e4d7fd37be25d100b4bdbe094bea4d05692 [file]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth;
import static android.bluetooth.AudioInputControl.MUTE_DISABLED;
import static android.bluetooth.AudioInputControl.MUTE_MUTED;
import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.hearingdevices.ui.AmbientVolumeUi;
import com.android.settingslib.widget.Expandable;
import com.android.settingslib.widget.SettingsThemeHelper;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.primitives.Ints;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A preference group of ambient volume controls.
*
* <p> It consists of a header with an expand icon and volume sliders for unified control and
* separated control for devices in the same set. Toggle the expand icon will make the UI switch
* between unified and separated control.
*/
public class AmbientVolumePreference extends PreferenceGroup implements AmbientVolumeUi,
Expandable {
private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0;
private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1;
private static final String METRIC_KEY_AMBIENT_SLIDER = "ambient_slider";
private static final String METRIC_KEY_AMBIENT_MUTE = "ambient_mute";
private static final String METRIC_KEY_AMBIENT_EXPAND = "ambient_expand";
@Nullable
private AmbientVolumeUiListener mListener;
@Nullable
private View mExpandIcon;
@Nullable
private ImageView mVolumeIcon;
private final BiMap<Integer, AmbientVolumeSliderPreference> mSideToSliderMap =
HashBiMap.create();
private boolean mExpandable = true;
private boolean mExpanded = false;
private int mMetricsCategory;
private final OnPreferenceChangeListener mPreferenceChangeListener =
(slider, v) -> {
if (slider instanceof AmbientVolumeSliderPreference
&& v instanceof final Integer value) {
final Integer side = mSideToSliderMap.inverse().get(slider);
if (side != null) {
logMetrics(METRIC_KEY_AMBIENT_SLIDER, side);
if (mListener != null) {
mListener.onSliderValueChange(side, value);
}
}
return true;
}
return false;
};
public AmbientVolumePreference(@NonNull Context context) {
super(context, null);
if (SettingsThemeHelper.isExpressiveTheme(context)) {
setLayoutResource(com.android.settingslib.widget.theme
.R.layout.settingslib_expressive_preference);
setWidgetLayoutResource(com.android.settingslib.widget.preference.expandable
.R.layout.settingslib_widget_expandable_icon);
} else {
setLayoutResource(R.layout.preference_ambient_volume);
}
setIcon(com.android.settingslib.R.drawable.ic_ambient_volume);
setTitle(R.string.bluetooth_ambient_volume_control);
setSelectable(false);
}
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(false);
holder.setDividerAllowedBelow(false);
mVolumeIcon = holder.itemView.requireViewById(com.android.internal.R.id.icon);
mVolumeIcon.getDrawable().mutate().setTint(getContext().getColor(
com.android.internal.R.color.materialColorOnSurface));
updateVolumeIcon();
mExpandIcon = holder.itemView.requireViewById(R.id.expand_icon);
View expandIconView = holder.itemView.findViewById(com.android.internal.R.id.widget_frame);
if (expandIconView == null) {
expandIconView = mExpandIcon;
}
expandIconView.setOnClickListener(v -> {
if (!isControlExpandable()) {
return;
}
setControlExpanded(!mExpanded);
logMetrics(METRIC_KEY_AMBIENT_EXPAND, mExpanded ? 1 : 0);
if (mListener != null) {
mListener.onExpandIconClick();
}
});
updateExpandIcon();
}
@Override
public void setControlExpandable(boolean expandable) {
if (mExpandable != expandable) {
mExpandable = expandable;
if (!mExpandable) {
setControlExpanded(false);
}
updateExpandIcon();
}
}
@Override
public boolean isControlExpandable() {
return mExpandable;
}
@Override
public void setControlExpanded(boolean expanded) {
if (mExpanded != expanded) {
mExpanded = expanded;
updateExpandIcon();
updateLayout();
}
}
@Override
public boolean isControlExpanded() {
return mExpanded;
}
@Override
public void setSliderMuteState(int side, int muteState) {
AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
if (slider != null) {
slider.setMuteState(muteState);
updateMuteStateFromSide(side);
}
}
@Override
public int getSliderMuteState(int side) {
AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
return slider == null ? MUTE_DISABLED : slider.getMuteState();
}
@Override
public void setListener(@Nullable AmbientVolumeUiListener listener) {
mListener = listener;
}
@Override
public void setupSliders(@NonNull Set<Integer> sides) {
sides.forEach(side -> createSlider(side, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + side));
createSlider(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED);
if (!mSideToSliderMap.isEmpty()) {
for (int side : VALID_SIDES) {
final AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
if (slider != null && findPreference(slider.getKey()) == null) {
addPreference(slider);
}
}
}
updateLayout();
}
@Override
public void setSliderEnabled(int side, boolean enabled) {
AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
if (slider != null) {
slider.setEnabled(enabled);
updateVolumeIcon();
}
}
@Override
public void setSliderValue(int side, int value) {
AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
if (slider != null && slider.getValue() != value && slider.getMin() <= value
&& slider.getMax() >= value) {
slider.setValue(value);
updateVolumeIcon();
}
}
@Override
public void setSliderRange(int side, int min, int max) {
AmbientVolumeSliderPreference slider = mSideToSliderMap.get(side);
if (slider != null) {
slider.setMin(min);
slider.setMax(max);
}
}
/** Sets the metrics category. */
public void setMetricsCategory(int category) {
mMetricsCategory = category;
}
private void updateLayout() {
mSideToSliderMap.forEach((side, slider) -> {
if (side == SIDE_UNIFIED) {
slider.setVisible(!mExpanded);
} else {
slider.setVisible(mExpanded);
}
});
updateVolumeIcon();
}
private void updateVolumeIcon() {
int leftLevel, rightLevel;
if (isControlExpanded()) {
AmbientVolumeSliderPreference leftSlider = mSideToSliderMap.get(SIDE_LEFT);
AmbientVolumeSliderPreference rightSlider = mSideToSliderMap.get(SIDE_RIGHT);
leftLevel = leftSlider == null ? 0 : leftSlider.getVolumeLevel();
rightLevel = rightSlider == null ? 0 : rightSlider.getVolumeLevel();
} else {
AmbientVolumeSliderPreference unifiedSlider = mSideToSliderMap.get(SIDE_UNIFIED);
final int unifiedLevel = unifiedSlider == null ? 0 : unifiedSlider.getVolumeLevel();
leftLevel = unifiedLevel;
rightLevel = unifiedLevel;
}
int volumeLevel = Ints.constrainToRange(
leftLevel * AMBIENT_VOLUME_LEVEL_NUMBER + rightLevel,
AMBIENT_VOLUME_LEVEL_MIN,
AMBIENT_VOLUME_LEVEL_MAX);
if (mVolumeIcon != null) {
mVolumeIcon.setImageLevel(volumeLevel);
}
}
private void updateExpandIcon() {
if (mExpandIcon == null) {
return;
}
mExpandIcon.setVisibility(isControlExpandable() ? VISIBLE : GONE);
mExpandIcon.setRotation(isControlExpanded() ? ROTATION_EXPANDED : ROTATION_COLLAPSED);
if (isControlExpandable()) {
final int stringRes = isControlExpanded()
? R.string.bluetooth_ambient_volume_control_collapse
: R.string.bluetooth_ambient_volume_control_expand;
mExpandIcon.setContentDescription(getContext().getString(stringRes));
} else {
mExpandIcon.setContentDescription(null);
}
}
private void updateMuteStateFromSide(int side) {
if (side == SIDE_UNIFIED) {
// propagate the mute state to all other sliders
mSideToSliderMap.forEach((entrySide, entrySlider) -> {
if (entrySide != SIDE_UNIFIED) {
entrySlider.setMuteState(getSliderMuteState(SIDE_UNIFIED));
}
});
} else {
AmbientVolumeSliderPreference unifiedSlider = mSideToSliderMap.get(SIDE_UNIFIED);
if (unifiedSlider != null) {
List<AmbientVolumeSliderPreference> sideSliders =
mSideToSliderMap.entrySet().stream()
.filter(entry -> entry.getKey() != SIDE_UNIFIED)
.map(Map.Entry::getValue)
.toList();
if (sideSliders.stream().anyMatch(s -> s.getMuteState() == MUTE_NOT_MUTED)) {
unifiedSlider.setMuteState(MUTE_NOT_MUTED);
} else if (sideSliders.stream().allMatch(s -> s.getMuteState() == MUTE_DISABLED)) {
unifiedSlider.setMuteState(MUTE_DISABLED);
} else {
unifiedSlider.setMuteState(MUTE_MUTED);
}
}
}
updateVolumeIcon();
}
private void createSlider(int side, int order) {
if (mSideToSliderMap.containsKey(side)) {
return;
}
int titleResId = 0;
int contentResId;
if (side == SIDE_LEFT) {
titleResId = R.string.bluetooth_ambient_volume_control_left;
contentResId = R.string.bluetooth_ambient_volume_control_left_description;
} else if (side == SIDE_RIGHT) {
titleResId = R.string.bluetooth_ambient_volume_control_right;
contentResId = R.string.bluetooth_ambient_volume_control_right_description;
} else {
contentResId = R.string.bluetooth_ambient_volume_control_description;
}
String title = titleResId == 0 ? null : getContext().getString(titleResId);
String content = contentResId == 0 ? null : getContext().getString(contentResId);
AmbientVolumeSliderPreference slider = new AmbientVolumeSliderPreference(getContext());
slider.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side);
slider.setOrder(order);
slider.setTitle(title);
slider.setSliderContentDescription(content);
slider.setOnPreferenceChangeListener(mPreferenceChangeListener);
slider.setOnMuteIconClickListener((v) -> {
updateMuteStateFromSide(side);
boolean muted = slider.getMuteState() == MUTE_MUTED;
logMetrics(METRIC_KEY_AMBIENT_MUTE, muted ? 1 : 0);
if (mListener != null) {
mListener.onSliderMuteChange(side, muted);
}
});
mSideToSliderMap.put(side, slider);
}
private void logMetrics(String key, int value) {
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().changed(
mMetricsCategory, key, value);
}
@Override
public boolean isExpanded() {
// isExpanded() is different from isControlExpanded(), this is at the point of view if a
// preference group shows any of its child preference.
// Should always return true for AmbientVolumePreference as it always shows at least one
// child preference no matter in collapsed or expanded mode.
return true;
}
// No-op, as this preference is always expanded. See isExpanded().
@Override
public void setExpanded(boolean expanded) {}
@VisibleForTesting
Map<Integer, AmbientVolumeSliderPreference> getSliders() {
return mSideToSliderMap;
}
}