/*
* Copyright (C) 2023 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.settings.privatespace;
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT;
import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK;
import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_DEVICE_RESTART;
import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK;
import static android.provider.Settings.Secure.SKIP_FIRST_USE_HINTS;
import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Flags;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.annotations.GuardedBy;
import com.android.settings.Utils;
import java.util.List;
// TODO(b/293569406): Update the javadoc when we have the setup flow in place to create PS
/** A class to help with the creation / deletion of Private Space */
public class PrivateSpaceMaintainer {
private static final String TAG = "PrivateSpaceMaintainer";
@GuardedBy("this")
private static PrivateSpaceMaintainer sPrivateSpaceMaintainer;
private final Context mContext;
private final UserManager mUserManager;
private final ActivityManager mActivityManager;
@GuardedBy("this")
private UserHandle mUserHandle;
private final KeyguardManager mKeyguardManager;
/** This variable should be accessed via {@link #getProfileBroadcastReceiver()} only. */
@Nullable
private ProfileBroadcastReceiver mProfileBroadcastReceiver;
/** This is the default value for the hide private space entry point settings. */
public static final int HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL = 0;
public static final int HIDE_PRIVATE_SPACE_ENTRY_POINT_ENABLED_VAL = 1;
/** Default value for private space auto lock settings. */
@Settings.Secure.PrivateSpaceAutoLockOption
public static final int PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL =
PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK;
/** Value for private space auto lock settings after private space creation. */
@Settings.Secure.PrivateSpaceAutoLockOption
public static final int PRIVATE_SPACE_CREATE_AUTO_LOCK_VAL =
PRIVATE_SPACE_AUTO_LOCK_AFTER_DEVICE_RESTART;
/** Default value for the hide private space sensitive notifications on lockscreen. */
public static final int HIDE_PRIVATE_SPACE_SENSITIVE_NOTIFICATIONS_DISABLED_VAL = 0;
public enum ErrorDeletingPrivateSpace {
DELETE_PS_ERROR_NONE,
DELETE_PS_ERROR_NO_PRIVATE_SPACE,
DELETE_PS_ERROR_INTERNAL
}
/**
* Returns true if the private space was successfully created.
*
*
This method should be used by the Private Space Setup Flow ONLY.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final synchronized boolean createPrivateSpace() {
if (!Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return false;
}
// Check if Private space already exists
if (doesPrivateSpaceExist()) {
return true;
}
// a name indicating that the profile was created from the PS Settings page
final String userName = "Private space";
if (mUserHandle == null) {
try {
mUserHandle = mUserManager.createProfile(
userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>());
} catch (Exception e) {
Log.e(TAG, "Error creating private space", e);
return false;
}
if (mUserHandle == null) {
Log.e(TAG, "Failed to create private space");
return false;
}
registerBroadcastReceiver();
if (!startProfile()) {
// TODO(b/333884792): Add test to mock when startProfile fails.
Log.e(TAG, "profile not started, created profile is deleted");
deletePrivateSpace();
return false;
}
Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
resetPrivateSpaceSettings();
setUserSetupComplete();
setSkipFirstUseHints();
disableComponentsToHidePrivateSpaceSettings();
}
return true;
}
/**
* Returns the {@link ErrorDeletingPrivateSpace} enum representing the result of operation.
*
*
This method should be used ONLY by the delete-PS controller in the PS Settings page.
*/
public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() {
if (!doesPrivateSpaceExist()) {
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE;
}
try {
Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier());
if (mUserManager.removeUser(mUserHandle)) {
Log.i(TAG, "Private space deleted");
mUserHandle = null;
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
} else {
Log.e(TAG, "Failed to delete private space");
}
} catch (Exception e) {
Log.e(TAG, "Error deleting private space", e);
}
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
}
/** Returns true if the Private space exists. */
public synchronized boolean doesPrivateSpaceExist() {
if (!Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return false;
}
if (mUserHandle != null) {
return true;
}
List users = mUserManager.getProfiles(mContext.getUserId());
for (UserInfo user : users) {
if (user.isPrivateProfile()) {
mUserHandle = user.getUserHandle();
registerBroadcastReceiver();
return true;
}
}
return false;
}
/** Returns true when the PS is locked or when PS doesn't exist, false otherwise. */
public synchronized boolean isPrivateSpaceLocked() {
if (!doesPrivateSpaceExist()) {
return true;
}
return mUserManager.isQuietModeEnabled(mUserHandle);
}
/**
* Returns an intent to prompt the user to confirm private profile credentials if it is set
* otherwise returns intent to confirm device credentials.
*/
@Nullable
public synchronized Intent getPrivateProfileLockCredentialIntent() {
//TODO(b/307281644): To replace with check for doesPrivateSpaceExist() method once Auth
// changes are merged.
if (isPrivateProfileLockSet()) {
return mKeyguardManager.createConfirmDeviceCredentialIntent(
/* title= */ null, /* description= */null, mUserHandle.getIdentifier());
}
// TODO(b/304796434) Need to try changing this intent to use BiometricPrompt
return mKeyguardManager.createConfirmDeviceCredentialIntent(
/* title= */ null, /* description= */ null);
}
/** Returns Private profile user handle if private profile exists otherwise returns null. */
@Nullable
public synchronized UserHandle getPrivateProfileHandle() {
if (doesPrivateSpaceExist()) {
return mUserHandle;
}
return null;
}
/** Returns the instance of {@link PrivateSpaceMaintainer} */
public static synchronized PrivateSpaceMaintainer getInstance(Context context) {
if (sPrivateSpaceMaintainer == null) {
sPrivateSpaceMaintainer = new PrivateSpaceMaintainer(context);
}
return sPrivateSpaceMaintainer;
}
private PrivateSpaceMaintainer(Context context) {
mContext = context.getApplicationContext();
mUserManager = mContext.getSystemService(UserManager.class);
mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
mActivityManager = mContext.getSystemService(ActivityManager.class);
}
// TODO(b/307281644): Remove this method once new auth change is merged
/**
* Returns true if private space exists and a separate private profile lock is set
* otherwise false when the private space does not exit or exists but does not have a
* separate profile lock.
*/
@GuardedBy("this")
private boolean isPrivateProfileLockSet() {
return doesPrivateSpaceExist()
&& mKeyguardManager.isDeviceSecure(mUserHandle.getIdentifier());
}
/** Sets the setting to show PS entry point to the provided value. */
public void setHidePrivateSpaceEntryPointSetting(int value) {
Log.d(TAG, "Setting HIDE_PRIVATE_SPACE_ENTRY_POINT = " + value);
Settings.Secure.putInt(mContext.getContentResolver(), HIDE_PRIVATESPACE_ENTRY_POINT, value);
}
/** Sets the setting for private space auto lock option. */
public void setPrivateSpaceAutoLockSetting(
@Settings.Secure.PrivateSpaceAutoLockOption int value) {
if (isPrivateSpaceAutoLockSupported()) {
Settings.Secure.putInt(mContext.getContentResolver(), PRIVATE_SPACE_AUTO_LOCK, value);
}
}
/** @return the setting to show PS entry point. */
public int getHidePrivateSpaceEntryPointSetting() {
return Settings.Secure.getInt(
mContext.getContentResolver(),
HIDE_PRIVATESPACE_ENTRY_POINT,
HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
}
/** @return the setting for PS auto lock option. */
@Settings.Secure.PrivateSpaceAutoLockOption
public int getPrivateSpaceAutoLockSetting() {
if (isPrivateSpaceAutoLockSupported()) {
return Settings.Secure.getInt(
mContext.getContentResolver(),
PRIVATE_SPACE_AUTO_LOCK,
PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL);
}
return PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL;
}
/**
* Returns true if private space exists and quiet mode is successfully enabled, otherwise
* returns false
*/
public synchronized boolean lockPrivateSpace() {
if (isPrivateProfileRunning()) {
Log.d(TAG, "Calling requestQuietModeEnabled to enableQuietMode");
return mUserManager.requestQuietModeEnabled(true, mUserHandle);
}
return false;
}
/**
* Checks if private space exists and requests to disable quiet mode.
*
* @param intentSender target to start when the user is unlocked
*/
public synchronized void unlockPrivateSpace(IntentSender intentSender) {
if (mUserHandle != null) {
mUserManager.requestQuietModeEnabled(false, mUserHandle, intentSender);
}
}
/**
* Returns true if private profile can be added to the device or if private space already
* exists, false otherwise.
*/
public boolean isPrivateSpaceEntryPointEnabled() {
return mUserManager.canAddPrivateProfile() || doesPrivateSpaceExist();
}
/** Returns true if private space exists and is running, otherwise returns false */
@VisibleForTesting
synchronized boolean isPrivateProfileRunning() {
if (doesPrivateSpaceExist() && mUserHandle != null) {
return mUserManager.isUserRunning(mUserHandle);
}
return false;
}
@GuardedBy("this")
private boolean startProfile() {
try {
return mActivityManager.startProfile(mUserHandle);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unexpected that " + mUserHandle.getIdentifier() + " is not a profile");
}
return false;
}
@GuardedBy("this")
private void resetPrivateSpaceSettings() {
setHidePrivateSpaceEntryPointSetting(HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
setPrivateSpaceAutoLockSetting(PRIVATE_SPACE_CREATE_AUTO_LOCK_VAL);
setPrivateSpaceSensitiveNotificationsDefaultValue();
}
/** Sets private space sensitive notifications hidden on lockscreen by default */
@GuardedBy("this")
private void setPrivateSpaceSensitiveNotificationsDefaultValue() {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS,
HIDE_PRIVATE_SPACE_SENSITIVE_NOTIFICATIONS_DISABLED_VAL,
mUserHandle.getIdentifier());
}
/**
* Sets the USER_SETUP_COMPLETE for private profile on which device theme is applied to the
* profile.
*/
@GuardedBy("this")
private void setUserSetupComplete() {
Log.d(TAG, "setting USER_SETUP_COMPLETE = 1 for private profile");
Settings.Secure.putIntForUser(mContext.getContentResolver(), USER_SETUP_COMPLETE,
1, mUserHandle.getIdentifier());
}
/**
* Disables the launcher icon and shortcut picker component for the Settings app instance
* inside the private space
*/
@GuardedBy("this")
private void disableComponentsToHidePrivateSpaceSettings() {
if (mUserHandle == null) {
Log.e(TAG, "User handle null while hiding settings icon");
return;
}
Context privateSpaceUserContext = mContext.createContextAsUser(mUserHandle, /* flags */ 0);
PackageManager packageManager = privateSpaceUserContext.getPackageManager();
Log.d(TAG, "Hiding settings app launcher icon for " + mUserHandle);
Utils.disableComponentsToHideSettings(privateSpaceUserContext, packageManager);
}
/**
* Sets the SKIP_FIRST_USE_HINTS for private profile so that the first launch of an app in
* private space will not display introductory hints.
*/
@GuardedBy("this")
private void setSkipFirstUseHints() {
Log.d(TAG, "setting SKIP_FIRST_USE_HINTS = 1 for private profile");
Settings.Secure.putIntForUser(mContext.getContentResolver(), SKIP_FIRST_USE_HINTS,
1, mUserHandle.getIdentifier());
}
private boolean isPrivateSpaceAutoLockSupported() {
return android.os.Flags.allowPrivateProfile()
&& android.multiuser.Flags.supportAutolockForPrivateSpace()
&& android.multiuser.Flags.enablePrivateSpaceFeatures();
}
/**
* {@link BroadcastReceiver} which handles the private profile's availability and deletion
* related broadcasts.
*/
private final class ProfileBroadcastReceiver extends BroadcastReceiver {
void register() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE);
filter.addAction(Intent.ACTION_PROFILE_REMOVED);
mContext.registerReceiver(/* receiver= */ this, filter, Context.RECEIVER_NOT_EXPORTED);
}
void unregister() {
Log.d(TAG, "Unregistering the receiver");
mContext.unregisterReceiver(/* receiver= */ this);
}
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
if (intent.getAction().equals(Intent.ACTION_PROFILE_REMOVED)) {
// This applies to all profiles getting removed, since there is no way to tell if
// it is a private profile that got removed.
removeSettingsAllTasks();
unregisterBroadcastReceiver();
return;
}
if (!userHandle.equals(getPrivateProfileHandle())) {
Log.d(TAG, "Ignoring intent for non-private profile with user id "
+ userHandle.getIdentifier());
return;
}
Log.i(TAG, "Removing all Settings tasks.");
removeSettingsAllTasks();
}
}
private synchronized void registerBroadcastReceiver() {
if (!android.os.Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return;
}
var broadcastReceiver = getProfileBroadcastReceiver();
if (broadcastReceiver == null) {
return;
}
broadcastReceiver.register();
}
private synchronized void unregisterBroadcastReceiver() {
if (!android.os.Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return;
}
if (mProfileBroadcastReceiver == null) {
Log.w(TAG, "Requested to unregister when there is no receiver.");
return;
}
mProfileBroadcastReceiver.unregister();
mProfileBroadcastReceiver = null;
}
/** Always use this getter to access {@link #mProfileBroadcastReceiver}. */
@VisibleForTesting
@Nullable
synchronized ProfileBroadcastReceiver getProfileBroadcastReceiver() {
if (!android.os.Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return null;
}
if (!doesPrivateSpaceExist()) {
Log.e(TAG, "Cannot return a broadcast receiver when private space doesn't exist");
return null;
}
if (mProfileBroadcastReceiver == null) {
mProfileBroadcastReceiver = new ProfileBroadcastReceiver();
}
return mProfileBroadcastReceiver;
}
/** This is purely for testing purpose only, and should not be used elsewhere. */
@VisibleForTesting
synchronized void resetBroadcastReceiver() {
mProfileBroadcastReceiver = null;
}
private void removeSettingsAllTasks() {
List appTasks = mActivityManager.getAppTasks();
for (var appTask : appTasks) {
if (!(appTask.getTaskInfo().isVisible() || appTask.getTaskInfo().isFocused)) {
appTask.finishAndRemoveTask();
}
}
}
}