All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.systemui.statusbar.NotificationRemoteInputManager Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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 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 mEntriesKeptForRemoteInputActive =
            new ArraySet<>();

    // Dependencies:
    private final NotificationLockscreenUserManager mLockscreenUserManager;
    private final SmartReplyController mSmartReplyController;
    private final NotificationEntryManager mEntryManager;
    private final Handler mMainHandler;

    private final Lazy 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 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 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,
            @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 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 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();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy