• 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 com.android.car.settings.sound.VolumeItemParser.VolumeItem;
20 
21 import android.car.Car;
22 import android.car.CarNotConnectedException;
23 import android.car.drivingstate.CarUxRestrictions;
24 import android.car.media.CarAudioManager;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.ServiceConnection;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.Looper;
32 import android.util.SparseArray;
33 
34 import androidx.annotation.DrawableRes;
35 import androidx.annotation.StringRes;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.annotation.XmlRes;
38 import androidx.preference.PreferenceGroup;
39 
40 import com.android.car.apps.common.util.Themes;
41 import com.android.car.settings.R;
42 import com.android.car.settings.common.FragmentController;
43 import com.android.car.settings.common.Logger;
44 import com.android.car.settings.common.PreferenceController;
45 import com.android.car.settings.common.SeekBarPreference;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Business logic which parses car volume items into groups, creates a seek bar preference for each
52  * group, and interfaces with the ringtone manager and audio manager.
53  *
54  * @see VolumeSettingsRingtoneManager
55  * @see android.car.media.CarAudioManager
56  */
57 public class VolumeSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
58     private static final Logger LOG = new Logger(VolumeSettingsPreferenceController.class);
59     private static final String VOLUME_GROUP_KEY = "volume_group_key";
60     private static final String VOLUME_USAGE_KEY = "volume_usage_key";
61 
62     private final SparseArray<VolumeItem> mVolumeItems;
63     private final List<SeekBarPreference> mVolumePreferences = new ArrayList<>();
64     private final VolumeSettingsRingtoneManager mRingtoneManager;
65 
66     private final Handler mUiHandler;
67 
68     @VisibleForTesting
69     final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
70             new CarAudioManager.CarVolumeCallback() {
71                 @Override
72                 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
73                     if (mCarAudioManager != null) {
74                         int value = mCarAudioManager.getGroupVolume(groupId);
75 
76                         for (SeekBarPreference volumePreference : mVolumePreferences) {
77                             Bundle extras = volumePreference.getExtras();
78                             if (extras.getInt(VOLUME_GROUP_KEY) == groupId) {
79                                 // Only setValue if the value is different, since changing the
80                                 // seekbar of the volume directly will trigger CarVolumeCallback as
81                                 // well, causing janky movement.
82                                 if (volumePreference.getValue() != value) {
83                                     // CarVolumeCallback is run on a binder thread. In order to
84                                     // make updates to the SeekBarPreference, we need to switch
85                                     // over to the UI thread.
86                                     mUiHandler.post(() -> {
87                                         volumePreference.setValue(value);
88                                     });
89                                 }
90                                 break;
91                             }
92                         }
93                     }
94                 }
95 
96                 @Override
97                 public void onMasterMuteChanged(int zoneId, int flags) {
98                     // Mute is not being used yet
99                 }
100             };
101 
102     private final ServiceConnection mServiceConnection = new ServiceConnection() {
103         @Override
104         public void onServiceConnected(ComponentName name, IBinder service) {
105             try {
106                 mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
107                 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount();
108                 cleanUpVolumePreferences();
109                 // Populates volume slider items from volume groups to UI.
110                 for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
111                     VolumeItem volumeItem = getVolumeItemForUsages(
112                             mCarAudioManager.getUsagesForVolumeGroupId(groupId));
113                     SeekBarPreference volumePreference = createVolumeSeekBarPreference(
114                             groupId, volumeItem.getUsage(), volumeItem.getIcon(),
115                             volumeItem.getTitle());
116                     mVolumePreferences.add(volumePreference);
117                 }
118                 mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback);
119 
120                 refreshUi();
121             } catch (CarNotConnectedException e) {
122                 LOG.e("Car is not connected!", e);
123             }
124         }
125 
126         /** Cleanup audio related fields when car is disconnected. */
127         @Override
128         public void onServiceDisconnected(ComponentName name) {
129             cleanupAudioManager();
130         }
131     };
132 
133     private Car mCar;
134     private CarAudioManager mCarAudioManager;
135 
VolumeSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)136     public VolumeSettingsPreferenceController(Context context, String preferenceKey,
137             FragmentController fragmentController,
138             CarUxRestrictions uxRestrictions) {
139         super(context, preferenceKey, fragmentController, uxRestrictions);
140         mCar = Car.createCar(getContext(), mServiceConnection);
141         mVolumeItems = VolumeItemParser.loadAudioUsageItems(context, carVolumeItemsXml());
142         mRingtoneManager = new VolumeSettingsRingtoneManager(getContext());
143         mUiHandler = new Handler(Looper.getMainLooper());
144     }
145 
146     @Override
getPreferenceType()147     protected Class<PreferenceGroup> getPreferenceType() {
148         return PreferenceGroup.class;
149     }
150 
151     /** Connect to car on create. */
152     @Override
onCreateInternal()153     protected void onCreateInternal() {
154         mCar.connect();
155     }
156 
157     /** Disconnect from car on destroy. */
158     @Override
onDestroyInternal()159     protected void onDestroyInternal() {
160         mCar.disconnect();
161     }
162 
163     @Override
updateState(PreferenceGroup preferenceGroup)164     protected void updateState(PreferenceGroup preferenceGroup) {
165         for (SeekBarPreference preference : mVolumePreferences) {
166             preferenceGroup.addPreference(preference);
167         }
168     }
169 
170     /**
171      * The resource which lists the car volume resources associated with the various usage enums.
172      */
173     @XmlRes
174     @VisibleForTesting
carVolumeItemsXml()175     int carVolumeItemsXml() {
176         return R.xml.car_volume_items;
177     }
178 
createVolumeSeekBarPreference( int volumeGroupId, int usage, @DrawableRes int iconResId, @StringRes int titleId)179     private SeekBarPreference createVolumeSeekBarPreference(
180             int volumeGroupId, int usage, @DrawableRes int iconResId,
181             @StringRes int titleId) {
182         SeekBarPreference preference = new SeekBarPreference(getContext());
183         preference.setTitle(getContext().getString(titleId));
184         preference.setIcon(getContext().getDrawable(iconResId));
185         preference.getIcon().setTintList(
186                 Themes.getAttrColorStateList(getContext(), R.attr.iconColor));
187         try {
188             preference.setValue(mCarAudioManager.getGroupVolume(volumeGroupId));
189             preference.setMin(mCarAudioManager.getGroupMinVolume(volumeGroupId));
190             preference.setMax(mCarAudioManager.getGroupMaxVolume(volumeGroupId));
191         } catch (CarNotConnectedException e) {
192             LOG.e("Car is not connected!", e);
193         }
194         preference.setContinuousUpdate(true);
195         preference.setShowSeekBarValue(false);
196         Bundle bundle = preference.getExtras();
197         bundle.putInt(VOLUME_GROUP_KEY, volumeGroupId);
198         bundle.putInt(VOLUME_USAGE_KEY, usage);
199         preference.setOnPreferenceChangeListener((pref, newValue) -> {
200             int prefGroup = pref.getExtras().getInt(VOLUME_GROUP_KEY);
201             int prefUsage = pref.getExtras().getInt(VOLUME_USAGE_KEY);
202             int newVolume = (Integer) newValue;
203             setGroupVolume(prefGroup, newVolume);
204             mRingtoneManager.playAudioFeedback(prefGroup, prefUsage);
205             return true;
206         });
207         return preference;
208     }
209 
setGroupVolume(int volumeGroupId, int newVolume)210     private void setGroupVolume(int volumeGroupId, int newVolume) {
211         try {
212             mCarAudioManager.setGroupVolume(volumeGroupId, newVolume, /* flags= */ 0);
213         } catch (CarNotConnectedException e) {
214             LOG.w("Ignoring volume change event because the car isn't connected", e);
215         }
216     }
217 
cleanupAudioManager()218     private void cleanupAudioManager() {
219         cleanUpVolumePreferences();
220         mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback);
221         mCarAudioManager = null;
222     }
223 
cleanUpVolumePreferences()224     private void cleanUpVolumePreferences() {
225         mRingtoneManager.stopCurrentRingtone();
226         mVolumePreferences.clear();
227     }
228 
getVolumeItemForUsages(int[] usages)229     private VolumeItem getVolumeItemForUsages(int[] usages) {
230         int rank = Integer.MAX_VALUE;
231         VolumeItem result = null;
232         for (int usage : usages) {
233             VolumeItem volumeItem = mVolumeItems.get(usage);
234             if (volumeItem.getRank() < rank) {
235                 rank = volumeItem.getRank();
236                 result = volumeItem;
237             }
238         }
239         return result;
240     }
241 }
242