| /* |
| * 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.provider.Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS; |
| 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.app.admin.DevicePolicyManager; |
| 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.NonNull; |
| 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 DevicePolicyManager mDevicePolicyManager; |
| 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); |
| mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); |
| mUserIconProvider = new UserIconProvider(); |
| mCar = Car.createCar(context); |
| mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE); |
| } |
| |
| @VisibleForTesting |
| ProfileSwitcher(Context context, UserManager userManager, |
| DevicePolicyManager devicePolicyManager, CarUserManager carUserManager) { |
| super(context); |
| mUserManager = userManager; |
| mDevicePolicyManager = devicePolicyManager; |
| mUserIconProvider = new UserIconProvider(); |
| mCar = null; |
| mCarUserManager = carUserManager; |
| } |
| |
| @Override |
| public QCItem getQCItem() { |
| QCList.Builder listBuilder = new QCList.Builder(); |
| |
| if (mDevicePolicyManager.isDeviceManaged() |
| || mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) { |
| listBuilder.addRow(createOrganizationOwnedDeviceRow()); |
| } |
| |
| 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<UserInfo> profiles = getProfileList(); |
| for (UserInfo profile : profiles) { |
| listBuilder.addRow(createUserProfileRow(profile)); |
| } |
| listBuilder.addRow(createGuestProfileRow()); |
| if (!hasAddUserRestriction(fgUserHandle)) { |
| listBuilder.addRow(createAddProfileRow()); |
| } |
| return listBuilder.build(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| if (mCar != null) { |
| mCar.disconnect(); |
| } |
| } |
| |
| private List<UserInfo> getProfileList() { |
| return mUserManager.getAliveUsers() |
| .stream() |
| .filter(userInfo -> userInfo.supportsSwitchToByUser() && !userInfo.isGuest()) |
| .collect(Collectors.toList()); |
| } |
| |
| private QCRow createOrganizationOwnedDeviceRow() { |
| Icon icon = Icon.createWithBitmap( |
| drawableToBitmap(mContext.getDrawable(R.drawable.car_ic_managed_device))); |
| QCRow row = new QCRow.Builder() |
| .setIcon(icon) |
| .setSubtitle(mContext.getString(R.string.do_disclosure_generic)) |
| .build(); |
| row.setActionHandler(new QCItem.ActionHandler() { |
| @Override |
| public void onAction(@NonNull QCItem item, @NonNull Context context, |
| @NonNull Intent intent) { |
| mContext.startActivityAsUser(new Intent(ACTION_ENTERPRISE_PRIVACY_SETTINGS), |
| UserHandle.CURRENT); |
| } |
| |
| @Override |
| public boolean isActivity() { |
| return true; |
| } |
| }); |
| return row; |
| } |
| |
| private QCRow createUserProfileRow(UserInfo userInfo) { |
| QCItem.ActionHandler actionHandler = (item, context, intent) -> { |
| if (mPendingUserAdd) { |
| return; |
| } |
| switchUser(userInfo.id); |
| }; |
| |
| return createProfileRow(userInfo.name, |
| mUserIconProvider.getDrawableWithBadge(mContext, userInfo), 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), |
| mUserIconProvider.getDrawableWithBadge(mContext, 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<UserSwitchResult> 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<UserCreationResult> 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<UserCreationResult> 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 boolean hasAddUserRestriction(UserHandle userHandle) { |
| return mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_ADD_USER, userHandle); |
| } |
| |
| private int getMaxSupportedRealUsers() { |
| int maxSupportedUsers = UserManager.getMaxSupportedUsers(); |
| if (UserManager.isHeadlessSystemUserMode()) { |
| maxSupportedUsers -= 1; |
| } |
| List<UserInfo> 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<String, Void, UserInfo> { |
| @Override |
| protected UserInfo doInBackground(String... userNames) { |
| AsyncFuture<UserCreationResult> 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); |
| } |
| } |
| } |
| } |