1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.policy; 18 19 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.app.ActivityManager; 24 import android.app.Notification; 25 import android.app.PendingIntent; 26 import android.app.RemoteInput; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ShortcutManager; 31 import android.content.res.ColorStateList; 32 import android.content.res.TypedArray; 33 import android.graphics.BlendMode; 34 import android.graphics.Color; 35 import android.graphics.PorterDuff; 36 import android.graphics.Rect; 37 import android.graphics.drawable.GradientDrawable; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.SystemClock; 41 import android.os.UserHandle; 42 import android.text.Editable; 43 import android.text.SpannedString; 44 import android.text.TextUtils; 45 import android.text.TextWatcher; 46 import android.util.ArraySet; 47 import android.util.AttributeSet; 48 import android.util.Log; 49 import android.util.Pair; 50 import android.view.ContentInfo; 51 import android.view.KeyEvent; 52 import android.view.LayoutInflater; 53 import android.view.MotionEvent; 54 import android.view.OnReceiveContentListener; 55 import android.view.View; 56 import android.view.ViewAnimationUtils; 57 import android.view.ViewGroup; 58 import android.view.WindowInsets; 59 import android.view.WindowInsetsAnimation; 60 import android.view.accessibility.AccessibilityEvent; 61 import android.view.inputmethod.CompletionInfo; 62 import android.view.inputmethod.EditorInfo; 63 import android.view.inputmethod.InputConnection; 64 import android.view.inputmethod.InputMethodManager; 65 import android.widget.EditText; 66 import android.widget.ImageButton; 67 import android.widget.ImageView; 68 import android.widget.LinearLayout; 69 import android.widget.ProgressBar; 70 import android.widget.TextView; 71 72 import androidx.annotation.NonNull; 73 import androidx.annotation.Nullable; 74 75 import com.android.internal.graphics.ColorUtils; 76 import com.android.internal.logging.MetricsLogger; 77 import com.android.internal.logging.UiEvent; 78 import com.android.internal.logging.UiEventLogger; 79 import com.android.internal.logging.nano.MetricsProto; 80 import com.android.systemui.Dependency; 81 import com.android.systemui.R; 82 import com.android.systemui.statusbar.NotificationRemoteInputManager; 83 import com.android.systemui.statusbar.RemoteInputController; 84 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 85 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; 86 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 87 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 88 import com.android.systemui.statusbar.phone.LightBarController; 89 import com.android.wm.shell.animation.Interpolators; 90 91 import java.util.ArrayList; 92 import java.util.Collection; 93 import java.util.HashMap; 94 import java.util.List; 95 import java.util.function.Consumer; 96 97 /** 98 * Host for the remote input. 99 */ 100 public class RemoteInputView extends LinearLayout implements View.OnClickListener { 101 102 private static final String TAG = "RemoteInput"; 103 104 // A marker object that let's us easily find views of this class. 105 public static final Object VIEW_TAG = new Object(); 106 107 public final Object mToken = new Object(); 108 109 private final SendButtonTextWatcher mTextWatcher; 110 private final TextView.OnEditorActionListener mEditorActionHandler; 111 private final ArrayList<OnSendRemoteInputListener> mOnSendListeners = new ArrayList<>(); 112 private final ArrayList<Consumer<Boolean>> mOnVisibilityChangedListeners = new ArrayList<>(); 113 private final ArrayList<OnFocusChangeListener> mEditTextFocusChangeListeners = 114 new ArrayList<>(); 115 116 private RemoteEditText mEditText; 117 private ImageButton mSendButton; 118 private GradientDrawable mContentBackground; 119 private ProgressBar mProgressBar; 120 private ImageView mDelete; 121 private ImageView mDeleteBg; 122 // TODO(b/193539698): remove reveal param fields, turn them into parameters where needed 123 private int mRevealCx; 124 private int mRevealCy; 125 private int mRevealR; 126 private boolean mColorized; 127 private int mTint; 128 private boolean mResetting; 129 130 // TODO(b/193539698): move these to a Controller 131 private RemoteInputController mController; 132 private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; 133 private final UiEventLogger mUiEventLogger; 134 private NotificationEntry mEntry; 135 private PendingIntent mPendingIntent; 136 private RemoteInput mRemoteInput; 137 private RemoteInput[] mRemoteInputs; 138 private NotificationRemoteInputManager.BouncerChecker mBouncerChecker; 139 private boolean mRemoved; 140 private NotificationViewWrapper mWrapper; 141 142 /** 143 * Enum for logged notification remote input UiEvents. 144 */ 145 enum NotificationRemoteInputEvent implements UiEventLogger.UiEventEnum { 146 @UiEvent(doc = "Notification remote input view was displayed") 147 NOTIFICATION_REMOTE_INPUT_OPEN(795), 148 @UiEvent(doc = "Notification remote input view was closed") 149 NOTIFICATION_REMOTE_INPUT_CLOSE(796), 150 @UiEvent(doc = "User sent data through the notification remote input view") 151 NOTIFICATION_REMOTE_INPUT_SEND(797), 152 @UiEvent(doc = "Failed attempt to send data through the notification remote input view") 153 NOTIFICATION_REMOTE_INPUT_FAILURE(798); 154 155 private final int mId; NotificationRemoteInputEvent(int id)156 NotificationRemoteInputEvent(int id) { 157 mId = id; 158 } getId()159 @Override public int getId() { 160 return mId; 161 } 162 } 163 RemoteInputView(Context context, AttributeSet attrs)164 public RemoteInputView(Context context, AttributeSet attrs) { 165 super(context, attrs); 166 mTextWatcher = new SendButtonTextWatcher(); 167 mEditorActionHandler = new EditorActionHandler(); 168 mRemoteInputQuickSettingsDisabler = Dependency.get(RemoteInputQuickSettingsDisabler.class); 169 mUiEventLogger = Dependency.get(UiEventLogger.class); 170 TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ 171 com.android.internal.R.attr.colorAccent, 172 com.android.internal.R.attr.colorSurface, 173 }); 174 mTint = ta.getColor(0, 0); 175 ta.recycle(); 176 } 177 colorStateListWithDisabledAlpha(int color, int disabledAlpha)178 private ColorStateList colorStateListWithDisabledAlpha(int color, int disabledAlpha) { 179 return new ColorStateList(new int[][]{ 180 new int[]{-com.android.internal.R.attr.state_enabled}, // disabled 181 new int[]{}, 182 }, new int[]{ 183 ColorUtils.setAlphaComponent(color, disabledAlpha), 184 color 185 }); 186 } 187 188 /** 189 * The remote view needs to adapt to colorized notifications when set 190 * It overrides the background of itself as well as all of its childern 191 * @param backgroundColor colorized notification color 192 */ setBackgroundTintColor(final int backgroundColor, boolean colorized)193 public void setBackgroundTintColor(final int backgroundColor, boolean colorized) { 194 if (colorized == mColorized && backgroundColor == mTint) return; 195 mColorized = colorized; 196 mTint = backgroundColor; 197 final int editBgColor; 198 final int deleteBgColor; 199 final int deleteFgColor; 200 final ColorStateList accentColor; 201 final ColorStateList textColor; 202 final int hintColor; 203 final int stroke = colorized ? mContext.getResources().getDimensionPixelSize( 204 R.dimen.remote_input_view_text_stroke) : 0; 205 if (colorized) { 206 final boolean dark = Notification.Builder.isColorDark(backgroundColor); 207 final int foregroundColor = dark ? Color.WHITE : Color.BLACK; 208 final int inverseColor = dark ? Color.BLACK : Color.WHITE; 209 editBgColor = backgroundColor; 210 deleteBgColor = foregroundColor; 211 deleteFgColor = inverseColor; 212 accentColor = colorStateListWithDisabledAlpha(foregroundColor, 0x4D); // 30% 213 textColor = colorStateListWithDisabledAlpha(foregroundColor, 0x99); // 60% 214 hintColor = ColorUtils.setAlphaComponent(foregroundColor, 0x99); 215 } else { 216 accentColor = mContext.getColorStateList(R.color.remote_input_send); 217 textColor = mContext.getColorStateList(R.color.remote_input_text); 218 hintColor = mContext.getColor(R.color.remote_input_hint); 219 deleteFgColor = textColor.getDefaultColor(); 220 try (TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ 221 com.android.internal.R.attr.colorSurfaceHighlight, 222 com.android.internal.R.attr.colorSurfaceVariant 223 })) { 224 editBgColor = ta.getColor(0, backgroundColor); 225 deleteBgColor = ta.getColor(1, Color.GRAY); 226 } 227 } 228 229 mEditText.setTextColor(textColor); 230 mEditText.setHintTextColor(hintColor); 231 mEditText.getTextCursorDrawable().setColorFilter( 232 accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN); 233 mContentBackground.setColor(editBgColor); 234 mContentBackground.setStroke(stroke, accentColor); 235 mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor)); 236 mDeleteBg.setImageTintList(ColorStateList.valueOf(deleteBgColor)); 237 mSendButton.setImageTintList(accentColor); 238 mProgressBar.setProgressTintList(accentColor); 239 mProgressBar.setIndeterminateTintList(accentColor); 240 mProgressBar.setSecondaryProgressTintList(accentColor); 241 setBackgroundColor(backgroundColor); 242 } 243 244 @Override onFinishInflate()245 protected void onFinishInflate() { 246 super.onFinishInflate(); 247 248 mProgressBar = findViewById(R.id.remote_input_progress); 249 mSendButton = findViewById(R.id.remote_input_send); 250 mSendButton.setOnClickListener(this); 251 mContentBackground = (GradientDrawable) 252 mContext.getDrawable(R.drawable.remote_input_view_text_bg).mutate(); 253 mDelete = findViewById(R.id.remote_input_delete); 254 mDeleteBg = findViewById(R.id.remote_input_delete_bg); 255 mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN); 256 mDelete.setImageTintBlendMode(BlendMode.SRC_IN); 257 mDelete.setOnClickListener(v -> setAttachment(null)); 258 LinearLayout contentView = findViewById(R.id.remote_input_content); 259 contentView.setBackground(mContentBackground); 260 mEditText = findViewById(R.id.remote_input_text); 261 mEditText.setInnerFocusable(false); 262 mEditText.setWindowInsetsAnimationCallback( 263 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { 264 @NonNull 265 @Override 266 public WindowInsets onProgress(@NonNull WindowInsets insets, 267 @NonNull List<WindowInsetsAnimation> runningAnimations) { 268 return insets; 269 } 270 @Override 271 public void onEnd(@NonNull WindowInsetsAnimation animation) { 272 super.onEnd(animation); 273 if (animation.getTypeMask() == WindowInsets.Type.ime()) { 274 mEntry.mRemoteEditImeAnimatingAway = false; 275 mEntry.mRemoteEditImeVisible = 276 mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()); 277 if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { 278 mController.removeRemoteInput(mEntry, mToken); 279 } 280 } 281 } 282 }); 283 } 284 setAttachment(ContentInfo item)285 private void setAttachment(ContentInfo item) { 286 if (mEntry.remoteInputAttachment != null && mEntry.remoteInputAttachment != item) { 287 // We need to release permissions when sending the attachment to the target 288 // app or if it is deleted by the user. When sending to the target app, we 289 // can safely release permissions as soon as the call to 290 // `mController.grantInlineReplyUriPermission` is made (ie, after the grant 291 // to the target app has been created). 292 mEntry.remoteInputAttachment.releasePermissions(); 293 } 294 mEntry.remoteInputAttachment = item; 295 if (item != null) { 296 mEntry.remoteInputUri = item.getClip().getItemAt(0).getUri(); 297 mEntry.remoteInputMimeType = item.getClip().getDescription().getMimeType(0); 298 } 299 View attachment = findViewById(R.id.remote_input_content_container); 300 ImageView iconView = findViewById(R.id.remote_input_attachment_image); 301 iconView.setImageDrawable(null); 302 if (item == null) { 303 attachment.setVisibility(GONE); 304 return; 305 } 306 iconView.setImageURI(item.getClip().getItemAt(0).getUri()); 307 if (iconView.getDrawable() == null) { 308 attachment.setVisibility(GONE); 309 } else { 310 attachment.setVisibility(VISIBLE); 311 } 312 updateSendButton(); 313 } 314 315 /** 316 * Reply intent 317 * @return returns intent with granted URI permissions that should be used immediately 318 */ prepareRemoteInput()319 private Intent prepareRemoteInput() { 320 return mEntry.remoteInputAttachment == null 321 ? prepareRemoteInputFromText() 322 : prepareRemoteInputFromData(mEntry.remoteInputMimeType, mEntry.remoteInputUri); 323 } 324 prepareRemoteInputFromText()325 private Intent prepareRemoteInputFromText() { 326 Bundle results = new Bundle(); 327 results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); 328 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 329 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, 330 results); 331 332 mEntry.remoteInputText = mEditText.getText().toString(); 333 setAttachment(null); 334 mEntry.remoteInputUri = null; 335 mEntry.remoteInputMimeType = null; 336 337 if (mEntry.editedSuggestionInfo == null) { 338 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT); 339 } else { 340 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE); 341 } 342 343 return fillInIntent; 344 } 345 prepareRemoteInputFromData(String contentType, Uri data)346 private Intent prepareRemoteInputFromData(String contentType, Uri data) { 347 HashMap<String, Uri> results = new HashMap<>(); 348 results.put(contentType, data); 349 // grant for the target app. 350 mController.grantInlineReplyUriPermission(mEntry.getSbn(), data); 351 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 352 RemoteInput.addDataResultToIntent(mRemoteInput, fillInIntent, results); 353 354 Bundle bundle = new Bundle(); 355 bundle.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); 356 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, 357 bundle); 358 359 CharSequence attachmentText = 360 mEntry.remoteInputAttachment.getClip().getDescription().getLabel(); 361 362 CharSequence attachmentLabel = TextUtils.isEmpty(attachmentText) 363 ? mContext.getString(R.string.remote_input_image_insertion_text) 364 : attachmentText; 365 // add content description to reply text for context 366 CharSequence fullText = TextUtils.isEmpty(mEditText.getText()) 367 ? attachmentLabel 368 : "\"" + attachmentLabel + "\" " + mEditText.getText(); 369 370 mEntry.remoteInputText = fullText; 371 372 // mirror prepareRemoteInputFromText for text input 373 if (mEntry.editedSuggestionInfo == null) { 374 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT); 375 } else if (mEntry.remoteInputAttachment == null) { 376 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE); 377 } 378 379 return fillInIntent; 380 } 381 sendRemoteInput(Intent intent)382 private void sendRemoteInput(Intent intent) { 383 if (mBouncerChecker != null && mBouncerChecker.showBouncerIfNecessary()) { 384 mEditText.hideIme(); 385 for (OnSendRemoteInputListener listener : new ArrayList<>(mOnSendListeners)) { 386 listener.onSendRequestBounced(); 387 } 388 return; 389 } 390 391 mEditText.setEnabled(false); 392 mSendButton.setVisibility(INVISIBLE); 393 mProgressBar.setVisibility(VISIBLE); 394 mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime(); 395 mEntry.mRemoteEditImeAnimatingAway = true; 396 mController.addSpinning(mEntry.getKey(), mToken); 397 mController.removeRemoteInput(mEntry, mToken); 398 mEditText.mShowImeOnInputConnection = false; 399 mController.remoteInputSent(mEntry); 400 mEntry.setHasSentReply(); 401 402 for (OnSendRemoteInputListener listener : new ArrayList<>(mOnSendListeners)) { 403 listener.onSendRemoteInput(); 404 } 405 406 // Tell ShortcutManager that this package has been "activated". ShortcutManager 407 // will reset the throttling for this package. 408 // Strictly speaking, the intent receiver may be different from the notification publisher, 409 // but that's an edge case, and also because we can't always know which package will receive 410 // an intent, so we just reset for the publisher. 411 getContext().getSystemService(ShortcutManager.class).onApplicationActive( 412 mEntry.getSbn().getPackageName(), 413 mEntry.getSbn().getUser().getIdentifier()); 414 415 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND, 416 mEntry.getSbn().getPackageName()); 417 mUiEventLogger.logWithInstanceId( 418 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_SEND, 419 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 420 mEntry.getSbn().getInstanceId()); 421 try { 422 mPendingIntent.send(mContext, 0, intent); 423 } catch (PendingIntent.CanceledException e) { 424 Log.i(TAG, "Unable to send remote input result", e); 425 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL, 426 mEntry.getSbn().getPackageName()); 427 mUiEventLogger.logWithInstanceId( 428 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_FAILURE, 429 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 430 mEntry.getSbn().getInstanceId()); 431 } 432 433 setAttachment(null); 434 } 435 getText()436 public CharSequence getText() { 437 return mEditText.getText(); 438 } 439 inflate(Context context, ViewGroup root, NotificationEntry entry, RemoteInputController controller)440 public static RemoteInputView inflate(Context context, ViewGroup root, 441 NotificationEntry entry, 442 RemoteInputController controller) { 443 RemoteInputView v = (RemoteInputView) 444 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); 445 v.mController = controller; 446 v.mEntry = entry; 447 UserHandle user = computeTextOperationUser(entry.getSbn().getUser()); 448 v.mEditText.mUser = user; 449 v.mEditText.setTextOperationUser(user); 450 v.setTag(VIEW_TAG); 451 452 return v; 453 } 454 455 @Override onClick(View v)456 public void onClick(View v) { 457 if (v == mSendButton) { 458 sendRemoteInput(prepareRemoteInput()); 459 } 460 } 461 462 @Override onTouchEvent(MotionEvent event)463 public boolean onTouchEvent(MotionEvent event) { 464 super.onTouchEvent(event); 465 466 // We never want for a touch to escape to an outer view or one we covered. 467 return true; 468 } 469 onDefocus(boolean animate, boolean logClose)470 private void onDefocus(boolean animate, boolean logClose) { 471 mController.removeRemoteInput(mEntry, mToken); 472 mEntry.remoteInputText = mEditText.getText(); 473 474 // During removal, we get reattached and lose focus. Not hiding in that 475 // case to prevent flicker. 476 if (!mRemoved) { 477 if (animate && mRevealR > 0) { 478 Animator reveal = ViewAnimationUtils.createCircularReveal( 479 this, mRevealCx, mRevealCy, mRevealR, 0); 480 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 481 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT); 482 reveal.addListener(new AnimatorListenerAdapter() { 483 @Override 484 public void onAnimationEnd(Animator animation) { 485 setVisibility(GONE); 486 if (mWrapper != null) { 487 mWrapper.setRemoteInputVisible(false); 488 } 489 } 490 }); 491 reveal.start(); 492 } else { 493 setVisibility(GONE); 494 if (mWrapper != null) { 495 mWrapper.setRemoteInputVisible(false); 496 } 497 } 498 } 499 500 mRemoteInputQuickSettingsDisabler.setRemoteInputActive(false); 501 502 if (logClose) { 503 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE, 504 mEntry.getSbn().getPackageName()); 505 mUiEventLogger.logWithInstanceId( 506 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_CLOSE, 507 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 508 mEntry.getSbn().getInstanceId()); 509 } 510 } 511 512 @Override onAttachedToWindow()513 protected void onAttachedToWindow() { 514 super.onAttachedToWindow(); 515 mEditText.mRemoteInputView = this; 516 mEditText.setOnEditorActionListener(mEditorActionHandler); 517 mEditText.addTextChangedListener(mTextWatcher); 518 if (mEntry.getRow().isChangingPosition()) { 519 if (getVisibility() == VISIBLE && mEditText.isFocusable()) { 520 mEditText.requestFocus(); 521 } 522 } 523 } 524 525 @Override onDetachedFromWindow()526 protected void onDetachedFromWindow() { 527 super.onDetachedFromWindow(); 528 mEditText.removeTextChangedListener(mTextWatcher); 529 mEditText.setOnEditorActionListener(null); 530 mEditText.mRemoteInputView = null; 531 if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { 532 return; 533 } 534 mController.removeRemoteInput(mEntry, mToken); 535 mController.removeSpinning(mEntry.getKey(), mToken); 536 } 537 setPendingIntent(PendingIntent pendingIntent)538 public void setPendingIntent(PendingIntent pendingIntent) { 539 mPendingIntent = pendingIntent; 540 } 541 542 /** 543 * Sets the remote input for this view. 544 * 545 * @param remoteInputs The remote inputs that need to be sent to the app. 546 * @param remoteInput The remote input that needs to be activated. 547 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or 548 * {@code null} if the user is not editing a smart reply. 549 */ setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput, @Nullable EditedSuggestionInfo editedSuggestionInfo)550 public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput, 551 @Nullable EditedSuggestionInfo editedSuggestionInfo) { 552 mRemoteInputs = remoteInputs; 553 mRemoteInput = remoteInput; 554 mEditText.setHint(mRemoteInput.getLabel()); 555 mEditText.setSupportedMimeTypes(remoteInput.getAllowedDataTypes()); 556 557 mEntry.editedSuggestionInfo = editedSuggestionInfo; 558 if (editedSuggestionInfo != null) { 559 mEntry.remoteInputText = editedSuggestionInfo.originalText; 560 mEntry.remoteInputAttachment = null; 561 } 562 } 563 564 /** Populates the text field of the remote input with the given content. */ setEditTextContent(@ullable CharSequence editTextContent)565 public void setEditTextContent(@Nullable CharSequence editTextContent) { 566 mEditText.setText(editTextContent); 567 } 568 focusAnimated()569 public void focusAnimated() { 570 if (getVisibility() != VISIBLE) { 571 Animator animator = ViewAnimationUtils.createCircularReveal( 572 this, mRevealCx, mRevealCy, 0, mRevealR); 573 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 574 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 575 animator.start(); 576 } 577 focus(); 578 } 579 computeTextOperationUser(UserHandle notificationUser)580 private static UserHandle computeTextOperationUser(UserHandle notificationUser) { 581 return UserHandle.ALL.equals(notificationUser) 582 ? UserHandle.of(ActivityManager.getCurrentUser()) : notificationUser; 583 } 584 focus()585 public void focus() { 586 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN, 587 mEntry.getSbn().getPackageName()); 588 mUiEventLogger.logWithInstanceId( 589 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN, 590 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 591 mEntry.getSbn().getInstanceId()); 592 593 setVisibility(VISIBLE); 594 if (mWrapper != null) { 595 mWrapper.setRemoteInputVisible(true); 596 } 597 mEditText.setInnerFocusable(true); 598 mEditText.mShowImeOnInputConnection = true; 599 mEditText.setText(mEntry.remoteInputText); 600 mEditText.setSelection(mEditText.length()); 601 mEditText.requestFocus(); 602 mController.addRemoteInput(mEntry, mToken); 603 setAttachment(mEntry.remoteInputAttachment); 604 605 mRemoteInputQuickSettingsDisabler.setRemoteInputActive(true); 606 607 updateSendButton(); 608 } 609 onNotificationUpdateOrReset()610 public void onNotificationUpdateOrReset() { 611 boolean sending = mProgressBar.getVisibility() == VISIBLE; 612 613 if (sending) { 614 // Update came in after we sent the reply, time to reset. 615 reset(); 616 } 617 618 if (isActive() && mWrapper != null) { 619 mWrapper.setRemoteInputVisible(true); 620 } 621 } 622 reset()623 private void reset() { 624 mResetting = true; 625 mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); 626 627 mEditText.getText().clear(); 628 mEditText.setEnabled(true); 629 mSendButton.setVisibility(VISIBLE); 630 mProgressBar.setVisibility(INVISIBLE); 631 mController.removeSpinning(mEntry.getKey(), mToken); 632 updateSendButton(); 633 onDefocus(false /* animate */, false /* logClose */); 634 setAttachment(null); 635 636 mResetting = false; 637 } 638 639 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)640 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 641 if (mResetting && child == mEditText) { 642 // Suppress text events if it happens during resetting. Ideally this would be 643 // suppressed by the text view not being shown, but that doesn't work here because it 644 // needs to stay visible for the animation. 645 return false; 646 } 647 return super.onRequestSendAccessibilityEvent(child, event); 648 } 649 updateSendButton()650 private void updateSendButton() { 651 mSendButton.setEnabled(mEditText.length() != 0 || mEntry.remoteInputAttachment != null); 652 } 653 close()654 public void close() { 655 mEditText.defocusIfNeeded(false /* animated */); 656 } 657 658 @Override onInterceptTouchEvent(MotionEvent ev)659 public boolean onInterceptTouchEvent(MotionEvent ev) { 660 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 661 mController.requestDisallowLongPressAndDismiss(); 662 } 663 return super.onInterceptTouchEvent(ev); 664 } 665 requestScrollTo()666 public boolean requestScrollTo() { 667 mController.lockScrollTo(mEntry); 668 return true; 669 } 670 isActive()671 public boolean isActive() { 672 return mEditText.isFocused() && mEditText.isEnabled(); 673 } 674 stealFocusFrom(RemoteInputView other)675 public void stealFocusFrom(RemoteInputView other) { 676 other.close(); 677 setPendingIntent(other.mPendingIntent); 678 setRemoteInput(other.mRemoteInputs, other.mRemoteInput, mEntry.editedSuggestionInfo); 679 setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR); 680 focus(); 681 } 682 683 /** 684 * Tries to find an action in {@param actions} that matches the current pending intent 685 * of this view and updates its state to that of the found action 686 * 687 * @return true if a matching action was found, false otherwise 688 */ updatePendingIntentFromActions(Notification.Action[] actions)689 public boolean updatePendingIntentFromActions(Notification.Action[] actions) { 690 if (mPendingIntent == null || actions == null) { 691 return false; 692 } 693 Intent current = mPendingIntent.getIntent(); 694 if (current == null) { 695 return false; 696 } 697 698 for (Notification.Action a : actions) { 699 RemoteInput[] inputs = a.getRemoteInputs(); 700 if (a.actionIntent == null || inputs == null) { 701 continue; 702 } 703 Intent candidate = a.actionIntent.getIntent(); 704 if (!current.filterEquals(candidate)) { 705 continue; 706 } 707 708 RemoteInput input = null; 709 for (RemoteInput i : inputs) { 710 if (i.getAllowFreeFormInput()) { 711 input = i; 712 } 713 } 714 if (input == null) { 715 continue; 716 } 717 setPendingIntent(a.actionIntent); 718 setRemoteInput(inputs, input, null /* editedSuggestionInfo*/); 719 return true; 720 } 721 return false; 722 } 723 getPendingIntent()724 public PendingIntent getPendingIntent() { 725 return mPendingIntent; 726 } 727 setRemoved()728 public void setRemoved() { 729 mRemoved = true; 730 } 731 setRevealParameters(int cx, int cy, int r)732 public void setRevealParameters(int cx, int cy, int r) { 733 mRevealCx = cx; 734 mRevealCy = cy; 735 mRevealR = r; 736 } 737 738 @Override dispatchStartTemporaryDetach()739 public void dispatchStartTemporaryDetach() { 740 super.dispatchStartTemporaryDetach(); 741 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and 742 // won't lose IME focus. 743 final int iEditText = indexOfChild(mEditText); 744 if (iEditText != -1) { 745 detachViewFromParent(iEditText); 746 } 747 } 748 749 @Override dispatchFinishTemporaryDetach()750 public void dispatchFinishTemporaryDetach() { 751 if (isAttachedToWindow()) { 752 attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); 753 } else { 754 removeDetachedView(mEditText, false /* animate */); 755 } 756 super.dispatchFinishTemporaryDetach(); 757 } 758 setWrapper(NotificationViewWrapper wrapper)759 public void setWrapper(NotificationViewWrapper wrapper) { 760 mWrapper = wrapper; 761 } 762 763 /** 764 * Register a listener to be notified when this view's visibility changes. 765 * 766 * Specifically, the passed {@link Consumer} will receive {@code true} when 767 * {@link #getVisibility()} would return {@link View#VISIBLE}, and {@code false} it would return 768 * any other value. 769 */ addOnVisibilityChangedListener(Consumer<Boolean> listener)770 public void addOnVisibilityChangedListener(Consumer<Boolean> listener) { 771 mOnVisibilityChangedListeners.add(listener); 772 } 773 774 /** 775 * Unregister a listener previously registered via 776 * {@link #addOnVisibilityChangedListener(Consumer)}. 777 */ removeOnVisibilityChangedListener(Consumer<Boolean> listener)778 public void removeOnVisibilityChangedListener(Consumer<Boolean> listener) { 779 mOnVisibilityChangedListeners.remove(listener); 780 } 781 782 @Override onVisibilityChanged(View changedView, int visibility)783 protected void onVisibilityChanged(View changedView, int visibility) { 784 super.onVisibilityChanged(changedView, visibility); 785 if (changedView == this) { 786 for (Consumer<Boolean> listener : new ArrayList<>(mOnVisibilityChangedListeners)) { 787 listener.accept(visibility == VISIBLE); 788 } 789 // Hide soft-keyboard when the input view became invisible 790 // (i.e. The notification shade collapsed by pressing the home key) 791 if (visibility != VISIBLE && !mEditText.isVisibleToUser() 792 && !mController.isRemoteInputActive()) { 793 mEditText.hideIme(); 794 } 795 } 796 } 797 isSending()798 public boolean isSending() { 799 return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken); 800 } 801 802 /** 803 * Sets a {@link com.android.systemui.statusbar.NotificationRemoteInputManager.BouncerChecker} 804 * that will be used to determine if the device needs to be unlocked before sending the 805 * RemoteInput. 806 */ setBouncerChecker( @ullable NotificationRemoteInputManager.BouncerChecker bouncerChecker)807 public void setBouncerChecker( 808 @Nullable NotificationRemoteInputManager.BouncerChecker bouncerChecker) { 809 mBouncerChecker = bouncerChecker; 810 } 811 812 /** Registers a listener for focus-change events on the EditText */ addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)813 public void addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) { 814 mEditTextFocusChangeListeners.add(listener); 815 } 816 817 /** Removes a previously-added listener for focus-change events on the EditText */ removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)818 public void removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) { 819 mEditTextFocusChangeListeners.remove(listener); 820 } 821 822 /** Determines if the EditText has focus. */ editTextHasFocus()823 public boolean editTextHasFocus() { 824 return mEditText != null && mEditText.hasFocus(); 825 } 826 onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused)827 private void onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused) { 828 for (View.OnFocusChangeListener listener : new ArrayList<>(mEditTextFocusChangeListeners)) { 829 listener.onFocusChange(remoteEditText, focused); 830 } 831 } 832 833 /** Registers a listener for send events on this RemoteInputView */ addOnSendRemoteInputListener(OnSendRemoteInputListener listener)834 public void addOnSendRemoteInputListener(OnSendRemoteInputListener listener) { 835 mOnSendListeners.add(listener); 836 } 837 838 /** Removes a previously-added listener for send events on this RemoteInputView */ removeOnSendRemoteInputListener(OnSendRemoteInputListener listener)839 public void removeOnSendRemoteInputListener(OnSendRemoteInputListener listener) { 840 mOnSendListeners.remove(listener); 841 } 842 843 /** Listener for send events */ 844 public interface OnSendRemoteInputListener { 845 /** Invoked when the remote input has been sent successfully. */ onSendRemoteInput()846 void onSendRemoteInput(); 847 /** 848 * Invoked when the user had requested to send the remote input, but authentication was 849 * required and the bouncer was shown instead. 850 */ onSendRequestBounced()851 void onSendRequestBounced(); 852 } 853 854 /** Handler for button click on send action in IME. */ 855 private class EditorActionHandler implements TextView.OnEditorActionListener { 856 857 @Override onEditorAction(TextView v, int actionId, KeyEvent event)858 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 859 final boolean isSoftImeEvent = event == null 860 && (actionId == EditorInfo.IME_ACTION_DONE 861 || actionId == EditorInfo.IME_ACTION_NEXT 862 || actionId == EditorInfo.IME_ACTION_SEND); 863 final boolean isKeyboardEnterKey = event != null 864 && KeyEvent.isConfirmKey(event.getKeyCode()) 865 && event.getAction() == KeyEvent.ACTION_DOWN; 866 867 if (isSoftImeEvent || isKeyboardEnterKey) { 868 if (mEditText.length() > 0 || mEntry.remoteInputAttachment != null) { 869 sendRemoteInput(prepareRemoteInput()); 870 } 871 // Consume action to prevent IME from closing. 872 return true; 873 } 874 return false; 875 } 876 } 877 878 /** Observes text change events and updates the visibility of the send button accordingly. */ 879 private class SendButtonTextWatcher implements TextWatcher { 880 881 @Override beforeTextChanged(CharSequence s, int start, int count, int after)882 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 883 884 @Override onTextChanged(CharSequence s, int start, int before, int count)885 public void onTextChanged(CharSequence s, int start, int before, int count) {} 886 887 @Override afterTextChanged(Editable s)888 public void afterTextChanged(Editable s) { 889 updateSendButton(); 890 } 891 } 892 893 /** 894 * An EditText that changes appearance based on whether it's focusable and becomes 895 * un-focusable whenever the user navigates away from it or it becomes invisible. 896 */ 897 public static class RemoteEditText extends EditText { 898 899 private final OnReceiveContentListener mOnReceiveContentListener = this::onReceiveContent; 900 901 private RemoteInputView mRemoteInputView; 902 boolean mShowImeOnInputConnection; 903 private LightBarController mLightBarController; 904 private InputMethodManager mInputMethodManager; 905 private ArraySet<String> mSupportedMimes = new ArraySet<>(); 906 UserHandle mUser; 907 RemoteEditText(Context context, AttributeSet attrs)908 public RemoteEditText(Context context, AttributeSet attrs) { 909 super(context, attrs); 910 mLightBarController = Dependency.get(LightBarController.class); 911 } 912 setSupportedMimeTypes(@ullable Collection<String> mimeTypes)913 void setSupportedMimeTypes(@Nullable Collection<String> mimeTypes) { 914 String[] types = null; 915 OnReceiveContentListener listener = null; 916 if (mimeTypes != null && !mimeTypes.isEmpty()) { 917 types = mimeTypes.toArray(new String[0]); 918 listener = mOnReceiveContentListener; 919 } 920 setOnReceiveContentListener(types, listener); 921 mSupportedMimes.clear(); 922 mSupportedMimes.addAll(mimeTypes); 923 } 924 hideIme()925 private void hideIme() { 926 if (mInputMethodManager != null) { 927 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 928 } 929 } 930 defocusIfNeeded(boolean animate)931 private void defocusIfNeeded(boolean animate) { 932 if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition() 933 || isTemporarilyDetached()) { 934 if (isTemporarilyDetached()) { 935 // We might get reattached but then the other one of HUN / expanded might steal 936 // our focus, so we'll need to save our text here. 937 if (mRemoteInputView != null) { 938 mRemoteInputView.mEntry.remoteInputText = getText(); 939 } 940 } 941 return; 942 } 943 if (isFocusable() && isEnabled()) { 944 setInnerFocusable(false); 945 if (mRemoteInputView != null) { 946 mRemoteInputView.onDefocus(animate, true /* logClose */); 947 } 948 mShowImeOnInputConnection = false; 949 } 950 } 951 952 @Override onVisibilityChanged(View changedView, int visibility)953 protected void onVisibilityChanged(View changedView, int visibility) { 954 super.onVisibilityChanged(changedView, visibility); 955 956 if (!isShown()) { 957 defocusIfNeeded(false /* animate */); 958 } 959 } 960 961 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)962 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 963 super.onFocusChanged(focused, direction, previouslyFocusedRect); 964 if (mRemoteInputView != null) { 965 mRemoteInputView.onEditTextFocusChanged(this, focused); 966 } 967 if (!focused) { 968 defocusIfNeeded(true /* animate */); 969 } 970 if (mRemoteInputView != null && !mRemoteInputView.mRemoved) { 971 mLightBarController.setDirectReplying(focused); 972 } 973 } 974 975 @Override getFocusedRect(Rect r)976 public void getFocusedRect(Rect r) { 977 super.getFocusedRect(r); 978 r.top = mScrollY; 979 r.bottom = mScrollY + (mBottom - mTop); 980 } 981 982 @Override requestRectangleOnScreen(Rect rectangle)983 public boolean requestRectangleOnScreen(Rect rectangle) { 984 return mRemoteInputView.requestScrollTo(); 985 } 986 987 @Override onKeyDown(int keyCode, KeyEvent event)988 public boolean onKeyDown(int keyCode, KeyEvent event) { 989 if (keyCode == KeyEvent.KEYCODE_BACK) { 990 // Eat the DOWN event here to prevent any default behavior. 991 return true; 992 } 993 return super.onKeyDown(keyCode, event); 994 } 995 996 @Override onKeyUp(int keyCode, KeyEvent event)997 public boolean onKeyUp(int keyCode, KeyEvent event) { 998 if (keyCode == KeyEvent.KEYCODE_BACK) { 999 defocusIfNeeded(true /* animate */); 1000 return true; 1001 } 1002 return super.onKeyUp(keyCode, event); 1003 } 1004 1005 @Override onKeyPreIme(int keyCode, KeyEvent event)1006 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1007 // When BACK key is pressed, this method would be invoked twice. 1008 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && 1009 event.getAction() == KeyEvent.ACTION_UP) { 1010 defocusIfNeeded(true /* animate */); 1011 } 1012 return super.onKeyPreIme(keyCode, event); 1013 } 1014 1015 @Override onCheckIsTextEditor()1016 public boolean onCheckIsTextEditor() { 1017 // Stop being editable while we're being removed. During removal, we get reattached, 1018 // and editable views get their spellchecking state re-evaluated which is too costly 1019 // during the removal animation. 1020 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved; 1021 return !flyingOut && super.onCheckIsTextEditor(); 1022 } 1023 1024 @Override onCreateInputConnection(EditorInfo outAttrs)1025 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 1026 final InputConnection ic = super.onCreateInputConnection(outAttrs); 1027 Context userContext = null; 1028 try { 1029 userContext = mContext.createPackageContextAsUser( 1030 mContext.getPackageName(), 0, mUser); 1031 } catch (PackageManager.NameNotFoundException e) { 1032 Log.e(TAG, "Unable to create user context:" + e.getMessage(), e); 1033 } 1034 1035 if (mShowImeOnInputConnection && ic != null) { 1036 Context targetContext = userContext != null ? userContext : getContext(); 1037 mInputMethodManager = targetContext.getSystemService(InputMethodManager.class); 1038 if (mInputMethodManager != null) { 1039 // onCreateInputConnection is called by InputMethodManager in the middle of 1040 // setting up the connection to the IME; wait with requesting the IME until that 1041 // work has completed. 1042 post(new Runnable() { 1043 @Override 1044 public void run() { 1045 mInputMethodManager.viewClicked(RemoteEditText.this); 1046 mInputMethodManager.showSoftInput(RemoteEditText.this, 0); 1047 } 1048 }); 1049 } 1050 } 1051 1052 return ic; 1053 } 1054 1055 @Override onCommitCompletion(CompletionInfo text)1056 public void onCommitCompletion(CompletionInfo text) { 1057 clearComposingText(); 1058 setText(text.getText()); 1059 setSelection(getText().length()); 1060 } 1061 setInnerFocusable(boolean focusable)1062 void setInnerFocusable(boolean focusable) { 1063 setFocusableInTouchMode(focusable); 1064 setFocusable(focusable); 1065 setCursorVisible(focusable); 1066 1067 if (focusable) { 1068 requestFocus(); 1069 } 1070 } 1071 onReceiveContent(View view, ContentInfo payload)1072 private ContentInfo onReceiveContent(View view, ContentInfo payload) { 1073 Pair<ContentInfo, ContentInfo> split = 1074 payload.partition(item -> item.getUri() != null); 1075 ContentInfo uriItems = split.first; 1076 ContentInfo remainingItems = split.second; 1077 if (uriItems != null) { 1078 mRemoteInputView.setAttachment(uriItems); 1079 } 1080 return remainingItems; 1081 } 1082 1083 } 1084 } 1085