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