/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.car.qc; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; import static android.view.WindowInsets.Type.statusBars; import static com.android.car.ui.utils.CarUiUtils.drawableToBitmap; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AlertDialog; import android.car.Car; import android.car.user.CarUserManager; import android.car.user.UserCreationResult; import android.car.user.UserSwitchResult; import android.car.userlib.UserHelper; import android.car.util.concurrent.AsyncFuture; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.UserHandle; import android.os.UserManager; import android.sysprop.CarProperties; import android.util.Log; import android.view.Window; import android.view.WindowManager; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import com.android.car.qc.QCItem; import com.android.car.qc.QCList; import com.android.car.qc.QCRow; import com.android.car.qc.provider.BaseLocalQCProvider; import com.android.internal.util.UserIcons; import com.android.systemui.R; import com.android.systemui.car.userswitcher.UserIconProvider; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * Local provider for the profile switcher panel. */ public class ProfileSwitcher extends BaseLocalQCProvider { private static final String TAG = ProfileSwitcher.class.getSimpleName(); private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500; private final UserManager mUserManager; private final UserIconProvider mUserIconProvider; private final Car mCar; private final CarUserManager mCarUserManager; private boolean mPendingUserAdd; public ProfileSwitcher(Context context) { super(context); mUserManager = context.getSystemService(UserManager.class); mUserIconProvider = new UserIconProvider(); mCar = Car.createCar(mContext); mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE); } @VisibleForTesting ProfileSwitcher(Context context, UserManager userManager, CarUserManager carUserManager) { super(context); mUserManager = userManager; mUserIconProvider = new UserIconProvider(); mCar = null; mCarUserManager = carUserManager; } @Override public QCItem getQCItem() { QCList.Builder listBuilder = new QCList.Builder(); int fgUserId = ActivityManager.getCurrentUser(); UserHandle fgUserHandle = UserHandle.of(fgUserId); // If the foreground user CANNOT switch to other users, only display the foreground user. if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) { UserInfo currentUser = mUserManager.getUserInfo(ActivityManager.getCurrentUser()); return listBuilder.addRow(createUserProfileRow(currentUser)).build(); } List profiles = getProfileList(); for (UserInfo profile : profiles) { listBuilder.addRow(createUserProfileRow(profile)); } listBuilder.addRow(createGuestProfileRow()); listBuilder.addRow(createAddProfileRow()); return listBuilder.build(); } @Override public void onDestroy() { super.onDestroy(); if (mCar != null) { mCar.disconnect(); } } private List getProfileList() { return mUserManager.getAliveUsers() .stream() .filter(userInfo -> userInfo.supportsSwitchToByUser() && !userInfo.isGuest()) .collect(Collectors.toList()); } private QCRow createUserProfileRow(UserInfo userInfo) { QCItem.ActionHandler actionHandler = (item, context, intent) -> { if (mPendingUserAdd) { return; } switchUser(userInfo.id); }; return createProfileRow(userInfo.name, mUserIconProvider.getRoundedUserIcon(userInfo, mContext), actionHandler); } private QCRow createGuestProfileRow() { QCItem.ActionHandler actionHandler = (item, context, intent) -> { if (mPendingUserAdd) { return; } UserInfo guest = createNewOrFindExistingGuest(mContext); if (guest != null) { switchUser(guest.id); } }; return createProfileRow(mContext.getString(R.string.start_guest_session), mUserIconProvider.getRoundedGuestDefaultIcon(mContext.getResources()), actionHandler); } private QCRow createAddProfileRow() { QCItem.ActionHandler actionHandler = (item, context, intent) -> { if (mPendingUserAdd) { return; } if (!mUserManager.canAddMoreUsers()) { showMaxUserLimitReachedDialog(); } else { showConfirmAddUserDialog(); } }; return createProfileRow(mContext.getString(R.string.car_add_user), getCircularAddUserIcon(), actionHandler); } private QCRow createProfileRow(String title, Drawable iconDrawable, QCItem.ActionHandler actionHandler) { Icon icon = Icon.createWithBitmap(drawableToBitmap(iconDrawable)); QCRow row = new QCRow.Builder() .setIcon(icon) .setIconTintable(false) .setTitle(title) .build(); row.setActionHandler(actionHandler); return row; } private void switchUser(@UserIdInt int userId) { mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT); AsyncFuture userSwitchResultFuture = mCarUserManager.switchUser(userId); UserSwitchResult userSwitchResult; try { userSwitchResult = userSwitchResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (Exception e) { Log.w(TAG, "Could not switch user.", e); return; } if (userSwitchResult == null) { Log.w(TAG, "Timed out while switching user: " + TIMEOUT_MS + "ms"); } else if (!userSwitchResult.isSuccess()) { Log.w(TAG, "Could not switch user: " + userSwitchResult); } } /** * Finds the existing Guest user, or creates one if it doesn't exist. * * @param context App context * @return UserInfo representing the Guest user */ @Nullable private UserInfo createNewOrFindExistingGuest(Context context) { AsyncFuture future = mCarUserManager.createGuest( context.getString(R.string.car_guest)); // CreateGuest will return null if a guest already exists. UserInfo newGuest = getUserInfo(future); if (newGuest != null) { new UserIconProvider().assignDefaultIcon( mUserManager, context.getResources(), newGuest); return newGuest; } return mUserManager.findCurrentGuestUser(); } @Nullable private UserInfo getUserInfo(AsyncFuture future) { UserCreationResult userCreationResult; try { userCreationResult = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (Exception e) { Log.w(TAG, "Could not create user.", e); return null; } if (userCreationResult == null) { Log.w(TAG, "Timed out while creating user: " + TIMEOUT_MS + "ms"); return null; } if (!userCreationResult.isSuccess() || userCreationResult.getUser() == null) { Log.w(TAG, "Could not create user: " + userCreationResult); return null; } return userCreationResult.getUser(); } private RoundedBitmapDrawable getCircularAddUserIcon() { RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create( mContext.getResources(), UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.car_add_circle_round))); circleIcon.setCircular(true); return circleIcon; } private int getMaxSupportedRealUsers() { int maxSupportedUsers = UserManager.getMaxSupportedUsers(); if (UserManager.isHeadlessSystemUserMode()) { maxSupportedUsers -= 1; } List users = mUserManager.getAliveUsers(); // Count all users that are managed profiles of another user. int managedProfilesCount = 0; for (UserInfo user : users) { if (user.isManagedProfile()) { managedProfilesCount++; } } return maxSupportedUsers - managedProfilesCount; } private void showMaxUserLimitReachedDialog() { AlertDialog maxUsersDialog = new AlertDialog.Builder(mContext, com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) .setTitle(R.string.profile_limit_reached_title) .setMessage(mContext.getResources().getQuantityString( R.plurals.profile_limit_reached_message, getMaxSupportedRealUsers(), getMaxSupportedRealUsers())) .setPositiveButton(android.R.string.ok, null) .create(); // Sets window flags for the SysUI dialog applyCarSysUIDialogFlags(maxUsersDialog); maxUsersDialog.show(); } private void showConfirmAddUserDialog() { String message = mContext.getString(R.string.user_add_user_message_setup) .concat(System.getProperty("line.separator")) .concat(System.getProperty("line.separator")) .concat(mContext.getString(R.string.user_add_user_message_update)); AlertDialog addUserDialog = new AlertDialog.Builder(mContext, com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) .setTitle(R.string.user_add_profile_title) .setMessage(message) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, (dialog, which) -> new AddNewUserTask().execute( mContext.getString(R.string.car_new_user))) .create(); // Sets window flags for the SysUI dialog applyCarSysUIDialogFlags(addUserDialog); addUserDialog.show(); } private void applyCarSysUIDialogFlags(AlertDialog dialog) { Window window = dialog.getWindow(); window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); window.getAttributes().setFitInsetsTypes( window.getAttributes().getFitInsetsTypes() & ~statusBars()); } private class AddNewUserTask extends AsyncTask { @Override protected UserInfo doInBackground(String... userNames) { AsyncFuture future = mCarUserManager.createUser(userNames[0], /* flags= */ 0); try { UserInfo user = getUserInfo(future); if (user != null) { UserHelper.setDefaultNonAdminRestrictions(mContext, user, /* enable= */ true); UserHelper.assignDefaultIcon(mContext, user); return user; } else { Log.e(TAG, "Failed to create user in the background"); return user; } } catch (Exception e) { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } Log.e(TAG, "Error creating new user: ", e); } return null; } @Override protected void onPreExecute() { mPendingUserAdd = true; } @Override protected void onPostExecute(UserInfo user) { mPendingUserAdd = false; if (user != null) { switchUser(user.id); } } } }