blob: 8f0a85fbb656cfc63d72173e1f16b9cc427d2648 [file] [log] [blame]
/*
* 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);
}
}
}
}