Send EXTRA_USER with DevicePolicy lock broadcasts

DeviceAdmins inside profiles may receive broadcasts referring either
to the parent profile or to themselves.

We need a way to differentiate that.

Same commit fixes a bug in DevicePolicyManagerTest where USER_SYSTEM
is returned twice in getProfiles() when called for a managed profile of
USER_SYSTEM. This does not happen in the real API.

Bug: 30185351
Bug: 31001762
Test: runtest -x services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
Change-Id: Iea2735357f4019b2b81b6784e7ea6aead63f2636
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e5c8d02..7538c95 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2213,15 +2213,13 @@
     /**
      * Send an update to all admins of a user that enforce a specified policy.
      */
-    void sendAdminCommandLocked(String action, int reqPolicy, int userHandle) {
+    void sendAdminCommandLocked(String action, int reqPolicy, int userHandle, Bundle adminExtras) {
         final DevicePolicyData policy = getUserData(userHandle);
         final int count = policy.mAdminList.size();
-        if (count > 0) {
-            for (int i = 0; i < count; i++) {
-                final ActiveAdmin admin = policy.mAdminList.get(i);
-                if (admin.info.usesPolicy(reqPolicy)) {
-                    sendAdminCommandLocked(admin, action);
-                }
+        for (int i = 0; i < count; i++) {
+            final ActiveAdmin admin = policy.mAdminList.get(i);
+            if (admin.info.usesPolicy(reqPolicy)) {
+                sendAdminCommandLocked(admin, action, adminExtras, null);
             }
         }
     }
@@ -2231,10 +2229,10 @@
      * enforce a specified policy.
      */
     private void sendAdminCommandToSelfAndProfilesLocked(String action, int reqPolicy,
-            int userHandle) {
+            int userHandle, Bundle adminExtras) {
         int[] profileIds = mUserManager.getProfileIdsWithDisabled(userHandle);
         for (int profileId : profileIds) {
-            sendAdminCommandLocked(action, reqPolicy, profileId);
+            sendAdminCommandLocked(action, reqPolicy, profileId, adminExtras);
         }
     }
 
@@ -2243,10 +2241,12 @@
      */
     private void sendAdminCommandForLockscreenPoliciesLocked(
             String action, int reqPolicy, int userHandle) {
+        final Bundle extras = new Bundle();
+        extras.putParcelable(Intent.EXTRA_USER, UserHandle.of(userHandle));
         if (isSeparateProfileChallengeEnabled(userHandle)) {
-            sendAdminCommandLocked(action, reqPolicy, userHandle);
+            sendAdminCommandLocked(action, reqPolicy, userHandle, extras);
         } else {
-            sendAdminCommandToSelfAndProfilesLocked(action, reqPolicy, userHandle);
+            sendAdminCommandToSelfAndProfilesLocked(action, reqPolicy, userHandle, extras);
         }
     }
 
@@ -2796,6 +2796,9 @@
     }
 
     private void handlePasswordExpirationNotification(int userHandle) {
+        final Bundle adminExtras = new Bundle();
+        adminExtras.putParcelable(Intent.EXTRA_USER, UserHandle.of(userHandle));
+
         synchronized (this) {
             final long now = System.currentTimeMillis();
 
@@ -2809,7 +2812,7 @@
                         && now >= admin.passwordExpirationDate - EXPIRATION_GRACE_PERIOD_MS
                         && admin.passwordExpirationDate > 0L) {
                     sendAdminCommandLocked(admin,
-                            DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING);
+                            DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING, adminExtras, null);
                 }
             }
             setExpirationAlarmCheckLocked(mContext, userHandle, /* parent */ false);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index d392459..c5e343e 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -71,6 +71,7 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -544,6 +545,83 @@
     }
 
     /**
+     * Test for: @{link DevicePolicyManager#setActivePasswordState}
+     *
+     * Validates that when the password for a user changes, the notification broadcast intent
+     * {@link DeviceAdminReceiver#ACTION_PASSWORD_CHANGED} is sent to managed profile owners, in
+     * addition to ones in the original user.
+     */
+    public void testSetActivePasswordState_sendToProfiles() throws Exception {
+        mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+
+        final int MANAGED_PROFILE_USER_ID = 78;
+        final int MANAGED_PROFILE_ADMIN_UID =
+                UserHandle.getUid(MANAGED_PROFILE_USER_ID, DpmMockContext.SYSTEM_UID);
+
+        // Setup device owner.
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        mContext.packageName = admin1.getPackageName();
+        setupDeviceOwner();
+
+        // Add a managed profile belonging to the system user.
+        addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+        // Change the parent user's password.
+        dpm.reportPasswordChanged(UserHandle.USER_SYSTEM);
+
+        // Both the device owner and the managed profile owner should receive this broadcast.
+        final Intent intent = new Intent(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED);
+        intent.setComponent(admin1);
+        intent.putExtra(Intent.EXTRA_USER, UserHandle.of(UserHandle.USER_SYSTEM));
+
+        verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
+                MockUtils.checkIntent(intent),
+                MockUtils.checkUserHandle(UserHandle.USER_SYSTEM));
+        verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
+                MockUtils.checkIntent(intent),
+                MockUtils.checkUserHandle(MANAGED_PROFILE_USER_ID));
+    }
+
+    /**
+     * Test for: @{link DevicePolicyManager#setActivePasswordState}
+     *
+     * Validates that when the password for a managed profile changes, the notification broadcast
+     * intent {@link DeviceAdminReceiver#ACTION_PASSWORD_CHANGED} is only sent to the profile, not
+     * its parent.
+     */
+    public void testSetActivePasswordState_notSentToParent() throws Exception {
+        mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+
+        final int MANAGED_PROFILE_USER_ID = 78;
+        final int MANAGED_PROFILE_ADMIN_UID =
+                UserHandle.getUid(MANAGED_PROFILE_USER_ID, DpmMockContext.SYSTEM_UID);
+
+        // Setup device owner.
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        mContext.packageName = admin1.getPackageName();
+        doReturn(true).when(mContext.lockPatternUtils)
+                .isSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID);
+        setupDeviceOwner();
+
+        // Add a managed profile belonging to the system user.
+        addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+        // Change the profile's password.
+        dpm.reportPasswordChanged(MANAGED_PROFILE_USER_ID);
+
+        // Both the device owner and the managed profile owner should receive this broadcast.
+        final Intent intent = new Intent(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED);
+        intent.setComponent(admin1);
+        intent.putExtra(Intent.EXTRA_USER, UserHandle.of(MANAGED_PROFILE_USER_ID));
+
+        verify(mContext.spiedContext, never()).sendBroadcastAsUser(
+                MockUtils.checkIntent(intent),
+                MockUtils.checkUserHandle(UserHandle.USER_SYSTEM));
+        verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
+                MockUtils.checkIntent(intent),
+                MockUtils.checkUserHandle(MANAGED_PROFILE_USER_ID));
+    }
+    /**
      * Test for: {@link DevicePolicyManager#setDeviceOwner} DO on system user installs successfully.
      */
     public void testSetDeviceOwner() throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 65255d9..a1b6769 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -18,6 +18,7 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.app.AlarmManager;
 import android.app.IActivityManager;
 import android.app.NotificationManager;
 import android.app.backup.IBackupManager;
@@ -276,6 +277,7 @@
     public final MockContentResolver contentResolver;
     public final TelephonyManager telephonyManager;
     public final AccountManager accountManager;
+    public final AlarmManager alarmManager;
 
     /** Note this is a partial mock, not a real mock. */
     public final PackageManager packageManager;
@@ -319,6 +321,7 @@
         settings = mock(SettingsForMock.class);
         telephonyManager = mock(TelephonyManager.class);
         accountManager = mock(AccountManager.class);
+        alarmManager = mock(AlarmManager.class);
 
         // Package manager is huge, so we use a partial mock instead.
         packageManager = spy(context.getPackageManager());
@@ -327,7 +330,8 @@
 
         contentResolver = new MockContentResolver();
 
-        // Add the system user
+        // Add the system user with a fake profile group already set up (this can happen in the real
+        // world if a managed profile is added and then removed).
         systemUserDataDir =
                 addUser(UserHandle.USER_SYSTEM, UserInfo.FLAG_PRIMARY, UserHandle.USER_SYSTEM);
 
@@ -410,9 +414,9 @@
         if (parent == null) {
             return ret;
         }
-        ret.add(parent);
         for (UserInfo ui : mUserInfos) {
-            if (ui.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
+            if (ui == parent
+                    || ui.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
                     && ui.profileGroupId == parent.profileGroupId) {
                 ret.add(ui);
             }
@@ -463,6 +467,8 @@
     @Override
     public Object getSystemService(String name) {
         switch (name) {
+            case Context.ALARM_SERVICE:
+                return alarmManager;
             case Context.USER_SERVICE:
                 return userManager;
             case Context.POWER_SERVICE: