1 /*
2  * Copyright (C) 2022 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 package com.google.android.car.kitchensink.users;
17 
18 import android.annotation.Nullable;
19 import android.annotation.UserIdInt;
20 import android.app.Activity;
21 import android.app.ActivityManager;
22 import android.car.Car;
23 import android.car.CarOccupantZoneManager;
24 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
25 import android.car.user.CarUserManager;
26 import android.car.user.UserCreationResult;
27 import android.car.user.UserLifecycleEventFilter;
28 import android.car.user.UserStartRequest;
29 import android.car.user.UserStopRequest;
30 import android.car.util.concurrent.AsyncFuture;
31 import android.content.Context;
32 import android.content.pm.UserInfo;
33 import android.graphics.Color;
34 import android.hardware.display.DisplayManager;
35 import android.os.Bundle;
36 import android.os.Process;
37 import android.os.UserHandle;
38 import android.os.UserManager;
39 import android.text.TextUtils;
40 import android.util.Log;
41 import android.view.Display;
42 import android.view.DisplayAddress;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.ArrayAdapter;
47 import android.widget.Button;
48 import android.widget.EditText;
49 import android.widget.Spinner;
50 import android.widget.TextView;
51 
52 import androidx.fragment.app.Fragment;
53 
54 import com.google.android.car.kitchensink.R;
55 import com.google.android.car.kitchensink.UserPickerActivity;
56 
57 import java.util.ArrayList;
58 import java.util.Collection;
59 import java.util.List;
60 import java.util.Set;
61 import java.util.concurrent.TimeUnit;
62 
63 public final class SimpleUserPickerFragment extends Fragment {
64 
65     private static final String TAG = SimpleUserPickerFragment.class.getSimpleName();
66 
67     private static final int ERROR_MESSAGE = 0;
68     private static final int WARN_MESSAGE = 1;
69     private static final int INFO_MESSAGE = 2;
70 
71     private static final long TIMEOUT_MS = 10_000;
72 
73     private SpinnerWrapper mUsersSpinner;
74     private SpinnerWrapper mDisplaysSpinner;
75 
76     private Button mStartUserButton;
77     private Button mStopUserButton;
78     private Button mSwitchUserButton;
79     private Button mCreateUserButton;
80 
81     private TextView mDisplayIdText;
82     private TextView mUserOnDisplayText;
83     private TextView mUserIdText;
84     private TextView mZoneInfoText;
85     private TextView mStatusMessageText;
86     private EditText mNewUserNameText;
87 
88     private UserManager mUserManager;
89     private DisplayManager mDisplayManager;
90     private CarOccupantZoneManager mZoneManager;
91     private CarUserManager mCarUserManager;
92 
93     // The logical display to which the view's window has been attached.
94     private Display mDisplayAttached;
95 
96     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)97     public View onCreateView(LayoutInflater inflater, ViewGroup container,
98             Bundle savedInstanceState) {
99         return inflater.inflate(R.layout.simple_user_picker, container, false);
100     }
101 
onViewCreated(View view, Bundle savedInstanceState)102     public void onViewCreated(View view, Bundle savedInstanceState) {
103         mUserManager = getContext().getSystemService(UserManager.class);
104         mDisplayManager = getContext().getSystemService(DisplayManager.class);
105 
106         Car car = ((UserPickerActivity) getHost()).getCar();
107         if (car == null) {
108             // Car service has crashed. Ignore other parts as it will be
109             // restarted anyway.
110             Log.i(TAG, "null car instance, finish");
111             ((Activity) getHost()).finish();
112             return;
113         }
114         mZoneManager = car.getCarManager(CarOccupantZoneManager.class);
115         mZoneManager.registerOccupantZoneConfigChangeListener(
116                 new ZoneChangeListener());
117 
118         mCarUserManager = car.getCarManager(CarUserManager.class);
119 
120         mDisplayAttached = getContext().getDisplay();
121         if (mDisplayAttached == null) {
122             Log.e(TAG, "Cannot find display");
123             ((Activity) getHost()).finish();
124         }
125 
126         int displayId = mDisplayAttached.getDisplayId();
127         int driverDisplayId = mZoneManager.getDisplayIdForDriver(
128                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
129         Log.i(TAG, "driver display id: " + driverDisplayId);
130         boolean isPassengerView = displayId != driverDisplayId;
131         boolean hasUserOnDisplay = mZoneManager.getUserForDisplayId(displayId)
132                 != CarOccupantZoneManager.INVALID_USER_ID;
133 
134         mDisplayIdText = view.findViewById(R.id.textView_display_id);
135         mUserOnDisplayText = view.findViewById(R.id.textView_user_on_display);
136         mUserIdText = view.findViewById(R.id.textView_state);
137         mZoneInfoText = view.findViewById(R.id.textView_zoneinfo);
138         updateTextInfo();
139 
140         mNewUserNameText = view.findViewById(R.id.new_user_name);
141 
142         mUsersSpinner = SpinnerWrapper.create(getContext(),
143                 view.findViewById(R.id.spinner_users), getUnassignedUsers());
144         if (isPassengerView && hasUserOnDisplay) {
145             view.findViewById(R.id.textView_users).setVisibility(View.GONE);
146             view.findViewById(R.id.spinner_users).setVisibility(View.GONE);
147         }
148 
149         // Listen to user created and removed events to refresh the user Spinner.
150         UserLifecycleEventFilter filter = new UserLifecycleEventFilter.Builder()
151                 .addEventType(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_CREATED)
152                 .addEventType(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_REMOVED).build();
153         mCarUserManager.addListener(getContext().getMainExecutor(), filter, (event) ->
154                 mUsersSpinner.updateEntries(getUnassignedUsers())
155         );
156 
157         mDisplaysSpinner = SpinnerWrapper.create(getContext(),
158                 view.findViewById(R.id.spinner_displays), getDisplays());
159         if (isPassengerView) {
160             view.findViewById(R.id.textView_displays).setVisibility(View.GONE);
161             view.findViewById(R.id.spinner_displays).setVisibility(View.GONE);
162         }
163 
164         mStartUserButton = view.findViewById(R.id.button_start_user);
165         mStartUserButton.setOnClickListener(v -> startUser());
166         if (isPassengerView) {
167             mStartUserButton.setVisibility(View.GONE);
168         }
169 
170         mStopUserButton = view.findViewById(R.id.button_stop_user);
171         mStopUserButton.setOnClickListener(v -> stopUser());
172         if (!isPassengerView || isPassengerView && !hasUserOnDisplay) {
173             mStopUserButton.setVisibility(View.GONE);
174         }
175 
176         mSwitchUserButton = view.findViewById(R.id.button_switch_user);
177         mSwitchUserButton.setOnClickListener(v -> switchUser());
178         if (!isPassengerView || isPassengerView && hasUserOnDisplay) {
179             mSwitchUserButton.setVisibility(View.GONE);
180         }
181 
182         mCreateUserButton = view.findViewById(R.id.button_create_user);
183         mCreateUserButton.setOnClickListener(v -> createUser());
184         if (isPassengerView && hasUserOnDisplay) {
185             view.findViewById(R.id.textView_name).setVisibility(View.GONE);
186             view.findViewById(R.id.new_user_name).setVisibility(View.GONE);
187             mCreateUserButton.setVisibility(View.GONE);
188         }
189 
190         mStatusMessageText = view.findViewById(R.id.status_message_text_view);
191     }
192 
193     private final class ZoneChangeListener implements
194             CarOccupantZoneManager.OccupantZoneConfigChangeListener {
195         @Override
onOccupantZoneConfigChanged(int changeFlags)196         public void onOccupantZoneConfigChanged(int changeFlags) {
197             Log.i(TAG, "onOccupantZoneConfigChanged changeFlags=" + changeFlags);
198             if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0) {
199                 Log.i(TAG, "Detected changes in display to zone assignment");
200                 mDisplaysSpinner.updateEntries(getDisplays());
201                 // When a display is removed, user on the display should be stopped.
202                 mUsersSpinner.updateEntries(getUnassignedUsers());
203                 updateTextInfo();
204             }
205 
206             if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER) != 0) {
207                 Log.i(TAG, "Detected changes in user to zone assignment");
208                 mDisplaysSpinner.updateEntries(getDisplays());
209                 mUsersSpinner.updateEntries(getUnassignedUsers());
210                 updateTextInfo();
211             }
212         }
213     }
214 
updateTextInfo()215     private void updateTextInfo() {
216         int displayId = mDisplayAttached.getDisplayId();
217         OccupantZoneInfo zoneInfo = getOccupantZoneForDisplayId(displayId);
218         int userId = CarOccupantZoneManager.INVALID_USER_ID;
219         try {
220             if (zoneInfo != null) {
221                 userId = mZoneManager.getUserForOccupant(zoneInfo);
222             }
223         } catch (Exception e) {
224             Log.w(TAG, "updateTextInfo: encountered exception in getting user for occupant", e);
225         }
226         int zoneId = zoneInfo == null ? CarOccupantZoneManager.OccupantZoneInfo.INVALID_ZONE_ID
227                 : zoneInfo.zoneId;
228         mDisplayIdText.setText("DisplayId: " + displayId + " ZoneId: " + zoneId);
229         String userString = userId == CarOccupantZoneManager.INVALID_USER_ID
230                 ? "unassigned" : Integer.toString(userId);
231         mUserOnDisplayText.setText("User on display: " + userString);
232 
233         int currentUserId = ActivityManager.getCurrentUser();
234         int myUserId = UserHandle.myUserId();
235         mUserIdText.setText("Current userId: " + currentUserId + " myUserId:" + myUserId);
236         StringBuilder zoneStateBuilder = new StringBuilder();
237         zoneStateBuilder.append("Zone-User-Displays: ");
238         List<CarOccupantZoneManager.OccupantZoneInfo> zonelist = mZoneManager.getAllOccupantZones();
239         for (CarOccupantZoneManager.OccupantZoneInfo zone : zonelist) {
240             zoneStateBuilder.append(zone.zoneId);
241             zoneStateBuilder.append("-");
242             int user = mZoneManager.getUserForOccupant(zone);
243             if (user == UserHandle.USER_NULL) {
244                 zoneStateBuilder.append("unassigned");
245             } else {
246                 zoneStateBuilder.append(user);
247             }
248             zoneStateBuilder.append("-");
249             List<Display> displays = mZoneManager.getAllDisplaysForOccupant(zone);
250             for (Display display : displays) {
251                 zoneStateBuilder.append(display.getDisplayId());
252                 zoneStateBuilder.append(",");
253             }
254             zoneStateBuilder.append(":");
255         }
256         mZoneInfoText.setText(zoneStateBuilder.toString());
257     }
258 
259     // startUser starts a selected user on a selected secondary display.
startUser()260     private void startUser() {
261         int userId = getSelectedUser();
262         if (userId == UserHandle.USER_NULL) {
263             return;
264         }
265 
266         int displayId = getSelectedDisplay();
267         if (displayId == Display.INVALID_DISPLAY) {
268             return;
269         }
270 
271         // Start the user on display.
272         startUserVisibleOnDisplay(userId, displayId);
273     }
274 
275     // stopUser stops the visible user on this secondary display.
stopUser()276     private void stopUser() {
277         int displayId = mDisplayAttached.getDisplayId();
278 
279         OccupantZoneInfo zoneInfo = getOccupantZoneForDisplayId(displayId);
280         if (zoneInfo == null) {
281             setMessage(ERROR_MESSAGE,
282                     "Cannot find occupant zone info associated with display " + displayId);
283             return;
284         }
285 
286         int userId = mZoneManager.getUserForOccupant(zoneInfo);
287         if (userId == CarOccupantZoneManager.INVALID_USER_ID) {
288             setMessage(ERROR_MESSAGE,
289                     "Cannot find the user assigned to the occupant zone " + zoneInfo.zoneId);
290             return;
291         }
292 
293         int currentUser = ActivityManager.getCurrentUser();
294         if (userId == currentUser) {
295             setMessage(WARN_MESSAGE, "Can not change current user");
296             return;
297         }
298 
299         if (!mUserManager.isUserRunning(userId)) {
300             setMessage(WARN_MESSAGE, "User " + userId + " is already stopped");
301             return;
302         }
303 
304         Log.i(TAG, "stop user:" + userId);
305         UserStopRequest request = new UserStopRequest.Builder(UserHandle.of(userId)).build();
306         mCarUserManager.stopUser(request, Runnable::run,
307                 response -> {
308                     if (!response.isSuccess()) {
309                         setMessage(ERROR_MESSAGE,
310                                 "Cannot stop user " + userId + ", Response: " + response);
311                         return;
312                     }
313                     getActivity().recreate();
314                 });
315     }
316 
switchUser()317     private void switchUser() {
318         // Pick an unassigned user to switch to on this display.
319         int userId = getSelectedUser();
320         if (userId == UserHandle.USER_NULL) {
321             setMessage(ERROR_MESSAGE, "Invalid user");
322             return;
323         }
324 
325         int displayId = mDisplayAttached.getDisplayId();
326         startUserVisibleOnDisplay(userId, displayId);
327     }
328 
startUserVisibleOnDisplay(@serIdInt int userId, int displayId)329     private void startUserVisibleOnDisplay(@UserIdInt int userId, int displayId) {
330         Log.i(TAG, "start user: " + userId + " in background on display: " + displayId);
331         UserStartRequest request = new UserStartRequest.Builder(UserHandle.of(userId))
332                 .setDisplayId(displayId).build();
333         mCarUserManager.startUser(request, Runnable::run,
334                 response -> {
335                     boolean isSuccess = response.isSuccess();
336                     if (!isSuccess) {
337                         setMessage(ERROR_MESSAGE,
338                                 "Cannot start user " + userId + " on display " + displayId
339                                         + ", response: " + response);
340                     } else {
341                         setMessage(INFO_MESSAGE,
342                                 "Started user " + userId + " on display " + displayId);
343                         mUsersSpinner.updateEntries(getUnassignedUsers());
344                         updateTextInfo();
345                     }
346                 });
347     }
348 
createUser()349     private void createUser() {
350         String name = mNewUserNameText.getText().toString();
351         if (TextUtils.isEmpty(name)) {
352             setMessage(ERROR_MESSAGE, "Cannot create user without a name");
353             return;
354         }
355 
356         AsyncFuture<UserCreationResult> future = mCarUserManager.createUser(name, /* flags= */ 0);
357         setMessage(INFO_MESSAGE, "Creating full secondary user with name " + name + " ...");
358 
359         UserCreationResult result = null;
360         try {
361             result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
362             if (result == null) {
363                 Log.e(TAG, "Timed out creating user after " + TIMEOUT_MS + "ms...");
364                 setMessage(ERROR_MESSAGE, "Timed out creating user after " + TIMEOUT_MS + "ms...");
365                 return;
366             }
367         } catch (InterruptedException e) {
368             Log.e(TAG, "Interrupted waiting for future " + future, e);
369             Thread.currentThread().interrupt();
370             setMessage(ERROR_MESSAGE, "Interrupted while creating user");
371             return;
372         } catch (Exception e) {
373             Log.e(TAG, "Exception getting future " + future, e);
374             setMessage(ERROR_MESSAGE, "Encountered Exception while creating user " + name);
375             return;
376         }
377 
378         StringBuilder message = new StringBuilder();
379         if (result.isSuccess()) {
380             message.append("User created: ").append(result.getUser().toString());
381             setMessage(INFO_MESSAGE, message.toString());
382             mUsersSpinner.updateEntries(getUnassignedUsers());
383         } else {
384             int status = result.getStatus();
385             message.append("Failed with code ").append(status).append('(')
386                     .append(UserCreationResult.statusToString(status)).append(')');
387             message.append("\nFull result: ").append(result);
388             String error = result.getErrorMessage();
389             if (error != null) {
390                 message.append("\nError message: ").append(error);
391             }
392             setMessage(ERROR_MESSAGE, message.toString());
393         }
394     }
395 
396     // TODO(b/248608281): Use API from CarOccupantZoneManager for convenience.
397     @Nullable
getOccupantZoneForDisplayId(int displayId)398     private OccupantZoneInfo getOccupantZoneForDisplayId(int displayId) {
399         List<OccupantZoneInfo> occupantZoneInfos = mZoneManager.getAllOccupantZones();
400         for (int index = 0; index < occupantZoneInfos.size(); index++) {
401             OccupantZoneInfo occupantZoneInfo = occupantZoneInfos.get(index);
402             List<Display> displays = mZoneManager.getAllDisplaysForOccupant(
403                     occupantZoneInfo);
404             for (int displayIndex = 0; displayIndex < displays.size(); displayIndex++) {
405                 if (displays.get(displayIndex).getDisplayId() == displayId) {
406                     return occupantZoneInfo;
407                 }
408             }
409         }
410         return null;
411     }
412 
setMessage(int messageType, String title, Exception e)413     private void setMessage(int messageType, String title, Exception e) {
414         StringBuilder messageTextBuilder = new StringBuilder()
415                 .append(title)
416                 .append(": ")
417                 .append(e.getMessage());
418         setMessage(messageType, messageTextBuilder.toString());
419     }
420 
setMessage(int messageType, String message)421     private void setMessage(int messageType, String message) {
422         int textColor;
423         switch (messageType) {
424             case ERROR_MESSAGE:
425                 Log.e(TAG, message);
426                 textColor = Color.RED;
427                 break;
428             case WARN_MESSAGE:
429                 Log.w(TAG, message);
430                 textColor = Color.YELLOW;
431                 break;
432             case INFO_MESSAGE:
433             default:
434                 Log.i(TAG, message);
435                 textColor = Color.GREEN;
436         }
437         mStatusMessageText.setTextColor(textColor);
438         mStatusMessageText.setText(message);
439     }
440 
getSelectedDisplay()441     private int getSelectedDisplay() {
442         String displayStr = mDisplaysSpinner.getSelectedEntry();
443         if (displayStr == null) {
444             Log.w(TAG, "getSelectedDisplay, no display selected", new RuntimeException());
445             return Display.INVALID_DISPLAY;
446         }
447         return Integer.parseInt(displayStr.split(",")[0]);
448     }
449 
getSelectedUser()450     private int getSelectedUser() {
451         String userStr = mUsersSpinner.getSelectedEntry();
452         if (userStr == null) {
453             Log.w(TAG, "getSelectedUser, user not selected", new RuntimeException());
454             return UserHandle.USER_NULL;
455         }
456         return Integer.parseInt(userStr.split(",")[0]);
457     }
458 
459     // format: id,type
getUnassignedUsers()460     private ArrayList<String> getUnassignedUsers() {
461         ArrayList<String> users = new ArrayList<>();
462         List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
463         Set<UserHandle> visibleUsers = mUserManager.getVisibleUsers();
464         // Exclude visible users and only show unassigned users.
465         for (int i = 0; i < aliveUsers.size(); ++i) {
466             UserInfo u = aliveUsers.get(i);
467             if (!u.isFull()) continue;
468             if (!isIncluded(u.id, visibleUsers)) {
469                 users.add(Integer.toString(u.id) + "," + u.name);
470             }
471         }
472 
473         return users;
474     }
475 
476     // format: displayId,[P,]?,address]
getDisplays()477     private ArrayList<String> getDisplays() {
478         ArrayList<String> displays = new ArrayList<>();
479         Display[] disps = mDisplayManager.getDisplays();
480         int uidSelf = Process.myUid();
481         for (Display disp : disps) {
482             if (!disp.hasAccess(uidSelf)) {
483                 continue;
484             }
485 
486             int displayId = disp.getDisplayId();
487             if (mZoneManager.getUserForDisplayId(displayId)
488                     != CarOccupantZoneManager.INVALID_USER_ID) {
489                 Log.d(TAG, "display " + displayId + " already has user on it, skipping");
490                 continue;
491             }
492             StringBuilder builder = new StringBuilder()
493                     .append(displayId)
494                     .append(",");
495             DisplayAddress address = disp.getAddress();
496             if (address instanceof  DisplayAddress.Physical) {
497                 builder.append("P,");
498             }
499             builder.append(address);
500             displays.add(builder.toString());
501         }
502         return displays;
503     }
504 
isIncluded(int userId, Collection<UserHandle> users)505     private static boolean isIncluded(int userId, Collection<UserHandle> users) {
506         return users.stream().anyMatch(u -> u.getIdentifier() == userId);
507     }
508 
509     private static final class SpinnerWrapper {
510         private final Spinner mSpinner;
511         private final ArrayList<String> mEntries;
512         private final ArrayAdapter<String> mAdapter;
513 
create(Context context, Spinner spinner, ArrayList<String> entries)514         private static SpinnerWrapper create(Context context, Spinner spinner,
515                 ArrayList<String> entries) {
516             SpinnerWrapper wrapper = new SpinnerWrapper(context, spinner, entries);
517             wrapper.init();
518             return wrapper;
519         }
520 
SpinnerWrapper(Context context, Spinner spinner, ArrayList<String> entries)521         private SpinnerWrapper(Context context, Spinner spinner, ArrayList<String> entries) {
522             mSpinner = spinner;
523             mEntries = new ArrayList<>(entries);
524             mAdapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item,
525                     mEntries);
526         }
527 
init()528         private void init() {
529             mAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
530             mSpinner.setAdapter(mAdapter);
531         }
532 
updateEntries(ArrayList<String> entries)533         private void updateEntries(ArrayList<String> entries) {
534             mEntries.clear();
535             mEntries.addAll(entries);
536             mAdapter.notifyDataSetChanged();
537         }
538 
539         @Nullable
getSelectedEntry()540         private String getSelectedEntry() {
541             return (String) mSpinner.getSelectedItem();
542         }
543     }
544 }
545