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