• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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