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