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

Commit a7d7205e authored by Steve Elliott's avatar Steve Elliott
Browse files

Add RemoteInput auth bypass flow

This change adds a way for a RemoteInputView activation to be allowed
even when the device is locked. If this is done, then authentication
will be deferred until the RemoteInput is sent. At that time, if
authentication is still necessary, the bouncer will be shown.

Authentication can be bypassed asynchronously while the RemoteInputView
is active via the AuthBypassPredicate passed to
NotificationRemoteInputManager#activateRemoteInput. If this predicate
returns true at the time that the RemoteInput is sent, then the send
will succeed, but the device will remain locked. If it returns false,
then the bouncer will be shown, at which point the user can choose to
unlock the device and send again, or dismiss the bouncer (and nothing
else will happen).

Test: manual
Bug: 170647553

Change-Id: Ie3b2089cefc6cfc2f5b6d218a61a51b1785ecf92
parent 076669f9
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -76,8 +76,8 @@ android_library {
        "androidx.dynamicanimation_dynamicanimation",
        "androidx-constraintlayout_constraintlayout",
        "androidx.exifinterface_exifinterface",
        "kotlinx-coroutines-android",
        "kotlinx-coroutines-core",
        "kotlinx_coroutines_android",
        "kotlinx_coroutines",
        "iconloader_base",
        "SystemUI-tags",
        "SystemUI-proto",
+108 −36
Original line number Diff line number Diff line
@@ -388,7 +388,28 @@ public class NotificationRemoteInputManager implements Dumpable {
     */
    public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
            PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
        return activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
                null /* userMessageContent */, null /* authBypassCheck */);
    }

    /**
     * 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.
     * @param userMessageContent User-entered text with which to initialize the remote input view.
     * @param authBypassCheck Optional auth bypass check associated with this remote input
     *         activation. If {@code null}, we never bypass.
     * @return Whether the {@link RemoteInput} was activated.
     */
    public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
            PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo,
            @Nullable String userMessageContent,
            @Nullable AuthBypassPredicate authBypassCheck) {
        ViewParent p = view.getParent();
        RemoteInputView riv = null;
        ExpandableNotificationRow row = null;
@@ -410,41 +431,10 @@ public class NotificationRemoteInputManager implements Dumpable {

        row.setUserExpanded(true);

        if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
            final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();

            final boolean isLockedManagedProfile =
                    mUserManager.getUserInfo(userId).isManagedProfile()
                    && mKeyguardManager.isDeviceLocked(userId);

            final boolean isParentUserLocked;
            if (isLockedManagedProfile) {
                final UserInfo profileParent = mUserManager.getProfileParent(userId);
                isParentUserLocked = (profileParent != null)
                        && mKeyguardManager.isDeviceLocked(profileParent.id);
            } else {
                isParentUserLocked = false;
            }

            if (mLockscreenUserManager.isLockscreenPublicMode(userId)
                    || mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                // If the parent user is no longer locked, and the user to which the remote input
                // is destined is a locked, managed profile, then onLockedWorkRemoteInput should be
                // called to unlock it.
                if (isLockedManagedProfile && !isParentUserLocked) {
                    mCallback.onLockedWorkRemoteInput(userId, row, view);
                } else {
                    // Even if we don't have security we should go through this flow, otherwise
                    // we won't go to the shade.
                    mCallback.onLockedRemoteInput(row, view);
                }
                return true;
            }
            if (isLockedManagedProfile) {
                mCallback.onLockedWorkRemoteInput(userId, row, view);
        final boolean deferBouncer = authBypassCheck != null;
        if (!deferBouncer && showBouncerForRemoteInput(view, pendingIntent, row)) {
            return true;
        }
        }

        if (riv != null && !riv.isAttachedToWindow()) {
            // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded
@@ -461,7 +451,10 @@ public class NotificationRemoteInputManager implements Dumpable {
                && !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);
            mCallback.onMakeExpandedVisibleForRemoteInput(row, view, deferBouncer, () -> {
                activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
                        userMessageContent, authBypassCheck);
            });
            return true;
        }

@@ -491,9 +484,61 @@ public class NotificationRemoteInputManager implements Dumpable {
        riv.setPendingIntent(pendingIntent);
        riv.setRemoteInput(inputs, input, editedSuggestionInfo);
        riv.focusAnimated();
        if (userMessageContent != null) {
            riv.setEditTextContent(userMessageContent);
        }
        if (deferBouncer) {
            final ExpandableNotificationRow finalRow = row;
            riv.setBouncerChecker(() -> !authBypassCheck.canSendRemoteInputWithoutBouncer()
                    && showBouncerForRemoteInput(view, pendingIntent, finalRow));
        }

        return true;
    }

    private boolean showBouncerForRemoteInput(View view, PendingIntent pendingIntent,
            ExpandableNotificationRow row) {
        if (mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
            return false;
        }

        final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();

        final boolean isLockedManagedProfile =
                mUserManager.getUserInfo(userId).isManagedProfile()
                        && mKeyguardManager.isDeviceLocked(userId);

        final boolean isParentUserLocked;
        if (isLockedManagedProfile) {
            final UserInfo profileParent = mUserManager.getProfileParent(userId);
            isParentUserLocked = (profileParent != null)
                    && mKeyguardManager.isDeviceLocked(profileParent.id);
        } else {
            isParentUserLocked = false;
        }

        if ((mLockscreenUserManager.isLockscreenPublicMode(userId)
                || mStatusBarStateController.getState() == StatusBarState.KEYGUARD)) {
            // If the parent user is no longer locked, and the user to which the remote
            // input
            // is destined is a locked, managed profile, then onLockedWorkRemoteInput
            // should be
            // called to unlock it.
            if (isLockedManagedProfile && !isParentUserLocked) {
                mCallback.onLockedWorkRemoteInput(userId, row, view);
            } else {
                // Even if we don't have security we should go through this flow, otherwise
                // we won't go to the shade.
                mCallback.onLockedRemoteInput(row, view);
            }
            return true;
        }
        if (isLockedManagedProfile) {
            mCallback.onLockedWorkRemoteInput(userId, row, view);
            return true;
        }
        return false;
    }

    private RemoteInputView findRemoteInputView(View v) {
        if (v == null) {
@@ -807,8 +852,11 @@ public class NotificationRemoteInputManager implements Dumpable {
         *
         * @param row
         * @param clickedView
         * @param deferBouncer
         * @param runnable
         */
        void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
        void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView,
                boolean deferBouncer, Runnable runnable);

        /**
         * Return whether or not remote input should be handled for this view.
@@ -845,4 +893,28 @@ public class NotificationRemoteInputManager implements Dumpable {
         */
        boolean handleClick();
    }

    /**
     * Predicate that is associated with a specific {@link #activateRemoteInput(View, RemoteInput[],
     * RemoteInput, PendingIntent, EditedSuggestionInfo, String, AuthBypassPredicate)}
     * invocation that determines whether or not the bouncer can be bypassed when sending the
     * RemoteInput.
     */
    public interface AuthBypassPredicate {
        /**
         * Determines if the RemoteInput can be sent without the bouncer. Should be checked the
         * same frame that the RemoteInput is to be sent.
         */
        boolean canSendRemoteInputWithoutBouncer();
    }

    /** Shows the bouncer if necessary */
    public interface BouncerChecker {
        /**
         * Shows the bouncer if necessary in order to send a RemoteInput.
         *
         * @return {@code true} if the bouncer was shown, {@code false} otherwise
         */
        boolean showBouncerIfNecessary();
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -180,8 +180,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,

    @Override
    public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
            View clickedView) {
        if (mKeyguardStateController.isShowing()) {
            View clickedView, boolean deferBouncer, Runnable runnable) {
        if (!deferBouncer && mKeyguardStateController.isShowing()) {
            onLockedRemoteInput(row, clickedView);
        } else {
            if (row.isChildInGroup() && !row.areChildrenExpanded()) {
@@ -189,7 +189,7 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
                mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
            }
            row.setUserExpanded(true);
            row.getPrivateLayout().setOnExpandedVisibleListener(clickedView::performClick);
            row.getPrivateLayout().setOnExpandedVisibleListener(runnable);
        }
    }

+61 −5
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
@@ -80,6 +81,7 @@ import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewW
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.LightBarController;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -99,6 +101,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene

    private final SendButtonTextWatcher mTextWatcher;
    private final TextView.OnEditorActionListener mEditorActionHandler;
    private final NotificationRemoteInputManager mRemoteInputManager;
    private final List<OnFocusChangeListener> mEditTextFocusChangeListeners = new ArrayList<>();
    private RemoteEditText mEditText;
    private ImageButton mSendButton;
    private ProgressBar mProgressBar;
@@ -121,12 +125,14 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    private boolean mResetting;
    private NotificationViewWrapper mWrapper;
    private Consumer<Boolean> mOnVisibilityChangedListener;
    private NotificationRemoteInputManager.BouncerChecker mBouncerChecker;

    public RemoteInputView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTextWatcher = new SendButtonTextWatcher();
        mEditorActionHandler = new EditorActionHandler();
        mRemoteInputQuickSettingsDisabler = Dependency.get(RemoteInputQuickSettingsDisabler.class);
        mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
        mStatusBarManagerService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
    }
@@ -200,6 +206,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    }

    private void sendRemoteInput(Intent intent) {
        if (mBouncerChecker != null && mBouncerChecker.showBouncerIfNecessary()) {
            mEditText.hideIme();
            return;
        }

        mEditText.setEnabled(false);
        mSendButton.setVisibility(INVISIBLE);
        mProgressBar.setVisibility(VISIBLE);
@@ -351,6 +362,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        }
    }

    /** Populates the text field of the remote input with the given content. */
    public void setEditTextContent(@Nullable CharSequence editTextContent) {
        mEditText.setText(editTextContent);
    }

    public void focusAnimated() {
        if (getVisibility() != VISIBLE) {
            Animator animator = ViewAnimationUtils.createCircularReveal(
@@ -552,6 +568,37 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken);
    }

    /**
     * Sets a {@link com.android.systemui.statusbar.NotificationRemoteInputManager.BouncerChecker}
     * that will be used to determine if the device needs to be unlocked before sending the
     * RemoteInput.
     */
    public void setBouncerChecker(
            @Nullable NotificationRemoteInputManager.BouncerChecker bouncerChecker) {
        mBouncerChecker = bouncerChecker;
    }

    /** Registers a listener for focus-change events on the EditText */
    public void addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) {
        mEditTextFocusChangeListeners.add(listener);
    }

    /** Removes a previously-added listener for focus-change events on the EditText */
    public void removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) {
        mEditTextFocusChangeListeners.remove(listener);
    }

    /** Determines if the EditText has focus. */
    public boolean editTextHasFocus() {
        return mEditText != null && mEditText.hasFocus();
    }

    private void onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused) {
        for (View.OnFocusChangeListener listener : mEditTextFocusChangeListeners) {
            listener.onFocusChange(remoteEditText, focused);
        }
    }

    /** Handler for button click on send action in IME. */
    private class EditorActionHandler implements TextView.OnEditorActionListener {

@@ -603,6 +650,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        private RemoteInputView mRemoteInputView;
        boolean mShowImeOnInputConnection;
        private LightBarController mLightBarController;
        private InputMethodManager mInputMethodManager;
        UserHandle mUser;

        public RemoteEditText(Context context, AttributeSet attrs) {
@@ -621,6 +669,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
            setOnReceiveContentListener(types, listener);
        }

        private void hideIme() {
            if (mInputMethodManager != null) {
                mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
            }
        }

        private void defocusIfNeeded(boolean animate) {
            if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition()
                    || isTemporarilyDetached()) {
@@ -654,6 +708,9 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        @Override
        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
            super.onFocusChanged(focused, direction, previouslyFocusedRect);
            if (mRemoteInputView != null) {
                mRemoteInputView.onEditTextFocusChanged(this, focused);
            }
            if (!focused) {
                defocusIfNeeded(true /* animate */);
            }
@@ -724,17 +781,16 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene

            if (mShowImeOnInputConnection && ic != null) {
                Context targetContext = userContext != null ? userContext : getContext();
                final InputMethodManager imm =
                        targetContext.getSystemService(InputMethodManager.class);
                if (imm != null) {
                mInputMethodManager = targetContext.getSystemService(InputMethodManager.class);
                if (mInputMethodManager != null) {
                    // onCreateInputConnection is called by InputMethodManager in the middle of
                    // setting up the connection to the IME; wait with requesting the IME until that
                    // work has completed.
                    post(new Runnable() {
                        @Override
                        public void run() {
                            imm.viewClicked(RemoteEditText.this);
                            imm.showSoftInput(RemoteEditText.this, 0);
                            mInputMethodManager.viewClicked(RemoteEditText.this);
                            mInputMethodManager.showSoftInput(RemoteEditText.this, 0);
                        }
                    });
                }