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