Merge "Remove Dependency#get from SmartReplyView"

This commit is contained in:
TreeHugger Robot
2020-10-08 02:48:54 +00:00
committed by Android (Google) Code Review
13 changed files with 842 additions and 646 deletions

View File

@@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfC
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.user.UserModule;
@@ -74,6 +75,7 @@ import dagger.Provides;
SensorModule.class,
SettingsModule.class,
SettingsUtilModule.class,
SmartRepliesInflationModule.class,
StatusBarPolicyModule.class,
SysUIConcurrencyModule.class,
TunerModule.class,
@@ -81,13 +83,15 @@ import dagger.Provides;
UtilModule.class,
VolumeModule.class
},
subcomponents = {StatusBarComponent.class,
NotificationRowComponent.class,
DozeComponent.class,
ExpandableNotificationRowComponent.class,
KeyguardBouncerComponent.class,
NotificationShelfComponent.class,
FragmentService.FragmentCreator.class})
subcomponents = {
StatusBarComponent.class,
NotificationRowComponent.class,
DozeComponent.class,
ExpandableNotificationRowComponent.class,
KeyguardBouncerComponent.class,
NotificationShelfComponent.class,
FragmentService.FragmentCreator.class
})
public abstract class SystemUIModule {
@Binds

View File

@@ -81,21 +81,27 @@ public class ExpandableNotificationRowController implements NodeController {
private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
@Inject
public ExpandableNotificationRowController(ExpandableNotificationRow view,
public ExpandableNotificationRowController(
ExpandableNotificationRow view,
NotificationListContainer listContainer,
ActivatableNotificationViewController activatableNotificationViewController,
NotificationMediaManager mediaManager, PluginManager pluginManager,
SystemClock clock, @AppName String appName, @NotificationKey String notificationKey,
NotificationMediaManager mediaManager,
PluginManager pluginManager,
SystemClock clock,
@AppName String appName,
@NotificationKey String notificationKey,
KeyguardBypassController keyguardBypassController,
GroupMembershipManager groupMembershipManager,
GroupExpansionManager groupExpansionManager,
RowContentBindStage rowContentBindStage,
NotificationLogger notificationLogger, HeadsUpManager headsUpManager,
NotificationLogger notificationLogger,
HeadsUpManager headsUpManager,
ExpandableNotificationRow.OnExpandClickListener onExpandClickListener,
StatusBarStateController statusBarStateController,
NotificationGutsManager notificationGutsManager,
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress,
OnUserInteractionCallback onUserInteractionCallback, FalsingManager falsingManager,
OnUserInteractionCallback onUserInteractionCallback,
FalsingManager falsingManager,
PeopleNotificationIdentifier peopleNotificationIdentifier) {
mView = view;
mListContainer = listContainer;

View File

@@ -42,17 +42,15 @@ import com.android.systemui.media.MediaDataManagerKt;
import com.android.systemui.media.MediaFeatureFlag;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.MediaNotificationProcessor;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflater;
import com.android.systemui.util.Assert;
import java.util.HashMap;
@@ -60,8 +58,6 @@ import java.util.concurrent.Executor;
import javax.inject.Inject;
import dagger.Lazy;
/**
* {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by
* asynchronously building the content's {@link RemoteViews} and applying it to the row.
@@ -76,27 +72,24 @@ public class NotificationContentInflater implements NotificationRowContentBinder
private final boolean mIsMediaInQS;
private final NotificationRemoteInputManager mRemoteInputManager;
private final NotifRemoteViewCache mRemoteViewCache;
private final Lazy<SmartReplyConstants> mSmartReplyConstants;
private final Lazy<SmartReplyController> mSmartReplyController;
private final ConversationNotificationProcessor mConversationProcessor;
private final Executor mBgExecutor;
private final SmartRepliesAndActionsInflater mSmartRepliesAndActionsInflater;
@Inject
NotificationContentInflater(
NotifRemoteViewCache remoteViewCache,
NotificationRemoteInputManager remoteInputManager,
Lazy<SmartReplyConstants> smartReplyConstants,
Lazy<SmartReplyController> smartReplyController,
ConversationNotificationProcessor conversationProcessor,
MediaFeatureFlag mediaFeatureFlag,
@Background Executor bgExecutor) {
@Background Executor bgExecutor,
SmartRepliesAndActionsInflater smartRepliesInflater) {
mRemoteViewCache = remoteViewCache;
mRemoteInputManager = remoteInputManager;
mSmartReplyConstants = smartReplyConstants;
mSmartReplyController = smartReplyController;
mConversationProcessor = conversationProcessor;
mIsMediaInQS = mediaFeatureFlag.getEnabled();
mBgExecutor = bgExecutor;
mSmartRepliesAndActionsInflater = smartRepliesInflater;
}
@Override
@@ -132,8 +125,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
contentToBind,
mRemoteViewCache,
entry,
mSmartReplyConstants.get(),
mSmartReplyController.get(),
mConversationProcessor,
row,
bindParams.isLowPriority,
@@ -141,7 +132,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
bindParams.usesIncreasedHeadsUpHeight,
callback,
mRemoteInputManager.getRemoteViewsOnClickHandler(),
mIsMediaInQS);
mIsMediaInQS,
mSmartRepliesAndActionsInflater);
if (mInflateSynchronously) {
task.onPostExecute(task.doInBackground());
} else {
@@ -157,17 +149,19 @@ public class NotificationContentInflater implements NotificationRowContentBinder
boolean inflateSynchronously,
@InflationFlag int reInflateFlags,
Notification.Builder builder,
Context packageContext) {
Context packageContext,
SmartRepliesAndActionsInflater smartRepliesInflater) {
InflationProgress result = createRemoteViews(reInflateFlags,
builder,
bindParams.isLowPriority,
bindParams.usesIncreasedHeight,
bindParams.usesIncreasedHeadsUpHeight,
packageContext);
result = inflateSmartReplyViews(result, reInflateFlags, entry,
row.getContext(), packageContext, row.getHeadsUpManager(),
mSmartReplyConstants.get(), mSmartReplyController.get(),
row.getExistingSmartRepliesAndActions());
row.getContext(), packageContext,
row.getExistingSmartRepliesAndActions(),
smartRepliesInflater);
apply(
mBgExecutor,
@@ -268,22 +262,21 @@ public class NotificationContentInflater implements NotificationRowContentBinder
}
}
private static InflationProgress inflateSmartReplyViews(InflationProgress result,
@InflationFlag int reInflateFlags, NotificationEntry entry, Context context,
Context packageContext, HeadsUpManager headsUpManager,
SmartReplyConstants smartReplyConstants, SmartReplyController smartReplyController,
SmartRepliesAndActions previousSmartRepliesAndActions) {
private static InflationProgress inflateSmartReplyViews(
InflationProgress result,
@InflationFlag int reInflateFlags,
NotificationEntry entry,
Context context,
Context packageContext,
SmartRepliesAndActions previousSmartRepliesAndActions,
SmartRepliesAndActionsInflater inflater) {
if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) {
result.expandedInflatedSmartReplies =
InflatedSmartReplies.inflate(
context, packageContext, entry, smartReplyConstants,
smartReplyController, headsUpManager, previousSmartRepliesAndActions);
result.expandedInflatedSmartReplies = inflater.inflateSmartReplies(
context, packageContext, entry, previousSmartRepliesAndActions);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) {
result.headsUpInflatedSmartReplies =
InflatedSmartReplies.inflate(
context, packageContext, entry, smartReplyConstants,
smartReplyController, headsUpManager, previousSmartRepliesAndActions);
result.headsUpInflatedSmartReplies = inflater.inflateSmartReplies(
context, packageContext, entry, previousSmartRepliesAndActions);
}
return result;
}
@@ -709,8 +702,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
private final boolean mUsesIncreasedHeadsUpHeight;
private final @InflationFlag int mReInflateFlags;
private final NotifRemoteViewCache mRemoteViewCache;
private final SmartReplyConstants mSmartReplyConstants;
private final SmartReplyController mSmartReplyController;
private final Executor mBgExecutor;
private ExpandableNotificationRow mRow;
private Exception mError;
@@ -718,6 +709,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
private CancellationSignal mCancellationSignal;
private final ConversationNotificationProcessor mConversationProcessor;
private final boolean mIsMediaInQS;
private final SmartRepliesAndActionsInflater mSmartRepliesInflater;
private AsyncInflationTask(
Executor bgExecutor,
@@ -725,8 +717,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
@InflationFlag int reInflateFlags,
NotifRemoteViewCache cache,
NotificationEntry entry,
SmartReplyConstants smartReplyConstants,
SmartReplyController smartReplyController,
ConversationNotificationProcessor conversationProcessor,
ExpandableNotificationRow row,
boolean isLowPriority,
@@ -734,15 +724,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder
boolean usesIncreasedHeadsUpHeight,
InflationCallback callback,
RemoteViews.OnClickHandler remoteViewClickHandler,
boolean isMediaFlagEnabled) {
boolean isMediaFlagEnabled,
SmartRepliesAndActionsInflater smartRepliesInflater) {
mEntry = entry;
mRow = row;
mSmartReplyConstants = smartReplyConstants;
mSmartReplyController = smartReplyController;
mBgExecutor = bgExecutor;
mInflateSynchronously = inflateSynchronously;
mReInflateFlags = reInflateFlags;
mRemoteViewCache = cache;
mSmartRepliesInflater = smartRepliesInflater;
mContext = mRow.getContext();
mIsLowPriority = isLowPriority;
mUsesIncreasedHeight = usesIncreasedHeight;
@@ -786,10 +776,16 @@ public class NotificationContentInflater implements NotificationRowContentBinder
InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
mUsesIncreasedHeadsUpHeight, packageContext);
return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mEntry,
mRow.getContext(), packageContext, mRow.getHeadsUpManager(),
mSmartReplyConstants, mSmartReplyController,
mRow.getExistingSmartRepliesAndActions());
SmartRepliesAndActions repliesAndActions =
mRow.getExistingSmartRepliesAndActions();
return inflateSmartReplyViews(
inflationProgress,
mReInflateFlags,
mEntry,
mContext,
packageContext,
repliesAndActions,
mSmartRepliesInflater);
} catch (Exception e) {
mError = e;
return null;

View File

@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewW
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflaterKt;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.SmartReplyView;
@@ -1191,23 +1192,31 @@ public class NotificationContentView extends FrameLayout {
View bigContentView = mExpandedChild;
if (bigContentView != null && (bigContentView instanceof ViewGroup)) {
mMediaTransferManager.applyMediaTransferView((ViewGroup) bigContentView,
entry);
mMediaTransferManager.applyMediaTransferView((ViewGroup) bigContentView, entry);
}
View smallContentView = mContractedChild;
if (smallContentView != null && (smallContentView instanceof ViewGroup)) {
mMediaTransferManager.applyMediaTransferView((ViewGroup) smallContentView,
entry);
mMediaTransferManager.applyMediaTransferView((ViewGroup) smallContentView, entry);
}
}
/**
* 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.
*/
public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
Notification notification = entry.getSbn().getNotification();
return null != notification.findRemoteInputActionPair(true /* freeform */);
}
private void applyRemoteInputAndSmartReply(final NotificationEntry entry) {
if (mRemoteInputController == null) {
return;
}
applyRemoteInput(entry, InflatedSmartReplies.hasFreeformRemoteInput(entry));
applyRemoteInput(entry, hasFreeformRemoteInput(entry));
if (mExpandedInflatedSmartReplies == null && mHeadsUpInflatedSmartReplies == null) {
if (DEBUG) {
@@ -1438,7 +1447,8 @@ public class NotificationContentView extends FrameLayout {
}
LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
if (!InflatedSmartReplies.shouldShowSmartReplyView(entry, smartRepliesAndActions)) {
if (!SmartRepliesAndActionsInflaterKt
.shouldShowSmartReplyView(entry, smartRepliesAndActions)) {
smartReplyContainer.setVisibility(View.GONE);
return null;
}

View File

@@ -19,27 +19,8 @@ package com.android.systemui.statusbar.policy;
import android.annotation.NonNull;
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.annotations.VisibleForTesting;
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.NotificationUiAdjustment;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -48,13 +29,11 @@ import java.util.List;
* thread, to later be accessed and modified on the (performance critical) UI thread.
*/
public class InflatedSmartReplies {
private static final String TAG = "InflatedSmartReplies";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@Nullable private final SmartReplyView mSmartReplyView;
@Nullable private final List<Button> mSmartSuggestionButtons;
@NonNull private final SmartRepliesAndActions mSmartRepliesAndActions;
private InflatedSmartReplies(
public InflatedSmartReplies(
@Nullable SmartReplyView smartReplyView,
@Nullable List<Button> smartSuggestionButtons,
@NonNull SmartRepliesAndActions smartRepliesAndActions) {
@@ -75,206 +54,6 @@ public class InflatedSmartReplies {
return mSmartRepliesAndActions;
}
/**
* Inflate a SmartReplyView and its smart suggestions.
*/
public static InflatedSmartReplies inflate(
Context context,
Context packageContext,
NotificationEntry entry,
SmartReplyConstants smartReplyConstants,
SmartReplyController smartReplyController,
HeadsUpManager headsUpManager,
SmartRepliesAndActions existingSmartRepliesAndActions) {
SmartRepliesAndActions newSmartRepliesAndActions =
chooseSmartRepliesAndActions(smartReplyConstants, entry);
if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
return new InflatedSmartReplies(null /* smartReplyView */,
null /* smartSuggestionButtons */, newSmartRepliesAndActions);
}
// Only block clicks if the smart buttons are different from the previous set - to avoid
// scenarios where a user incorrectly cannot click smart buttons because the notification is
// updated.
boolean delayOnClickListener =
!areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions);
SmartReplyView smartReplyView = SmartReplyView.inflate(context);
List<Button> suggestionButtons = new ArrayList<>();
if (newSmartRepliesAndActions.smartReplies != null) {
suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
delayOnClickListener));
}
if (newSmartRepliesAndActions.smartActions != null) {
suggestionButtons.addAll(
smartReplyView.inflateSmartActions(packageContext,
newSmartRepliesAndActions.smartActions, smartReplyController, entry,
headsUpManager, delayOnClickListener));
}
return new InflatedSmartReplies(smartReplyView, suggestionButtons,
newSmartRepliesAndActions);
}
@VisibleForTesting
static boolean areSuggestionsSimilar(
SmartRepliesAndActions left, SmartRepliesAndActions right) {
if (left == right) return true;
if (left == null || right == null) return false;
if (!left.getSmartReplies().equals(right.getSmartReplies())) {
return false;
}
return !NotificationUiAdjustment.areDifferent(
left.getSmartActions(), right.getSmartActions());
}
/**
* Returns whether we should show the smart reply view and its smart suggestions.
*/
public static boolean shouldShowSmartReplyView(
NotificationEntry entry,
SmartRepliesAndActions smartRepliesAndActions) {
if (smartRepliesAndActions.smartReplies == null
&& smartRepliesAndActions.smartActions == null) {
// There are no smart replies and no smart actions.
return false;
}
// If we are showing the spinner we don't want to add the buttons.
boolean showingSpinner = entry.getSbn().getNotification()
.extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
if (showingSpinner) {
return false;
}
// If we are keeping the notification around while sending we don't want to add the buttons.
boolean hideSmartReplies = entry.getSbn().getNotification()
.extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
if (hideSmartReplies) {
return false;
}
return true;
}
/**
* Chose what smart replies and smart actions to display. App generated suggestions take
* precedence. So if the app provides any smart replies, we don't show any
* replies or actions generated by the NotificationAssistantService (NAS), and if the app
* provides any smart actions we also don't show any NAS-generated replies or actions.
*/
@NonNull
public static SmartRepliesAndActions chooseSmartRepliesAndActions(
SmartReplyConstants smartReplyConstants,
final NotificationEntry entry) {
Notification notification = entry.getSbn().getNotification();
Pair<RemoteInput, Notification.Action> remoteInputActionPair =
notification.findRemoteInputActionPair(false /* freeform */);
Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
notification.findRemoteInputActionPair(true /* freeform */);
if (!smartReplyConstants.isEnabled()) {
if (DEBUG) {
Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
+ entry.getSbn().getKey());
}
return new SmartRepliesAndActions(null, null);
}
// Only use smart replies from the app if they target P or above. We have this check because
// the smart reply API has been used for other things (Wearables) in the past. The API to
// add smart actions is new in Q so it doesn't require a target-sdk check.
boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
|| entry.targetSdk >= Build.VERSION_CODES.P);
boolean appGeneratedSmartRepliesExist =
enableAppGeneratedSmartReplies
&& remoteInputActionPair != null
&& !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
&& remoteInputActionPair.second.actionIntent != null;
List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
SmartReplyView.SmartReplies smartReplies = null;
SmartReplyView.SmartActions smartActions = null;
if (appGeneratedSmartRepliesExist) {
smartReplies = new SmartReplyView.SmartReplies(
Arrays.asList(remoteInputActionPair.first.getChoices()),
remoteInputActionPair.first,
remoteInputActionPair.second.actionIntent,
false /* fromAssistant */);
}
if (appGeneratedSmartActionsExist) {
smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
false /* fromAssistant */);
}
// Apps didn't provide any smart replies / actions, use those from NAS (if any).
if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.getSmartReplies())
&& freeformRemoteInputActionPair != null
&& freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
&& freeformRemoteInputActionPair.second.actionIntent != null;
if (useGeneratedReplies) {
smartReplies = new SmartReplyView.SmartReplies(
entry.getSmartReplies(),
freeformRemoteInputActionPair.first,
freeformRemoteInputActionPair.second.actionIntent,
true /* fromAssistant */);
}
boolean useSmartActions = !ArrayUtils.isEmpty(entry.getSmartActions())
&& notification.getAllowSystemGeneratedContextualActions();
if (useSmartActions) {
List<Notification.Action> systemGeneratedActions =
entry.getSmartActions();
// 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(
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.
*/
public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
Notification notification = entry.getSbn().getNotification();
return null != notification.findRemoteInputActionPair(true /* freeform */);
}
/**
* A storage for smart replies and smart action.
*/

View File

@@ -0,0 +1,462 @@
/*
* Copyright (C) 2020 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.statusbar.policy
import android.app.Notification
import android.app.PendingIntent
import android.app.RemoteInput
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import android.widget.Button
import com.android.systemui.R
import com.android.systemui.plugins.ActivityStarter
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.NotificationRemoteInputManager
import com.android.systemui.statusbar.NotificationUiAdjustment
import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartButtonType
import com.android.systemui.statusbar.policy.SmartReplyView.SmartReplies
import javax.inject.Inject
/** Returns whether we should show the smart reply view and its smart suggestions. */
fun shouldShowSmartReplyView(
entry: NotificationEntry,
smartRepliesAndActions: SmartRepliesAndActions
): Boolean {
if (smartRepliesAndActions.smartReplies == null
&& smartRepliesAndActions.smartActions == null) {
// There are no smart replies and no smart actions.
return false
}
// If we are showing the spinner we don't want to add the buttons.
val showingSpinner = entry.sbn.notification.extras
.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false)
if (showingSpinner) {
return false
}
// If we are keeping the notification around while sending we don't want to add the buttons.
return !entry.sbn.notification.extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false)
}
/** Determines if two [SmartRepliesAndActions] are visually similar. */
fun areSuggestionsSimilar(
left: SmartRepliesAndActions?,
right: SmartRepliesAndActions?
): Boolean = when {
left === right -> true
left == null || right == null -> false
left.getSmartReplies() != right.getSmartReplies() -> false
else -> !NotificationUiAdjustment.areDifferent(left.getSmartActions(), right.getSmartActions())
}
interface SmartRepliesAndActionsInflater {
fun inflateSmartReplies(
sysuiContext: Context,
notifPackageContext: Context,
entry: NotificationEntry,
existingRepliesAndAction: SmartRepliesAndActions
): InflatedSmartReplies
}
/*internal*/ class SmartRepliesAndActionsInflaterImpl @Inject constructor(
private val constants: SmartReplyConstants,
private val activityManagerWrapper: ActivityManagerWrapper,
private val packageManagerWrapper: PackageManagerWrapper,
private val devicePolicyManagerWrapper: DevicePolicyManagerWrapper,
private val smartRepliesInflater: SmartReplyInflater,
private val smartActionsInflater: SmartActionInflater
) : SmartRepliesAndActionsInflater {
override fun inflateSmartReplies(
sysuiContext: Context,
notifPackageContext: Context,
entry: NotificationEntry,
existingRepliesAndAction: SmartRepliesAndActions
): InflatedSmartReplies {
val newRepliesAndActions = chooseSmartRepliesAndActions(entry)
if (!shouldShowSmartReplyView(entry, newRepliesAndActions)) {
return InflatedSmartReplies(
null /* smartReplyView */,
null /* smartSuggestionButtons */,
newRepliesAndActions)
}
// Only block clicks if the smart buttons are different from the previous set - to avoid
// scenarios where a user incorrectly cannot click smart buttons because the
// notification is updated.
val delayOnClickListener =
!areSuggestionsSimilar(existingRepliesAndAction, newRepliesAndActions)
val smartReplyView = SmartReplyView.inflate(sysuiContext, constants)
val smartReplies = newRepliesAndActions.smartReplies
smartReplyView.setSmartRepliesGeneratedByAssistant(smartReplies?.fromAssistant ?: false)
val smartReplyButtons = smartReplies?.let {
smartReplies.choices.asSequence().mapIndexed { index, choice ->
smartRepliesInflater.inflateReplyButton(
smartReplyView,
entry,
smartReplies,
index,
choice,
delayOnClickListener)
}
} ?: emptySequence()
val smartActionButtons = newRepliesAndActions.smartActions?.let { smartActions ->
val themedPackageContext =
ContextThemeWrapper(notifPackageContext, sysuiContext.theme)
smartActions.actions.asSequence()
.filter { it.actionIntent != null }
.mapIndexed { index, action ->
smartActionsInflater.inflateActionButton(
smartReplyView,
entry,
smartActions,
index,
action,
delayOnClickListener,
themedPackageContext)
}
} ?: emptySequence()
return InflatedSmartReplies(
smartReplyView,
(smartReplyButtons + smartActionButtons).toList(),
newRepliesAndActions)
}
/**
* Chose what smart replies and smart actions to display. App generated suggestions take
* precedence. So if the app provides any smart replies, we don't show any
* replies or actions generated by the NotificationAssistantService (NAS), and if the app
* provides any smart actions we also don't show any NAS-generated replies or actions.
*/
fun chooseSmartRepliesAndActions(entry: NotificationEntry): SmartRepliesAndActions {
val notification = entry.sbn.notification
val remoteInputActionPair = notification.findRemoteInputActionPair(false /* freeform */)
val freeformRemoteInputActionPair =
notification.findRemoteInputActionPair(true /* freeform */)
if (!constants.isEnabled) {
if (DEBUG) {
Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
+ entry.sbn.key)
}
return SmartRepliesAndActions(null, null)
}
// Only use smart replies from the app if they target P or above. We have this check because
// the smart reply API has been used for other things (Wearables) in the past. The API to
// add smart actions is new in Q so it doesn't require a target-sdk check.
val enableAppGeneratedSmartReplies = (!constants.requiresTargetingP()
|| entry.targetSdk >= Build.VERSION_CODES.P)
val appGeneratedSmartActions = notification.contextualActions
var smartReplies: SmartReplies? = when {
enableAppGeneratedSmartReplies -> remoteInputActionPair?.let { pair ->
pair.second.actionIntent?.let { actionIntent ->
if (pair.first.choices?.isNotEmpty() == true)
SmartReplies(
pair.first.choices.asList(),
pair.first,
actionIntent,
false /* fromAssistant */)
else null
}
}
else -> null
}
var smartActions: SmartActions? = when {
appGeneratedSmartActions.isNotEmpty() ->
SmartActions(appGeneratedSmartActions, false /* fromAssistant */)
else -> null
}
// Apps didn't provide any smart replies / actions, use those from NAS (if any).
if (smartReplies == null && smartActions == null) {
if (entry.smartReplies.isNotEmpty()
&& freeformRemoteInputActionPair != null
&& freeformRemoteInputActionPair.second.allowGeneratedReplies
&& freeformRemoteInputActionPair.second.actionIntent != null) {
smartReplies = SmartReplies(
entry.smartReplies,
freeformRemoteInputActionPair.first,
freeformRemoteInputActionPair.second.actionIntent,
true /* fromAssistant */)
}
if (entry.smartActions.isNotEmpty()
&& notification.allowSystemGeneratedContextualActions) {
val systemGeneratedActions: List<Notification.Action> = when {
activityManagerWrapper.isLockTaskKioskModeActive ->
// Filter actions if we're in kiosk-mode - we don't care about screen
// pinning mode, since notifications aren't shown there anyway.
filterAllowlistedLockTaskApps(entry.smartActions)
else -> entry.smartActions
}
smartActions = SmartActions(systemGeneratedActions, true /* fromAssistant */)
}
}
return SmartRepliesAndActions(smartReplies, smartActions)
}
/**
* Filter actions so that only actions pointing to allowlisted apps are permitted.
* This filtering is only meaningful when in lock-task mode.
*/
private fun filterAllowlistedLockTaskApps(
actions: List<Notification.Action>
): List<Notification.Action> = actions.filter { action ->
// Only allow actions that are explicit (implicit intents are not handled in lock-task
// mode), and link to allowlisted apps.
action.actionIntent?.intent?.let { intent ->
packageManagerWrapper.resolveActivity(intent, 0 /* flags */)
}?.let { resolveInfo ->
devicePolicyManagerWrapper.isLockTaskPermitted(resolveInfo.activityInfo.packageName)
} ?: false
}
}
interface SmartActionInflater {
fun inflateActionButton(
parent: ViewGroup,
entry: NotificationEntry,
smartActions: SmartActions,
actionIndex: Int,
action: Notification.Action,
delayOnClickListener: Boolean,
packageContext: Context
): Button
}
/* internal */ class SmartActionInflaterImpl @Inject constructor(
private val constants: SmartReplyConstants,
private val activityStarter: ActivityStarter,
private val smartReplyController: SmartReplyController,
private val headsUpManager: HeadsUpManager
) : SmartActionInflater {
override fun inflateActionButton(
parent: ViewGroup,
entry: NotificationEntry,
smartActions: SmartActions,
actionIndex: Int,
action: Notification.Action,
delayOnClickListener: Boolean,
packageContext: Context
): Button =
(LayoutInflater.from(parent.context)
.inflate(R.layout.smart_action_button, parent, false) as Button
).apply {
text = action.title
// We received the Icon from the application - so use the Context of the application to
// reference icon resources.
val iconDrawable = action.getIcon().loadDrawable(packageContext)
.apply {
val newIconSize: Int = context.resources.getDimensionPixelSize(
R.dimen.smart_action_button_icon_size)
setBounds(0, 0, newIconSize, newIconSize)
}
// Add the action icon to the Smart Action button.
setCompoundDrawables(iconDrawable, null, null, null)
val onClickListener = View.OnClickListener {
onSmartActionClick(entry, smartActions, actionIndex, action)
}
setOnClickListener(
if (delayOnClickListener)
DelayedOnClickListener(onClickListener, constants.onClickInitDelay)
else onClickListener)
// Mark this as an Action button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION
}
private fun onSmartActionClick(
entry: NotificationEntry,
smartActions: SmartActions,
actionIndex: Int,
action: Notification.Action
) =
activityStarter.startPendingIntentDismissingKeyguard(action.actionIntent, entry.row) {
smartReplyController
.smartActionClicked(entry, actionIndex, action, smartActions.fromAssistant)
headsUpManager.removeNotification(entry.key, true /* releaseImmediately */)
}
}
interface SmartReplyInflater {
fun inflateReplyButton(
parent: SmartReplyView,
entry: NotificationEntry,
smartReplies: SmartReplies,
replyIndex: Int,
choice: CharSequence,
delayOnClickListener: Boolean
): Button
}
class SmartReplyInflaterImpl @Inject constructor(
private val constants: SmartReplyConstants,
private val keyguardDismissUtil: KeyguardDismissUtil,
private val remoteInputManager: NotificationRemoteInputManager,
private val smartReplyController: SmartReplyController,
private val context: Context
) : SmartReplyInflater {
override fun inflateReplyButton(
parent: SmartReplyView,
entry: NotificationEntry,
smartReplies: SmartReplies,
replyIndex: Int,
choice: CharSequence,
delayOnClickListener: Boolean
): Button =
(LayoutInflater.from(parent.context)
.inflate(R.layout.smart_reply_button, parent, false) as Button
).apply {
text = choice
val onClickListener = View.OnClickListener {
onSmartReplyClick(
entry,
smartReplies,
replyIndex,
parent,
this,
choice)
}
setOnClickListener(
if (delayOnClickListener)
DelayedOnClickListener(onClickListener, constants.onClickInitDelay)
else onClickListener)
accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(host, info)
val label = parent.resources
.getString(R.string.accessibility_send_smart_reply)
val action = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)
info.addAction(action)
}
}
// TODO: probably shouldn't do this here, bad API
// Mark this as a Reply button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY
}
private fun onSmartReplyClick(
entry: NotificationEntry,
smartReplies: SmartReplies,
replyIndex: Int,
smartReplyView: SmartReplyView,
button: Button,
choice: CharSequence
) = keyguardDismissUtil.executeWhenUnlocked(!entry.isRowPinned) {
val canEditBeforeSend = constants.getEffectiveEditChoicesBeforeSending(
smartReplies.remoteInput.editChoicesBeforeSending)
if (canEditBeforeSend) {
remoteInputManager.activateRemoteInput(
button,
arrayOf(smartReplies.remoteInput),
smartReplies.remoteInput,
smartReplies.pendingIntent,
NotificationEntry.EditedSuggestionInfo(choice, replyIndex))
} else {
smartReplyController.smartReplySent(
entry,
replyIndex,
button.text,
NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(),
false /* modifiedBeforeSending */)
entry.setHasSentReply()
try {
val intent = createRemoteInputIntent(smartReplies, choice)
smartReplies.pendingIntent.send(context, 0, intent)
} catch (e: PendingIntent.CanceledException) {
Log.w(TAG, "Unable to send smart reply", e)
}
smartReplyView.hideSmartSuggestions()
}
false // do not defer
}
private fun createRemoteInputIntent(smartReplies: SmartReplies, choice: CharSequence): Intent {
val results = Bundle()
results.putString(smartReplies.remoteInput.resultKey, choice.toString())
val intent = Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
RemoteInput.addResultsToIntent(arrayOf(smartReplies.remoteInput), intent, results)
RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE)
return intent
}
}
/**
* An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of
* time.
*/
private class DelayedOnClickListener(
private val mActualListener: View.OnClickListener,
private val mInitDelayMs: Long
) : View.OnClickListener {
private val mInitTimeMs = SystemClock.elapsedRealtime()
override fun onClick(v: View) {
if (hasFinishedInitialization()) {
mActualListener.onClick(v)
} else {
Log.i(TAG, "Accidental Smart Suggestion click registered, delay: $mInitDelayMs")
}
}
private fun hasFinishedInitialization(): Boolean =
SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs
}
private const val TAG = "SmartReplyViewInflater"
private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
// convenience function that swaps parameter order so that lambda can be placed at the end
private fun KeyguardDismissUtil.executeWhenUnlocked(
requiresShadeOpen: Boolean,
onDismissAction: () -> Boolean
) = executeWhenUnlocked(onDismissAction, requiresShadeOpen)
// convenience function that swaps parameter order so that lambda can be placed at the end
private fun ActivityStarter.startPendingIntentDismissingKeyguard(
intent: PendingIntent,
associatedView: View?,
runnable: () -> Unit
) = startPendingIntentDismissingKeyguard(intent, runnable::invoke, associatedView)

View File

@@ -6,7 +6,6 @@ import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -15,34 +14,20 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.Layout;
import android.text.TextPaint;
import android.text.method.TransformationMethod;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import java.text.BreakIterator;
import java.util.ArrayList;
@@ -64,10 +49,6 @@ public class SmartReplyView extends ViewGroup {
private static final int SQUEEZE_FAILED = -1;
private final SmartReplyConstants mConstants;
private final KeyguardDismissUtil mKeyguardDismissUtil;
private final NotificationRemoteInputManager mRemoteInputManager;
/**
* The upper bound for the height of this view in pixels. Notifications are automatically
* recreated on density or font size changes so caching this should be fine.
@@ -98,30 +79,25 @@ public class SmartReplyView extends ViewGroup {
*/
private boolean mSmartRepliesGeneratedByAssistant = false;
@ColorInt
private int mCurrentBackgroundColor;
@ColorInt
private final int mDefaultBackgroundColor;
@ColorInt
private final int mDefaultStrokeColor;
@ColorInt
private final int mDefaultTextColor;
@ColorInt
private final int mDefaultTextColorDarkBg;
@ColorInt
private final int mRippleColorDarkBg;
@ColorInt
private final int mRippleColor;
@ColorInt private int mCurrentBackgroundColor;
@ColorInt private final int mDefaultBackgroundColor;
@ColorInt private final int mDefaultStrokeColor;
@ColorInt private final int mDefaultTextColor;
@ColorInt private final int mDefaultTextColorDarkBg;
@ColorInt private final int mRippleColorDarkBg;
@ColorInt private final int mRippleColor;
private final int mStrokeWidth;
private final double mMinStrokeContrast;
private ActivityStarter mActivityStarter;
@ColorInt private int mCurrentStrokeColor;
@ColorInt private int mCurrentTextColor;
@ColorInt private int mCurrentRippleColor;
private int mMaxSqueezeRemeasureAttempts;
private int mMaxNumActions;
private int mMinNumSystemGeneratedReplies;
public SmartReplyView(Context context, AttributeSet attrs) {
super(context, attrs);
mConstants = Dependency.get(SmartReplyConstants.class);
mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
R.dimen.smart_reply_button_max_height);
@@ -171,6 +147,18 @@ public class SmartReplyView extends ViewGroup {
reallocateCandidateButtonQueueForSqueezing();
}
/**
* Inflate an instance of this class.
*/
public static SmartReplyView inflate(Context context, SmartReplyConstants constants) {
SmartReplyView view = (SmartReplyView) LayoutInflater.from(context).inflate(
R.layout.smart_reply_view, null /* root */);
view.setMaxNumActions(constants.getMaxNumActions());
view.setMaxSqueezeRemeasureAttempts(constants.getMaxSqueezeRemeasureAttempts());
view.setMinNumSystemGeneratedReplies(constants.getMinNumSystemGeneratedReplies());
return view;
}
/**
* Returns an upper bound for the height of this view in pixels. This method is intended to be
* invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
@@ -197,174 +185,25 @@ public class SmartReplyView extends ViewGroup {
mCurrentBackgroundColor = mDefaultBackgroundColor;
}
/**
* Add buttons to the {@link SmartReplyView} - these buttons must have been preinflated using
* one of the methods in this class.
*/
/** Add buttons to the {@link SmartReplyView} */
public void addPreInflatedButtons(List<Button> smartSuggestionButtons) {
for (Button button : smartSuggestionButtons) {
addView(button);
setButtonColors(button);
}
reallocateCandidateButtonQueueForSqueezing();
}
/**
* Add smart replies to this view, using the provided {@link RemoteInput} and
* {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit
* into the notification are shown.
*/
public List<Button> inflateRepliesFromRemoteInput(
@NonNull SmartReplies smartReplies,
SmartReplyController smartReplyController, NotificationEntry entry,
boolean delayOnClickListener) {
List<Button> buttons = new ArrayList<>();
if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
if (smartReplies.choices != null) {
for (int i = 0; i < smartReplies.choices.size(); ++i) {
buttons.add(inflateReplyButton(
this, getContext(), i, smartReplies, smartReplyController, entry,
delayOnClickListener));
}
this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
}
}
return buttons;
public void setMaxNumActions(int maxNumActions) {
mMaxNumActions = maxNumActions;
}
/**
* Add smart actions to be shown next to smart replies. Only the actions that fit into the
* notification are shown.
*/
public List<Button> inflateSmartActions(Context packageContext,
@NonNull SmartActions smartActions, SmartReplyController smartReplyController,
NotificationEntry entry, HeadsUpManager headsUpManager, boolean delayOnClickListener) {
Context themedPackageContext = new ContextThemeWrapper(packageContext, mContext.getTheme());
List<Button> buttons = new ArrayList<>();
int numSmartActions = smartActions.actions.size();
for (int n = 0; n < numSmartActions; n++) {
Notification.Action action = smartActions.actions.get(n);
if (action.actionIntent != null) {
buttons.add(inflateActionButton(
this, getContext(), themedPackageContext, n, smartActions,
smartReplyController,
entry, headsUpManager, delayOnClickListener));
}
}
return buttons;
public void setMinNumSystemGeneratedReplies(int minNumSystemGeneratedReplies) {
mMinNumSystemGeneratedReplies = minNumSystemGeneratedReplies;
}
/**
* Inflate an instance of this class.
*/
public static SmartReplyView inflate(Context context) {
return (SmartReplyView) LayoutInflater.from(context).inflate(
R.layout.smart_reply_view, null /* root */);
}
@VisibleForTesting
static Button inflateReplyButton(SmartReplyView smartReplyView, Context context,
int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController,
NotificationEntry entry, boolean useDelayedOnClickListener) {
Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, smartReplyView, false);
CharSequence choice = smartReplies.choices.get(replyIndex);
b.setText(choice);
OnDismissAction action = () -> {
if (smartReplyView.mConstants.getEffectiveEditChoicesBeforeSending(
smartReplies.remoteInput.getEditChoicesBeforeSending())) {
EditedSuggestionInfo editedSuggestionInfo =
new EditedSuggestionInfo(choice, replyIndex);
smartReplyView.mRemoteInputManager.activateRemoteInput(b,
new RemoteInput[] { smartReplies.remoteInput }, smartReplies.remoteInput,
smartReplies.pendingIntent, editedSuggestionInfo);
return false;
}
smartReplyController.smartReplySent(entry, replyIndex, b.getText(),
NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(),
false /* modifiedBeforeSending */);
Bundle results = new Bundle();
results.putString(smartReplies.remoteInput.getResultKey(), choice.toString());
Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
RemoteInput.addResultsToIntent(new RemoteInput[] { smartReplies.remoteInput }, intent,
results);
RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
entry.setHasSentReply();
try {
smartReplies.pendingIntent.send(context, 0, intent);
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Unable to send smart reply", e);
}
// Note that as inflateReplyButton is called mSmartReplyContainer is null, but when the
// reply Button is added to the SmartReplyView mSmartReplyContainer will be set. So, it
// will not be possible for a user to trigger this on-click-listener without
// mSmartReplyContainer being set.
smartReplyView.mSmartReplyContainer.setVisibility(View.GONE);
return false; // do not defer
};
OnClickListener onClickListener = view ->
smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action, !entry.isRowPinned());
if (useDelayedOnClickListener) {
onClickListener = new DelayedOnClickListener(onClickListener,
smartReplyView.mConstants.getOnClickInitDelay());
}
b.setOnClickListener(onClickListener);
b.setAccessibilityDelegate(new AccessibilityDelegate() {
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
String label = smartReplyView.getResources().getString(
R.string.accessibility_send_smart_reply);
info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
}
});
SmartReplyView.setButtonColors(b, smartReplyView.mCurrentBackgroundColor,
smartReplyView.mDefaultStrokeColor, smartReplyView.mDefaultTextColor,
smartReplyView.mRippleColor, smartReplyView.mStrokeWidth);
return b;
}
@VisibleForTesting
static Button inflateActionButton(SmartReplyView smartReplyView, Context context,
Context packageContext, int actionIndex, SmartActions smartActions,
SmartReplyController smartReplyController, NotificationEntry entry,
HeadsUpManager headsUpManager, boolean useDelayedOnClickListener) {
Notification.Action action = smartActions.actions.get(actionIndex);
Button button = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_action_button, smartReplyView, false);
button.setText(action.title);
// We received the Icon from the application - so use the Context of the application to
// reference icon resources.
Drawable iconDrawable = action.getIcon().loadDrawable(packageContext);
// Add the action icon to the Smart Action button.
int newIconSize = context.getResources().getDimensionPixelSize(
R.dimen.smart_action_button_icon_size);
iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
button.setCompoundDrawables(iconDrawable, null, null, null);
OnClickListener onClickListener = view ->
smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
action.actionIntent,
() -> {
smartReplyController.smartActionClicked(
entry, actionIndex, action, smartActions.fromAssistant);
headsUpManager.removeNotification(entry.getKey(), true);
}, entry.getRow());
if (useDelayedOnClickListener) {
onClickListener = new DelayedOnClickListener(onClickListener,
smartReplyView.mConstants.getOnClickInitDelay());
}
button.setOnClickListener(onClickListener);
// Mark this as an Action button
final LayoutParams lp = (LayoutParams) button.getLayoutParams();
lp.buttonType = SmartButtonType.ACTION;
return button;
public void setMaxSqueezeRemeasureAttempts(int maxSqueezeRemeasureAttempts) {
mMaxSqueezeRemeasureAttempts = maxSqueezeRemeasureAttempts;
}
@Override
@@ -416,13 +255,13 @@ public class SmartReplyView extends ViewGroup {
// reply button is added.
SmartSuggestionMeasures actionsMeasures = null;
final int maxNumActions = mConstants.getMaxNumActions();
final int maxNumActions = mMaxNumActions;
int numShownActions = 0;
for (View child : smartSuggestions) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (maxNumActions != -1 // -1 means 'no limit'
&& lp.buttonType == SmartButtonType.ACTION
&& lp.mButtonType == SmartButtonType.ACTION
&& numShownActions >= maxNumActions) {
// We've reached the maximum number of actions, don't add another one!
continue;
@@ -446,7 +285,7 @@ public class SmartReplyView extends ViewGroup {
// Remember the current measurements in case the current button doesn't fit in.
SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) {
if (actionsMeasures == null && lp.mButtonType == SmartButtonType.REPLY) {
// We've added all actions (we go through actions first), now add their
// measurements.
actionsMeasures = accumulatedMeasures.clone();
@@ -510,7 +349,7 @@ public class SmartReplyView extends ViewGroup {
lp.show = true;
displayedChildCount++;
if (lp.buttonType == SmartButtonType.ACTION) {
if (lp.mButtonType == SmartButtonType.ACTION) {
numShownActions++;
}
}
@@ -551,6 +390,19 @@ public class SmartReplyView extends ViewGroup {
resolveSize(buttonHeight, heightMeasureSpec));
}
// TODO: this should be replaced, and instead, setMinSystemGenerated... should be invoked
// with MAX_VALUE if mSmartRepliesGeneratedByAssistant would be false (essentially, this is a
// ViewModel decision, as opposed to a View decision)
void setSmartRepliesGeneratedByAssistant(boolean fromAssistant) {
mSmartRepliesGeneratedByAssistant = fromAssistant;
}
void hideSmartSuggestions() {
if (mSmartReplyContainer != null) {
mSmartReplyContainer.setVisibility(View.GONE);
}
}
/**
* Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
* on which suggestions are added.
@@ -577,6 +429,7 @@ public class SmartReplyView extends ViewGroup {
* Returns whether our notification contains at least N smart replies (or 0) where N is
* determined by {@link SmartReplyConstants}.
*/
// TODO: we probably sholdn't make this deliberation in the View
private boolean gotEnoughSmartReplies(List<View> smartReplies) {
int numShownReplies = 0;
for (View smartReplyButton : smartReplies) {
@@ -585,8 +438,7 @@ public class SmartReplyView extends ViewGroup {
numShownReplies++;
}
}
if (numShownReplies == 0
|| numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) {
if (numShownReplies == 0 || numShownReplies >= mMinNumSystemGeneratedReplies) {
// We have enough replies, yay!
return true;
}
@@ -602,7 +454,7 @@ public class SmartReplyView extends ViewGroup {
if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
continue;
}
if (lp.buttonType == buttonType) {
if (lp.mButtonType == buttonType) {
actions.add(child);
}
}
@@ -656,7 +508,7 @@ public class SmartReplyView extends ViewGroup {
// See if there's a better line-break point (leading to a more narrow button) in
// either left or right direction.
final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
final int maxSqueezeRemeasureAttempts = mMaxSqueezeRemeasureAttempts;
for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
final int newPosition =
moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
@@ -833,41 +685,38 @@ public class SmartReplyView extends ViewGroup {
final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
int textColor = ContrastColorUtil.ensureTextContrast(
mCurrentTextColor = ContrastColorUtil.ensureTextContrast(
dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
backgroundColor | 0xff000000, dark);
int strokeColor = ContrastColorUtil.ensureContrast(
mCurrentStrokeColor = ContrastColorUtil.ensureContrast(
mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
mCurrentRippleColor = dark ? mRippleColorDarkBg : mRippleColor;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final Button child = (Button) getChildAt(i);
setButtonColors(child, backgroundColor, strokeColor, textColor, rippleColor,
mStrokeWidth);
setButtonColors((Button) getChildAt(i));
}
}
private static void setButtonColors(Button button, int backgroundColor, int strokeColor,
int textColor, int rippleColor, int strokeWidth) {
private void setButtonColors(Button button) {
Drawable drawable = button.getBackground();
if (drawable instanceof RippleDrawable) {
// Mutate in case other notifications are using this drawable.
drawable = drawable.mutate();
RippleDrawable ripple = (RippleDrawable) drawable;
ripple.setColor(ColorStateList.valueOf(rippleColor));
ripple.setColor(ColorStateList.valueOf(mCurrentRippleColor));
Drawable inset = ripple.getDrawable(0);
if (inset instanceof InsetDrawable) {
Drawable background = ((InsetDrawable) inset).getDrawable();
if (background instanceof GradientDrawable) {
GradientDrawable gradientDrawable = (GradientDrawable) background;
gradientDrawable.setColor(backgroundColor);
gradientDrawable.setStroke(strokeWidth, strokeColor);
gradientDrawable.setColor(mCurrentBackgroundColor);
gradientDrawable.setStroke(mStrokeWidth, mCurrentStrokeColor);
}
}
button.setBackground(drawable);
}
button.setTextColor(textColor);
button.setTextColor(mCurrentTextColor);
}
private void setCornerRadius(Button button, float radius) {
@@ -887,14 +736,7 @@ public class SmartReplyView extends ViewGroup {
}
}
private ActivityStarter getActivityStarter() {
if (mActivityStarter == null) {
mActivityStarter = Dependency.get(ActivityStarter.class);
}
return mActivityStarter;
}
private enum SmartButtonType {
enum SmartButtonType {
REPLY,
ACTION
}
@@ -924,7 +766,7 @@ public class SmartReplyView extends ViewGroup {
private boolean show = false;
private int squeezeStatus = SQUEEZE_STATUS_NONE;
private SmartButtonType buttonType = SmartButtonType.REPLY;
SmartButtonType mButtonType = SmartButtonType.REPLY;
private LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
@@ -975,32 +817,4 @@ public class SmartReplyView extends ViewGroup {
this.fromAssistant = fromAssistant;
}
}
/**
* An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of
* time.
*/
private static class DelayedOnClickListener implements OnClickListener {
private final OnClickListener mActualListener;
private final long mInitDelayMs;
private final long mInitTimeMs;
DelayedOnClickListener(OnClickListener actualOnClickListener, long initDelayMs) {
mActualListener = actualOnClickListener;
mInitDelayMs = initDelayMs;
mInitTimeMs = SystemClock.elapsedRealtime();
}
public void onClick(View v) {
if (hasFinishedInitialization()) {
mActualListener.onClick(v);
} else {
Log.i(TAG, "Accidental Smart Suggestion click registered, delay: " + mInitDelayMs);
}
}
private boolean hasFinishedInitialization() {
return SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs;
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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.statusbar.policy.dagger
import com.android.systemui.statusbar.policy.SmartActionInflater
import com.android.systemui.statusbar.policy.SmartActionInflaterImpl
import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflater
import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflaterImpl
import com.android.systemui.statusbar.policy.SmartReplyInflater
import com.android.systemui.statusbar.policy.SmartReplyInflaterImpl
import dagger.Binds
import dagger.Module
@Module
interface SmartRepliesInflationModule {
@Binds fun bindSmartActionsInflater(impl: SmartActionInflaterImpl): SmartActionInflater
@Binds fun bindSmartReplyInflater(impl: SmartReplyInflaterImpl): SmartReplyInflater
@Binds fun bindsInflatedSmartRepliesProvider(
impl: SmartRepliesAndActionsInflaterImpl
): SmartRepliesAndActionsInflater
}

View File

@@ -54,13 +54,13 @@ import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.MediaFeatureFlag;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflater;
import org.junit.Assert;
import org.junit.Before;
@@ -87,6 +87,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
@Mock private NotifRemoteViewCache mCache;
@Mock private ConversationNotificationProcessor mConversationNotificationProcessor;
@Mock private InflatedSmartReplies mInflatedSmartReplies;
private final SmartRepliesAndActionsInflater mSmartRepliesAndActionsInflater =
(sysuiContext, notifPackageContext, entry, existingRepliesAndAction) ->
mInflatedSmartReplies;
@Before
public void setUp() throws Exception {
@@ -103,16 +108,13 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
ExpandableNotificationRow row = helper.createRow(mBuilder.build());
mRow = spy(row);
final SmartReplyConstants smartReplyConstants = mock(SmartReplyConstants.class);
final SmartReplyController smartReplyController = mock(SmartReplyController.class);
mNotificationInflater = new NotificationContentInflater(
mCache,
mock(NotificationRemoteInputManager.class),
() -> smartReplyConstants,
() -> smartReplyController,
mConversationNotificationProcessor,
mock(MediaFeatureFlag.class),
mock(Executor.class));
mock(Executor.class),
mSmartRepliesAndActionsInflater);
}
@Test
@@ -120,13 +122,15 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
BindParams params = new BindParams();
params.usesIncreasedHeadsUpHeight = true;
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(mRow.getEntry(),
mNotificationInflater.inflateNotificationViews(
mRow.getEntry(),
mRow,
params,
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
mContext,
mSmartRepliesAndActionsInflater);
verify(builder).createHeadsUpContentView(true);
}
@@ -135,13 +139,15 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
BindParams params = new BindParams();
params.usesIncreasedHeight = true;
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(mRow.getEntry(),
mNotificationInflater.inflateNotificationViews(
mRow.getEntry(),
mRow,
params,
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
mContext,
mSmartRepliesAndActionsInflater);
verify(builder).createContentView(true);
}
@@ -366,7 +372,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
}
}
private class AsyncFailRemoteView extends RemoteViews {
private static class AsyncFailRemoteView extends RemoteViews {
Handler mHandler = Handler.createAsync(Looper.getMainLooper());
public AsyncFailRemoteView(String packageName, int layoutId) {

View File

@@ -79,7 +79,7 @@ import com.android.systemui.statusbar.notification.row.dagger.NotificationRowCom
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.leak.LeakDetector;
import com.android.systemui.util.time.FakeSystemClock;
@@ -138,6 +138,7 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase {
@Mock private ActivatableNotificationViewController mActivatableNotificationViewController;
@Mock private NotificationRowComponent.Builder mNotificationRowComponentBuilder;
@Mock private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
@Mock private InflatedSmartReplies mInflatedSmartReplies;
private StatusBarNotification mSbn;
private NotificationListenerService.RankingMap mRankingMap;
@@ -199,11 +200,11 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase {
NotificationContentInflater binder = new NotificationContentInflater(
cache,
mRemoteInputManager,
() -> mock(SmartReplyConstants.class),
() -> mock(SmartReplyController.class),
mock(ConversationNotificationProcessor.class),
mock(MediaFeatureFlag.class),
mBgExecutor);
mBgExecutor,
(sysuiContext, notifPackageContext, entry, existingRepliesAndAction) ->
mInflatedSmartReplies);
mRowContentBindStage = new RowContentBindStage(
binder,
mock(NotifInflationErrorManager.class),

View File

@@ -53,7 +53,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -69,7 +68,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import org.mockito.ArgumentCaptor;
@@ -134,11 +133,11 @@ public class NotificationTestHelper {
NotificationContentInflater contentBinder = new NotificationContentInflater(
mock(NotifRemoteViewCache.class),
mock(NotificationRemoteInputManager.class),
() -> mock(SmartReplyConstants.class),
() -> mock(SmartReplyController.class),
mock(ConversationNotificationProcessor.class),
mock(MediaFeatureFlag.class),
mock(Executor.class));
mock(Executor.class),
(sysuiContext, notifPackageContext, entry, existingRepliesAndAction) ->
mock(InflatedSmartReplies.class));
contentBinder.setInflateSynchronously(true);
mBindStage = new RowContentBindStage(contentBinder,
mock(NotifInflationErrorManager.class),

View File

@@ -67,16 +67,20 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
private static final Intent WHITELISTED_TEST_INTENT =
new Intent("com.android.WHITELISTED_TEST_ACTION");
@Mock SmartReplyConstants mSmartReplyConstants;
@Mock Notification mNotification;
NotificationEntry mEntry;
@Mock RemoteInput mRemoteInput;
@Mock RemoteInput mFreeFormRemoteInput;
@Mock ActivityManagerWrapper mActivityManagerWrapper;
@Mock PackageManagerWrapper mPackageManagerWrapper;
@Mock DevicePolicyManagerWrapper mDevicePolicyManagerWrapper;
@Mock private SmartReplyConstants mSmartReplyConstants;
@Mock private Notification mNotification;
@Mock private RemoteInput mRemoteInput;
@Mock private RemoteInput mFreeFormRemoteInput;
@Mock private ActivityManagerWrapper mActivityManagerWrapper;
@Mock private PackageManagerWrapper mPackageManagerWrapper;
@Mock private DevicePolicyManagerWrapper mDevicePolicyManagerWrapper;
@Mock private SmartRepliesAndActions mSmartRepliesAndActions;
@Mock private SmartReplyInflater mSmartReplyInflater;
@Mock private SmartActionInflater mSmartActionInflater;
private Icon mActionIcon;
private NotificationEntry mEntry;
private SmartRepliesAndActionsInflaterImpl mSmartRepliesInflater;
@Before
@UiThreadTest
@@ -96,6 +100,14 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
when(mActivityManagerWrapper.isLockTaskKioskModeActive()).thenReturn(false);
mSmartRepliesInflater = new SmartRepliesAndActionsInflaterImpl(
mSmartReplyConstants,
mActivityManagerWrapper,
mPackageManagerWrapper,
mDevicePolicyManagerWrapper,
mSmartReplyInflater,
mSmartActionInflater);
}
@Test
@@ -107,7 +119,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
when(mSmartReplyConstants.isEnabled()).thenReturn(false);
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions).isNull();
@@ -123,7 +135,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
when(mSmartReplyConstants.isEnabled()).thenReturn(false);
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions).isNull();
@@ -135,7 +147,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
setupAppGeneratedReplies(smartReplies);
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(Arrays.asList(smartReplies));
assertThat(repliesAndActions.smartReplies.fromAssistant).isFalse();
@@ -150,7 +162,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
setupAppGeneratedSuggestions(smartReplies, smartActions);
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(Arrays.asList(smartReplies));
assertThat(repliesAndActions.smartReplies.fromAssistant).isFalse();
@@ -169,7 +181,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(
mEntry.getSmartReplies());
@@ -187,7 +199,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.setSmartReplies(createReplies("Sys Smart Reply 1", "Sys Smart Reply 2"))
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions).isNull();
@@ -202,8 +214,9 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
modifyRanking(mEntry)
.setSmartActions(createActions("Sys Smart Action 1", "Sys Smart Action 2"))
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies).isNull();
assertThat(repliesAndActions.smartActions.actions)
@@ -226,7 +239,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies.choices)
.isEqualTo(Arrays.asList(appGenSmartReplies));
@@ -248,7 +261,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartActions).isNull();
assertThat(repliesAndActions.smartReplies).isNull();
@@ -270,7 +283,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(
mEntry.getSmartReplies());
@@ -306,7 +319,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
// Only the action for the whitelisted package should be allowed.
assertThat(repliesAndActions.smartActions.actions.size()).isEqualTo(1);
@@ -329,7 +342,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
.build();
SmartRepliesAndActions repliesAndActions =
InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
mSmartRepliesInflater.chooseSmartRepliesAndActions(mEntry);
// We don't restrict replies or actions in screen pinning mode.
assertThat(repliesAndActions.smartReplies.choices).isEqualTo(
@@ -356,8 +369,10 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
new SmartActions(rightActions, false /* fromAssistant */));
assertThat(InflatedSmartReplies.areSuggestionsSimilar(
leftRepliesAndActions, rightRepliesAndActions)).isTrue();
assertThat(
SmartRepliesAndActionsInflaterKt
.areSuggestionsSimilar(leftRepliesAndActions, rightRepliesAndActions))
.isTrue();
}
@Test
@@ -378,7 +393,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
new SmartActions(rightActions, false /* fromAssistant */));
assertThat(InflatedSmartReplies.areSuggestionsSimilar(
assertThat(SmartRepliesAndActionsInflaterKt.areSuggestionsSimilar(
leftRepliesAndActions, rightRepliesAndActions)).isFalse();
}
@@ -400,7 +415,7 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
new SmartActions(rightActions, false /* fromAssistant */));
assertThat(InflatedSmartReplies.areSuggestionsSimilar(
assertThat(SmartRepliesAndActionsInflaterKt.areSuggestionsSimilar(
leftRepliesAndActions, rightRepliesAndActions)).isFalse();
}

View File

@@ -70,6 +70,12 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import kotlin.sequences.Sequence;
import kotlin.sequences.SequencesKt;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -98,31 +104,32 @@ public class SmartReplyViewTest extends SysuiTestCase {
private int mDoubleLinePaddingHorizontal;
private int mSpacing;
@Mock private SmartReplyController mLogger;
private NotificationEntry mEntry;
private Notification mNotification;
@Mock private SmartReplyConstants mConstants;
@Mock ActivityStarter mActivityStarter;
@Mock HeadsUpManager mHeadsUpManager;
private SmartReplyInflaterImpl mSmartReplyInflater;
private SmartActionInflaterImpl mSmartActionInflater;
@Mock private SmartReplyConstants mConstants;
@Mock private ActivityStarter mActivityStarter;
@Mock private HeadsUpManager mHeadsUpManager;
@Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
@Mock private SmartReplyController mSmartReplyController;
private final KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mReceiver = new BlockingQueueIntentReceiver();
mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION));
mDependency.get(KeyguardDismissUtil.class).setDismissHandler((action, unused) -> {
action.onDismiss();
});
mKeyguardDismissUtil.setDismissHandler((action, unused) -> action.onDismiss());
mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
mDependency.injectMockDependency(ShadeController.class);
mDependency.injectMockDependency(NotificationRemoteInputManager.class);
mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter);
mDependency.injectTestDependency(SmartReplyConstants.class, mConstants);
mContainer = new View(mContext, null);
mView = SmartReplyView.inflate(mContext);
// Any number of replies are fine.
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0);
when(mConstants.getMaxSqueezeRemeasureAttempts()).thenReturn(3);
@@ -130,6 +137,9 @@ public class SmartReplyViewTest extends SysuiTestCase {
// Ensure there's no delay before we can click smart suggestions.
when(mConstants.getOnClickInitDelay()).thenReturn(0L);
mContainer = new View(mContext, null);
mView = SmartReplyView.inflate(mContext, mConstants);
final Resources res = mContext.getResources();
mSingleLinePaddingHorizontal = res.getDimensionPixelSize(
R.dimen.smart_reply_button_padding_horizontal_single_line);
@@ -147,6 +157,18 @@ public class SmartReplyViewTest extends SysuiTestCase {
.build();
mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
mSmartReplyInflater = new SmartReplyInflaterImpl(
mConstants,
mKeyguardDismissUtil,
mNotificationRemoteInputManager,
mSmartReplyController,
mContext);
mSmartActionInflater = new SmartActionInflaterImpl(
mConstants,
mActivityStarter,
mSmartReplyController,
mHeadsUpManager);
}
@After
@@ -168,7 +190,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
@Test
public void testSendSmartReply_keyguardCancelled() throws InterruptedException {
mDependency.get(KeyguardDismissUtil.class).setDismissHandler((action, unused) -> {});
mKeyguardDismissUtil.setDismissHandler((action, unused) -> { });
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -179,9 +201,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
@Test
public void testSendSmartReply_waitsForKeyguard() throws InterruptedException {
AtomicReference<OnDismissAction> actionRef = new AtomicReference<>();
mDependency.get(KeyguardDismissUtil.class).setDismissHandler((action, unused) -> {
actionRef.set(action);
});
mKeyguardDismissUtil.setDismissHandler((action, unused) -> actionRef.set(action));
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -202,7 +223,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
public void testSendSmartReply_controllerCalled() {
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
verify(mLogger).smartReplySent(mEntry, 2, TEST_CHOICES[2],
verify(mSmartReplyController).smartReplySent(mEntry, 2, TEST_CHOICES[2],
MetricsEvent.LOCATION_UNKNOWN, false /* modifiedBeforeSending */);
}
@@ -461,24 +482,28 @@ public class SmartReplyViewTest extends SysuiTestCase {
private void setSmartReplies(CharSequence[] choices, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
List<Button> replyButtons = inflateSmartReplies(choices, false /* fromAssistant */,
useDelayedOnClickListener);
List<Button> replyButtons =
inflateSmartReplies(
choices, false /* fromAssistant */, useDelayedOnClickListener)
.collect(Collectors.toList());
mView.addPreInflatedButtons(replyButtons);
}
private List<Button> inflateSmartReplies(CharSequence[] choices, boolean fromAssistant,
boolean useDelayedOnClickListener) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0);
private SmartReplyView.SmartReplies createSmartReplies(CharSequence[] choices,
boolean fromAssistant) {
PendingIntent pendingIntent =
PendingIntent.getBroadcast(mContext, 0, new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(
Arrays.asList(choices),
input,
pendingIntent,
fromAssistant);
return mView.inflateRepliesFromRemoteInput(smartReplies, mLogger, mEntry,
useDelayedOnClickListener);
return new SmartReplyView.SmartReplies(
Arrays.asList(choices), input, pendingIntent, fromAssistant);
}
private Stream<Button> inflateSmartReplies(CharSequence[] choices, boolean fromAssistant,
boolean useDelayedOnClickListener) {
SmartReplyView.SmartReplies smartReplies = createSmartReplies(choices, fromAssistant);
return IntStream.range(0, choices.length).mapToObj(idx ->
mSmartReplyInflater.inflateReplyButton(
mView, mEntry, smartReplies, idx, choices[idx], useDelayedOnClickListener));
}
private Notification.Action createAction(String actionTitle) {
@@ -501,14 +526,20 @@ public class SmartReplyViewTest extends SysuiTestCase {
private void setSmartActions(String[] actionTitles, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
List<Button> actions = mView.inflateSmartActions(
getContext(),
new SmartReplyView.SmartActions(createActions(actionTitles), false),
mLogger,
mEntry,
mHeadsUpManager,
useDelayedOnClickListener);
mView.addPreInflatedButtons(actions);
SmartReplyView.SmartActions smartActions = new SmartReplyView.SmartActions(
createActions(actionTitles), false);
Stream<Button> buttons = IntStream.range(0, smartActions.actions.size()).mapToObj(idx ->
mSmartActionInflater.inflateActionButton(
mView,
mEntry,
smartActions,
idx,
smartActions.actions.get(idx),
useDelayedOnClickListener,
getContext()));
mView.addPreInflatedButtons(buttons.collect(Collectors.toList()));
}
private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) {
@@ -520,16 +551,25 @@ public class SmartReplyViewTest extends SysuiTestCase {
CharSequence[] choices, String[] actionTitles, boolean fromAssistant,
boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
List<Button> smartSuggestions = inflateSmartReplies(choices, fromAssistant,
useDelayedOnClickListener);
smartSuggestions.addAll(mView.inflateSmartActions(
getContext(),
new SmartReplyView.SmartActions(createActions(actionTitles), fromAssistant),
mLogger,
mEntry,
mHeadsUpManager,
useDelayedOnClickListener));
mView.addPreInflatedButtons(smartSuggestions);
Sequence<Button> inflatedReplies = SequencesKt.asSequence(
inflateSmartReplies(choices, fromAssistant, useDelayedOnClickListener)
.iterator());
SmartReplyView.SmartActions smartActions = new SmartReplyView.SmartActions(
createActions(actionTitles), fromAssistant);
Sequence<Button> inflatedSmartActions = SequencesKt.asSequence(
IntStream.range(0, smartActions.actions.size())
.mapToObj(idx -> mSmartActionInflater.inflateActionButton(
mView,
mEntry,
smartActions,
idx,
smartActions.actions.get(idx),
useDelayedOnClickListener,
getContext()))
.iterator());
mView.addPreInflatedButtons(
SequencesKt.toList(SequencesKt.plus(inflatedReplies, inflatedSmartActions)));
mView.setSmartRepliesGeneratedByAssistant(fromAssistant);
}
private ViewGroup buildExpectedView(CharSequence[] choices, int lineCount) {
@@ -564,10 +604,18 @@ public class SmartReplyViewTest extends SysuiTestCase {
Button previous = null;
SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(Arrays.asList(choices), null, null, false);
for (int i = 0; i < choices.length; ++i) {
Button current = SmartReplyView.inflateReplyButton(mView, mContext, i, smartReplies,
null /* SmartReplyController */, null /* NotificationEntry */,
true /* useDelayedOnClickListener */);
Iterable<Button> inflatedReplies = SequencesKt.asIterable(SequencesKt.asSequence(
IntStream.range(0, smartReplies.choices.size()).mapToObj(
idx -> mSmartReplyInflater.inflateReplyButton(
mView,
mEntry,
smartReplies,
idx,
smartReplies.choices.get(idx),
true /* delayOnClickListener */))
.iterator()));
for (Button current : inflatedReplies) {
current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom());
if (previous != null) {
@@ -583,9 +631,21 @@ public class SmartReplyViewTest extends SysuiTestCase {
previous = current;
}
SmartReplyView.SmartActions smartActions = new SmartReplyView.SmartActions(actions, false);
Iterable<Button> inflatedSmartActions = SequencesKt.asIterable(SequencesKt.asSequence(
IntStream.range(0, smartActions.actions.size())
.mapToObj(idx -> mSmartActionInflater.inflateActionButton(
mView,
mEntry,
smartActions,
idx,
smartActions.actions.get(idx),
true /* delayOnClickListener */,
getContext()))
.iterator()));
// Add smart actions
for (int i = 0; i < actions.size(); ++i) {
Button current = inflateActionButton(actions.get(i));
for (Button current : inflatedSmartActions) {
current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom());
if (previous != null) {
@@ -672,8 +732,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
Thread.sleep(delayMs);
mView.getChildAt(2).performClick();
verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any(),
any());
verify(mActivityStarter, times(1))
.startPendingIntentDismissingKeyguard(any(), any(), any());
}
@Test
@@ -684,8 +744,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
mView.getChildAt(2).performClick();
verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any(),
any());
verify(mActivityStarter, times(1))
.startPendingIntentDismissingKeyguard(any(), any(), any());
}
@Test
@@ -869,18 +929,26 @@ public class SmartReplyViewTest extends SysuiTestCase {
assertReplyButtonHidden(mView.getChildAt(2));
}
private Button inflateActionButton(Notification.Action action) {
return SmartReplyView.inflateActionButton(mView, getContext(), getContext(), 0,
new SmartReplyView.SmartActions(Collections.singletonList(action), false),
mLogger, mEntry, mHeadsUpManager, true /* useDelayedOnClickListener */);
}
@Test
public void testInflateActionButton_smartActionIconSingleLineSizeForTwoLineButton() {
// Ensure smart action icons are the same size regardless of the number of text rows in the
// button.
Button singleLineButton = inflateActionButton(createAction("One line"));
Button doubleLineButton = inflateActionButton(createAction("Two\nlines"));
List<Notification.Action> actions = Stream.of("One line", "Two\nlines")
.map(this::createAction)
.collect(Collectors.toList());
SmartReplyView.SmartActions smartActions = new SmartReplyView.SmartActions(actions, false);
List<Button> buttons = IntStream.range(0, smartActions.actions.size())
.mapToObj(idx -> mSmartActionInflater.inflateActionButton(
mView,
mEntry,
smartActions,
idx,
smartActions.actions.get(idx),
true /* delayOnClickListener */,
getContext()))
.collect(Collectors.toList());
Button singleLineButton = buttons.get(0);
Button doubleLineButton = buttons.get(1);
Drawable singleLineDrawable = singleLineButton.getCompoundDrawables()[0]; // left drawable
Drawable doubleLineDrawable = doubleLineButton.getCompoundDrawables()[0]; // left drawable
assertEquals(singleLineDrawable.getBounds().width(),
@@ -1068,7 +1136,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
@Test
public void testMeasure_minNumSystemGeneratedSmartReplies_notEnoughReplies() {
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(3);
mView.setMinNumSystemGeneratedReplies(3);
// Add 2 replies when the minimum is 3 -> we should end up with 0 replies.
String[] choices = new String[] {"reply1", "reply2"};
@@ -1082,7 +1150,9 @@ public class SmartReplyViewTest extends SysuiTestCase {
choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
// 395, 168
assertEqualMeasures(expectedView, mView);
// smart replies
assertReplyButtonHidden(mView.getChildAt(0));
assertReplyButtonHidden(mView.getChildAt(1));
@@ -1121,7 +1191,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
*/
@Test
public void testMeasure_minNumSystemGeneratedSmartReplies_unSqueezeActions() {
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(2);
mView.setMinNumSystemGeneratedReplies(2);
// Add 2 replies when the minimum is 3 -> we should end up with 0 replies.
String[] choices = new String[] {"This is a very long two-line reply."};