Loading core/api/current.txt +2 −0 Original line number Original line Diff line number Diff line Loading @@ -12385,6 +12385,8 @@ package android.content.pm { method @NonNull public android.graphics.drawable.Drawable getProfileSwitchingIconDrawable(@NonNull android.os.UserHandle); method @NonNull public android.graphics.drawable.Drawable getProfileSwitchingIconDrawable(@NonNull android.os.UserHandle); method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle); method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle); method @NonNull public java.util.List<android.os.UserHandle> getTargetUserProfiles(); method @NonNull public java.util.List<android.os.UserHandle> getTargetUserProfiles(); method @FlaggedApi("android.app.admin.flags.allow_querying_profile_type") public boolean isManagedProfile(@NonNull android.os.UserHandle); method @FlaggedApi("android.app.admin.flags.allow_querying_profile_type") public boolean isProfile(@NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity, @Nullable android.os.Bundle); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity, @Nullable android.os.Bundle); method public void startMainActivity(@NonNull android.content.ComponentName, @NonNull android.os.UserHandle); method public void startMainActivity(@NonNull android.content.ComponentName, @NonNull android.os.UserHandle); core/java/android/app/admin/flags/flags.aconfig +7 −0 Original line number Original line Diff line number Diff line Loading @@ -91,6 +91,13 @@ flag { bug: "304999634" bug: "304999634" } } flag { name: "allow_querying_profile_type" namespace: "enterprise" description: "Public APIs to query if a user is a profile and what kind of profile type it is." bug: "323001115" } flag { flag { name: "quiet_mode_credential_bug_fix" name: "quiet_mode_credential_bug_fix" namespace: "enterprise" namespace: "enterprise" Loading core/java/android/content/pm/CrossProfileApps.java +42 −0 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,9 @@ import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_PERSONAL_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_PERSONAL_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_WORK_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_WORK_LABEL; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.app.admin.flags.Flags.FLAG_ALLOW_QUERYING_PROFILE_TYPE; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.RequiresPermission; Loading Loading @@ -314,6 +316,41 @@ public class CrossProfileApps { } } } } /** * Checks if the specified user is a profile, i.e. not the parent user. * * @param userHandle The UserHandle of the target profile, must be one of the users returned by * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will * be thrown. * @return whether the specified user is a profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) public boolean isProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. verifyCanAccessUser(userHandle); return mUserManager.isProfile(userHandle.getIdentifier()); } /** * Checks if the specified user is a managed profile. * * @param userHandle The UserHandle of the target profile, must be one of the users returned by * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will * be thrown. * @return whether the specified user is a managed profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) public boolean isManagedProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. verifyCanAccessUser(userHandle); return mUserManager.isManagedProfile(userHandle.getIdentifier()); } /** /** * Return a label that calling app can show to user for the semantic of profile switching -- * Return a label that calling app can show to user for the semantic of profile switching -- * launching its own activity in specified user profile. For example, it may return * launching its own activity in specified user profile. For example, it may return Loading Loading @@ -677,6 +714,11 @@ public class CrossProfileApps { } } } } /** * A validation method to check that the methods in this class are only being applied to user * handles returned by {@link #getTargetUserProfiles()}. As this is run client-side for * input validation purposes, this should never replace a real security check service-side. */ private void verifyCanAccessUser(UserHandle userHandle) { private void verifyCanAccessUser(UserHandle userHandle) { if (!getTargetUserProfiles().contains(userHandle)) { if (!getTargetUserProfiles().contains(userHandle)) { throw new SecurityException("Not allowed to access " + userHandle); throw new SecurityException("Not allowed to access " + userHandle); Loading core/java/android/os/UserManager.java +5 −1 Original line number Original line Diff line number Diff line Loading @@ -3258,7 +3258,11 @@ public class UserManager { return isProfile(mUserId); return isProfile(mUserId); } } private boolean isProfile(@UserIdInt int userId) { /** * Returns whether the specified user is a profile. * @hide */ public boolean isProfile(@UserIdInt int userId) { final String profileType = getProfileType(userId); final String profileType = getProfileType(userId); return profileType != null && !profileType.equals(""); return profileType != null && !profileType.equals(""); } } Loading core/tests/coretests/src/android/content/pm/CrossProfileAppsTest.java +45 −0 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,8 @@ import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyResourcesManager; import android.app.admin.DevicePolicyResourcesManager; Loading Loading @@ -118,11 +120,54 @@ public class CrossProfileAppsTest { public void initUsers() throws Exception { public void initUsers() throws Exception { when(mUserManager.isManagedProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isManagedProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isManagedProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); when(mUserManager.isManagedProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); when(mUserManager.isProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); mTargetProfiles = new ArrayList<>(); mTargetProfiles = new ArrayList<>(); when(mService.getTargetUserProfiles(MY_PACKAGE)).thenReturn(mTargetProfiles); when(mService.getTargetUserProfiles(MY_PACKAGE)).thenReturn(mTargetProfiles); } } @Test public void isProfile_managedProfile_returnsTrue() { setValidTargetProfile(MANAGED_PROFILE); boolean result = mCrossProfileApps.isProfile(MANAGED_PROFILE); assertTrue(result); } @Test public void isProfile_personalProfile_returnsFalse() { setValidTargetProfile(PERSONAL_PROFILE); boolean result = mCrossProfileApps.isProfile(PERSONAL_PROFILE); assertFalse(result); } @Test public void isManagedProfile_managedProfile_returnsTrue() { setValidTargetProfile(MANAGED_PROFILE); boolean result = mCrossProfileApps.isManagedProfile(MANAGED_PROFILE); assertTrue(result); } @Test public void isManagedProfile_personalProfile_returnsFalse() { setValidTargetProfile(PERSONAL_PROFILE); boolean result = mCrossProfileApps.isManagedProfile(PERSONAL_PROFILE); assertFalse(result); } @Test(expected = SecurityException.class) public void isManagedProfile_notValidTarget_throwsSecurityException() { mCrossProfileApps.isManagedProfile(PERSONAL_PROFILE); } @Test @Test public void getProfileSwitchingLabel_managedProfile() { public void getProfileSwitchingLabel_managedProfile() { setValidTargetProfile(MANAGED_PROFILE); setValidTargetProfile(MANAGED_PROFILE); Loading Loading
core/api/current.txt +2 −0 Original line number Original line Diff line number Diff line Loading @@ -12385,6 +12385,8 @@ package android.content.pm { method @NonNull public android.graphics.drawable.Drawable getProfileSwitchingIconDrawable(@NonNull android.os.UserHandle); method @NonNull public android.graphics.drawable.Drawable getProfileSwitchingIconDrawable(@NonNull android.os.UserHandle); method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle); method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle); method @NonNull public java.util.List<android.os.UserHandle> getTargetUserProfiles(); method @NonNull public java.util.List<android.os.UserHandle> getTargetUserProfiles(); method @FlaggedApi("android.app.admin.flags.allow_querying_profile_type") public boolean isManagedProfile(@NonNull android.os.UserHandle); method @FlaggedApi("android.app.admin.flags.allow_querying_profile_type") public boolean isProfile(@NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity, @Nullable android.os.Bundle); method @RequiresPermission(anyOf={android.Manifest.permission.INTERACT_ACROSS_PROFILES, "android.permission.INTERACT_ACROSS_USERS"}) public void startActivity(@NonNull android.content.Intent, @NonNull android.os.UserHandle, @Nullable android.app.Activity, @Nullable android.os.Bundle); method public void startMainActivity(@NonNull android.content.ComponentName, @NonNull android.os.UserHandle); method public void startMainActivity(@NonNull android.content.ComponentName, @NonNull android.os.UserHandle);
core/java/android/app/admin/flags/flags.aconfig +7 −0 Original line number Original line Diff line number Diff line Loading @@ -91,6 +91,13 @@ flag { bug: "304999634" bug: "304999634" } } flag { name: "allow_querying_profile_type" namespace: "enterprise" description: "Public APIs to query if a user is a profile and what kind of profile type it is." bug: "323001115" } flag { flag { name: "quiet_mode_credential_bug_fix" name: "quiet_mode_credential_bug_fix" namespace: "enterprise" namespace: "enterprise" Loading
core/java/android/content/pm/CrossProfileApps.java +42 −0 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,9 @@ import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_PERSONAL_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_PERSONAL_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_WORK_LABEL; import static android.app.admin.DevicePolicyResources.Strings.Core.SWITCH_TO_WORK_LABEL; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.app.admin.flags.Flags.FLAG_ALLOW_QUERYING_PROFILE_TYPE; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.RequiresPermission; Loading Loading @@ -314,6 +316,41 @@ public class CrossProfileApps { } } } } /** * Checks if the specified user is a profile, i.e. not the parent user. * * @param userHandle The UserHandle of the target profile, must be one of the users returned by * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will * be thrown. * @return whether the specified user is a profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) public boolean isProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. verifyCanAccessUser(userHandle); return mUserManager.isProfile(userHandle.getIdentifier()); } /** * Checks if the specified user is a managed profile. * * @param userHandle The UserHandle of the target profile, must be one of the users returned by * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will * be thrown. * @return whether the specified user is a managed profile. */ @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE) public boolean isManagedProfile(@NonNull UserHandle userHandle) { // Note that this is not a security check, but rather a check for correct use. // The actual security check is performed by UserManager. verifyCanAccessUser(userHandle); return mUserManager.isManagedProfile(userHandle.getIdentifier()); } /** /** * Return a label that calling app can show to user for the semantic of profile switching -- * Return a label that calling app can show to user for the semantic of profile switching -- * launching its own activity in specified user profile. For example, it may return * launching its own activity in specified user profile. For example, it may return Loading Loading @@ -677,6 +714,11 @@ public class CrossProfileApps { } } } } /** * A validation method to check that the methods in this class are only being applied to user * handles returned by {@link #getTargetUserProfiles()}. As this is run client-side for * input validation purposes, this should never replace a real security check service-side. */ private void verifyCanAccessUser(UserHandle userHandle) { private void verifyCanAccessUser(UserHandle userHandle) { if (!getTargetUserProfiles().contains(userHandle)) { if (!getTargetUserProfiles().contains(userHandle)) { throw new SecurityException("Not allowed to access " + userHandle); throw new SecurityException("Not allowed to access " + userHandle); Loading
core/java/android/os/UserManager.java +5 −1 Original line number Original line Diff line number Diff line Loading @@ -3258,7 +3258,11 @@ public class UserManager { return isProfile(mUserId); return isProfile(mUserId); } } private boolean isProfile(@UserIdInt int userId) { /** * Returns whether the specified user is a profile. * @hide */ public boolean isProfile(@UserIdInt int userId) { final String profileType = getProfileType(userId); final String profileType = getProfileType(userId); return profileType != null && !profileType.equals(""); return profileType != null && !profileType.equals(""); } } Loading
core/tests/coretests/src/android/content/pm/CrossProfileAppsTest.java +45 −0 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,8 @@ import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyResourcesManager; import android.app.admin.DevicePolicyResourcesManager; Loading Loading @@ -118,11 +120,54 @@ public class CrossProfileAppsTest { public void initUsers() throws Exception { public void initUsers() throws Exception { when(mUserManager.isManagedProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isManagedProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isManagedProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); when(mUserManager.isManagedProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); when(mUserManager.isProfile(PERSONAL_PROFILE.getIdentifier())).thenReturn(false); when(mUserManager.isProfile(MANAGED_PROFILE.getIdentifier())).thenReturn(true); mTargetProfiles = new ArrayList<>(); mTargetProfiles = new ArrayList<>(); when(mService.getTargetUserProfiles(MY_PACKAGE)).thenReturn(mTargetProfiles); when(mService.getTargetUserProfiles(MY_PACKAGE)).thenReturn(mTargetProfiles); } } @Test public void isProfile_managedProfile_returnsTrue() { setValidTargetProfile(MANAGED_PROFILE); boolean result = mCrossProfileApps.isProfile(MANAGED_PROFILE); assertTrue(result); } @Test public void isProfile_personalProfile_returnsFalse() { setValidTargetProfile(PERSONAL_PROFILE); boolean result = mCrossProfileApps.isProfile(PERSONAL_PROFILE); assertFalse(result); } @Test public void isManagedProfile_managedProfile_returnsTrue() { setValidTargetProfile(MANAGED_PROFILE); boolean result = mCrossProfileApps.isManagedProfile(MANAGED_PROFILE); assertTrue(result); } @Test public void isManagedProfile_personalProfile_returnsFalse() { setValidTargetProfile(PERSONAL_PROFILE); boolean result = mCrossProfileApps.isManagedProfile(PERSONAL_PROFILE); assertFalse(result); } @Test(expected = SecurityException.class) public void isManagedProfile_notValidTarget_throwsSecurityException() { mCrossProfileApps.isManagedProfile(PERSONAL_PROFILE); } @Test @Test public void getProfileSwitchingLabel_managedProfile() { public void getProfileSwitchingLabel_managedProfile() { setValidTargetProfile(MANAGED_PROFILE); setValidTargetProfile(MANAGED_PROFILE); Loading