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