From a7d7205e6ee1a5c0cf29fee9bcae0af34c368d49 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 16 Dec 2020 23:04:19 -0500 Subject: [PATCH] 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 --- packages/SystemUI/Android.bp | 4 +- .../NotificationRemoteInputManager.java | 144 +++++++++++++----- .../phone/StatusBarRemoteInputCallback.java | 6 +- .../statusbar/policy/RemoteInputView.java | 66 +++++++- 4 files changed, 174 insertions(+), 46 deletions(-) diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 9021864a5a866..7bfb42b7cfad0 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -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", diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index a816eccebe335..cfadcd7f01ea4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -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,40 +431,9 @@ 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); - return true; - } + final boolean deferBouncer = authBypassCheck != null; + if (!deferBouncer && showBouncerForRemoteInput(view, pendingIntent, row)) { + return true; } if (riv != null && !riv.isAttachedToWindow()) { @@ -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,10 +484,62 @@ 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) { return 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(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 36519ac0d808a..983b296e006ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -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); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 9380d9110c35d..8e833c05b4472 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -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 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 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); } }); }