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 46ed715bd48fb..0f71ffb34938f 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; @@ -463,6 +464,17 @@ public class ActivityManagerWrapper { } } + /** + * @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 0000000000000..c6722aece9fc5 --- /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 32e4bbf4cd501..443c1e1280fa4 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.Intent; 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 @@ public class PackageManagerWrapper { return sInstance; } + private PackageManagerWrapper() {} + /** * @return the activity info for a given {@param componentName} and {@param userId}. */ @@ -65,4 +69,19 @@ public class PackageManagerWrapper { 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 87f004fc12e6a..f946cc103927b 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.power.PowerUI; 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 @@ public class Dependency extends SystemUI { @Nullable @Inject @Named(LEAK_REPORT_EMAIL_NAME) Lazy mLeakReportEmail; @Inject Lazy mClockManager; + @Inject Lazy mActivityManagerWrapper; + @Inject Lazy mDevicePolicyManagerWrapper; + @Inject Lazy mPackageManagerWrapper; @Inject public Dependency() { @@ -452,6 +458,9 @@ public class Dependency extends SystemUI { 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 a517d7ce8e0e8..895f9b9a1cdf0 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.settingslib.bluetooth.LocalBluetoothManager; 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 @@ public class DependencyProvider { @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 d8ea1f6eef5fa..5b2e398b66e14 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.annotation.Nullable; 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,13 +197,46 @@ public class InflatedSmartReplies { boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions) && notification.getAllowSystemGeneratedContextualActions(); if (useSmartActions) { + List 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 filterWhiteListedLockTaskApps( + List actions) { + PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class); + DevicePolicyManagerWrapper devicePolicyManagerWrapper = + Dependency.get(DevicePolicyManagerWrapper.class); + List 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 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 3382a906d057c..4d0834e9a68ec 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 android.util.Pair; 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 @@ import java.util.List; @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 class InflatedSmartRepliesTest extends SysuiTestCase { 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 @@ public class InflatedSmartRepliesTest extends SysuiTestCase { 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 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 @@ public class InflatedSmartRepliesTest extends SysuiTestCase { 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 @@ public class InflatedSmartRepliesTest extends SysuiTestCase { } 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 @@ public class InflatedSmartRepliesTest extends SysuiTestCase { return createActionBuilder(actionTitle).build(); } + private Notification.Action createAction(String actionTitle, Intent intent) { + return createActionBuilder(actionTitle, intent).build(); + } + private List createActions(String[] actionTitles) { List actions = new ArrayList<>(); for (String title : actionTitles) {