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_AUDIO_MIRRORING; 22 import static android.car.media.CarAudioManager.AUDIO_MIRROR_CAN_ENABLE; 23 import static android.car.media.CarAudioManager.AUDIO_REQUEST_STATUS_APPROVED; 24 import static android.car.media.CarAudioManager.INVALID_AUDIO_ZONE; 25 import static android.car.media.CarAudioManager.INVALID_REQUEST_ID; 26 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE; 27 28 import android.car.Car; 29 import android.car.CarOccupantZoneManager; 30 import android.car.CarOccupantZoneManager.OccupantZoneInfo; 31 import android.car.media.AudioZonesMirrorStatusCallback; 32 import android.car.media.CarAudioManager; 33 import android.content.Context; 34 import android.os.Bundle; 35 import android.util.ArraySet; 36 import android.util.Log; 37 import android.util.SparseArray; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.ArrayAdapter; 42 import android.widget.Button; 43 import android.widget.Spinner; 44 import android.widget.Toast; 45 46 import androidx.core.content.ContextCompat; 47 import androidx.fragment.app.Fragment; 48 49 import com.google.android.car.kitchensink.R; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 public final class AudioMirrorTestFragment extends Fragment { 55 56 public static final String FRAGMENT_NAME = "Mirror Audio Zones"; 57 private static final String TAG = AudioMirrorTestFragment.class.getSimpleName(); 58 59 private final SparseArray<Long> mZoneIdToMirrorId = new SparseArray<>(); 60 private final List<Long> mMirrorRequestIds = new ArrayList<>(); 61 private Context mContext; 62 private CarAudioManager mCarAudioManager; 63 private CarOccupantZoneManager mCarOccupantZoneManager; 64 private Spinner mZoneOneSpinner; 65 private Spinner mZoneTwoSpinner; 66 private Spinner mMirrorIdSpinner; 67 private ArrayAdapter<Integer> mZoneOneAdapter; 68 private ArrayAdapter<Integer> mZoneTwoAdapter; 69 private ArrayAdapter<Long> mMirrorIdAdapter; 70 private boolean mIsMirrorCallbackSet; 71 72 private AudioZonesMirrorStatusCallback mMirrorCallback = (mirroredAudioZones, status) -> { 73 long requestId = mZoneIdToMirrorId.get(mirroredAudioZones.get(0), INVALID_REQUEST_ID); 74 if (requestId == INVALID_REQUEST_ID) { 75 showToast("Mirror enabled from outside app for zones " + mirroredAudioZones); 76 return; 77 } 78 if (status == AUDIO_REQUEST_STATUS_APPROVED) { 79 mMirrorRequestIds.add(requestId); 80 updateMirrorIds(); 81 showToast("Mirror request " + requestId + " approved"); 82 return; 83 } 84 85 showToast("Mirror request " + requestId + " status changed: " + status); 86 for (int audioZoneId : mirroredAudioZones) { 87 mZoneIdToMirrorId.remove(audioZoneId); 88 } 89 90 mMirrorRequestIds.remove(requestId); 91 updateMirrorIds(); 92 }; 93 handleResetMirrorForAudioZones(long requestId)94 private void handleResetMirrorForAudioZones(long requestId) { 95 Log.v(TAG, "handleResetMirrorForAudioZones mirror id " + requestId); 96 List<Integer> mirroringZones = mCarAudioManager 97 .getMirrorAudioZonesForMirrorRequest(requestId); 98 if (mirroringZones.size() == 0) { 99 showToast("Request " + requestId + " is no longer valid"); 100 return; 101 } 102 103 showToast("Disabling mirror request " + requestId); 104 mCarAudioManager.disableAudioMirror(requestId); 105 } 106 handleEnableMirroringForZones(int zoneOne, int zoneTwo)107 private void handleEnableMirroringForZones(int zoneOne, int zoneTwo) { 108 if (zoneOne == zoneTwo) { 109 showToast("Must select two distinct zones to mirror"); 110 return; 111 } 112 if (isZoneCurrentlyMirroringOrCastingToPrimaryZone(zoneOne) 113 || isZoneCurrentlyMirroringOrCastingToPrimaryZone(zoneTwo)) { 114 return; 115 } 116 117 int status; 118 try { 119 status = mCarAudioManager.canEnableAudioMirror(); 120 } catch (Exception e) { 121 showToast("Error while enabling mirror: " + e.getMessage()); 122 return; 123 } 124 125 if (status != AUDIO_MIRROR_CAN_ENABLE) { 126 showToast("Can not enable any more zones for mirroring, status " + status); 127 return; 128 } 129 130 List<Integer> zonesToMirror = List.of(zoneOne, zoneTwo); 131 long requestId = mCarAudioManager.enableMirrorForAudioZones(zonesToMirror); 132 133 if (requestId == INVALID_REQUEST_ID) { 134 showToast("Could not enable mirroring for zones [" + zoneOne + ", " + zoneTwo + "]"); 135 return; 136 } 137 138 for (int zoneId : zonesToMirror) { 139 mZoneIdToMirrorId.put(zoneId, requestId); 140 } 141 showToast("Requested mirroring for audio zones [" + zoneOne + ", " + zoneTwo + "]"); 142 } 143 isZoneCurrentlyMirroringOrCastingToPrimaryZone(int audioZoneId)144 private boolean isZoneCurrentlyMirroringOrCastingToPrimaryZone(int audioZoneId) { 145 List<Integer> mirroringZones = mCarAudioManager.getMirrorAudioZonesForAudioZone( 146 audioZoneId); 147 if (mirroringZones.size() != 0) { 148 showToast("Zone " + audioZoneId + " is already mirroring"); 149 return true; 150 } 151 152 OccupantZoneInfo info = mCarOccupantZoneManager.getOccupantForAudioZoneId(audioZoneId); 153 if (mCarAudioManager.isMediaAudioAllowedInPrimaryZone(info)) { 154 showToast("Can not enable mirror, occupant in audio zone " + audioZoneId 155 + " is currently playing audio in primary zone"); 156 return true; 157 } 158 159 return false; 160 } 161 162 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)163 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { 164 Log.i(TAG, "onCreateView"); 165 View view = inflater 166 .inflate(R.layout.allow_audio_mirror, container, /* attachToRoot= */ false); 167 168 mZoneOneSpinner = view.findViewById(R.id.zone_one_spinner); 169 mZoneTwoSpinner = view.findViewById(R.id.zone_two_spinner); 170 mMirrorIdSpinner = view.findViewById(R.id.mirror_id_spinner); 171 172 Button audioMirrorButton = view.findViewById(R.id.enable_audio_mirror_button); 173 audioMirrorButton.setOnClickListener(v -> handleEnableAudioMirror()); 174 Button resetMirrorButton = view.findViewById(R.id.disable_audio_mirror_button); 175 resetMirrorButton.setOnClickListener(v -> handleResetAudioMirror()); 176 Button updateZones = view.findViewById(R.id.update_zones_button); 177 updateZones.setOnClickListener(v -> setupAudioZones()); 178 179 connectCar(); 180 181 return view; 182 } 183 handleResetAudioMirror()184 private void handleResetAudioMirror() { 185 long requestId = mMirrorIdAdapter.getItem(mMirrorIdSpinner.getSelectedItemPosition()); 186 handleResetMirrorForAudioZones(requestId); 187 } 188 handleEnableAudioMirror()189 private void handleEnableAudioMirror() { 190 int zoneOne = mZoneOneAdapter.getItem(mZoneOneSpinner.getSelectedItemPosition()); 191 int zoneTwo = mZoneTwoAdapter.getItem(mZoneTwoSpinner.getSelectedItemPosition()); 192 handleEnableMirroringForZones(zoneOne, zoneTwo); 193 } 194 195 @Override onDestroyView()196 public void onDestroyView() { 197 super.onDestroyView(); 198 if (mIsMirrorCallbackSet) { 199 mCarAudioManager.clearPrimaryZoneMediaAudioRequestCallback(); 200 } 201 mIsMirrorCallbackSet = false; 202 Log.i(TAG, "onDestroyView"); 203 } 204 connectCar()205 private void connectCar() { 206 mContext = requireContext(); 207 Car.createCar(mContext, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car, 208 ready) -> onCarReady(car, ready)); 209 } 210 onCarReady(Car car, boolean ready)211 private void onCarReady(Car car, boolean ready) { 212 Log.i(TAG, String.format("connectCar ready %b", ready)); 213 if (!ready) { 214 return; 215 } 216 217 mCarAudioManager = car.getCarManager(CarAudioManager.class); 218 mCarOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); 219 220 if (!mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_AUDIO_MIRRORING)) { 221 showToast("Audio mirror is not enable on this device"); 222 return; 223 } 224 225 mCarAudioManager.setAudioZoneMirrorStatusCallback(ContextCompat.getMainExecutor( 226 getActivity().getApplicationContext()), mMirrorCallback); 227 mIsMirrorCallbackSet = true; 228 229 setupAudioZones(); 230 } 231 setupAudioZones()232 private void setupAudioZones() { 233 List<OccupantZoneInfo> occupantZones = mCarOccupantZoneManager.getAllOccupantZones(); 234 235 ArraySet<Integer> audioZones = new ArraySet<>(); 236 for (OccupantZoneInfo info : occupantZones) { 237 int userId = mCarOccupantZoneManager.getUserForOccupant(info); 238 if (userId == CarOccupantZoneManager.INVALID_USER_ID) { 239 Log.i(TAG, "setupAudioZones occupant zone " + info + " has invalid user"); 240 continue; 241 } 242 int audioZoneId = mCarOccupantZoneManager.getAudioZoneIdForOccupant(info); 243 if (audioZoneId == INVALID_AUDIO_ZONE || audioZoneId == PRIMARY_AUDIO_ZONE) { 244 Log.i(TAG, "setupAudioZones audio zone " + audioZoneId 245 + " for user " + userId + " not supported for audio mirror"); 246 continue; 247 } 248 audioZones.add(audioZoneId); 249 } 250 251 Integer[] zones = audioZones.toArray(Integer[]::new); 252 mZoneOneAdapter = new ArrayAdapter<>(mContext, simple_spinner_item, zones); 253 mZoneOneAdapter.setDropDownViewResource(simple_spinner_dropdown_item); 254 mZoneOneSpinner.setAdapter(mZoneOneAdapter); 255 mZoneOneSpinner.setEnabled(true); 256 257 mZoneTwoAdapter = new ArrayAdapter<>(mContext, simple_spinner_item, zones); 258 mZoneTwoAdapter.setDropDownViewResource(simple_spinner_dropdown_item); 259 mZoneTwoSpinner.setAdapter(mZoneTwoAdapter); 260 mZoneTwoSpinner.setEnabled(true); 261 } 262 updateMirrorIds()263 private void updateMirrorIds() { 264 if (mMirrorRequestIds.isEmpty()) { 265 mMirrorIdSpinner.setEnabled(false); 266 return; 267 } 268 269 mMirrorIdAdapter = new ArrayAdapter<>(mContext, simple_spinner_item, 270 mMirrorRequestIds.toArray(Long[]::new)); 271 mMirrorIdAdapter.setDropDownViewResource(simple_spinner_dropdown_item); 272 mMirrorIdSpinner.setAdapter(mMirrorIdAdapter); 273 mMirrorIdSpinner.setEnabled(true); 274 } 275 showToast(String message)276 private void showToast(String message) { 277 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 278 Log.v(TAG, "Showed toast message: " + message); 279 } 280 } 281