1 /*
2  * Copyright (C) 2023 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.google.android.car.kitchensink.audio;
18 
19 import static android.R.layout.simple_spinner_dropdown_item;
20 import static android.R.layout.simple_spinner_item;
21 import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
22 import static android.car.media.CarAudioManager.CONFIG_STATUS_CHANGED;
23 
24 import android.annotation.Nullable;
25 import android.car.feature.Flags;
26 import android.car.media.AudioZoneConfigurationsChangeCallback;
27 import android.car.media.CarAudioManager;
28 import android.car.media.CarAudioZoneConfigInfo;
29 import android.car.media.SwitchAudioZoneConfigCallback;
30 import android.content.Context;
31 import android.graphics.Color;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.AdapterView;
36 import android.widget.ArrayAdapter;
37 import android.widget.Button;
38 import android.widget.LinearLayout;
39 import android.widget.Spinner;
40 import android.widget.TextView;
41 import android.widget.Toast;
42 
43 import com.google.android.car.kitchensink.R;
44 
45 import java.util.List;
46 
47 import javax.annotation.concurrent.GuardedBy;
48 
49 final class ZoneConfigSelectionController {
50 
51     private static final boolean DBG = true;
52     private static final String TAG = "CAR.AUDIO.KS";
53     private final Context mContext;
54     private final Spinner mZoneConfigurationSpinner;
55     private final LinearLayout mZoneConfigurationLayout;
56     private final ArrayAdapter<CarAudioZoneConfigInfoWrapper> mZoneConfigurationAdapter;
57     private final Object mLock = new Object();
58     private final TextView mCurrentZoneConfigurationView;
59 
60     private final CarAudioManager mCarAudioManager;
61     private final int mAudioZone;
62     private final CarAudioZoneConfigSelectedListener mConfigSelectedListener;
63     @Nullable
64     private final CarAudioZoneConfigsUpdatedListener mConfigUpdatedListener;
65     private final SwitchAudioZoneConfigCallback mSwitchAudioZoneConfigCallback =
66             (zoneConfig, isSuccessful) -> {
67                 Log.i(TAG, "Car audio zone switching to " + zoneConfig + " successful? "
68                         + isSuccessful);
69                 if (!isSuccessful) {
70                     return;
71                 }
72                 updateSelectedAudioZoneConfig(zoneConfig, /* autoSelected= */ false);
73             };
74     @GuardedBy("mLock")
75     private CarAudioZoneConfigInfo mZoneConfigInfoSelected;
76 
ZoneConfigSelectionController(View view, CarAudioManager carAudioManager, Context context, int audioZone, CarAudioZoneConfigSelectedListener configSelectedListener, @Nullable CarAudioZoneConfigsUpdatedListener configUpdatedListener)77     ZoneConfigSelectionController(View view, CarAudioManager carAudioManager, Context context,
78             int audioZone, CarAudioZoneConfigSelectedListener configSelectedListener,
79             @Nullable CarAudioZoneConfigsUpdatedListener configUpdatedListener) {
80         mCarAudioManager = carAudioManager;
81         mContext = context;
82         mAudioZone = audioZone;
83         mConfigSelectedListener = configSelectedListener;
84         mConfigUpdatedListener = configUpdatedListener;
85         mZoneConfigurationLayout = view.findViewById(R.id.audio_zone_configuration_layout);
86         mCurrentZoneConfigurationView = view.findViewById(R.id.text_current_configuration);
87         mZoneConfigurationSpinner = view.findViewById(R.id.zone_configuration_spinner);
88         mZoneConfigurationSpinner.setEnabled(false);
89         mZoneConfigurationSpinner.setOnItemSelectedListener(
90                 new AdapterView.OnItemSelectedListener() {
91                     @Override
92                     public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
93                         handleZoneConfigurationsSelection();
94                     }
95 
96                     @Override
97                     public void onNothingSelected(AdapterView<?> parent) {
98                     }
99                 });
100 
101         List<CarAudioZoneConfigInfo> zoneConfigInfos = mCarAudioManager.getAudioZoneConfigInfos(
102                 mAudioZone);
103 
104         Button zoneConfigSwitchButton = view.findViewById(
105                 R.id.switch_zone_configuration_key_event_button);
106         zoneConfigSwitchButton.setOnClickListener((v) -> switchToZoneConfigSelected());
107 
108         CarAudioZoneConfigInfoWrapper[] zoneConfigArray =
109                 new CarAudioZoneConfigInfoWrapper[zoneConfigInfos.size()];
110         for (int index = 0; index < zoneConfigArray.length; index++) {
111             zoneConfigArray[index] = new CarAudioZoneConfigInfoWrapper(zoneConfigInfos.get(index));
112         }
113         mZoneConfigurationAdapter = new ArrayAdapter<>(mContext, simple_spinner_item,
114                 zoneConfigArray) {
115             @Override
116             public View getDropDownView(int position, @Nullable View convertView,
117                     ViewGroup parent) {
118                 View v = super.getDropDownView(position, /* convertView= */ null, parent);
119                 CarAudioZoneConfigInfo info = getItem(position).getZoneConfigInfo();
120                 CarAudioZoneConfigInfo updatedInfo = getUpdatedConfigInfo(info);
121                 if (Flags.carAudioDynamicDevices()) {
122                     if (!updatedInfo.isActive()) {
123                         v.setBackgroundColor(Color.LTGRAY);
124                     }
125                     if (updatedInfo.isSelected()) {
126                         v.setBackgroundColor(Color.CYAN);
127                     }
128                 }
129                 return v;
130             }
131         };
132         mZoneConfigurationAdapter.setDropDownViewResource(simple_spinner_dropdown_item);
133 
134         if (!mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)) {
135             mZoneConfigurationLayout.setVisibility(View.GONE);
136             return;
137         }
138 
139         mZoneConfigurationLayout.setVisibility(View.VISIBLE);
140 
141         CarAudioZoneConfigInfo currentZoneConfigInfo =
142                 mCarAudioManager.getCurrentAudioZoneConfigInfo(mAudioZone);
143         mCurrentZoneConfigurationView.setText(currentZoneConfigInfo.getName());
144         synchronized (mLock) {
145             mZoneConfigInfoSelected = currentZoneConfigInfo;
146         }
147         mZoneConfigurationSpinner.setAdapter(mZoneConfigurationAdapter);
148         mZoneConfigurationSpinner.setEnabled(true);
149         int selected = mZoneConfigurationAdapter.getPosition(
150                 new CarAudioZoneConfigInfoWrapper(currentZoneConfigInfo));
151         mZoneConfigurationSpinner.setSelection(selected);
152 
153         if (Flags.carAudioDynamicDevices()) {
154             mCarAudioManager.setAudioZoneConfigsChangeCallback(mContext.getMainExecutor(),
155                     new AudioZoneConfigurationsChangeCallback() {
156                         @Override
157                         public void onAudioZoneConfigurationsChanged(
158                                 List<CarAudioZoneConfigInfo> configs, int status) {
159                             handleAudioZoneConfigsUpdated(configs, status);
160                         }
161                     });
162         }
163     }
164 
switchToZoneConfigSelected()165     private void switchToZoneConfigSelected() {
166         CarAudioZoneConfigInfo zoneConfigInfoSelected;
167         synchronized (mLock) {
168             zoneConfigInfoSelected = mZoneConfigInfoSelected;
169         }
170         if (DBG) {
171             Log.d(TAG, "Switch to zone configuration selected: " + zoneConfigInfoSelected);
172         }
173         switchToAudioConfiguration(zoneConfigInfoSelected);
174     }
175 
switchToAudioConfiguration(CarAudioZoneConfigInfo zoneConfigInfoSelected)176     private void switchToAudioConfiguration(CarAudioZoneConfigInfo zoneConfigInfoSelected) {
177         CarAudioZoneConfigInfo info = getUpdatedConfigInfo(zoneConfigInfoSelected);
178         if (Flags.carAudioDynamicDevices()) {
179             if (!info.isActive()) {
180                 showToast(info.getName() + ": not active");
181                 return;
182             }
183             if (info.isSelected()) {
184                 showToast(info.getName() + ": already selected");
185                 return;
186             }
187         }
188         mCarAudioManager.switchAudioZoneToConfig(zoneConfigInfoSelected, mContext.getMainExecutor(),
189                 mSwitchAudioZoneConfigCallback);
190     }
191 
handleZoneConfigurationsSelection()192     private void handleZoneConfigurationsSelection() {
193         int position = mZoneConfigurationSpinner.getSelectedItemPosition();
194         synchronized (mLock) {
195             mZoneConfigInfoSelected = mZoneConfigurationAdapter.getItem(
196                     position).getZoneConfigInfo();
197             CarAudioZoneConfigInfo updatedInfo = getUpdatedConfigInfo(mZoneConfigInfoSelected);
198             if (Flags.carAudioDynamicDevices()) {
199                 if (!updatedInfo.isActive()) {
200                     showToast(updatedInfo.getName() + ": not active");
201                     return;
202                 }
203                 if (updatedInfo.isSelected()) {
204                     showToast(updatedInfo.getName() + ": already selected");
205                 }
206             }
207         }
208     }
209 
updateSelectedAudioZoneConfig(CarAudioZoneConfigInfo zoneConfig, boolean autoSelected)210     private void updateSelectedAudioZoneConfig(CarAudioZoneConfigInfo zoneConfig,
211             boolean autoSelected) {
212         mCurrentZoneConfigurationView.setText(zoneConfig.getName());
213         mConfigSelectedListener.configSelected(autoSelected);
214     }
215 
handleAudioZoneConfigsUpdated(List<CarAudioZoneConfigInfo> configs, int status)216     private void handleAudioZoneConfigsUpdated(List<CarAudioZoneConfigInfo> configs, int status) {
217         if (mConfigUpdatedListener != null) {
218             mConfigUpdatedListener.configsUpdated(configs);
219         }
220         if (status == CONFIG_STATUS_CHANGED) {
221             showToast("Config status changed " + status);
222             return;
223         }
224         for (CarAudioZoneConfigInfo info : configs) {
225             if (!info.isSelected()) {
226                 continue;
227             }
228             updateSelectedAudioZoneConfig(info, /* autoSelected= */ true);
229             showToast(info.getName() + ": auto selected");
230             return;
231         }
232     }
233 
getUpdatedConfigInfo(CarAudioZoneConfigInfo zoneConfigInfo)234     private CarAudioZoneConfigInfo getUpdatedConfigInfo(CarAudioZoneConfigInfo zoneConfigInfo) {
235         List<CarAudioZoneConfigInfo> configs = mCarAudioManager.getAudioZoneConfigInfos(
236                 zoneConfigInfo.getZoneId());
237         return configs.stream().filter(
238                 c -> c.getConfigId() == zoneConfigInfo.getConfigId()).findFirst().orElse(
239                 zoneConfigInfo);
240     }
241 
showToast(String message)242     private void showToast(String message) {
243         Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
244     }
245 
release()246     public void release() {
247         if (!Flags.carAudioDynamicDevices()) {
248             return;
249         }
250         mCarAudioManager.clearAudioZoneConfigsCallback();
251     }
252 
connectToConfig(CarAudioZoneConfigInfo info)253     void connectToConfig(CarAudioZoneConfigInfo info) {
254         switchToAudioConfiguration(info);
255     }
256 
257     interface CarAudioZoneConfigSelectedListener {
configSelected(boolean autoSelected)258         void configSelected(boolean autoSelected);
259     }
260 
261     interface CarAudioZoneConfigsUpdatedListener {
configsUpdated(List<CarAudioZoneConfigInfo> configs)262         void configsUpdated(List<CarAudioZoneConfigInfo> configs);
263     }
264 
265     private static final class CarAudioZoneConfigInfoWrapper {
266         private final CarAudioZoneConfigInfo mZoneConfigInfo;
267 
CarAudioZoneConfigInfoWrapper(CarAudioZoneConfigInfo configInfo)268         CarAudioZoneConfigInfoWrapper(CarAudioZoneConfigInfo configInfo) {
269             mZoneConfigInfo = configInfo;
270         }
271 
getZoneConfigInfo()272         CarAudioZoneConfigInfo getZoneConfigInfo() {
273             return mZoneConfigInfo;
274         }
275 
276         @Override
toString()277         public String toString() {
278             return mZoneConfigInfo.getName() + ", Id: " + mZoneConfigInfo.getConfigId();
279         }
280 
281         @Override
equals(Object o)282         public boolean equals(Object o) {
283             if (!(o instanceof CarAudioZoneConfigInfoWrapper)) {
284                 return false;
285             }
286             CarAudioZoneConfigInfoWrapper wrapper = (CarAudioZoneConfigInfoWrapper) o;
287             return Flags.carAudioDynamicDevices()
288                     ? mZoneConfigInfo.hasSameConfigInfo(wrapper.mZoneConfigInfo)
289                     : mZoneConfigInfo.equals(wrapper.mZoneConfigInfo);
290         }
291 
292         @Override
hashCode()293         public int hashCode() {
294             return mZoneConfigInfo.hashCode();
295         }
296     }
297 }
298