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_VOLUME_GROUP_MUTING; 20 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE; 21 import static android.os.UserManager.DISALLOW_ADJUST_VOLUME; 22 23 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 24 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm; 25 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm; 26 import static com.android.car.settings.sound.VolumeItemParser.VolumeItem; 27 28 import android.car.Car; 29 import android.car.CarNotConnectedException; 30 import android.car.drivingstate.CarUxRestrictions; 31 import android.car.media.CarAudioManager; 32 import android.content.Context; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.util.SparseArray; 37 import android.widget.Toast; 38 39 import androidx.annotation.DrawableRes; 40 import androidx.annotation.StringRes; 41 import androidx.annotation.VisibleForTesting; 42 import androidx.annotation.XmlRes; 43 import androidx.preference.PreferenceGroup; 44 45 import com.android.car.settings.R; 46 import com.android.car.settings.common.FragmentController; 47 import com.android.car.settings.common.Logger; 48 import com.android.car.settings.common.PreferenceController; 49 import com.android.car.settings.common.SeekBarPreference; 50 import com.android.car.settings.enterprise.EnterpriseUtils; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 55 /** 56 * Business logic which parses car volume items into groups, creates a seek bar preference for each 57 * group, and interfaces with the ringtone manager and audio manager. 58 * 59 * @see VolumeSettingsRingtoneManager 60 * @see android.car.media.CarAudioManager 61 */ 62 public class VolumeSettingsPreferenceController extends PreferenceController<PreferenceGroup> { 63 private static final Logger LOG = new Logger(VolumeSettingsPreferenceController.class); 64 private static final String VOLUME_GROUP_KEY = "volume_group_key"; 65 private static final String VOLUME_USAGE_KEY = "volume_usage_key"; 66 67 private final SparseArray<VolumeItem> mVolumeItems; 68 private final List<VolumeSeekBarPreference> mVolumePreferences = new ArrayList<>(); 69 private final VolumeSettingsRingtoneManager mRingtoneManager; 70 71 private final Handler mUiHandler; 72 73 @VisibleForTesting 74 final CarAudioManager.CarVolumeCallback mVolumeChangeCallback = 75 new CarAudioManager.CarVolumeCallback() { 76 @Override 77 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 78 updateVolumeAndMute(zoneId, groupId); 79 } 80 81 @Override 82 public void onMasterMuteChanged(int zoneId, int flags) { 83 84 // Mute is not being used yet 85 } 86 87 @Override 88 public void onGroupMuteChanged(int zoneId, int groupId, int flags) { 89 updateVolumeAndMute(zoneId, groupId); 90 } 91 }; 92 93 private Car mCar; 94 private CarAudioManager mCarAudioManager; 95 VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)96 public VolumeSettingsPreferenceController(Context context, String preferenceKey, 97 FragmentController fragmentController, 98 CarUxRestrictions uxRestrictions) { 99 this(context, preferenceKey, fragmentController, uxRestrictions, Car.createCar(context), 100 new VolumeSettingsRingtoneManager(context)); 101 } 102 103 @VisibleForTesting VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, Car car, VolumeSettingsRingtoneManager ringtoneManager)104 VolumeSettingsPreferenceController(Context context, String preferenceKey, 105 FragmentController fragmentController, 106 CarUxRestrictions uxRestrictions, Car car, 107 VolumeSettingsRingtoneManager ringtoneManager) { 108 super(context, preferenceKey, fragmentController, uxRestrictions); 109 mCar = car; 110 mRingtoneManager = ringtoneManager; 111 mVolumeItems = VolumeItemParser.loadAudioUsageItems(context, carVolumeItemsXml()); 112 mUiHandler = new Handler(Looper.getMainLooper()); 113 114 mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE); 115 if (mCarAudioManager != null) { 116 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(); 117 cleanUpVolumePreferences(); 118 // Populates volume slider items from volume groups to UI. 119 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 120 VolumeItem volumeItem = getVolumeItemForUsages( 121 mCarAudioManager.getUsagesForVolumeGroupId(groupId)); 122 VolumeSeekBarPreference volumePreference = createVolumeSeekBarPreference( 123 groupId, volumeItem.getUsage(), volumeItem.getIcon(), 124 volumeItem.getMuteIcon(), volumeItem.getTitle()); 125 setClickableWhileDisabled(volumePreference, /* clickable= */ true, p -> { 126 if (hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) { 127 showActionDisabledByAdminDialog(); 128 } else { 129 Toast.makeText(getContext(), 130 getContext().getString(R.string.action_unavailable), 131 Toast.LENGTH_LONG).show(); 132 } 133 }); 134 mVolumePreferences.add(volumePreference); 135 } 136 mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback); 137 } 138 } 139 140 @Override getPreferenceType()141 protected Class<PreferenceGroup> getPreferenceType() { 142 return PreferenceGroup.class; 143 } 144 145 /** Disconnect from car on destroy. */ 146 @Override onDestroyInternal()147 protected void onDestroyInternal() { 148 mCar.disconnect(); 149 cleanupAudioManager(); 150 } 151 152 @Override updateState(PreferenceGroup preferenceGroup)153 protected void updateState(PreferenceGroup preferenceGroup) { 154 for (SeekBarPreference preference : mVolumePreferences) { 155 preferenceGroup.addPreference(preference); 156 } 157 } 158 159 /** 160 * The resource which lists the car volume resources associated with the various usage enums. 161 */ 162 @XmlRes 163 @VisibleForTesting carVolumeItemsXml()164 int carVolumeItemsXml() { 165 return R.xml.car_volume_items; 166 } 167 createVolumeSeekBarPreference( int volumeGroupId, int usage, @DrawableRes int primaryIconResId, @DrawableRes int secondaryIconResId, @StringRes int titleId)168 private VolumeSeekBarPreference createVolumeSeekBarPreference( 169 int volumeGroupId, int usage, @DrawableRes int primaryIconResId, 170 @DrawableRes int secondaryIconResId, @StringRes int titleId) { 171 VolumeSeekBarPreference preference = new VolumeSeekBarPreference(getContext()); 172 preference.setTitle(getContext().getString(titleId)); 173 preference.setUnMutedIcon(getContext().getDrawable(primaryIconResId)); 174 preference.getUnMutedIcon().setTintList( 175 getContext().getColorStateList(R.color.icon_color_default)); 176 preference.setMutedIcon(getContext().getDrawable(secondaryIconResId)); 177 preference.getMutedIcon().setTintList( 178 getContext().getColorStateList(R.color.icon_color_default)); 179 try { 180 preference.setValue(mCarAudioManager.getGroupVolume(volumeGroupId)); 181 preference.setMin(mCarAudioManager.getGroupMinVolume(volumeGroupId)); 182 preference.setMax(mCarAudioManager.getGroupMaxVolume(volumeGroupId)); 183 if (mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) { 184 preference.setIsMuted(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, 185 volumeGroupId)); 186 } 187 } catch (CarNotConnectedException e) { 188 LOG.e("Car is not connected!", e); 189 } 190 preference.setContinuousUpdate(true); 191 preference.setShowSeekBarValue(false); 192 Bundle bundle = preference.getExtras(); 193 bundle.putInt(VOLUME_GROUP_KEY, volumeGroupId); 194 bundle.putInt(VOLUME_USAGE_KEY, usage); 195 preference.setOnPreferenceChangeListener((pref, newValue) -> { 196 int prefGroup = pref.getExtras().getInt(VOLUME_GROUP_KEY); 197 int prefUsage = pref.getExtras().getInt(VOLUME_USAGE_KEY); 198 int newVolume = (Integer) newValue; 199 setGroupVolume(prefGroup, newVolume); 200 mRingtoneManager.playAudioFeedback(prefGroup, prefUsage); 201 return true; 202 }); 203 return preference; 204 } 205 updateVolumeAndMute(int zoneId, int groupId)206 private void updateVolumeAndMute(int zoneId, int groupId) { 207 // Settings only handles primary zone changes 208 if (zoneId != PRIMARY_AUDIO_ZONE) { 209 return; 210 } 211 if (mCarAudioManager != null) { 212 boolean isMuted = 213 mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId); 214 int value = mCarAudioManager.getGroupVolume(groupId); 215 216 for (VolumeSeekBarPreference volumePreference : mVolumePreferences) { 217 Bundle extras = volumePreference.getExtras(); 218 if (extras.getInt(VOLUME_GROUP_KEY) == groupId) { 219 if (volumePreference.isMuted() != isMuted 220 || value != volumePreference.getValue()) { 221 mUiHandler.post(() -> { 222 volumePreference.setIsMuted(isMuted); 223 volumePreference.setValue(value); 224 }); 225 } 226 break; 227 } 228 } 229 } 230 } 231 setGroupVolume(int volumeGroupId, int newVolume)232 private void setGroupVolume(int volumeGroupId, int newVolume) { 233 try { 234 mCarAudioManager.setGroupVolume(volumeGroupId, newVolume, /* flags= */ 0); 235 } catch (CarNotConnectedException e) { 236 LOG.w("Ignoring volume change event because the car isn't connected", e); 237 } 238 } 239 cleanupAudioManager()240 private void cleanupAudioManager() { 241 cleanUpVolumePreferences(); 242 mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback); 243 mCarAudioManager = null; 244 } 245 cleanUpVolumePreferences()246 private void cleanUpVolumePreferences() { 247 mRingtoneManager.stopCurrentRingtone(); 248 mVolumePreferences.clear(); 249 } 250 getVolumeItemForUsages(int[] usages)251 private VolumeItem getVolumeItemForUsages(int[] usages) { 252 int rank = Integer.MAX_VALUE; 253 VolumeItem result = null; 254 for (int usage : usages) { 255 VolumeItem volumeItem = mVolumeItems.get(usage); 256 if (volumeItem.getRank() < rank) { 257 rank = volumeItem.getRank(); 258 result = volumeItem; 259 } 260 } 261 return result; 262 } 263 264 @Override getAvailabilityStatus()265 public int getAvailabilityStatus() { 266 if (hasUserRestrictionByUm(getContext(), DISALLOW_ADJUST_VOLUME) 267 || hasUserRestrictionByDpm(getContext(), DISALLOW_ADJUST_VOLUME)) { 268 return AVAILABLE_FOR_VIEWING; 269 } 270 return AVAILABLE; 271 } 272 showActionDisabledByAdminDialog()273 private void showActionDisabledByAdminDialog() { 274 getFragmentController().showDialog( 275 EnterpriseUtils.getActionDisabledByAdminDialog(getContext(), 276 DISALLOW_ADJUST_VOLUME), 277 DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 278 } 279 } 280