1 /* 2 * Copyright (C) 2023 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 17 package com.android.settings.privatespace; 18 19 import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; 20 import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT; 21 import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK; 22 import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_DEVICE_RESTART; 23 import static android.provider.Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK; 24 import static android.provider.Settings.Secure.SKIP_FIRST_USE_HINTS; 25 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; 26 27 import android.app.ActivityManager; 28 import android.app.KeyguardManager; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.IntentSender; 34 import android.content.pm.PackageManager; 35 import android.content.pm.UserInfo; 36 import android.os.Flags; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.provider.Settings; 40 import android.util.ArraySet; 41 import android.util.Log; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.annotation.VisibleForTesting; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.settings.Utils; 49 50 import java.util.List; 51 52 // TODO(b/293569406): Update the javadoc when we have the setup flow in place to create PS 53 54 /** A class to help with the creation / deletion of Private Space */ 55 public class PrivateSpaceMaintainer { 56 private static final String TAG = "PrivateSpaceMaintainer"; 57 @GuardedBy("this") 58 private static PrivateSpaceMaintainer sPrivateSpaceMaintainer; 59 60 private final Context mContext; 61 private final UserManager mUserManager; 62 private final ActivityManager mActivityManager; 63 @GuardedBy("this") 64 private UserHandle mUserHandle; 65 private final KeyguardManager mKeyguardManager; 66 /** This variable should be accessed via {@link #getProfileBroadcastReceiver()} only. */ 67 @Nullable 68 private ProfileBroadcastReceiver mProfileBroadcastReceiver; 69 70 /** This is the default value for the hide private space entry point settings. */ 71 public static final int HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL = 0; 72 public static final int HIDE_PRIVATE_SPACE_ENTRY_POINT_ENABLED_VAL = 1; 73 /** Default value for private space auto lock settings. */ 74 @Settings.Secure.PrivateSpaceAutoLockOption 75 public static final int PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL = 76 PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK; 77 /** Value for private space auto lock settings after private space creation. */ 78 @Settings.Secure.PrivateSpaceAutoLockOption 79 public static final int PRIVATE_SPACE_CREATE_AUTO_LOCK_VAL = 80 PRIVATE_SPACE_AUTO_LOCK_AFTER_DEVICE_RESTART; 81 /** Default value for the hide private space sensitive notifications on lockscreen. */ 82 public static final int HIDE_PRIVATE_SPACE_SENSITIVE_NOTIFICATIONS_DISABLED_VAL = 0; 83 84 public enum ErrorDeletingPrivateSpace { 85 DELETE_PS_ERROR_NONE, 86 DELETE_PS_ERROR_NO_PRIVATE_SPACE, 87 DELETE_PS_ERROR_INTERNAL 88 } 89 90 /** 91 * Returns true if the private space was successfully created. 92 * 93 * <p> This method should be used by the Private Space Setup Flow ONLY. 94 */ 95 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) createPrivateSpace()96 public final synchronized boolean createPrivateSpace() { 97 if (!Flags.allowPrivateProfile() 98 || !android.multiuser.Flags.enablePrivateSpaceFeatures()) { 99 return false; 100 } 101 // Check if Private space already exists 102 if (doesPrivateSpaceExist()) { 103 return true; 104 } 105 // a name indicating that the profile was created from the PS Settings page 106 final String userName = "Private space"; 107 108 if (mUserHandle == null) { 109 try { 110 mUserHandle = mUserManager.createProfile( 111 userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>()); 112 } catch (Exception e) { 113 Log.e(TAG, "Error creating private space", e); 114 return false; 115 } 116 117 if (mUserHandle == null) { 118 Log.e(TAG, "Failed to create private space"); 119 return false; 120 } 121 122 registerBroadcastReceiver(); 123 124 if (!startProfile()) { 125 // TODO(b/333884792): Add test to mock when startProfile fails. 126 Log.e(TAG, "profile not started, created profile is deleted"); 127 deletePrivateSpace(); 128 return false; 129 } 130 131 Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier()); 132 resetPrivateSpaceSettings(); 133 setUserSetupComplete(); 134 setSkipFirstUseHints(); 135 disableComponentsToHidePrivateSpaceSettings(); 136 } 137 return true; 138 } 139 140 /** 141 * Returns the {@link ErrorDeletingPrivateSpace} enum representing the result of operation. 142 * 143 * <p> This method should be used ONLY by the delete-PS controller in the PS Settings page. 144 */ deletePrivateSpace()145 public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() { 146 if (!doesPrivateSpaceExist()) { 147 return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE; 148 } 149 150 try { 151 Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier()); 152 if (mUserManager.removeUser(mUserHandle)) { 153 Log.i(TAG, "Private space deleted"); 154 mUserHandle = null; 155 156 return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE; 157 } else { 158 Log.e(TAG, "Failed to delete private space"); 159 } 160 } catch (Exception e) { 161 Log.e(TAG, "Error deleting private space", e); 162 } 163 return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL; 164 } 165 166 /** Returns true if the Private space exists. */ doesPrivateSpaceExist()167 public synchronized boolean doesPrivateSpaceExist() { 168 if (!Flags.allowPrivateProfile() 169 || !android.multiuser.Flags.enablePrivateSpaceFeatures()) { 170 return false; 171 } 172 if (mUserHandle != null) { 173 return true; 174 } 175 176 List<UserInfo> users = mUserManager.getProfiles(mContext.getUserId()); 177 for (UserInfo user : users) { 178 if (user.isPrivateProfile()) { 179 mUserHandle = user.getUserHandle(); 180 registerBroadcastReceiver(); 181 return true; 182 } 183 } 184 return false; 185 } 186 187 /** Returns true when the PS is locked or when PS doesn't exist, false otherwise. */ isPrivateSpaceLocked()188 public synchronized boolean isPrivateSpaceLocked() { 189 if (!doesPrivateSpaceExist()) { 190 return true; 191 } 192 193 return mUserManager.isQuietModeEnabled(mUserHandle); 194 } 195 196 /** 197 * Returns an intent to prompt the user to confirm private profile credentials if it is set 198 * otherwise returns intent to confirm device credentials. 199 */ 200 @Nullable getPrivateProfileLockCredentialIntent()201 public synchronized Intent getPrivateProfileLockCredentialIntent() { 202 //TODO(b/307281644): To replace with check for doesPrivateSpaceExist() method once Auth 203 // changes are merged. 204 if (isPrivateProfileLockSet()) { 205 return mKeyguardManager.createConfirmDeviceCredentialIntent( 206 /* title= */ null, /* description= */null, mUserHandle.getIdentifier()); 207 } 208 // TODO(b/304796434) Need to try changing this intent to use BiometricPrompt 209 return mKeyguardManager.createConfirmDeviceCredentialIntent( 210 /* title= */ null, /* description= */ null); 211 } 212 213 /** Returns Private profile user handle if private profile exists otherwise returns null. */ 214 @Nullable getPrivateProfileHandle()215 public synchronized UserHandle getPrivateProfileHandle() { 216 if (doesPrivateSpaceExist()) { 217 return mUserHandle; 218 } 219 return null; 220 } 221 222 /** Returns the instance of {@link PrivateSpaceMaintainer} */ getInstance(Context context)223 public static synchronized PrivateSpaceMaintainer getInstance(Context context) { 224 if (sPrivateSpaceMaintainer == null) { 225 sPrivateSpaceMaintainer = new PrivateSpaceMaintainer(context); 226 } 227 return sPrivateSpaceMaintainer; 228 } 229 PrivateSpaceMaintainer(Context context)230 private PrivateSpaceMaintainer(Context context) { 231 mContext = context.getApplicationContext(); 232 mUserManager = mContext.getSystemService(UserManager.class); 233 mKeyguardManager = mContext.getSystemService(KeyguardManager.class); 234 mActivityManager = mContext.getSystemService(ActivityManager.class); 235 } 236 237 238 // TODO(b/307281644): Remove this method once new auth change is merged 239 240 /** 241 * Returns true if private space exists and a separate private profile lock is set 242 * otherwise false when the private space does not exit or exists but does not have a 243 * separate profile lock. 244 */ 245 @GuardedBy("this") isPrivateProfileLockSet()246 private boolean isPrivateProfileLockSet() { 247 return doesPrivateSpaceExist() 248 && mKeyguardManager.isDeviceSecure(mUserHandle.getIdentifier()); 249 } 250 251 /** Sets the setting to show PS entry point to the provided value. */ setHidePrivateSpaceEntryPointSetting(int value)252 public void setHidePrivateSpaceEntryPointSetting(int value) { 253 Log.d(TAG, "Setting HIDE_PRIVATE_SPACE_ENTRY_POINT = " + value); 254 Settings.Secure.putInt(mContext.getContentResolver(), HIDE_PRIVATESPACE_ENTRY_POINT, value); 255 } 256 257 /** Sets the setting for private space auto lock option. */ setPrivateSpaceAutoLockSetting( @ettings.Secure.PrivateSpaceAutoLockOption int value)258 public void setPrivateSpaceAutoLockSetting( 259 @Settings.Secure.PrivateSpaceAutoLockOption int value) { 260 if (isPrivateSpaceAutoLockSupported()) { 261 Settings.Secure.putInt(mContext.getContentResolver(), PRIVATE_SPACE_AUTO_LOCK, value); 262 } 263 } 264 265 /** @return the setting to show PS entry point. */ getHidePrivateSpaceEntryPointSetting()266 public int getHidePrivateSpaceEntryPointSetting() { 267 return Settings.Secure.getInt( 268 mContext.getContentResolver(), 269 HIDE_PRIVATESPACE_ENTRY_POINT, 270 HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL); 271 } 272 273 /** @return the setting for PS auto lock option. */ 274 @Settings.Secure.PrivateSpaceAutoLockOption getPrivateSpaceAutoLockSetting()275 public int getPrivateSpaceAutoLockSetting() { 276 if (isPrivateSpaceAutoLockSupported()) { 277 return Settings.Secure.getInt( 278 mContext.getContentResolver(), 279 PRIVATE_SPACE_AUTO_LOCK, 280 PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL); 281 } 282 return PRIVATE_SPACE_AUTO_LOCK_DEFAULT_VAL; 283 } 284 285 /** 286 * Returns true if private space exists and quiet mode is successfully enabled, otherwise 287 * returns false 288 */ lockPrivateSpace()289 public synchronized boolean lockPrivateSpace() { 290 if (isPrivateProfileRunning()) { 291 Log.d(TAG, "Calling requestQuietModeEnabled to enableQuietMode"); 292 return mUserManager.requestQuietModeEnabled(true, mUserHandle); 293 } 294 return false; 295 } 296 297 /** 298 * Checks if private space exists and requests to disable quiet mode. 299 * 300 * @param intentSender target to start when the user is unlocked 301 */ unlockPrivateSpace(IntentSender intentSender)302 public synchronized void unlockPrivateSpace(IntentSender intentSender) { 303 if (mUserHandle != null) { 304 mUserManager.requestQuietModeEnabled(false, mUserHandle, intentSender); 305 } 306 } 307 308 /** 309 * Returns true if private profile can be added to the device or if private space already 310 * exists, false otherwise. 311 */ isPrivateSpaceEntryPointEnabled()312 public boolean isPrivateSpaceEntryPointEnabled() { 313 return mUserManager.canAddPrivateProfile() || doesPrivateSpaceExist(); 314 } 315 316 /** Returns true if private space exists and is running, otherwise returns false */ 317 @VisibleForTesting isPrivateProfileRunning()318 synchronized boolean isPrivateProfileRunning() { 319 if (doesPrivateSpaceExist() && mUserHandle != null) { 320 return mUserManager.isUserRunning(mUserHandle); 321 } 322 return false; 323 } 324 325 @GuardedBy("this") startProfile()326 private boolean startProfile() { 327 try { 328 return mActivityManager.startProfile(mUserHandle); 329 } catch (IllegalArgumentException e) { 330 Log.e(TAG, "Unexpected that " + mUserHandle.getIdentifier() + " is not a profile"); 331 } 332 return false; 333 } 334 335 @GuardedBy("this") resetPrivateSpaceSettings()336 private void resetPrivateSpaceSettings() { 337 setHidePrivateSpaceEntryPointSetting(HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL); 338 setPrivateSpaceAutoLockSetting(PRIVATE_SPACE_CREATE_AUTO_LOCK_VAL); 339 setPrivateSpaceSensitiveNotificationsDefaultValue(); 340 } 341 342 /** Sets private space sensitive notifications hidden on lockscreen by default */ 343 @GuardedBy("this") setPrivateSpaceSensitiveNotificationsDefaultValue()344 private void setPrivateSpaceSensitiveNotificationsDefaultValue() { 345 Settings.Secure.putIntForUser(mContext.getContentResolver(), 346 Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 347 HIDE_PRIVATE_SPACE_SENSITIVE_NOTIFICATIONS_DISABLED_VAL, 348 mUserHandle.getIdentifier()); 349 } 350 351 /** 352 * Sets the USER_SETUP_COMPLETE for private profile on which device theme is applied to the 353 * profile. 354 */ 355 @GuardedBy("this") setUserSetupComplete()356 private void setUserSetupComplete() { 357 Log.d(TAG, "setting USER_SETUP_COMPLETE = 1 for private profile"); 358 Settings.Secure.putIntForUser(mContext.getContentResolver(), USER_SETUP_COMPLETE, 359 1, mUserHandle.getIdentifier()); 360 } 361 362 /** 363 * Disables the launcher icon and shortcut picker component for the Settings app instance 364 * inside the private space 365 */ 366 @GuardedBy("this") disableComponentsToHidePrivateSpaceSettings()367 private void disableComponentsToHidePrivateSpaceSettings() { 368 if (mUserHandle == null) { 369 Log.e(TAG, "User handle null while hiding settings icon"); 370 return; 371 } 372 373 Context privateSpaceUserContext = mContext.createContextAsUser(mUserHandle, /* flags */ 0); 374 PackageManager packageManager = privateSpaceUserContext.getPackageManager(); 375 376 Log.d(TAG, "Hiding settings app launcher icon for " + mUserHandle); 377 Utils.disableComponentsToHideSettings(privateSpaceUserContext, packageManager); 378 } 379 380 /** 381 * Sets the SKIP_FIRST_USE_HINTS for private profile so that the first launch of an app in 382 * private space will not display introductory hints. 383 */ 384 @GuardedBy("this") setSkipFirstUseHints()385 private void setSkipFirstUseHints() { 386 Log.d(TAG, "setting SKIP_FIRST_USE_HINTS = 1 for private profile"); 387 Settings.Secure.putIntForUser(mContext.getContentResolver(), SKIP_FIRST_USE_HINTS, 388 1, mUserHandle.getIdentifier()); 389 } 390 isPrivateSpaceAutoLockSupported()391 private boolean isPrivateSpaceAutoLockSupported() { 392 return android.os.Flags.allowPrivateProfile() 393 && android.multiuser.Flags.supportAutolockForPrivateSpace() 394 && android.multiuser.Flags.enablePrivateSpaceFeatures(); 395 } 396 397 /** 398 * {@link BroadcastReceiver} which handles the private profile's availability and deletion 399 * related broadcasts. 400 */ 401 private final class ProfileBroadcastReceiver extends BroadcastReceiver { register()402 void register() { 403 IntentFilter filter = new IntentFilter(); 404 filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE); 405 filter.addAction(Intent.ACTION_PROFILE_REMOVED); 406 mContext.registerReceiver(/* receiver= */ this, filter, Context.RECEIVER_NOT_EXPORTED); 407 } 408 unregister()409 void unregister() { 410 Log.d(TAG, "Unregistering the receiver"); 411 mContext.unregisterReceiver(/* receiver= */ this); 412 } 413 414 @Override onReceive(@onNull Context context, @NonNull Intent intent)415 public void onReceive(@NonNull Context context, @NonNull Intent intent) { 416 UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class); 417 if (intent.getAction().equals(Intent.ACTION_PROFILE_REMOVED)) { 418 // This applies to all profiles getting removed, since there is no way to tell if 419 // it is a private profile that got removed. 420 removeSettingsAllTasks(); 421 unregisterBroadcastReceiver(); 422 return; 423 } 424 if (!userHandle.equals(getPrivateProfileHandle())) { 425 Log.d(TAG, "Ignoring intent for non-private profile with user id " 426 + userHandle.getIdentifier()); 427 return; 428 } 429 430 Log.i(TAG, "Removing all Settings tasks."); 431 removeSettingsAllTasks(); 432 } 433 } 434 registerBroadcastReceiver()435 private synchronized void registerBroadcastReceiver() { 436 if (!android.os.Flags.allowPrivateProfile() 437 || !android.multiuser.Flags.enablePrivateSpaceFeatures()) { 438 return; 439 } 440 var broadcastReceiver = getProfileBroadcastReceiver(); 441 if (broadcastReceiver == null) { 442 return; 443 } 444 broadcastReceiver.register(); 445 } 446 unregisterBroadcastReceiver()447 private synchronized void unregisterBroadcastReceiver() { 448 if (!android.os.Flags.allowPrivateProfile() 449 || !android.multiuser.Flags.enablePrivateSpaceFeatures()) { 450 return; 451 } 452 if (mProfileBroadcastReceiver == null) { 453 Log.w(TAG, "Requested to unregister when there is no receiver."); 454 return; 455 } 456 mProfileBroadcastReceiver.unregister(); 457 mProfileBroadcastReceiver = null; 458 } 459 460 /** Always use this getter to access {@link #mProfileBroadcastReceiver}. */ 461 @VisibleForTesting 462 @Nullable getProfileBroadcastReceiver()463 synchronized ProfileBroadcastReceiver getProfileBroadcastReceiver() { 464 if (!android.os.Flags.allowPrivateProfile() 465 || !android.multiuser.Flags.enablePrivateSpaceFeatures()) { 466 return null; 467 } 468 if (!doesPrivateSpaceExist()) { 469 Log.e(TAG, "Cannot return a broadcast receiver when private space doesn't exist"); 470 return null; 471 } 472 if (mProfileBroadcastReceiver == null) { 473 mProfileBroadcastReceiver = new ProfileBroadcastReceiver(); 474 } 475 return mProfileBroadcastReceiver; 476 } 477 478 /** This is purely for testing purpose only, and should not be used elsewhere. */ 479 @VisibleForTesting resetBroadcastReceiver()480 synchronized void resetBroadcastReceiver() { 481 mProfileBroadcastReceiver = null; 482 } 483 removeSettingsAllTasks()484 private void removeSettingsAllTasks() { 485 List<ActivityManager.AppTask> appTasks = mActivityManager.getAppTasks(); 486 for (var appTask : appTasks) { 487 if (!(appTask.getTaskInfo().isVisible() || appTask.getTaskInfo().isFocused)) { 488 appTask.finishAndRemoveTask(); 489 } 490 } 491 } 492 } 493