• Home
  • History
  • Annotate
  • 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.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