Merge "Remove Dependency#get from SmartReplyView"
This commit is contained in:
committed by
Android (Google) Code Review
commit
f2b2eb1ae3
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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."};
|
||||
|
||||
Reference in New Issue
Block a user