Only show smart actions for whitelisted apps in lock task mode.

In lock task mode only apps from a specific whitelist can be started. To
avoid showing buttons that won't do anything when clicked we remove
smart actions linking to apps that are not whitelisted.

In this change we add several IPC calls during smart suggestions (in
notification) inflation - one in the common code flow, and several
others only for the case where lock task kiosk mode is enabled. This is
OK from a performance perspective because we inflate smart suggestions
on a background thread.

Bug: 117976013
Test: atest InflatedSmartRepliesTest
Test: start lock-task mode with 1. chrome whitelisted -> chrome actions
show up, 2. chrome not whitelisted -> chrome actions don't show up.
Test: ensure smart replies are still enabled in lock task mode.
Change-Id: I664ff2cdcfd1b212744d85d36d7a2b305bf4b3a9
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 46ed715b..0f71ffb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shared.system;
 
+import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
@@ -464,6 +465,17 @@
     }
 
     /**
+     * @return whether lock task mode is active in kiosk-mode (not screen pinning).
+     */
+    public boolean isLockTaskKioskModeActive() {
+        try {
+            return ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_LOCKED;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
      * Shows a voice session identified by {@code token}
      * @return true if the session was shown, false otherwise
      */
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/DevicePolicyManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/DevicePolicyManagerWrapper.java
new file mode 100644
index 0000000..c6722ae
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/DevicePolicyManagerWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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.systemui.shared.system;
+
+import android.app.AppGlobals;
+import android.app.admin.DevicePolicyManager;
+
+/**
+ * Wrapper for {@link DevicePolicyManager}.
+ */
+public class DevicePolicyManagerWrapper {
+    private static final DevicePolicyManagerWrapper sInstance = new DevicePolicyManagerWrapper();
+
+    private static final DevicePolicyManager sDevicePolicyManager =
+            AppGlobals.getInitialApplication().getSystemService(DevicePolicyManager.class);
+
+    private DevicePolicyManagerWrapper() { }
+
+    public static DevicePolicyManagerWrapper getInstance() {
+        return sInstance;
+    }
+
+    /**
+     * Returns whether the given package is allowed to run in Lock Task mode.
+     */
+    public boolean isLockTaskPermitted(String pkg) {
+        return sDevicePolicyManager.isLockTaskPermitted(pkg);
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/PackageManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/PackageManagerWrapper.java
index 32e4bbf..443c1e1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/PackageManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/PackageManagerWrapper.java
@@ -22,8 +22,10 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ResolveInfoFlags;
 import android.content.pm.ResolveInfo;
 import android.os.RemoteException;
+import android.os.UserHandle;
 
 import java.util.List;
 
@@ -40,6 +42,8 @@
         return sInstance;
     }
 
+    private PackageManagerWrapper() {}
+
     /**
      * @return the activity info for a given {@param componentName} and {@param userId}.
      */
@@ -65,4 +69,19 @@
             return null;
         }
     }
+
+    /**
+     * Determine the best Activity to perform for a given Intent.
+     */
+    public ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlags int flags) {
+        final String resolvedType =
+                intent.resolveTypeIfNeeded(AppGlobals.getInitialApplication().getContentResolver());
+        try {
+            return mIPackageManager.resolveIntent(
+                    intent, resolvedType, flags, UserHandle.getCallingUserId());
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 87f004f..f946cc1 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -48,6 +48,9 @@
 import com.android.systemui.privacy.PrivacyItemController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.statusbar.AmbientPulseManager;
 import com.android.systemui.statusbar.NavigationBarController;
 import com.android.systemui.statusbar.NotificationListener;
@@ -285,6 +288,9 @@
     @Nullable
     @Inject @Named(LEAK_REPORT_EMAIL_NAME) Lazy<String> mLeakReportEmail;
     @Inject Lazy<ClockManager> mClockManager;
+    @Inject Lazy<ActivityManagerWrapper> mActivityManagerWrapper;
+    @Inject Lazy<DevicePolicyManagerWrapper> mDevicePolicyManagerWrapper;
+    @Inject Lazy<PackageManagerWrapper> mPackageManagerWrapper;
 
     @Inject
     public Dependency() {
@@ -452,6 +458,9 @@
                 mForegroundServiceNotificationListener::get);
         mProviders.put(ClockManager.class, mClockManager::get);
         mProviders.put(PrivacyItemController.class, mPrivacyItemController::get);
+        mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
+        mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
+        mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::get);
 
 
         // TODO(b/118592525): to support multi-display , we start to add something which is
diff --git a/packages/SystemUI/src/com/android/systemui/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
index a517d7c..895f9b9 100644
--- a/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
@@ -41,6 +41,9 @@
 import com.android.systemui.plugins.PluginInitializerImpl;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.plugins.PluginManagerImpl;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.statusbar.NavigationBarController;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
@@ -181,4 +184,22 @@
             @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
         return new AutoHideController(context, mainHandler);
     }
+
+    @Singleton
+    @Provides
+    public ActivityManagerWrapper provideActivityManagerWrapper() {
+        return ActivityManagerWrapper.getInstance();
+    }
+
+    @Singleton
+    @Provides
+    public DevicePolicyManagerWrapper provideDevicePolicyManagerWrapper() {
+        return DevicePolicyManagerWrapper.getInstance();
+    }
+
+    @Singleton
+    @Provides
+    public PackageManagerWrapper providePackageManagerWrapper() {
+        return PackageManagerWrapper.getInstance();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
index d8ea1f6..5b2e398 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
@@ -21,12 +21,18 @@
 import android.app.Notification;
 import android.app.RemoteInput;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
 import android.os.Build;
 import android.util.Log;
 import android.util.Pair;
 import android.widget.Button;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
@@ -191,14 +197,47 @@
             boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
                     && notification.getAllowSystemGeneratedContextualActions();
             if (useSmartActions) {
+                List<Notification.Action> systemGeneratedActions =
+                        entry.systemGeneratedSmartActions;
+                // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode,
+                // since notifications aren't shown there anyway.
+                ActivityManagerWrapper activityManagerWrapper =
+                        Dependency.get(ActivityManagerWrapper.class);
+                if (activityManagerWrapper.isLockTaskKioskModeActive()) {
+                    systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions);
+                }
                 smartActions = new SmartReplyView.SmartActions(
-                        entry.systemGeneratedSmartActions, true /* fromAssistant */);
+                        systemGeneratedActions, true /* fromAssistant */);
             }
         }
         return new SmartRepliesAndActions(smartReplies, smartActions);
     }
 
     /**
+     * Filter actions so that only actions pointing to whitelisted apps are allowed.
+     * This filtering is only meaningful when in lock-task mode.
+     */
+    private static List<Notification.Action> filterWhiteListedLockTaskApps(
+            List<Notification.Action> actions) {
+        PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class);
+        DevicePolicyManagerWrapper devicePolicyManagerWrapper =
+                Dependency.get(DevicePolicyManagerWrapper.class);
+        List<Notification.Action> filteredActions = new ArrayList<>();
+        for (Notification.Action action : actions) {
+            if (action.actionIntent == null) continue;
+            Intent intent = action.actionIntent.getIntent();
+            //  Only allow actions that are explicit (implicit intents are not handled in lock-task
+            //  mode), and link to whitelisted apps.
+            ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */);
+            if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted(
+                    resolveInfo.activityInfo.packageName)) {
+                filteredActions.add(action);
+            }
+        }
+        return filteredActions;
+    }
+
+    /**
      * Returns whether the {@link Notification} represented by entry has a free-form remote input.
      * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
      * through the remote input.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
index 3382a90..4d0834e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
@@ -11,19 +11,23 @@
  * 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.
- */
+ * limitations under the License.  */
 
 package com.android.systemui.statusbar.policy;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.RemoteInput;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Icon;
 import android.service.notification.StatusBarNotification;
 import android.support.test.annotation.UiThreadTest;
@@ -33,6 +37,9 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
 
@@ -49,19 +56,19 @@
 @RunWith(AndroidJUnit4.class)
 public class InflatedSmartRepliesTest extends SysuiTestCase {
 
-    private static final String TEST_ACTION = "com.android.SMART_REPLY_VIEW_ACTION";
+    private static final Intent TEST_INTENT = new Intent("com.android.SMART_REPLY_VIEW_ACTION");
+    private static final Intent WHITELISTED_TEST_INTENT =
+            new Intent("com.android.WHITELISTED_TEST_ACTION");
 
-    @Mock
-    SmartReplyConstants mSmartReplyConstants;
-    @Mock
-    StatusBarNotification mStatusBarNotification;
-    @Mock
-    Notification mNotification;
+    @Mock SmartReplyConstants mSmartReplyConstants;
+    @Mock StatusBarNotification mStatusBarNotification;
+    @Mock Notification mNotification;
     NotificationEntry mEntry;
-    @Mock
-    RemoteInput mRemoteInput;
-    @Mock
-    RemoteInput mFreeFormRemoteInput;
+    @Mock RemoteInput mRemoteInput;
+    @Mock RemoteInput mFreeFormRemoteInput;
+    @Mock ActivityManagerWrapper mActivityManagerWrapper;
+    @Mock PackageManagerWrapper mPackageManagerWrapper;
+    @Mock DevicePolicyManagerWrapper mDevicePolicyManagerWrapper;
 
     private Icon mActionIcon;
 
@@ -70,11 +77,18 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
+        mDependency.injectTestDependency(ActivityManagerWrapper.class, mActivityManagerWrapper);
+        mDependency.injectTestDependency(
+                DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper);
+        mDependency.injectTestDependency(PackageManagerWrapper.class, mPackageManagerWrapper);
+
         when(mNotification.getAllowSystemGeneratedContextualActions()).thenReturn(true);
         when(mStatusBarNotification.getNotification()).thenReturn(mNotification);
         mEntry = new NotificationEntry(mStatusBarNotification);
         when(mSmartReplyConstants.isEnabled()).thenReturn(true);
         mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
+
+        when(mActivityManagerWrapper.isLockTaskKioskModeActive()).thenReturn(false);
     }
 
     @Test
@@ -226,6 +240,87 @@
         assertThat(repliesAndActions.smartReplies).isNull();
     }
 
+    @Test
+    public void chooseSmartRepliesAndActions_lockTaskKioskModeEnabled_smartRepliesUnaffected() {
+        when(mActivityManagerWrapper.isLockTaskKioskModeActive()).thenReturn(true);
+        // No apps are white-listed
+        when(mDevicePolicyManagerWrapper.isLockTaskPermitted(anyString())).thenReturn(false);
+
+        // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+        // suggestions.
+        setupAppGeneratedReplies(null /* smartReplies */);
+        mEntry.systemGeneratedSmartReplies =
+                new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+        mEntry.systemGeneratedSmartActions =
+                createActions(new String[] {"Sys Smart Action 1", "Sys Smart Action 2"});
+
+        SmartRepliesAndActions repliesAndActions =
+                InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+        assertThat(repliesAndActions.smartReplies.choices).isEqualTo(
+                mEntry.systemGeneratedSmartReplies);
+        // Since no apps are whitelisted no actions should be shown.
+        assertThat(repliesAndActions.smartActions.actions).isEmpty();
+    }
+
+    @Test
+    public void chooseSmartRepliesAndActions_lockTaskKioskModeEnabled_smartActionsAffected() {
+        when(mActivityManagerWrapper.isLockTaskKioskModeActive()).thenReturn(true);
+        String allowedPackage = "allowedPackage";
+        ResolveInfo allowedResolveInfo = new ResolveInfo();
+        allowedResolveInfo.activityInfo = new ActivityInfo();
+        allowedResolveInfo.activityInfo.packageName = allowedPackage;
+        when(mPackageManagerWrapper
+                .resolveActivity(
+                        argThat(intent -> WHITELISTED_TEST_INTENT.getAction().equals(
+                                intent.getAction())),
+                        anyInt() /* flags */))
+                .thenReturn(allowedResolveInfo);
+        when(mDevicePolicyManagerWrapper.isLockTaskPermitted(allowedPackage)).thenReturn(true);
+
+        // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+        // suggestions.
+        setupAppGeneratedReplies(null /* smartReplies */);
+        mEntry.systemGeneratedSmartReplies =
+                new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+        List<Notification.Action> actions = new ArrayList<>();
+        actions.add(createAction("allowed action", WHITELISTED_TEST_INTENT));
+        actions.add(createAction("non-allowed action", TEST_INTENT));
+        mEntry.systemGeneratedSmartActions = actions;
+
+        SmartRepliesAndActions repliesAndActions =
+                InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+        // Only the action for the whitelisted package should be allowed.
+        assertThat(repliesAndActions.smartActions.actions.size()).isEqualTo(1);
+        assertThat(repliesAndActions.smartActions.actions.get(0)).isEqualTo(
+                mEntry.systemGeneratedSmartActions.get(0));
+    }
+
+    @Test
+    public void chooseSmartRepliesAndActions_screenPinningModeEnabled_suggestionsUnaffected() {
+        when(mActivityManagerWrapper.isLockToAppActive()).thenReturn(true);
+        // No apps are white-listed
+        when(mDevicePolicyManagerWrapper.isLockTaskPermitted(anyString())).thenReturn(false);
+
+        // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+        // suggestions.
+        setupAppGeneratedReplies(null /* smartReplies */);
+        mEntry.systemGeneratedSmartReplies =
+                new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+        mEntry.systemGeneratedSmartActions =
+                createActions(new String[] {"Sys Smart Action 1", "Sys Smart Action 2"});
+
+        SmartRepliesAndActions repliesAndActions =
+                InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+        // We don't restrict replies or actions in screen pinning mode.
+        assertThat(repliesAndActions.smartReplies.choices).isEqualTo(
+                mEntry.systemGeneratedSmartReplies);
+        assertThat(repliesAndActions.smartActions.actions).isEqualTo(
+                mEntry.systemGeneratedSmartActions);
+    }
+
     private void setupAppGeneratedReplies(CharSequence[] smartReplies) {
         setupAppGeneratedReplies(smartReplies, true /* allowSystemGeneratedReplies */);
     }
@@ -233,7 +328,7 @@
     private void setupAppGeneratedReplies(
             CharSequence[] smartReplies, boolean allowSystemGeneratedReplies) {
         PendingIntent pendingIntent =
-                PendingIntent.getBroadcast(mContext, 0, new Intent(TEST_ACTION), 0);
+                PendingIntent.getBroadcast(mContext, 0, TEST_INTENT, 0);
         Notification.Action action =
                 new Notification.Action.Builder(null, "Test Action", pendingIntent).build();
         when(mRemoteInput.getChoices()).thenReturn(smartReplies);
@@ -260,8 +355,11 @@
     }
 
     private Notification.Action.Builder createActionBuilder(String actionTitle) {
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
-                new Intent(TEST_ACTION), 0);
+        return createActionBuilder(actionTitle, TEST_INTENT);
+    }
+
+    private Notification.Action.Builder createActionBuilder(String actionTitle, Intent intent) {
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
         return new Notification.Action.Builder(mActionIcon, actionTitle, pendingIntent);
     }
 
@@ -269,6 +367,10 @@
         return createActionBuilder(actionTitle).build();
     }
 
+    private Notification.Action createAction(String actionTitle, Intent intent) {
+        return createActionBuilder(actionTitle, intent).build();
+    }
+
     private List<Notification.Action> createActions(String[] actionTitles) {
         List<Notification.Action> actions = new ArrayList<>();
         for (String title : actionTitles) {