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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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 @@ 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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
|
||||
@@ -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<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 @@ 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<Notification.Action> createActions(String[] actionTitles) {
|
||||
List<Notification.Action> actions = new ArrayList<>();
|
||||
for (String title : actionTitles) {
|
||||
|
||||
Reference in New Issue
Block a user