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.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER;
22 import static android.car.media.CarAudioManager.AUDIO_FEATURE_AUDIO_MIRRORING;
23 import static android.car.media.CarAudioManager.INVALID_REQUEST_ID;
24 
25 import android.car.Car;
26 import android.car.CarOccupantZoneManager;
27 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
28 import android.car.media.CarAudioManager;
29 import android.car.media.MediaAudioRequestStatusCallback;
30 import android.car.media.PrimaryZoneMediaAudioRequestCallback;
31 import android.content.Context;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.os.UserManager;
35 import android.util.ArrayMap;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.AdapterView;
41 import android.widget.ArrayAdapter;
42 import android.widget.Button;
43 import android.widget.RadioGroup;
44 import android.widget.Spinner;
45 import android.widget.Toast;
46 
47 import androidx.core.content.ContextCompat;
48 import androidx.fragment.app.Fragment;
49 
50 import com.google.android.car.kitchensink.R;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 public final class AudioUserAssignmentFragment extends Fragment {
56 
57     public static final String FRAGMENT_NAME = "Play User Audio In Primary Zone";
58     private static final String TAG = AudioUserAssignmentFragment.class.getSimpleName();
59 
60     private final ArrayMap<Long, OccupantZoneInfo> mRequestIdToOccupantZone =
61             new ArrayMap<>();
62     private Context mContext;
63     private CarAudioManager mCarAudioManager;
64     private CarOccupantZoneManager mCarOccupantZoneManager;
65     private Spinner mUserSpinner;
66     private ArrayAdapter<Integer> mUserAdapter;
67     private Button mToggleUserAssignButton;
68 
69     private PrimaryZoneMediaAudioRequestCallback mPrimaryZoneMediaAudioRequestCallback =
70             new PrimaryZoneMediaAudioRequestCallback() {
71         @Override
72         public void onRequestMediaOnPrimaryZone(OccupantZoneInfo info, long requestId) {
73             handleRequestMediaOnPrimaryZone(info, requestId);
74         }
75 
76         @Override
77         public void onMediaAudioRequestStatusChanged(OccupantZoneInfo info, long requestId,
78                 @CarAudioManager.MediaAudioRequestStatus int status) {
79             if (status == CarAudioManager.AUDIO_REQUEST_STATUS_CANCELLED) {
80                 handleResetMediaOnPrimaryZone(info, requestId);
81             }
82         }
83     };
84 
85     private MediaAudioRequestStatusCallback mCallback = (info, requestId, status) -> {
86         Log.d(TAG, "onMediaAudioRequestStatusChanged request " + requestId
87                 + " for occupant " + info + " status " + status);
88         if (status == CarAudioManager.AUDIO_REQUEST_STATUS_APPROVED) {
89             handleRequestAccepted(info);
90             return;
91         }
92 
93         handleRequestRejected(info);
94         mRequestIdToOccupantZone.remove(requestId);
95     };
96     private AcceptAudioDialog mAcceptDialog;
97     private RadioGroup mAssignAudioRadioGroup;
98     private boolean mIsPrimaryZoneMediaCallbackSet;
99 
handleResetMediaOnPrimaryZone(OccupantZoneInfo info, long requestId)100     private void handleResetMediaOnPrimaryZone(OccupantZoneInfo info,
101             long requestId) {
102         Log.v(TAG, "handleResetMediaOnPrimaryZone request " + requestId + " for occupant "
103                 + info + " reset");
104         if (mAcceptDialog == null) {
105             return;
106         }
107         mAcceptDialog.dismiss();
108         mAcceptDialog = null;
109     }
110 
handleRequestMediaOnPrimaryZone(OccupantZoneInfo info, long requestId)111     private void handleRequestMediaOnPrimaryZone(OccupantZoneInfo info,
112             long requestId) {
113         Log.v(TAG, "handleRequestMediaOnPrimaryZone request " + requestId + " allowed to play");
114 
115         int currentSelectionRule = mAssignAudioRadioGroup.getCheckedRadioButtonId();
116         switch (currentSelectionRule) {
117             case R.id.assign_audio_reject_audio_button:
118             case R.id.assign_audio_accept_audio_button:
119                 boolean allow = currentSelectionRule == R.id.assign_audio_accept_audio_button;
120                 handleAllowAudioInPrimaryZone(requestId, allow);
121                 return;
122             case R.id.assign_audio_ask_user_button:
123             default:
124                 // Fall through to ask user
125         }
126 
127         mAcceptDialog = AcceptAudioDialog.newInstance(
128                 (allowed) -> handleAllowAudioInPrimaryZone(requestId, allowed), getUserName(info));
129         mAcceptDialog.show(getActivity().getSupportFragmentManager(),
130                 "fragment_accept_audio_playback");
131     }
132 
getUserName(OccupantZoneInfo info)133     private String getUserName(OccupantZoneInfo info) {
134         int userId = mCarOccupantZoneManager.getUserForOccupant(info);
135         Context userContext = getContext()
136                 .createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
137 
138         UserManager userManager = userContext.getSystemService(UserManager.class);
139         return userManager.getUserName();
140     }
141 
handleAllowAudioInPrimaryZone(long requestId, boolean allowed)142     private void handleAllowAudioInPrimaryZone(long requestId, boolean allowed) {
143         Log.v(TAG, "handleAllowAudioInPrimaryZone request allowed " + allowed);
144         mCarAudioManager.allowMediaAudioOnPrimaryZone(requestId, allowed);
145         mAcceptDialog = null;
146     }
147 
148 
149     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)150     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
151         Log.i(TAG, "onCreateView");
152         View view = inflater
153                 .inflate(R.layout.assign_user_to_primary_audio_zone, container,
154                         /* attachToRoot= */ false);
155         mUserSpinner = view.findViewById(R.id.user_spinner);
156         mUserSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
157             @Override
158             public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
159                 handleUserSelection();
160             }
161 
162             @Override
163             public void onNothingSelected(AdapterView<?> parent) {
164             }
165         });
166 
167         mToggleUserAssignButton = view.findViewById(R.id.user_assign_to_main_zone_button);
168         mToggleUserAssignButton.setOnClickListener(v -> handleToggleAssignUserAudio());
169 
170         Button resetUser  = view.findViewById(R.id.user_reset_from_main_zone_button);
171         resetUser.setOnClickListener(v -> handleResetUserAudio());
172 
173         mAssignAudioRadioGroup = view.findViewById(R.id.assign_audio_radio_group);
174 
175         connectCar();
176 
177         return view;
178     }
179 
handleResetUserAudio()180     private void handleResetUserAudio() {
181         Log.d(TAG, "handleResetUserAudio");
182         int position = mUserSpinner.getSelectedItemPosition();
183         int userId = mUserAdapter.getItem(position);
184         OccupantZoneInfo info = getOccupantZoneForUser(userId);
185 
186         mCarAudioManager.resetMediaAudioOnPrimaryZone(info);
187     }
188 
handleToggleAssignUserAudio()189     private void handleToggleAssignUserAudio() {
190         Log.d(TAG, "handleToggleAssignUserAudio");
191         int position = mUserSpinner.getSelectedItemPosition();
192         int userId = mUserAdapter.getItem(position);
193         OccupantZoneInfo info = getOccupantZoneForUser(userId);
194 
195         if (mRequestIdToOccupantZone.containsValue(info)) {
196             handleCancelMediaAudioOnPrimaryZone(info);
197             return;
198         }
199 
200         handleRequestUserToPlayInMainCabin(userId);
201     }
202 
handleRequestUserToPlayInMainCabin(int userId)203     private void handleRequestUserToPlayInMainCabin(int userId) {
204         Log.d(TAG, "requestUserToPlayInMainCabin");
205         OccupantZoneInfo info = getOccupantZoneForUser(userId);
206         if (info == null) {
207             Log.e(TAG, "Can not find occupant zone info for user" + userId);
208             showToast("User " + userId + " is not currently assigned to any occupant zone");
209             return;
210         }
211 
212         if (mCarAudioManager.isMediaAudioAllowedInPrimaryZone(info)) {
213             showToast("User " + userId + " is already allowed to play in primary zone");
214             return;
215         }
216 
217         int carAudioZoneId = mCarOccupantZoneManager.getAudioZoneIdForOccupant(info);
218 
219         if (mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_AUDIO_MIRRORING)
220                 && !mCarAudioManager.getMirrorAudioZonesForAudioZone(carAudioZoneId).isEmpty()) {
221             showToast("Can not enable primary zone playback as user " + userId
222                     + " is currently mirroring with another zone");
223             return;
224         }
225 
226         requestToPlayAudioInPrimaryZone(info);
227     }
228 
requestToPlayAudioInPrimaryZone(OccupantZoneInfo info)229     private void requestToPlayAudioInPrimaryZone(OccupantZoneInfo info) {
230         Log.d(TAG, "requestUserToPlayInMainCabin occupant " + info);
231         long requestId;
232         try {
233             requestId = mCarAudioManager.requestMediaAudioOnPrimaryZone(info,
234                     ContextCompat.getMainExecutor(getActivity().getApplicationContext()),
235                     mCallback);
236         } catch (Exception e) {
237             showToast("Error while requesting media playback: " + e.getMessage());
238             return;
239         }
240         if (requestId == INVALID_REQUEST_ID) {
241             handleRequestRejected(info);
242             return;
243         }
244 
245         mRequestIdToOccupantZone.put(requestId, info);
246         mToggleUserAssignButton.setText(R.string.cancel_request);
247     }
248 
handleRequestRejected(OccupantZoneInfo info)249     private void handleRequestRejected(OccupantZoneInfo info) {
250         if (!mRequestIdToOccupantZone.containsValue(info)) {
251             Log.d(TAG, "handleRequestRejected info " + info + " has no pending request");
252             return;
253         }
254         mToggleUserAssignButton.setText(R.string.assign_user);
255 
256         AudioPlaybackRequestResults dialog =
257                 AudioPlaybackRequestResults.newInstance(getUserName(info), /* allowed= */ false);
258         dialog.show(getActivity().getSupportFragmentManager(), "rejected_results");
259 
260     }
261 
handleRequestAccepted(OccupantZoneInfo info)262     private void handleRequestAccepted(OccupantZoneInfo info) {
263         AudioPlaybackRequestResults dialog =
264                 AudioPlaybackRequestResults.newInstance(getUserName(info), /* allowed= */ true);
265         mToggleUserAssignButton.setText(R.string.unassign_user);
266         dialog.show(getActivity().getSupportFragmentManager(), "allowed_results");
267     }
268 
getOccupantZoneForUser(int userId)269     private OccupantZoneInfo getOccupantZoneForUser(int userId) {
270         List<OccupantZoneInfo> occupants =
271                 mCarOccupantZoneManager.getAllOccupantZones();
272 
273         for (int index = 0; index < occupants.size(); index++) {
274             OccupantZoneInfo info = occupants.get(index);
275             int occupantZoneUser = mCarOccupantZoneManager.getUserForOccupant(info);
276             if (occupantZoneUser == userId) {
277                 return info;
278             }
279         }
280 
281         return null;
282     }
283 
handleCancelMediaAudioOnPrimaryZone(OccupantZoneInfo info)284     private void handleCancelMediaAudioOnPrimaryZone(OccupantZoneInfo info) {
285         Log.d(TAG, "handleCancelMediaAudioOnPrimaryZone");
286         int index = mRequestIdToOccupantZone.indexOfValue(info);
287         if (index < 0) {
288             showToast("Occupant " + info + " not currently assign to play media in primary zone");
289             return;
290         }
291 
292         long requestId = mRequestIdToOccupantZone.keyAt(index);
293 
294         mRequestIdToOccupantZone.remove(requestId);
295         boolean cancelled;
296         try {
297             cancelled = mCarAudioManager.cancelMediaAudioOnPrimaryZone(requestId);
298         } catch (Exception e) {
299             showToast("Could not cancel media on primary zone: " + e.getMessage());
300             return;
301         }
302         if (!cancelled) {
303             showToast("Could not unassigned request " + requestId + " for occupant " + info);
304             return;
305         }
306         showToast("Unassigned request " + requestId + " for occupant " + info);
307         mToggleUserAssignButton.setText(R.string.assign_user);
308     }
309 
handleUserSelection()310     private void handleUserSelection() {
311         Log.d(TAG, "handleUserSelection");
312         int position = mUserSpinner.getSelectedItemPosition();
313         int userId = mUserAdapter.getItem(position);
314         Log.d(TAG, String.format("User Selected: %d", userId));
315 
316         boolean isUserAssigned =
317                 mCarAudioManager.isMediaAudioAllowedInPrimaryZone(getOccupantZoneForUser(userId));
318         Log.d(TAG, String.format("User Selected: %d is assigned %b", userId, isUserAssigned));
319         mToggleUserAssignButton.setText(isUserAssigned
320                 ? R.string.unassign_user : R.string.assign_user);
321     }
322 
323     @Override
onDestroyView()324     public void onDestroyView() {
325         super.onDestroyView();
326         if (mIsPrimaryZoneMediaCallbackSet) {
327             mCarAudioManager.clearPrimaryZoneMediaAudioRequestCallback();
328         }
329         mIsPrimaryZoneMediaCallbackSet = false;
330         Log.i(TAG, "onDestroyView");
331     }
332 
connectCar()333     private void connectCar() {
334         mContext = getContext();
335         Car.createCar(mContext, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car,
336                 ready) -> onCarReady(car, ready));
337     }
338 
onCarReady(Car car, boolean ready)339     private void onCarReady(Car car, boolean ready) {
340         Log.i(TAG, String.format("connectCar ready %b", ready));
341         if (!ready) {
342             showToast("Car service not ready!");
343             return;
344         }
345 
346         mCarAudioManager = car.getCarManager(CarAudioManager.class);
347         mCarOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class);
348 
349         setUserInfo();
350         setDriverOnlyAbilities();
351     }
352 
setDriverOnlyAbilities()353     private void setDriverOnlyAbilities() {
354         if (mCarOccupantZoneManager.getMyOccupantZone().occupantType != OCCUPANT_TYPE_DRIVER) {
355             mAssignAudioRadioGroup.setVisibility(View.INVISIBLE);
356             return;
357         }
358         mIsPrimaryZoneMediaCallbackSet = mCarAudioManager.setPrimaryZoneMediaAudioRequestCallback(
359                 ContextCompat.getMainExecutor(getActivity().getApplicationContext()),
360                 mPrimaryZoneMediaAudioRequestCallback);
361     }
362 
setUserInfo()363     private void setUserInfo() {
364         List<OccupantZoneInfo> occupantZones =
365                 mCarOccupantZoneManager.getAllOccupantZones();
366 
367         OccupantZoneInfo myInfo =
368                 mCarOccupantZoneManager.getMyOccupantZone();
369 
370         Log.i(TAG, String.format("setUserInfo occupant zones %d", occupantZones.size()));
371 
372         List<Integer> userList = new ArrayList<>();
373         int myIndex = 0;
374         int counter = 0;
375         for (OccupantZoneInfo occupantZoneInfo : occupantZones) {
376             int userId = mCarOccupantZoneManager.getUserForOccupant(occupantZoneInfo);
377             if (userId == CarOccupantZoneManager.INVALID_USER_ID) {
378                 Log.i(TAG, String.format("setUserInfo occupant zone %s has invalid user",
379                         occupantZoneInfo));
380                 continue;
381             }
382 
383             // Do not include driver in the list as driver already owns the primary zone
384             if (occupantZoneInfo.occupantType == OCCUPANT_TYPE_DRIVER) {
385                 continue;
386             }
387             Log.i(TAG, String.format("setUserInfo occupant zone %s has user %d",
388                     occupantZoneInfo, userId));
389             userList.add(userId);
390             if (myInfo.equals(occupantZoneInfo)) {
391                 myIndex = counter;
392             }
393             counter++;
394         }
395 
396         if (userList.isEmpty()) {
397             showToast("Audio playback to primary zone is not supported on this device");
398             return;
399         }
400 
401         Integer[] userArray = userList.toArray(Integer[]::new);
402         mUserAdapter = new ArrayAdapter<>(mContext, simple_spinner_item, userArray);
403         mUserAdapter.setDropDownViewResource(simple_spinner_dropdown_item);
404         mUserSpinner.setAdapter(mUserAdapter);
405         mUserSpinner.setEnabled(true);
406         mUserSpinner.setSelection(myIndex);
407     }
408 
showToast(String message)409     private void showToast(String message) {
410         Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
411         Log.d(TAG, "Showed toast message: " + message);
412     }
413 }
414