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