Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e77edea2 authored by Eliot Courtney's avatar Eliot Courtney
Browse files

Add NotificationRemoteInputManager and associated tests.

This splits out several remote input related pieces of logic:
  1. Handling clicks on remote views
  2. Handling notifications kept for remote input
  3. Handling notifications to be removed on NotificationPresenter
      collapse.

Bug: 63874929
Bug: 62602530
Test: runtest systemui
Test: Compile and run
Change-Id: I7acd4bcb2ab7bde67d307408f509d3ca038eb3d4
parent d3616f77
Loading
Loading
Loading
Loading
+6 −2
Original line number Original line Diff line number Diff line
@@ -26,9 +26,11 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.NotificationGutsManager;
import com.android.systemui.statusbar.NotificationGutsManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
@@ -36,9 +38,8 @@ import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.LockIcon;
import com.android.systemui.statusbar.phone.LockIcon;
import com.android.systemui.statusbar.phone.LockscreenWallpaper;
import com.android.systemui.statusbar.phone.LockscreenWallpaper;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;


@@ -119,5 +120,8 @@ public class SystemUIFactory {
                () -> new NotificationLockscreenUserManager(context));
                () -> new NotificationLockscreenUserManager(context));
        providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(
        providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(
                Dependency.get(NotificationLockscreenUserManager.class), context));
                Dependency.get(NotificationLockscreenUserManager.class), context));
        providers.put(NotificationRemoteInputManager.class,
                () -> new NotificationRemoteInputManager(
                        Dependency.get(NotificationLockscreenUserManager.class), context));
    }
    }
}
}
+5 −2
Original line number Original line Diff line number Diff line
@@ -37,10 +37,13 @@ public class NotificationListener extends NotificationListenerWithPlugins {
    private static final String TAG = "NotificationListener";
    private static final String TAG = "NotificationListener";


    private final NotificationPresenter mPresenter;
    private final NotificationPresenter mPresenter;
    private final NotificationRemoteInputManager mRemoteInputManager;
    private final Context mContext;
    private final Context mContext;


    public NotificationListener(NotificationPresenter presenter, Context context) {
    public NotificationListener(NotificationPresenter presenter,
            NotificationRemoteInputManager remoteInputManager, Context context) {
        mPresenter = presenter;
        mPresenter = presenter;
        mRemoteInputManager = remoteInputManager;
        mContext = context;
        mContext = context;
    }
    }


@@ -69,7 +72,7 @@ public class NotificationListener extends NotificationListenerWithPlugins {
            mPresenter.getHandler().post(() -> {
            mPresenter.getHandler().post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                String key = sbn.getKey();
                mPresenter.getKeysKeptForRemoteInput().remove(key);
                mRemoteInputManager.getKeysKeptForRemoteInput().remove(key);
                boolean isUpdate = mPresenter.getNotificationData().get(key) != null;
                boolean isUpdate = mPresenter.getNotificationData().get(key) != null;
                // In case we don't allow child notifications, we ignore children of
                // In case we don't allow child notifications, we ignore children of
                // notifications that have a summary, since` we're not going to show them
                // notifications that have a summary, since` we're not going to show them
+18 −9
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar;
import android.content.Intent;
import android.content.Intent;
import android.os.Handler;
import android.os.Handler;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService;
import android.view.View;


import java.util.Set;
import java.util.Set;


@@ -29,7 +30,7 @@ import java.util.Set;
 * want to perform some action before doing so).
 * want to perform some action before doing so).
 */
 */
public interface NotificationPresenter extends NotificationUpdateHandler,
public interface NotificationPresenter extends NotificationUpdateHandler,
        NotificationData.Environment {
        NotificationData.Environment, NotificationRemoteInputManager.Callback {


    /**
    /**
     * Returns true if the presenter is not visible. For example, it may not be necessary to do
     * Returns true if the presenter is not visible. For example, it may not be necessary to do
@@ -80,14 +81,6 @@ public interface NotificationPresenter extends NotificationUpdateHandler,
     */
     */
    void onWorkChallengeChanged();
    void onWorkChallengeChanged();


    /**
     * Notifications in this set are kept around when they were canceled in response to a remote
     * input interaction. This allows us to show what you replied and allows you to continue typing
     * into it.
     */
    // TODO: Create NotificationEntryManager and move this method to there.
    Set<String> getKeysKeptForRemoteInput();

    /**
    /**
     * Called when the current user changes.
     * Called when the current user changes.
     * @param newUserId new user id
     * @param newUserId new user id
@@ -98,4 +91,20 @@ public interface NotificationPresenter extends NotificationUpdateHandler,
     * Gets the NotificationLockscreenUserManager for this Presenter.
     * Gets the NotificationLockscreenUserManager for this Presenter.
     */
     */
    NotificationLockscreenUserManager getNotificationLockscreenUserManager();
    NotificationLockscreenUserManager getNotificationLockscreenUserManager();

    /**
     * Wakes the device up if dozing.
     *
     * @param time the time when the request to wake up was issued
     * @param where which view caused this wake up request
     */
    void wakeUpIfDozing(long time, View where);

    /**
     * True if the device currently requires a PIN, pattern, or password to unlock.
     *
     * @param userId user id to query about
     * @return true iff the device is locked
     */
    boolean isDeviceLocked(int userId);
}
}
+441 −0
Original line number Original line Diff line number Diff line
/*
 * 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 android.app.ActivityManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
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.util.ArraySet;
import android.util.Log;
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.systemui.Dumpable;
import com.android.systemui.statusbar.policy.RemoteInputView;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Set;

/**
 * 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.
 */
public class NotificationRemoteInputManager implements Dumpable {
    public static final boolean ENABLE_REMOTE_INPUT =
            SystemProperties.getBoolean("debug.enable_remote_input", true);
    public static final boolean FORCE_REMOTE_INPUT_HISTORY =
            SystemProperties.getBoolean("debug.force_remoteinput_history", true);
    private static final boolean DEBUG = false;
    private static final String TAG = "NotificationRemoteInputManager";

    /**
     * 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;

    protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse =
            new ArraySet<>();
    protected final NotificationLockscreenUserManager mLockscreenUserManager;

    /**
     * Notifications with keys in this set are not actually around anymore. We kept them around
     * when they were canceled in response to a remote input interaction. This allows us to show
     * what you replied and allows you to continue typing into it.
     */
    protected final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
    protected final Context mContext;
    private final UserManager mUserManager;

    protected RemoteInputController mRemoteInputController;
    protected NotificationPresenter mPresenter;
    protected IStatusBarService mBarService;
    protected Callback mCallback;

    private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {

        @Override
        public boolean onClickHandler(
                final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
            mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), view);

            if (handleRemoteInput(view, pendingIntent)) {
                return true;
            }

            if (DEBUG) {
                Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
            }
            logActionClick(view);
            // 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, fillInIntent,
                    () -> superOnClickHandler(view, pendingIntent, fillInIntent));
        }

        private void logActionClick(View view) {
            ViewParent parent = view.getParent();
            String key = getNotificationKeyForParent(parent);
            if (key == null) {
                Log.w(TAG, "Couldn't determine notification for click.");
                return;
            }
            int index = -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;
                index = actionGroup.indexOfChild(view);
            }
            try {
                mBarService.onNotificationActionClick(key, index);
            } catch (RemoteException e) {
                // Ignore
            }
        }

        private String getNotificationKeyForParent(ViewParent parent) {
            while (parent != null) {
                if (parent instanceof ExpandableNotificationRow) {
                    return ((ExpandableNotificationRow) parent)
                            .getStatusBarNotification().getKey();
                }
                parent = parent.getParent();
            }
            return null;
        }

        private boolean superOnClickHandler(View view, PendingIntent pendingIntent,
                Intent fillInIntent) {
            return super.onClickHandler(view, pendingIntent, fillInIntent,
                    WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
        }

        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;
            }

            ViewParent p = view.getParent();
            RemoteInputView riv = null;
            while (p != null) {
                if (p instanceof View) {
                    View pv = (View) p;
                    if (pv.isRootNamespace()) {
                        riv = findRemoteInputView(pv);
                        break;
                    }
                }
                p = p.getParent();
            }
            ExpandableNotificationRow row = null;
            while (p != null) {
                if (p instanceof ExpandableNotificationRow) {
                    row = (ExpandableNotificationRow) p;
                    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()
                        && mPresenter.isDeviceLocked(userId)) {
                    mCallback.onLockedWorkRemoteInput(userId, row, view);
                    return true;
                }
            }

            if (riv == null) {
                riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
                if (riv == null) {
                    return false;
                }
                if (!row.getPrivateLayout().getExpandedChild().isShown()) {
                    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);
            riv.focusAnimated();

            return true;
        }

        private RemoteInputView findRemoteInputView(View v) {
            if (v == null) {
                return null;
            }
            return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
        }
    };

    public NotificationRemoteInputManager(NotificationLockscreenUserManager lockscreenUserManager,
            Context context) {
        mLockscreenUserManager = lockscreenUserManager;
        mContext = context;
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
    }

    public void setUpWithPresenter(NotificationPresenter presenter,
            Callback callback,
            RemoteInputController.Delegate delegate) {
        mPresenter = presenter;
        mCallback = callback;
        mRemoteInputController = new RemoteInputController(delegate);
        mRemoteInputController.addCallback(new RemoteInputController.Callback() {
            @Override
            public void onRemoteInputSent(NotificationData.Entry entry) {
                if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
                    mPresenter.removeNotification(entry.key, null);
                } else if (mRemoteInputEntriesToRemoveOnCollapse.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.
                    mPresenter.getHandler().postDelayed(() -> {
                        if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
                            mPresenter.removeNotification(entry.key, null);
                        }
                    }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
                }
            }
        });

    }

    public RemoteInputController getController() {
        return mRemoteInputController;
    }

    public void onUpdateNotification(NotificationData.Entry entry) {
        mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
    }

    /**
     * Returns true if NotificationRemoteInputManager wants to keep this notification around.
     *
     * @param entry notification being removed
     */
    public boolean onRemoveNotification(NotificationData.Entry entry) {
        if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
                && (entry.row != null && !entry.row.isDismissed())) {
            mRemoteInputEntriesToRemoveOnCollapse.add(entry);
            return true;
        }
        return false;
    }

    public void onPerformRemoveNotification(StatusBarNotification n,
            NotificationData.Entry entry) {
        if (mRemoteInputController.isRemoteInputActive(entry)) {
            mRemoteInputController.removeRemoteInput(entry, null);
        }
        if (FORCE_REMOTE_INPUT_HISTORY
                && mKeysKeptForRemoteInput.contains(n.getKey())) {
            mKeysKeptForRemoteInput.remove(n.getKey());
        }
    }

    public void removeRemoteInputEntriesKeptUntilCollapsed() {
        for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
            NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
            mRemoteInputController.removeRemoteInput(entry, null);
            mPresenter.removeNotification(entry.key, mPresenter.getLatestRankingMap());
        }
        mRemoteInputEntriesToRemoveOnCollapse.clear();
    }

    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();
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("NotificationRemoteInputManager state:");
        pw.print("  mRemoteInputEntriesToRemoveOnCollapse: ");
        pw.println(mRemoteInputEntriesToRemoveOnCollapse);
        pw.print("  mKeysKeptForRemoteInput: ");
        pw.println(mKeysKeptForRemoteInput);
    }

    public void bindRow(ExpandableNotificationRow row) {
        row.setRemoteInputController(mRemoteInputController);
        row.setRemoteViewClickHandler(mOnClickHandler);
    }

    public Set<String> getKeysKeptForRemoteInput() {
        return mKeysKeptForRemoteInput;
    }

    @VisibleForTesting
    public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
        return mRemoteInputEntriesToRemoveOnCollapse;
    }

    /**
     * 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 fillInIntent
         * @param defaultHandler
         * @return  true iff the click was handled
         */
        boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent,
                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();
    }
}
+92 −322

File changed.

Preview size limit exceeded, changes collapsed.

Loading