blob: 73eb2703855ac4f77b30390ece92326044213ad1 [file] [log] [blame]
/*
* Copyright (C) 2018 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.car.settings.sound;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED;
import static android.os.UserManager.DISALLOW_ADJUST_VOLUME;
import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm;
import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
import static com.android.car.settings.sound.VolumeItemParser.VolumeItem;
import android.car.CarNotConnectedException;
import android.car.drivingstate.CarUxRestrictions;
import android.car.media.CarAudioManager;
import android.car.media.CarVolumeGroupEvent;
import android.car.media.CarVolumeGroupEventCallback;
import android.car.media.CarVolumeGroupInfo;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.XmlRes;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.CarSettingsApplication;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.common.SeekBarPreference;
import com.android.car.settings.enterprise.EnterpriseUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Business logic which parses car volume items into groups, creates a seek bar preference for each
* group, and interfaces with the ringtone manager and audio manager.
*
* @see VolumeSettingsRingtoneManager
* @see android.car.media.CarAudioManager
*/
public class VolumeSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
private static final Logger LOG = new Logger(VolumeSettingsPreferenceController.class);
private static final String VOLUME_GROUP_KEY = "volume_group_key";
private static final String VOLUME_USAGE_KEY = "volume_usage_key";
private final SparseArray<VolumeItem> mVolumeItems;
private final List<VolumeSeekBarPreference> mVolumePreferences = new ArrayList<>();
private final VolumeSettingsRingtoneManager mRingtoneManager;
private final Handler mUiHandler;
private final Executor mExecutor;
@VisibleForTesting
final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
new CarAudioManager.CarVolumeCallback() {
@Override
public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
updateVolumeAndMute(zoneId, groupId, EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED);
}
@Override
public void onMasterMuteChanged(int zoneId, int flags) {
// Mute is not being used yet
}
@Override
public void onGroupMuteChanged(int zoneId, int groupId, int flags) {
updateVolumeAndMute(zoneId, groupId, EVENT_TYPE_MUTE_CHANGED);
}
};
@VisibleForTesting
final CarVolumeGroupEventCallback mCarVolumeGroupEventCallback =
new CarVolumeGroupEventCallback() {
@Override
public void onVolumeGroupEvent(List<CarVolumeGroupEvent> volumeGroupEvents) {
updateVolumeGroupForEvents(volumeGroupEvents);
}
};
public VolumeSettingsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
this(context, preferenceKey, fragmentController, uxRestrictions,
new VolumeSettingsRingtoneManager(context));
}
@VisibleForTesting
VolumeSettingsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
VolumeSettingsRingtoneManager ringtoneManager) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mRingtoneManager = ringtoneManager;
mVolumeItems = VolumeItemParser.loadAudioUsageItems(context, carVolumeItemsXml());
mUiHandler = new Handler(Looper.getMainLooper());
mExecutor = context.getMainExecutor();
CarAudioManager carAudioManager = getCarAudioManager();
if (carAudioManager != null) {
int zoneId = getMyAudioZoneId();
int volumeGroupCount = carAudioManager.getVolumeGroupCount(zoneId);
cleanUpVolumePreferences();
// Populates volume slider items from volume groups to UI.
for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
VolumeItem volumeItem = getVolumeItemForUsages(
carAudioManager.getUsagesForVolumeGroupId(zoneId, groupId));
VolumeSeekBarPreference volumePreference = createVolumeSeekBarPreference(
groupId, volumeItem.getUsage(), volumeItem.getIcon(),
volumeItem.getMuteIcon(), volumeItem.getTitle());
setClickableWhileDisabled(volumePreference, /* clickable= */ true, p -> {
if (hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) {
showActionDisabledByAdminDialog();
} else {
Toast.makeText(getContext(),
getContext().getString(R.string.action_unavailable),
Toast.LENGTH_LONG).show();
}
});
mVolumePreferences.add(volumePreference);
}
if (carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) {
carAudioManager.registerCarVolumeGroupEventCallback(mExecutor,
mCarVolumeGroupEventCallback);
} else {
carAudioManager.registerCarVolumeCallback(mVolumeChangeCallback);
}
}
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
/** Disconnect from car on destroy. */
@Override
protected void onDestroyInternal() {
cleanupAudioManager();
}
@Override
protected void updateState(PreferenceGroup preferenceGroup) {
for (SeekBarPreference preference : mVolumePreferences) {
preferenceGroup.addPreference(preference);
}
}
/**
* The resource which lists the car volume resources associated with the various usage enums.
*/
@XmlRes
@VisibleForTesting
int carVolumeItemsXml() {
return R.xml.car_volume_items;
}
private VolumeSeekBarPreference createVolumeSeekBarPreference(
int volumeGroupId, int usage, @DrawableRes int primaryIconResId,
@DrawableRes int secondaryIconResId, @StringRes int titleId) {
VolumeSeekBarPreference preference = new VolumeSeekBarPreference(getContext());
preference.setTitle(getContext().getString(titleId));
preference.setUnMutedIcon(getContext().getDrawable(primaryIconResId));
preference.getUnMutedIcon().setTintList(
getContext().getColorStateList(R.color.icon_color_default));
preference.setMutedIcon(getContext().getDrawable(secondaryIconResId));
preference.getMutedIcon().setTintList(
getContext().getColorStateList(R.color.icon_color_default));
int zoneId = getMyAudioZoneId();
CarAudioManager carAudioManager = getCarAudioManager();
try {
if (carAudioManager != null) {
preference.setValue(carAudioManager.getGroupVolume(zoneId, volumeGroupId));
preference.setMin(carAudioManager.getGroupMinVolume(zoneId, volumeGroupId));
preference.setMax(carAudioManager.getGroupMaxVolume(zoneId, volumeGroupId));
preference.setIsMuted(isGroupMuted(carAudioManager, volumeGroupId));
}
} catch (CarNotConnectedException e) {
LOG.e("Car is not connected!", e);
}
preference.setContinuousUpdate(true);
preference.setShowSeekBarValue(false);
Bundle bundle = preference.getExtras();
bundle.putInt(VOLUME_GROUP_KEY, volumeGroupId);
bundle.putInt(VOLUME_USAGE_KEY, usage);
preference.setOnPreferenceChangeListener((pref, newValue) -> {
int prefGroup = pref.getExtras().getInt(VOLUME_GROUP_KEY);
int prefUsage = pref.getExtras().getInt(VOLUME_USAGE_KEY);
int newVolume = (Integer) newValue;
setGroupVolume(prefGroup, newVolume);
if (carAudioManager != null
&& (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)
|| !carAudioManager.isPlaybackOnVolumeGroupActive(zoneId, volumeGroupId))) {
mRingtoneManager.playAudioFeedback(prefGroup, prefUsage);
}
return true;
});
return preference;
}
private boolean isGroupMuted(CarAudioManager carAudioManager, int volumeGroupId) {
if (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) {
return false;
}
return carAudioManager.isVolumeGroupMuted(getMyAudioZoneId(), volumeGroupId);
}
private void updateVolumeAndMute(int zoneId, int groupId, int eventTypes) {
if (zoneId != getMyAudioZoneId()) {
return;
}
CarAudioManager carAudioManager = getCarAudioManager();
if (carAudioManager != null) {
updateVolumePreference(carAudioManager.getVolumeGroupInfo(zoneId, groupId), eventTypes);
}
}
private void setGroupVolume(int volumeGroupId, int newVolume) {
try {
getCarAudioManager()
.setGroupVolume(getMyAudioZoneId(), volumeGroupId, newVolume, /* flags= */ 0);
} catch (CarNotConnectedException e) {
LOG.w("Ignoring volume change event because the car isn't connected", e);
}
}
private void cleanupAudioManager() {
cleanUpVolumePreferences();
CarAudioManager carAudioManager = getCarAudioManager();
if (carAudioManager != null) {
carAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback);
if (carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) {
carAudioManager.unregisterCarVolumeGroupEventCallback(mCarVolumeGroupEventCallback);
}
}
}
private void cleanUpVolumePreferences() {
mRingtoneManager.stopCurrentRingtone();
mVolumePreferences.clear();
}
private VolumeItem getVolumeItemForUsages(int[] usages) {
int rank = Integer.MAX_VALUE;
VolumeItem result = null;
for (int usage : usages) {
VolumeItem volumeItem = mVolumeItems.get(usage);
if (volumeItem.getRank() < rank) {
rank = volumeItem.getRank();
result = volumeItem;
}
}
return result;
}
@Override
public int getDefaultAvailabilityStatus() {
if (hasUserRestrictionByUm(getContext(), DISALLOW_ADJUST_VOLUME)
|| hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) {
return AVAILABLE_FOR_VIEWING;
}
return AVAILABLE;
}
private void showActionDisabledByAdminDialog() {
getFragmentController().showDialog(
EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
DISALLOW_ADJUST_VOLUME),
DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
}
private int getMyAudioZoneId() {
return ((CarSettingsApplication) getContext().getApplicationContext())
.getMyAudioZoneId();
}
private CarAudioManager getCarAudioManager() {
return ((CarSettingsApplication) getContext().getApplicationContext())
.getCarAudioManager();
}
private void updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents) {
List<CarVolumeGroupEvent> filteredEvents =
filterVolumeGroupEventForZoneId(getMyAudioZoneId(), volumeGroupEvents);
for (int index = 0; index < filteredEvents.size(); index++) {
CarVolumeGroupEvent event = filteredEvents.get(index);
int eventTypes = event.getEventTypes();
List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos();
for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) {
updateVolumePreference(infos.get(infoIndex), eventTypes);
}
}
}
private List<CarVolumeGroupEvent> filterVolumeGroupEventForZoneId(int zoneId,
List<CarVolumeGroupEvent> volumeGroupEvents) {
List<CarVolumeGroupEvent> filteredEvents = new ArrayList<>();
for (int index = 0; index < volumeGroupEvents.size(); index++) {
CarVolumeGroupEvent event = volumeGroupEvents.get(index);
List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos();
for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) {
if (infos.get(infoIndex).getZoneId() == zoneId) {
filteredEvents.add(event);
break;
}
}
}
return filteredEvents;
}
private void updateVolumePreference(CarVolumeGroupInfo groupInfo, int eventTypes) {
int groupId = groupInfo.getId();
for (VolumeSeekBarPreference volumePreference : mVolumePreferences) {
Bundle extras = volumePreference.getExtras();
if (extras.getInt(VOLUME_GROUP_KEY) == groupId) {
mUiHandler.post(() -> {
if ((eventTypes & EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED) != 0) {
volumePreference.setValue(groupInfo.getVolumeGainIndex());
}
if ((eventTypes & EVENT_TYPE_MUTE_CHANGED) != 0) {
volumePreference.setIsMuted(groupInfo.isMuted());
}
if ((eventTypes & EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED) != 0) {
volumePreference.setMin(groupInfo.getMinVolumeGainIndex());
}
if ((eventTypes & EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED) != 0) {
volumePreference.setMax(groupInfo.getMaxVolumeGainIndex());
}
});
break;
}
}
}
}