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