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 android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING; 20 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS; 21 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING; 22 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED; 23 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED; 24 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED; 25 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED; 26 import static android.os.UserManager.DISALLOW_ADJUST_VOLUME; 27 28 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 29 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm; 30 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm; 31 import static com.android.car.settings.sound.VolumeItemParser.VolumeItem; 32 33 import android.car.CarNotConnectedException; 34 import android.car.drivingstate.CarUxRestrictions; 35 import android.car.media.CarAudioManager; 36 import android.car.media.CarVolumeGroupEvent; 37 import android.car.media.CarVolumeGroupEventCallback; 38 import android.car.media.CarVolumeGroupInfo; 39 import android.content.Context; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.Looper; 43 import android.util.SparseArray; 44 import android.widget.Toast; 45 46 import androidx.annotation.DrawableRes; 47 import androidx.annotation.StringRes; 48 import androidx.annotation.VisibleForTesting; 49 import androidx.annotation.XmlRes; 50 import androidx.preference.PreferenceGroup; 51 52 import com.android.car.settings.CarSettingsApplication; 53 import com.android.car.settings.R; 54 import com.android.car.settings.common.FragmentController; 55 import com.android.car.settings.common.Logger; 56 import com.android.car.settings.common.PreferenceController; 57 import com.android.car.settings.common.SeekBarPreference; 58 import com.android.car.settings.enterprise.EnterpriseUtils; 59 60 import java.util.ArrayList; 61 import java.util.List; 62 import java.util.concurrent.Executor; 63 64 /** 65 * Business logic which parses car volume items into groups, creates a seek bar preference for each 66 * group, and interfaces with the ringtone manager and audio manager. 67 * 68 * @see VolumeSettingsRingtoneManager 69 * @see android.car.media.CarAudioManager 70 */ 71 public class VolumeSettingsPreferenceController extends PreferenceController<PreferenceGroup> { 72 private static final Logger LOG = new Logger(VolumeSettingsPreferenceController.class); 73 private static final String VOLUME_GROUP_KEY = "volume_group_key"; 74 private static final String VOLUME_USAGE_KEY = "volume_usage_key"; 75 76 private final SparseArray<VolumeItem> mVolumeItems; 77 private final List<VolumeSeekBarPreference> mVolumePreferences = new ArrayList<>(); 78 private final VolumeSettingsRingtoneManager mRingtoneManager; 79 80 private final Handler mUiHandler; 81 private final Executor mExecutor; 82 83 @VisibleForTesting 84 final CarAudioManager.CarVolumeCallback mVolumeChangeCallback = 85 new CarAudioManager.CarVolumeCallback() { 86 @Override 87 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 88 updateVolumeAndMute(zoneId, groupId, EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED); 89 } 90 91 @Override 92 public void onMasterMuteChanged(int zoneId, int flags) { 93 94 // Mute is not being used yet 95 } 96 97 @Override 98 public void onGroupMuteChanged(int zoneId, int groupId, int flags) { 99 updateVolumeAndMute(zoneId, groupId, EVENT_TYPE_MUTE_CHANGED); 100 } 101 }; 102 103 @VisibleForTesting 104 final CarVolumeGroupEventCallback mCarVolumeGroupEventCallback = 105 new CarVolumeGroupEventCallback() { 106 @Override 107 public void onVolumeGroupEvent(List<CarVolumeGroupEvent> volumeGroupEvents) { 108 updateVolumeGroupForEvents(volumeGroupEvents); 109 } 110 }; 111 VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)112 public VolumeSettingsPreferenceController(Context context, String preferenceKey, 113 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 114 this(context, preferenceKey, fragmentController, uxRestrictions, 115 new VolumeSettingsRingtoneManager(context)); 116 } 117 118 @VisibleForTesting VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, VolumeSettingsRingtoneManager ringtoneManager)119 VolumeSettingsPreferenceController(Context context, String preferenceKey, 120 FragmentController fragmentController, CarUxRestrictions uxRestrictions, 121 VolumeSettingsRingtoneManager ringtoneManager) { 122 super(context, preferenceKey, fragmentController, uxRestrictions); 123 mRingtoneManager = ringtoneManager; 124 mVolumeItems = VolumeItemParser.loadAudioUsageItems(context, carVolumeItemsXml()); 125 mUiHandler = new Handler(Looper.getMainLooper()); 126 mExecutor = context.getMainExecutor(); 127 128 CarAudioManager carAudioManager = getCarAudioManager(); 129 if (carAudioManager != null) { 130 int zoneId = getMyAudioZoneId(); 131 int volumeGroupCount = carAudioManager.getVolumeGroupCount(zoneId); 132 cleanUpVolumePreferences(); 133 // Populates volume slider items from volume groups to UI. 134 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 135 VolumeItem volumeItem = getVolumeItemForUsages( 136 carAudioManager.getUsagesForVolumeGroupId(zoneId, groupId)); 137 VolumeSeekBarPreference volumePreference = createVolumeSeekBarPreference( 138 groupId, volumeItem.getUsage(), volumeItem.getIcon(), 139 volumeItem.getMuteIcon(), volumeItem.getTitle()); 140 setClickableWhileDisabled(volumePreference, /* clickable= */ true, p -> { 141 if (hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) { 142 showActionDisabledByAdminDialog(); 143 } else { 144 Toast.makeText(getContext(), 145 getContext().getString(R.string.action_unavailable), 146 Toast.LENGTH_LONG).show(); 147 } 148 }); 149 mVolumePreferences.add(volumePreference); 150 } 151 152 if (carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) { 153 carAudioManager.registerCarVolumeGroupEventCallback(mExecutor, 154 mCarVolumeGroupEventCallback); 155 } else { 156 carAudioManager.registerCarVolumeCallback(mVolumeChangeCallback); 157 } 158 } 159 } 160 161 @Override getPreferenceType()162 protected Class<PreferenceGroup> getPreferenceType() { 163 return PreferenceGroup.class; 164 } 165 166 /** Disconnect from car on destroy. */ 167 @Override onDestroyInternal()168 protected void onDestroyInternal() { 169 cleanupAudioManager(); 170 } 171 172 @Override updateState(PreferenceGroup preferenceGroup)173 protected void updateState(PreferenceGroup preferenceGroup) { 174 for (SeekBarPreference preference : mVolumePreferences) { 175 preferenceGroup.addPreference(preference); 176 } 177 } 178 179 /** 180 * The resource which lists the car volume resources associated with the various usage enums. 181 */ 182 @XmlRes 183 @VisibleForTesting carVolumeItemsXml()184 int carVolumeItemsXml() { 185 return R.xml.car_volume_items; 186 } 187 createVolumeSeekBarPreference( int volumeGroupId, int usage, @DrawableRes int primaryIconResId, @DrawableRes int secondaryIconResId, @StringRes int titleId)188 private VolumeSeekBarPreference createVolumeSeekBarPreference( 189 int volumeGroupId, int usage, @DrawableRes int primaryIconResId, 190 @DrawableRes int secondaryIconResId, @StringRes int titleId) { 191 VolumeSeekBarPreference preference = new VolumeSeekBarPreference(getContext()); 192 preference.setTitle(getContext().getString(titleId)); 193 preference.setUnMutedIcon(getContext().getDrawable(primaryIconResId)); 194 preference.getUnMutedIcon().setTintList( 195 getContext().getColorStateList(R.color.icon_color_default)); 196 preference.setMutedIcon(getContext().getDrawable(secondaryIconResId)); 197 preference.getMutedIcon().setTintList( 198 getContext().getColorStateList(R.color.icon_color_default)); 199 200 int zoneId = getMyAudioZoneId(); 201 CarAudioManager carAudioManager = getCarAudioManager(); 202 try { 203 if (carAudioManager != null) { 204 preference.setValue(carAudioManager.getGroupVolume(zoneId, volumeGroupId)); 205 preference.setMin(carAudioManager.getGroupMinVolume(zoneId, volumeGroupId)); 206 preference.setMax(carAudioManager.getGroupMaxVolume(zoneId, volumeGroupId)); 207 preference.setIsMuted(isGroupMuted(carAudioManager, volumeGroupId)); 208 } 209 } catch (CarNotConnectedException e) { 210 LOG.e("Car is not connected!", e); 211 } 212 preference.setContinuousUpdate(true); 213 preference.setShowSeekBarValue(false); 214 Bundle bundle = preference.getExtras(); 215 bundle.putInt(VOLUME_GROUP_KEY, volumeGroupId); 216 bundle.putInt(VOLUME_USAGE_KEY, usage); 217 preference.setOnPreferenceChangeListener((pref, newValue) -> { 218 int prefGroup = pref.getExtras().getInt(VOLUME_GROUP_KEY); 219 int prefUsage = pref.getExtras().getInt(VOLUME_USAGE_KEY); 220 int newVolume = (Integer) newValue; 221 setGroupVolume(prefGroup, newVolume); 222 223 if (carAudioManager != null 224 && (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING) 225 || !carAudioManager.isPlaybackOnVolumeGroupActive(zoneId, volumeGroupId))) { 226 mRingtoneManager.playAudioFeedback(prefGroup, prefUsage); 227 } 228 return true; 229 }); 230 return preference; 231 } 232 isGroupMuted(CarAudioManager carAudioManager, int volumeGroupId)233 private boolean isGroupMuted(CarAudioManager carAudioManager, int volumeGroupId) { 234 if (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) { 235 return false; 236 } 237 return carAudioManager.isVolumeGroupMuted(getMyAudioZoneId(), volumeGroupId); 238 } 239 updateVolumeAndMute(int zoneId, int groupId, int eventTypes)240 private void updateVolumeAndMute(int zoneId, int groupId, int eventTypes) { 241 if (zoneId != getMyAudioZoneId()) { 242 return; 243 } 244 245 CarAudioManager carAudioManager = getCarAudioManager(); 246 if (carAudioManager != null) { 247 updateVolumePreference(carAudioManager.getVolumeGroupInfo(zoneId, groupId), eventTypes); 248 } 249 } 250 setGroupVolume(int volumeGroupId, int newVolume)251 private void setGroupVolume(int volumeGroupId, int newVolume) { 252 try { 253 getCarAudioManager() 254 .setGroupVolume(getMyAudioZoneId(), volumeGroupId, newVolume, /* flags= */ 0); 255 } catch (CarNotConnectedException e) { 256 LOG.w("Ignoring volume change event because the car isn't connected", e); 257 } 258 } 259 cleanupAudioManager()260 private void cleanupAudioManager() { 261 cleanUpVolumePreferences(); 262 CarAudioManager carAudioManager = getCarAudioManager(); 263 if (carAudioManager != null) { 264 carAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback); 265 if (carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) { 266 carAudioManager.unregisterCarVolumeGroupEventCallback(mCarVolumeGroupEventCallback); 267 } 268 } 269 } 270 cleanUpVolumePreferences()271 private void cleanUpVolumePreferences() { 272 mRingtoneManager.stopCurrentRingtone(); 273 mVolumePreferences.clear(); 274 } 275 getVolumeItemForUsages(int[] usages)276 private VolumeItem getVolumeItemForUsages(int[] usages) { 277 int rank = Integer.MAX_VALUE; 278 VolumeItem result = null; 279 for (int usage : usages) { 280 VolumeItem volumeItem = mVolumeItems.get(usage); 281 if (volumeItem.getRank() < rank) { 282 rank = volumeItem.getRank(); 283 result = volumeItem; 284 } 285 } 286 return result; 287 } 288 289 @Override getDefaultAvailabilityStatus()290 public int getDefaultAvailabilityStatus() { 291 if (hasUserRestrictionByUm(getContext(), DISALLOW_ADJUST_VOLUME) 292 || hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) { 293 return AVAILABLE_FOR_VIEWING; 294 } 295 return AVAILABLE; 296 } 297 showActionDisabledByAdminDialog()298 private void showActionDisabledByAdminDialog() { 299 getFragmentController().showDialog( 300 EnterpriseUtils.getActionDisabledByAdminDialog(getContext(), 301 DISALLOW_ADJUST_VOLUME), 302 DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 303 } 304 getMyAudioZoneId()305 private int getMyAudioZoneId() { 306 return ((CarSettingsApplication) getContext().getApplicationContext()) 307 .getMyAudioZoneId(); 308 } 309 getCarAudioManager()310 private CarAudioManager getCarAudioManager() { 311 return ((CarSettingsApplication) getContext().getApplicationContext()) 312 .getCarAudioManager(); 313 } 314 updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents)315 private void updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents) { 316 List<CarVolumeGroupEvent> filteredEvents = 317 filterVolumeGroupEventForZoneId(getMyAudioZoneId(), volumeGroupEvents); 318 for (int index = 0; index < filteredEvents.size(); index++) { 319 CarVolumeGroupEvent event = filteredEvents.get(index); 320 int eventTypes = event.getEventTypes(); 321 List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos(); 322 for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) { 323 updateVolumePreference(infos.get(infoIndex), eventTypes); 324 } 325 } 326 } 327 filterVolumeGroupEventForZoneId(int zoneId, List<CarVolumeGroupEvent> volumeGroupEvents)328 private List<CarVolumeGroupEvent> filterVolumeGroupEventForZoneId(int zoneId, 329 List<CarVolumeGroupEvent> volumeGroupEvents) { 330 List<CarVolumeGroupEvent> filteredEvents = new ArrayList<>(); 331 for (int index = 0; index < volumeGroupEvents.size(); index++) { 332 CarVolumeGroupEvent event = volumeGroupEvents.get(index); 333 List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos(); 334 for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) { 335 if (infos.get(infoIndex).getZoneId() == zoneId) { 336 filteredEvents.add(event); 337 break; 338 } 339 } 340 } 341 return filteredEvents; 342 } 343 updateVolumePreference(CarVolumeGroupInfo groupInfo, int eventTypes)344 private void updateVolumePreference(CarVolumeGroupInfo groupInfo, int eventTypes) { 345 int groupId = groupInfo.getId(); 346 for (VolumeSeekBarPreference volumePreference : mVolumePreferences) { 347 Bundle extras = volumePreference.getExtras(); 348 if (extras.getInt(VOLUME_GROUP_KEY) == groupId) { 349 mUiHandler.post(() -> { 350 if ((eventTypes & EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED) != 0) { 351 volumePreference.setValue(groupInfo.getVolumeGainIndex()); 352 } 353 if ((eventTypes & EVENT_TYPE_MUTE_CHANGED) != 0) { 354 volumePreference.setIsMuted(groupInfo.isMuted()); 355 } 356 if ((eventTypes & EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED) != 0) { 357 volumePreference.setMin(groupInfo.getMinVolumeGainIndex()); 358 } 359 if ((eventTypes & EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED) != 0) { 360 volumePreference.setMax(groupInfo.getMaxVolumeGainIndex()); 361 } 362 }); 363 break; 364 } 365 } 366 } 367 } 368