1 /* 2 * Copyright (C) 2018 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.car.settings.sound; 18 19 import static com.android.car.settings.sound.VolumeItemParser.VolumeItem; 20 21 import android.car.Car; 22 import android.car.CarNotConnectedException; 23 import android.car.drivingstate.CarUxRestrictions; 24 import android.car.media.CarAudioManager; 25 import android.content.Context; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.util.SparseArray; 30 31 import androidx.annotation.DrawableRes; 32 import androidx.annotation.StringRes; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.annotation.XmlRes; 35 import androidx.preference.PreferenceGroup; 36 37 import com.android.car.apps.common.util.Themes; 38 import com.android.car.settings.R; 39 import com.android.car.settings.common.FragmentController; 40 import com.android.car.settings.common.Logger; 41 import com.android.car.settings.common.PreferenceController; 42 import com.android.car.settings.common.SeekBarPreference; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 47 /** 48 * Business logic which parses car volume items into groups, creates a seek bar preference for each 49 * group, and interfaces with the ringtone manager and audio manager. 50 * 51 * @see VolumeSettingsRingtoneManager 52 * @see android.car.media.CarAudioManager 53 */ 54 public class VolumeSettingsPreferenceController extends PreferenceController<PreferenceGroup> { 55 private static final Logger LOG = new Logger(VolumeSettingsPreferenceController.class); 56 private static final String VOLUME_GROUP_KEY = "volume_group_key"; 57 private static final String VOLUME_USAGE_KEY = "volume_usage_key"; 58 59 private final SparseArray<VolumeItem> mVolumeItems; 60 private final List<SeekBarPreference> mVolumePreferences = new ArrayList<>(); 61 private final VolumeSettingsRingtoneManager mRingtoneManager; 62 63 private final Handler mUiHandler; 64 65 @VisibleForTesting 66 final CarAudioManager.CarVolumeCallback mVolumeChangeCallback = 67 new CarAudioManager.CarVolumeCallback() { 68 @Override 69 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 70 if (mCarAudioManager != null) { 71 int value = mCarAudioManager.getGroupVolume(groupId); 72 73 for (SeekBarPreference volumePreference : mVolumePreferences) { 74 Bundle extras = volumePreference.getExtras(); 75 if (extras.getInt(VOLUME_GROUP_KEY) == groupId) { 76 // Only setValue if the value is different, since changing the 77 // seekbar of the volume directly will trigger CarVolumeCallback as 78 // well, causing janky movement. 79 if (volumePreference.getValue() != value) { 80 // CarVolumeCallback is run on a binder thread. In order to 81 // make updates to the SeekBarPreference, we need to switch 82 // over to the UI thread. 83 mUiHandler.post(() -> { 84 volumePreference.setValue(value); 85 }); 86 } 87 break; 88 } 89 } 90 } 91 } 92 93 @Override 94 public void onMasterMuteChanged(int zoneId, int flags) { 95 // Mute is not being used yet 96 } 97 }; 98 99 private Car mCar; 100 private CarAudioManager mCarAudioManager; 101 VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)102 public VolumeSettingsPreferenceController(Context context, String preferenceKey, 103 FragmentController fragmentController, 104 CarUxRestrictions uxRestrictions) { 105 this(context, preferenceKey, fragmentController, uxRestrictions, Car.createCar(context), 106 new VolumeSettingsRingtoneManager(context)); 107 } 108 109 @VisibleForTesting VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, Car car, VolumeSettingsRingtoneManager ringtoneManager)110 VolumeSettingsPreferenceController(Context context, String preferenceKey, 111 FragmentController fragmentController, 112 CarUxRestrictions uxRestrictions, Car car, 113 VolumeSettingsRingtoneManager ringtoneManager) { 114 super(context, preferenceKey, fragmentController, uxRestrictions); 115 mCar = car; 116 mRingtoneManager = ringtoneManager; 117 mVolumeItems = VolumeItemParser.loadAudioUsageItems(context, carVolumeItemsXml()); 118 mUiHandler = new Handler(Looper.getMainLooper()); 119 120 mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE); 121 if (mCarAudioManager != null) { 122 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(); 123 cleanUpVolumePreferences(); 124 // Populates volume slider items from volume groups to UI. 125 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 126 VolumeItem volumeItem = getVolumeItemForUsages( 127 mCarAudioManager.getUsagesForVolumeGroupId(groupId)); 128 SeekBarPreference volumePreference = createVolumeSeekBarPreference( 129 groupId, volumeItem.getUsage(), volumeItem.getIcon(), 130 volumeItem.getTitle()); 131 mVolumePreferences.add(volumePreference); 132 } 133 mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback); 134 } 135 } 136 137 @Override getPreferenceType()138 protected Class<PreferenceGroup> getPreferenceType() { 139 return PreferenceGroup.class; 140 } 141 142 /** Disconnect from car on destroy. */ 143 @Override onDestroyInternal()144 protected void onDestroyInternal() { 145 mCar.disconnect(); 146 cleanupAudioManager(); 147 } 148 149 @Override updateState(PreferenceGroup preferenceGroup)150 protected void updateState(PreferenceGroup preferenceGroup) { 151 for (SeekBarPreference preference : mVolumePreferences) { 152 preferenceGroup.addPreference(preference); 153 } 154 } 155 156 /** 157 * The resource which lists the car volume resources associated with the various usage enums. 158 */ 159 @XmlRes 160 @VisibleForTesting carVolumeItemsXml()161 int carVolumeItemsXml() { 162 return R.xml.car_volume_items; 163 } 164 createVolumeSeekBarPreference( int volumeGroupId, int usage, @DrawableRes int iconResId, @StringRes int titleId)165 private SeekBarPreference createVolumeSeekBarPreference( 166 int volumeGroupId, int usage, @DrawableRes int iconResId, 167 @StringRes int titleId) { 168 SeekBarPreference preference = new SeekBarPreference(getContext()); 169 preference.setTitle(getContext().getString(titleId)); 170 preference.setIcon(getContext().getDrawable(iconResId)); 171 preference.getIcon().setTintList( 172 Themes.getAttrColorStateList(getContext(), R.attr.iconColor)); 173 try { 174 preference.setValue(mCarAudioManager.getGroupVolume(volumeGroupId)); 175 preference.setMin(mCarAudioManager.getGroupMinVolume(volumeGroupId)); 176 preference.setMax(mCarAudioManager.getGroupMaxVolume(volumeGroupId)); 177 } catch (CarNotConnectedException e) { 178 LOG.e("Car is not connected!", e); 179 } 180 preference.setContinuousUpdate(true); 181 preference.setShowSeekBarValue(false); 182 Bundle bundle = preference.getExtras(); 183 bundle.putInt(VOLUME_GROUP_KEY, volumeGroupId); 184 bundle.putInt(VOLUME_USAGE_KEY, usage); 185 preference.setOnPreferenceChangeListener((pref, newValue) -> { 186 int prefGroup = pref.getExtras().getInt(VOLUME_GROUP_KEY); 187 int prefUsage = pref.getExtras().getInt(VOLUME_USAGE_KEY); 188 int newVolume = (Integer) newValue; 189 setGroupVolume(prefGroup, newVolume); 190 mRingtoneManager.playAudioFeedback(prefGroup, prefUsage); 191 return true; 192 }); 193 return preference; 194 } 195 setGroupVolume(int volumeGroupId, int newVolume)196 private void setGroupVolume(int volumeGroupId, int newVolume) { 197 try { 198 mCarAudioManager.setGroupVolume(volumeGroupId, newVolume, /* flags= */ 0); 199 } catch (CarNotConnectedException e) { 200 LOG.w("Ignoring volume change event because the car isn't connected", e); 201 } 202 } 203 cleanupAudioManager()204 private void cleanupAudioManager() { 205 cleanUpVolumePreferences(); 206 mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback); 207 mCarAudioManager = null; 208 } 209 cleanUpVolumePreferences()210 private void cleanUpVolumePreferences() { 211 mRingtoneManager.stopCurrentRingtone(); 212 mVolumePreferences.clear(); 213 } 214 getVolumeItemForUsages(int[] usages)215 private VolumeItem getVolumeItemForUsages(int[] usages) { 216 int rank = Integer.MAX_VALUE; 217 VolumeItem result = null; 218 for (int usage : usages) { 219 VolumeItem volumeItem = mVolumeItems.get(usage); 220 if (volumeItem.getRank() < rank) { 221 rank = volumeItem.getRank(); 222 result = volumeItem; 223 } 224 } 225 return result; 226 } 227 } 228