diff options
13 files changed, 841 insertions, 645 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 90dc213ce6e6..63d9a831b33f 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 1d72557c6a89..c995e324ecfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 9bcac1163acc..c2c4590fa6cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 1de9308a40b1..8a644ed4d3ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -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; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java index c6ae669d5d08..cbc8405cc057 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java @@ -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) { @@ -76,206 +55,6 @@ public class InflatedSmartReplies { } /** - * 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. */ public static class SmartRepliesAndActions { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt new file mode 100644 index 000000000000..b2c1f4840068 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt @@ -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)
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java index 949ac4df88ba..e7f84a55eb5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -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); @@ -172,6 +148,18 @@ public class SmartReplyView extends ViewGroup { } /** + * 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; - } - - /** - * 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; - } - - /** - * 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 */); + public void setMaxNumActions(int maxNumActions) { + mMaxNumActions = maxNumActions; } - @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; + public void setMinNumSystemGeneratedReplies(int minNumSystemGeneratedReplies) { + mMinNumSystemGeneratedReplies = minNumSystemGeneratedReplies; } - @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; - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt new file mode 100644 index 000000000000..803d26ec3286 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt @@ -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 +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index a2f8c1cb0ad3..6b0a23f2b4ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java index aff8ade6f1ae..1255b6de6608 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -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), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 2ce8b34b193a..fb37ed57c1da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -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), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java index 53d8e5866347..e93c5dbdadc1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java @@ -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(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java index f1a6e67edb43..836a81e42193 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java @@ -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; + + 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; - @Mock ActivityStarter mActivityStarter; - @Mock HeadsUpManager mHeadsUpManager; + 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."}; |