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