/*
|
* Copyright (C) 2017 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;
|
|
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
|
|
import static com.android.systemui.Dependency.MAIN_HANDLER_NAME;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.app.ActivityManager;
|
import android.app.ActivityOptions;
|
import android.app.KeyguardManager;
|
import android.app.Notification;
|
import android.app.PendingIntent;
|
import android.app.RemoteInput;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.os.Handler;
|
import android.os.RemoteException;
|
import android.os.ServiceManager;
|
import android.os.SystemClock;
|
import android.os.SystemProperties;
|
import android.os.UserManager;
|
import android.service.notification.StatusBarNotification;
|
import android.text.TextUtils;
|
import android.util.ArraySet;
|
import android.util.Log;
|
import android.util.Pair;
|
import android.view.MotionEvent;
|
import android.view.View;
|
import android.view.ViewGroup;
|
import android.view.ViewParent;
|
import android.widget.RemoteViews;
|
import android.widget.TextView;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.statusbar.IStatusBarService;
|
import com.android.internal.statusbar.NotificationVisibility;
|
import com.android.systemui.Dumpable;
|
import com.android.systemui.R;
|
import com.android.systemui.statusbar.notification.NotificationEntryListener;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
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.notification.row.ExpandableNotificationRow;
|
import com.android.systemui.statusbar.phone.ShadeController;
|
import com.android.systemui.statusbar.policy.RemoteInputView;
|
|
import java.io.FileDescriptor;
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Objects;
|
import java.util.Set;
|
|
import javax.inject.Inject;
|
import javax.inject.Named;
|
import javax.inject.Singleton;
|
|
import dagger.Lazy;
|
|
/**
|
* Class for handling remote input state over a set of notifications. This class handles things
|
* like keeping notifications temporarily that were cancelled as a response to a remote input
|
* interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
|
* and handling clicks on remote views.
|
*/
|
@Singleton
|
public class NotificationRemoteInputManager implements Dumpable {
|
public static final boolean ENABLE_REMOTE_INPUT =
|
SystemProperties.getBoolean("debug.enable_remote_input", true);
|
public static boolean FORCE_REMOTE_INPUT_HISTORY =
|
SystemProperties.getBoolean("debug.force_remoteinput_history", true);
|
private static final boolean DEBUG = false;
|
private static final String TAG = "NotifRemoteInputManager";
|
|
/**
|
* How long to wait before auto-dismissing a notification that was kept for remote input, and
|
* has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
|
* these given that they technically don't exist anymore. We wait a bit in case the app issues
|
* an update.
|
*/
|
private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
|
|
/**
|
* Notifications that are already removed but are kept around because we want to show the
|
* remote input history. See {@link RemoteInputHistoryExtender} and
|
* {@link SmartReplyHistoryExtender}.
|
*/
|
protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
|
|
/**
|
* Notifications that are already removed but are kept around because the remote input is
|
* actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
|
*/
|
protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
|
new ArraySet<>();
|
|
// Dependencies:
|
private final NotificationLockscreenUserManager mLockscreenUserManager;
|
private final SmartReplyController mSmartReplyController;
|
private final NotificationEntryManager mEntryManager;
|
private final Handler mMainHandler;
|
|
private final Lazy<ShadeController> mShadeController;
|
|
protected final Context mContext;
|
private final UserManager mUserManager;
|
private final KeyguardManager mKeyguardManager;
|
|
protected RemoteInputController mRemoteInputController;
|
protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
|
mNotificationLifetimeFinishedCallback;
|
protected IStatusBarService mBarService;
|
protected Callback mCallback;
|
protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
|
|
private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
|
|
@Override
|
public boolean onClickHandler(
|
View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
|
mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
|
"NOTIFICATION_CLICK");
|
|
if (handleRemoteInput(view, pendingIntent)) {
|
return true;
|
}
|
|
if (DEBUG) {
|
Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
|
}
|
logActionClick(view, pendingIntent);
|
// The intent we are sending is for the application, which
|
// won't have permission to immediately start an activity after
|
// the user switches to home. We know it is safe to do at this
|
// point, so make sure new activity switches are now allowed.
|
try {
|
ActivityManager.getService().resumeAppSwitches();
|
} catch (RemoteException e) {
|
}
|
return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
|
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
|
options.second.setLaunchWindowingMode(
|
WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
|
return RemoteViews.startPendingIntent(view, pendingIntent, options);
|
});
|
}
|
|
private void logActionClick(View view, PendingIntent actionIntent) {
|
Integer actionIndex = (Integer)
|
view.getTag(com.android.internal.R.id.notification_action_index_tag);
|
if (actionIndex == null) {
|
// Custom action button, not logging.
|
return;
|
}
|
ViewParent parent = view.getParent();
|
StatusBarNotification statusBarNotification = getNotificationForParent(parent);
|
if (statusBarNotification == null) {
|
Log.w(TAG, "Couldn't determine notification for click.");
|
return;
|
}
|
String key = statusBarNotification.getKey();
|
int buttonIndex = -1;
|
// If this is a default template, determine the index of the button.
|
if (view.getId() == com.android.internal.R.id.action0 &&
|
parent != null && parent instanceof ViewGroup) {
|
ViewGroup actionGroup = (ViewGroup) parent;
|
buttonIndex = actionGroup.indexOfChild(view);
|
}
|
final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
|
final int rank = mEntryManager.getNotificationData().getRank(key);
|
|
// Notification may be updated before this function is executed, and thus play safe
|
// here and verify that the action object is still the one that where the click happens.
|
Notification.Action[] actions = statusBarNotification.getNotification().actions;
|
if (actions == null || actionIndex >= actions.length) {
|
Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
|
return;
|
}
|
final Notification.Action action =
|
statusBarNotification.getNotification().actions[actionIndex];
|
if (!Objects.equals(action.actionIntent, actionIntent)) {
|
Log.w(TAG, "actionIntent does not match");
|
return;
|
}
|
NotificationVisibility.NotificationLocation location =
|
NotificationLogger.getNotificationLocation(
|
mEntryManager.getNotificationData().get(key));
|
final NotificationVisibility nv =
|
NotificationVisibility.obtain(key, rank, count, true, location);
|
try {
|
mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
|
} catch (RemoteException e) {
|
// Ignore
|
}
|
}
|
|
private StatusBarNotification getNotificationForParent(ViewParent parent) {
|
while (parent != null) {
|
if (parent instanceof ExpandableNotificationRow) {
|
return ((ExpandableNotificationRow) parent).getStatusBarNotification();
|
}
|
parent = parent.getParent();
|
}
|
return null;
|
}
|
|
private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
|
if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
|
return true;
|
}
|
|
Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
|
RemoteInput[] inputs = null;
|
if (tag instanceof RemoteInput[]) {
|
inputs = (RemoteInput[]) tag;
|
}
|
|
if (inputs == null) {
|
return false;
|
}
|
|
RemoteInput input = null;
|
|
for (RemoteInput i : inputs) {
|
if (i.getAllowFreeFormInput()) {
|
input = i;
|
}
|
}
|
|
if (input == null) {
|
return false;
|
}
|
|
return activateRemoteInput(view, inputs, input, pendingIntent,
|
null /* editedSuggestionInfo */);
|
}
|
};
|
|
@Inject
|
public NotificationRemoteInputManager(
|
Context context,
|
NotificationLockscreenUserManager lockscreenUserManager,
|
SmartReplyController smartReplyController,
|
NotificationEntryManager notificationEntryManager,
|
Lazy<ShadeController> shadeController,
|
@Named(MAIN_HANDLER_NAME) Handler mainHandler) {
|
mContext = context;
|
mLockscreenUserManager = lockscreenUserManager;
|
mSmartReplyController = smartReplyController;
|
mEntryManager = notificationEntryManager;
|
mShadeController = shadeController;
|
mMainHandler = mainHandler;
|
mBarService = IStatusBarService.Stub.asInterface(
|
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
|
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
|
addLifetimeExtenders();
|
mKeyguardManager = context.getSystemService(KeyguardManager.class);
|
|
notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
|
@Override
|
public void onPreEntryUpdated(NotificationEntry entry) {
|
// Mark smart replies as sent whenever a notification is updated - otherwise the
|
// smart replies are never marked as sent.
|
mSmartReplyController.stopSending(entry);
|
}
|
|
@Override
|
public void onEntryRemoved(
|
@Nullable NotificationEntry entry,
|
NotificationVisibility visibility,
|
boolean removedByUser) {
|
// We're removing the notification, the smart controller can forget about it.
|
mSmartReplyController.stopSending(entry);
|
|
if (removedByUser && entry != null) {
|
onPerformRemoveNotification(entry, entry.key);
|
}
|
}
|
});
|
}
|
|
/** Initializes this component with the provided dependencies. */
|
public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
|
mCallback = callback;
|
mRemoteInputController = new RemoteInputController(delegate);
|
mRemoteInputController.addCallback(new RemoteInputController.Callback() {
|
@Override
|
public void onRemoteInputSent(NotificationEntry entry) {
|
if (FORCE_REMOTE_INPUT_HISTORY
|
&& isNotificationKeptForRemoteInputHistory(entry.key)) {
|
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
|
} else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
|
// We're currently holding onto this notification, but from the apps point of
|
// view it is already canceled, so we'll need to cancel it on the apps behalf
|
// after sending - unless the app posts an update in the mean time, so wait a
|
// bit.
|
mMainHandler.postDelayed(() -> {
|
if (mEntriesKeptForRemoteInputActive.remove(entry)) {
|
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
|
}
|
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
|
}
|
try {
|
mBarService.onNotificationDirectReplied(entry.notification.getKey());
|
if (entry.editedSuggestionInfo != null) {
|
boolean modifiedBeforeSending =
|
!TextUtils.equals(entry.remoteInputText,
|
entry.editedSuggestionInfo.originalText);
|
mBarService.onNotificationSmartReplySent(
|
entry.notification.getKey(),
|
entry.editedSuggestionInfo.index,
|
entry.editedSuggestionInfo.originalText,
|
NotificationLogger
|
.getNotificationLocation(entry)
|
.toMetricsEventEnum(),
|
modifiedBeforeSending);
|
}
|
} catch (RemoteException e) {
|
// Nothing to do, system going down
|
}
|
}
|
});
|
mSmartReplyController.setCallback((entry, reply) -> {
|
StatusBarNotification newSbn =
|
rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
|
mEntryManager.updateNotification(newSbn, null /* ranking */);
|
});
|
}
|
|
/**
|
* Activates a given {@link RemoteInput}
|
*
|
* @param view The view of the action button or suggestion chip that was tapped.
|
* @param inputs The remote inputs that need to be sent to the app.
|
* @param input The remote input that needs to be activated.
|
* @param pendingIntent The pending intent to be sent to the app.
|
* @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
|
* {@code null} if the user is not editing a smart reply.
|
* @return Whether the {@link RemoteInput} was activated.
|
*/
|
public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
|
PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
|
|
ViewParent p = view.getParent();
|
RemoteInputView riv = null;
|
ExpandableNotificationRow row = null;
|
while (p != null) {
|
if (p instanceof View) {
|
View pv = (View) p;
|
if (pv.isRootNamespace()) {
|
riv = findRemoteInputView(pv);
|
row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
|
break;
|
}
|
}
|
p = p.getParent();
|
}
|
|
if (row == null) {
|
return false;
|
}
|
|
row.setUserExpanded(true);
|
|
if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
|
final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
|
if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
|
mCallback.onLockedRemoteInput(row, view);
|
return true;
|
}
|
if (mUserManager.getUserInfo(userId).isManagedProfile()
|
&& mKeyguardManager.isDeviceLocked(userId)) {
|
mCallback.onLockedWorkRemoteInput(userId, row, view);
|
return true;
|
}
|
}
|
|
if (riv == null) {
|
riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
|
if (riv == null) {
|
return false;
|
}
|
}
|
if (riv == row.getPrivateLayout().getExpandedRemoteInput()
|
&& !row.getPrivateLayout().getExpandedChild().isShown()) {
|
// The expanded layout is selected, but it's not shown yet, let's wait on it to
|
// show before we do the animation.
|
mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
|
return true;
|
}
|
|
int width = view.getWidth();
|
if (view instanceof TextView) {
|
// Center the reveal on the text which might be off-center from the TextView
|
TextView tv = (TextView) view;
|
if (tv.getLayout() != null) {
|
int innerWidth = (int) tv.getLayout().getLineWidth(0);
|
innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
|
width = Math.min(width, innerWidth);
|
}
|
}
|
int cx = view.getLeft() + width / 2;
|
int cy = view.getTop() + view.getHeight() / 2;
|
int w = riv.getWidth();
|
int h = riv.getHeight();
|
int r = Math.max(
|
Math.max(cx + cy, cx + (h - cy)),
|
Math.max((w - cx) + cy, (w - cx) + (h - cy)));
|
|
riv.setRevealParameters(cx, cy, r);
|
riv.setPendingIntent(pendingIntent);
|
riv.setRemoteInput(inputs, input, editedSuggestionInfo);
|
riv.focusAnimated();
|
|
return true;
|
}
|
|
private RemoteInputView findRemoteInputView(View v) {
|
if (v == null) {
|
return null;
|
}
|
return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
|
}
|
|
/**
|
* Adds all the notification lifetime extenders. Each extender represents a reason for the
|
* NotificationRemoteInputManager to keep a notification lifetime extended.
|
*/
|
protected void addLifetimeExtenders() {
|
mLifetimeExtenders.add(new RemoteInputHistoryExtender());
|
mLifetimeExtenders.add(new SmartReplyHistoryExtender());
|
mLifetimeExtenders.add(new RemoteInputActiveExtender());
|
}
|
|
public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
|
return mLifetimeExtenders;
|
}
|
|
public RemoteInputController getController() {
|
return mRemoteInputController;
|
}
|
|
@VisibleForTesting
|
void onPerformRemoveNotification(NotificationEntry entry, final String key) {
|
if (mKeysKeptForRemoteInputHistory.contains(key)) {
|
mKeysKeptForRemoteInputHistory.remove(key);
|
}
|
if (mRemoteInputController.isRemoteInputActive(entry)) {
|
mRemoteInputController.removeRemoteInput(entry, null);
|
}
|
}
|
|
public void onPanelCollapsed() {
|
for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
|
NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
|
mRemoteInputController.removeRemoteInput(entry, null);
|
if (mNotificationLifetimeFinishedCallback != null) {
|
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
|
}
|
}
|
mEntriesKeptForRemoteInputActive.clear();
|
}
|
|
public boolean isNotificationKeptForRemoteInputHistory(String key) {
|
return mKeysKeptForRemoteInputHistory.contains(key);
|
}
|
|
public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
|
if (!FORCE_REMOTE_INPUT_HISTORY) {
|
return false;
|
}
|
return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
|
}
|
|
public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
|
if (!FORCE_REMOTE_INPUT_HISTORY) {
|
return false;
|
}
|
return mSmartReplyController.isSendingSmartReply(entry.key);
|
}
|
|
public void checkRemoteInputOutside(MotionEvent event) {
|
if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
|
&& event.getX() == 0 && event.getY() == 0 // a touch outside both bars
|
&& mRemoteInputController.isRemoteInputActive()) {
|
mRemoteInputController.closeRemoteInputs();
|
}
|
}
|
|
@VisibleForTesting
|
StatusBarNotification rebuildNotificationForCanceledSmartReplies(
|
NotificationEntry entry) {
|
return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
|
false /* showSpinner */);
|
}
|
|
@VisibleForTesting
|
StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry,
|
CharSequence remoteInputText, boolean showSpinner) {
|
StatusBarNotification sbn = entry.notification;
|
|
Notification.Builder b = Notification.Builder
|
.recoverBuilder(mContext, sbn.getNotification().clone());
|
if (remoteInputText != null) {
|
CharSequence[] oldHistory = sbn.getNotification().extras
|
.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
|
CharSequence[] newHistory;
|
if (oldHistory == null) {
|
newHistory = new CharSequence[1];
|
} else {
|
newHistory = new CharSequence[oldHistory.length + 1];
|
System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
|
}
|
newHistory[0] = String.valueOf(remoteInputText);
|
b.setRemoteInputHistory(newHistory);
|
}
|
b.setShowRemoteInputSpinner(showSpinner);
|
b.setHideSmartReplies(true);
|
|
Notification newNotification = b.build();
|
|
// Undo any compatibility view inflation
|
newNotification.contentView = sbn.getNotification().contentView;
|
newNotification.bigContentView = sbn.getNotification().bigContentView;
|
newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
|
|
return new StatusBarNotification(
|
sbn.getPackageName(),
|
sbn.getOpPkg(),
|
sbn.getId(),
|
sbn.getTag(),
|
sbn.getUid(),
|
sbn.getInitialPid(),
|
newNotification,
|
sbn.getUser(),
|
sbn.getOverrideGroupKey(),
|
sbn.getPostTime());
|
}
|
|
@Override
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
pw.println("NotificationRemoteInputManager state:");
|
pw.print(" mKeysKeptForRemoteInputHistory: ");
|
pw.println(mKeysKeptForRemoteInputHistory);
|
pw.print(" mEntriesKeptForRemoteInputActive: ");
|
pw.println(mEntriesKeptForRemoteInputActive);
|
}
|
|
public void bindRow(ExpandableNotificationRow row) {
|
row.setRemoteInputController(mRemoteInputController);
|
row.setRemoteViewClickHandler(mOnClickHandler);
|
}
|
|
@VisibleForTesting
|
public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
|
return mEntriesKeptForRemoteInputActive;
|
}
|
|
/**
|
* NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
|
* so we implement multiple NotificationLifetimeExtenders
|
*/
|
protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
|
@Override
|
public void setCallback(NotificationSafeToRemoveCallback callback) {
|
if (mNotificationLifetimeFinishedCallback == null) {
|
mNotificationLifetimeFinishedCallback = callback;
|
}
|
}
|
}
|
|
/**
|
* Notification is kept alive as it was cancelled in response to a remote input interaction.
|
* This allows us to show what you replied and allows you to continue typing into it.
|
*/
|
protected class RemoteInputHistoryExtender extends RemoteInputExtender {
|
@Override
|
public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
|
return shouldKeepForRemoteInputHistory(entry);
|
}
|
|
@Override
|
public void setShouldManageLifetime(NotificationEntry entry,
|
boolean shouldExtend) {
|
if (shouldExtend) {
|
CharSequence remoteInputText = entry.remoteInputText;
|
if (TextUtils.isEmpty(remoteInputText)) {
|
remoteInputText = entry.remoteInputTextWhenReset;
|
}
|
StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
|
remoteInputText, false /* showSpinner */);
|
entry.onRemoteInputInserted();
|
|
if (newSbn == null) {
|
return;
|
}
|
|
mEntryManager.updateNotification(newSbn, null);
|
|
// Ensure the entry hasn't already been removed. This can happen if there is an
|
// inflation exception while updating the remote history
|
if (entry.isRemoved()) {
|
return;
|
}
|
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
Log.d(TAG, "Keeping notification around after sending remote input "
|
+ entry.key);
|
}
|
|
mKeysKeptForRemoteInputHistory.add(entry.key);
|
} else {
|
mKeysKeptForRemoteInputHistory.remove(entry.key);
|
}
|
}
|
}
|
|
/**
|
* Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
|
* {@link SmartReplyController} specific logic
|
*/
|
protected class SmartReplyHistoryExtender extends RemoteInputExtender {
|
@Override
|
public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
|
return shouldKeepForSmartReplyHistory(entry);
|
}
|
|
@Override
|
public void setShouldManageLifetime(NotificationEntry entry,
|
boolean shouldExtend) {
|
if (shouldExtend) {
|
StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
|
|
if (newSbn == null) {
|
return;
|
}
|
|
mEntryManager.updateNotification(newSbn, null);
|
|
if (entry.isRemoved()) {
|
return;
|
}
|
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
Log.d(TAG, "Keeping notification around after sending smart reply "
|
+ entry.key);
|
}
|
|
mKeysKeptForRemoteInputHistory.add(entry.key);
|
} else {
|
mKeysKeptForRemoteInputHistory.remove(entry.key);
|
mSmartReplyController.stopSending(entry);
|
}
|
}
|
}
|
|
/**
|
* Notification is kept alive because the user is still using the remote input
|
*/
|
protected class RemoteInputActiveExtender extends RemoteInputExtender {
|
@Override
|
public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
|
return mRemoteInputController.isRemoteInputActive(entry);
|
}
|
|
@Override
|
public void setShouldManageLifetime(NotificationEntry entry,
|
boolean shouldExtend) {
|
if (shouldExtend) {
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
Log.d(TAG, "Keeping notification around while remote input active "
|
+ entry.key);
|
}
|
mEntriesKeptForRemoteInputActive.add(entry);
|
} else {
|
mEntriesKeptForRemoteInputActive.remove(entry);
|
}
|
}
|
}
|
|
/**
|
* Callback for various remote input related events, or for providing information that
|
* NotificationRemoteInputManager needs to know to decide what to do.
|
*/
|
public interface Callback {
|
|
/**
|
* Called when remote input was activated but the device is locked.
|
*
|
* @param row
|
* @param clicked
|
*/
|
void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
|
|
/**
|
* Called when remote input was activated but the device is locked and in a managed profile.
|
*
|
* @param userId
|
* @param row
|
* @param clicked
|
*/
|
void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
|
|
/**
|
* Called when a row should be made expanded for the purposes of remote input.
|
*
|
* @param row
|
* @param clickedView
|
*/
|
void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
|
|
/**
|
* Return whether or not remote input should be handled for this view.
|
*
|
* @param view
|
* @param pendingIntent
|
* @return true iff the remote input should be handled
|
*/
|
boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
|
|
/**
|
* Performs any special handling for a remote view click. The default behaviour can be
|
* called through the defaultHandler parameter.
|
*
|
* @param view
|
* @param pendingIntent
|
* @param defaultHandler
|
* @return true iff the click was handled
|
*/
|
boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
|
ClickHandler defaultHandler);
|
}
|
|
/**
|
* Helper interface meant for passing the default on click behaviour to NotificationPresenter,
|
* so it may do its own handling before invoking the default behaviour.
|
*/
|
public interface ClickHandler {
|
/**
|
* Tries to handle a click on a remote view.
|
*
|
* @return true iff the click was handled
|
*/
|
boolean handleClick();
|
}
|
}
|