Implement DISALLOW_SHARE_INTO_MANAGED_PROFILE

Add a user restriction to allow profile owners to enforce a stronger
isolation of managed profile by preventing users sharing data into
the profile. This is achieved by disabling a subset of built-in cross
profile intent filters added by ManagedProvisioning during profile
inflation.

Implementation wise, DevicePolicyManagerService listens for the restriction
change and notifies ManagedProvisioning to modify the built-in intent
filters. This is needed since ManagedProvisioning has ground truth of all
built-in intent filters and manages them. It also has the advantage that
ManagedProvisioning only needs to run when a policy change happens.

Test: cts-tradefed run cts-dev -m CtsDevicePolicyManagerTestCases -t com.android.cts.devicepolicy.ManagedProfileTest#testDisallowSharingIntoProfileFromPersonal
Test: cts-tradefed run cts-dev -m CtsDevicePolicyManagerTestCases -t com.android.cts.devicepolicy.ManagedProfileTest#testDisallowSharingIntoProfileFromProfile
Bug: 63911046
Change-Id: Ia6d12a5086627d1280325cd19d6e3a0752dae633
diff --git a/api/current.txt b/api/current.txt
index a75bbfa..8e2012c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -32465,6 +32465,7 @@
     field public static final java.lang.String DISALLOW_SAFE_BOOT = "no_safe_boot";
     field public static final java.lang.String DISALLOW_SET_USER_ICON = "no_set_user_icon";
     field public static final java.lang.String DISALLOW_SET_WALLPAPER = "no_set_wallpaper";
+    field public static final java.lang.String DISALLOW_SHARE_INTO_MANAGED_PROFILE = "no_sharing_into_profile";
     field public static final java.lang.String DISALLOW_SHARE_LOCATION = "no_share_location";
     field public static final java.lang.String DISALLOW_SMS = "no_sms";
     field public static final java.lang.String DISALLOW_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs";
diff --git a/api/test-current.txt b/api/test-current.txt
index b16e700..285fcb3 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -156,6 +156,7 @@
     method public boolean isDeviceManaged();
     field public static final java.lang.String ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_ALLOWED = "android.account.DEVICE_OR_PROFILE_OWNER_ALLOWED";
     field public static final java.lang.String ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_DISALLOWED = "android.account.DEVICE_OR_PROFILE_OWNER_DISALLOWED";
+    field public static final java.lang.String ACTION_DATA_SHARING_RESTRICTION_APPLIED = "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
     field public static final java.lang.String EXTRA_RESTRICTION = "android.app.extra.RESTRICTION";
   }
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ab85fdc..10539d1 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -1254,6 +1254,26 @@
             = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
 
     /**
+     * Broadcast action to notify ManagedProvisioning that
+     * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has changed.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_SHARING_RESTRICTION_CHANGED =
+            "android.app.action.DATA_SHARING_RESTRICTION_CHANGED";
+
+    /**
+     * Broadcast action from ManagedProvisioning to notify that the latest change to
+     * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has been successfully
+     * applied (cross profile intent filters updated). Only usesd for CTS tests.
+     * @hide
+     */
+    @TestApi
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED =
+            "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
+
+    /**
      * Permission policy to prompt user for new permission requests for runtime permissions.
      * Already granted or denied permissions are not affected by this.
      */
@@ -6057,6 +6077,13 @@
      * Called by a profile owner of a managed profile to remove the cross-profile intent filters
      * that go from the managed profile to the parent, or from the parent to the managed profile.
      * Only removes those that have been set by the profile owner.
+     * <p>
+     * <em>Note</em>: A list of default cross profile intent filters are set up by the system when
+     * the profile is created, some of them ensure the proper functioning of the profile, while
+     * others enable sharing of data from the parent to the managed profile for user convenience.
+     * These default intent filters are not cleared when this API is called. If the default cross
+     * profile data sharing is not desired, they can be disabled with
+     * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE}.
      *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @throws SecurityException if {@code admin} is not a device or profile owner.
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 4e94c32..bb55afb 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -892,6 +892,27 @@
     public static final String DISALLOW_USER_SWITCH = "no_user_switch";
 
     /**
+     * Specifies whether the user can share file / picture / data from the primary user into the
+     * managed profile, either by sending them from the primary side, or by picking up data within
+     * an app in the managed profile.
+     * <p>
+     * When a managed profile is created, the system allows the user to send data from the primary
+     * side to the profile by setting up certain default cross profile intent filters. If
+     * this is undesired, this restriction can be set to disallow it. Note that this restriction
+     * will not block any sharing allowed by explicit
+     * {@link DevicePolicyManager#addCrossProfileIntentFilter} calls by the profile owner.
+     * <p>
+     * This restriction is only meaningful when set by profile owner. When it is set by device
+     * owner, it does not have any effect.
+     * <p>
+     * The default value is <code>false</code>.
+     *
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_SHARE_INTO_MANAGED_PROFILE = "no_sharing_into_profile";
+    /**
      * Application restriction key that is used to indicate the pending arrival
      * of real restrictions for the app.
      *
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 35dc624..8c61039 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -576,6 +576,9 @@
     <protected-broadcast android:name="android.app.action.PROFILE_OWNER_CHANGED" />
     <protected-broadcast android:name="android.app.action.TRANSFER_OWNERSHIP_COMPLETE" />
 
+    <!-- Added in P -->
+    <protected-broadcast android:name="android.app.action.DATA_SHARING_RESTRICTION_CHANGED" />
+
     <!-- ====================================================================== -->
     <!--                          RUNTIME PERMISSIONS                           -->
     <!-- ====================================================================== -->
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index b6a094c..6964e9d 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -117,7 +117,8 @@
             UserManager.DISALLOW_UNIFIED_PASSWORD,
             UserManager.DISALLOW_CONFIG_LOCATION_MODE,
             UserManager.DISALLOW_AIRPLANE_MODE,
-            UserManager.DISALLOW_CONFIG_BRIGHTNESS
+            UserManager.DISALLOW_CONFIG_BRIGHTNESS,
+            UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE,
     });
 
     /**
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 3b0937b..43ced44 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -151,6 +151,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
 import android.os.storage.StorageManager;
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsInternal;
@@ -703,6 +704,33 @@
         }
     };
 
+    protected static class RestrictionsListener implements UserRestrictionsListener {
+        private Context mContext;
+
+        public RestrictionsListener(Context context) {
+            mContext = context;
+        }
+
+        public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+                Bundle prevRestrictions) {
+            final boolean newlyDisallowed =
+                    newRestrictions.getBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE);
+            final boolean previouslyDisallowed =
+                    prevRestrictions.getBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE);
+            final boolean restrictionChanged = (newlyDisallowed != previouslyDisallowed);
+
+            if (restrictionChanged) {
+                // Notify ManagedProvisioning to update the built-in cross profile intent filters.
+                Intent intent = new Intent(
+                        DevicePolicyManager.ACTION_DATA_SHARING_RESTRICTION_CHANGED);
+                intent.setPackage(MANAGED_PROVISIONING_PKG);
+                intent.putExtra(Intent.EXTRA_USER_ID, userId);
+                intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+                mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+            }
+        }
+    }
+
     static class ActiveAdmin {
         private static final String TAG_DISABLE_KEYGUARD_FEATURES = "disable-keyguard-features";
         private static final String TAG_TEST_ONLY_ADMIN = "test-only-admin";
@@ -1982,6 +2010,8 @@
         LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService);
 
         mSetupContentObserver = new SetupContentObserver(mHandler);
+
+        mUserManagerInternal.addUserRestrictionsListener(new RestrictionsListener(mContext));
     }
 
     /**
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 57030e4..1df0ff2 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -87,6 +87,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.devicepolicy.DevicePolicyManagerService.RestrictionsListener;
 import com.android.server.pm.UserRestrictionsUtils;
 
 import org.hamcrest.BaseMatcher;
@@ -4020,6 +4021,8 @@
         dpm.setPasswordMinimumNumeric(admin1, 1);
         dpm.setPasswordMinimumSymbols(admin1, 0);
 
+        reset(mContext.spiedContext);
+
         PasswordMetrics passwordMetricsNoSymbols = new PasswordMetrics(
                 DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, 9,
                 8, 2,
@@ -4069,9 +4072,16 @@
         dpm.setActivePasswordState(passwordMetrics, userHandle);
         dpm.reportPasswordChanged(userHandle);
 
+        // Drain ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED broadcasts as part of
+        // reportPasswordChanged()
+        verify(mContext.spiedContext, times(3)).sendBroadcastAsUser(
+                MockUtils.checkIntentAction(
+                        DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+                MockUtils.checkUserHandle(userHandle));
+
         final Intent intent = new Intent(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED);
         intent.setComponent(admin1);
-        intent.putExtra(Intent.EXTRA_USER, UserHandle.of(mContext.binder.callingUid));
+        intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userHandle));
 
         verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
                 MockUtils.checkIntent(intent),
@@ -4482,6 +4492,45 @@
         verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(null, caller);
     }
 
+    public void testDisallowSharingIntoProfileSetRestriction() {
+        Bundle restriction = new Bundle();
+        restriction.putBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, true);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        RestrictionsListener listener = new RestrictionsListener(mContext);
+        listener.onUserRestrictionsChanged(DpmMockContext.CALLER_USER_HANDLE, restriction,
+                new Bundle());
+        verifyDataSharingChangedBroadcast();
+    }
+
+    public void testDisallowSharingIntoProfileClearRestriction() {
+        Bundle restriction = new Bundle();
+        restriction.putBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, true);
+
+        mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+        RestrictionsListener listener = new RestrictionsListener(mContext);
+        listener.onUserRestrictionsChanged(DpmMockContext.CALLER_USER_HANDLE, new Bundle(),
+                restriction);
+        verifyDataSharingChangedBroadcast();
+    }
+
+    public void testDisallowSharingIntoProfileUnchanged() {
+        RestrictionsListener listener = new RestrictionsListener(mContext);
+        listener.onUserRestrictionsChanged(DpmMockContext.CALLER_USER_HANDLE, new Bundle(),
+                new Bundle());
+        verify(mContext.spiedContext, never()).sendBroadcastAsUser(any(), any());
+    }
+
+    private void verifyDataSharingChangedBroadcast() {
+        Intent expectedIntent = new Intent(
+                DevicePolicyManager.ACTION_DATA_SHARING_RESTRICTION_CHANGED);
+        expectedIntent.setPackage("com.android.managedprovisioning");
+        expectedIntent.putExtra(Intent.EXTRA_USER_ID, DpmMockContext.CALLER_USER_HANDLE);
+        verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
+                MockUtils.checkIntent(expectedIntent),
+                MockUtils.checkUserHandle(UserHandle.USER_SYSTEM));
+    }
+
     private void verifyCanGetOwnerInstalledCaCerts(
             final ComponentName caller, final DpmMockContext callerContext) throws Exception {
         final String alias = "cert";
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockUtils.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockUtils.java
index 288a8be..dec962e 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockUtils.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockUtils.java
@@ -91,7 +91,8 @@
             @Override
             public boolean matches(Object item) {
                 if (item == null) return false;
-                return intent.filterEquals((Intent) item);
+                if (!intent.filterEquals((Intent) item)) return false;
+                return intent.getExtras().kindofEquals(((Intent) item).getExtras());
             }
             @Override
             public void describeTo(Description description) {