| /* |
| * 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; |
| } |
| } |
| } |
| } |