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 static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD; 22 23 import android.app.ActivityManager; 24 import android.app.Notification; 25 import android.content.Context; 26 import android.content.pm.PackageManager; 27 import android.content.res.ColorStateList; 28 import android.content.res.TypedArray; 29 import android.graphics.BlendMode; 30 import android.graphics.Color; 31 import android.graphics.PorterDuff; 32 import android.graphics.Rect; 33 import android.graphics.drawable.GradientDrawable; 34 import android.os.UserHandle; 35 import android.text.Editable; 36 import android.text.SpannedString; 37 import android.text.TextWatcher; 38 import android.util.ArraySet; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.util.Pair; 42 import android.view.ContentInfo; 43 import android.view.KeyEvent; 44 import android.view.LayoutInflater; 45 import android.view.MotionEvent; 46 import android.view.OnReceiveContentListener; 47 import android.view.View; 48 import android.view.ViewAnimationUtils; 49 import android.view.ViewGroup; 50 import android.view.ViewGroupOverlay; 51 import android.view.ViewRootImpl; 52 import android.view.WindowInsets; 53 import android.view.WindowInsetsAnimation; 54 import android.view.WindowInsetsController; 55 import android.view.accessibility.AccessibilityEvent; 56 import android.view.inputmethod.CompletionInfo; 57 import android.view.inputmethod.EditorInfo; 58 import android.view.inputmethod.InputConnection; 59 import android.view.inputmethod.InputMethodManager; 60 import android.widget.EditText; 61 import android.widget.ImageButton; 62 import android.widget.ImageView; 63 import android.widget.LinearLayout; 64 import android.widget.ProgressBar; 65 import android.widget.TextView; 66 import android.window.OnBackInvokedCallback; 67 import android.window.OnBackInvokedDispatcher; 68 69 import androidx.annotation.NonNull; 70 import androidx.annotation.Nullable; 71 import androidx.core.animation.Animator; 72 import androidx.core.animation.AnimatorListenerAdapter; 73 import androidx.core.animation.AnimatorSet; 74 import androidx.core.animation.ObjectAnimator; 75 import androidx.core.animation.ValueAnimator; 76 77 import com.android.internal.annotations.VisibleForTesting; 78 import com.android.internal.graphics.ColorUtils; 79 import com.android.internal.logging.UiEvent; 80 import com.android.internal.logging.UiEventLogger; 81 import com.android.systemui.Dependency; 82 import com.android.systemui.R; 83 import com.android.systemui.animation.InterpolatorsAndroidX; 84 import com.android.systemui.statusbar.RemoteInputController; 85 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 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.List; 94 import java.util.function.Consumer; 95 96 /** 97 * Host for the remote input. 98 */ 99 public class RemoteInputView extends LinearLayout implements View.OnClickListener { 100 101 private static final boolean DEBUG = false; 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 private static final long FOCUS_ANIMATION_TOTAL_DURATION = ANIMATION_DURATION_STANDARD; 108 private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50; 109 private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; 110 private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; 111 private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; 112 private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; 113 private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; 114 115 public final Object mToken = new Object(); 116 117 private final SendButtonTextWatcher mTextWatcher; 118 private final TextView.OnEditorActionListener mEditorActionHandler; 119 private final ArrayList<Runnable> mOnSendListeners = new ArrayList<>(); 120 private final ArrayList<Consumer<Boolean>> mOnVisibilityChangedListeners = new ArrayList<>(); 121 private final ArrayList<OnFocusChangeListener> mEditTextFocusChangeListeners = 122 new ArrayList<>(); 123 124 private RemoteEditText mEditText; 125 private ImageButton mSendButton; 126 private LinearLayout mContentView; 127 private GradientDrawable mContentBackground; 128 private ProgressBar mProgressBar; 129 private ImageView mDelete; 130 private ImageView mDeleteBg; 131 private boolean mColorized; 132 private int mTint; 133 private boolean mResetting; 134 @Nullable 135 private RevealParams mRevealParams; 136 private Rect mContentBackgroundBounds; 137 private boolean mIsFocusAnimationFlagActive; 138 private boolean mIsAnimatingAppearance = false; 139 140 // TODO(b/193539698): move these to a Controller 141 private RemoteInputController mController; 142 private final UiEventLogger mUiEventLogger; 143 private NotificationEntry mEntry; 144 private boolean mRemoved; 145 private boolean mSending; 146 private NotificationViewWrapper mWrapper; 147 148 // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places 149 // that need the controller shouldn't have access to the view 150 private RemoteInputViewController mViewController; 151 private ViewRootImpl mTestableViewRootImpl; 152 153 /** 154 * Enum for logged notification remote input UiEvents. 155 */ 156 enum NotificationRemoteInputEvent implements UiEventLogger.UiEventEnum { 157 @UiEvent(doc = "Notification remote input view was displayed") 158 NOTIFICATION_REMOTE_INPUT_OPEN(795), 159 @UiEvent(doc = "Notification remote input view was closed") 160 NOTIFICATION_REMOTE_INPUT_CLOSE(796), 161 @UiEvent(doc = "User sent data through the notification remote input view") 162 NOTIFICATION_REMOTE_INPUT_SEND(797), 163 @UiEvent(doc = "Failed attempt to send data through the notification remote input view") 164 NOTIFICATION_REMOTE_INPUT_FAILURE(798), 165 @UiEvent(doc = "User attached an image to the remote input view") 166 NOTIFICATION_REMOTE_INPUT_ATTACH_IMAGE(825); 167 168 private final int mId; NotificationRemoteInputEvent(int id)169 NotificationRemoteInputEvent(int id) { 170 mId = id; 171 } getId()172 @Override public int getId() { 173 return mId; 174 } 175 } 176 RemoteInputView(Context context, AttributeSet attrs)177 public RemoteInputView(Context context, AttributeSet attrs) { 178 super(context, attrs); 179 mTextWatcher = new SendButtonTextWatcher(); 180 mEditorActionHandler = new EditorActionHandler(); 181 mUiEventLogger = Dependency.get(UiEventLogger.class); 182 TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ 183 com.android.internal.R.attr.colorAccent, 184 com.android.internal.R.attr.colorSurface, 185 }); 186 mTint = ta.getColor(0, 0); 187 ta.recycle(); 188 } 189 190 // TODO(b/193539698): move to Controller, since we're just directly accessing a system service 191 /** Hide the IME, if visible. */ hideIme()192 public void hideIme() { 193 mEditText.hideIme(); 194 } 195 colorStateListWithDisabledAlpha(int color, int disabledAlpha)196 private ColorStateList colorStateListWithDisabledAlpha(int color, int disabledAlpha) { 197 return new ColorStateList(new int[][]{ 198 new int[]{-com.android.internal.R.attr.state_enabled}, // disabled 199 new int[]{}, 200 }, new int[]{ 201 ColorUtils.setAlphaComponent(color, disabledAlpha), 202 color 203 }); 204 } 205 206 /** 207 * The remote view needs to adapt to colorized notifications when set 208 * It overrides the background of itself as well as all of its childern 209 * @param backgroundColor colorized notification color 210 */ setBackgroundTintColor(final int backgroundColor, boolean colorized)211 public void setBackgroundTintColor(final int backgroundColor, boolean colorized) { 212 if (colorized == mColorized && backgroundColor == mTint) return; 213 mColorized = colorized; 214 mTint = backgroundColor; 215 final int editBgColor; 216 final int deleteBgColor; 217 final int deleteFgColor; 218 final ColorStateList accentColor; 219 final ColorStateList textColor; 220 final int hintColor; 221 final int stroke = colorized ? mContext.getResources().getDimensionPixelSize( 222 R.dimen.remote_input_view_text_stroke) : 0; 223 if (colorized) { 224 final boolean dark = Notification.Builder.isColorDark(backgroundColor); 225 final int foregroundColor = dark ? Color.WHITE : Color.BLACK; 226 final int inverseColor = dark ? Color.BLACK : Color.WHITE; 227 editBgColor = backgroundColor; 228 deleteBgColor = foregroundColor; 229 deleteFgColor = inverseColor; 230 accentColor = colorStateListWithDisabledAlpha(foregroundColor, 0x4D); // 30% 231 textColor = colorStateListWithDisabledAlpha(foregroundColor, 0x99); // 60% 232 hintColor = ColorUtils.setAlphaComponent(foregroundColor, 0x99); 233 } else { 234 accentColor = mContext.getColorStateList(R.color.remote_input_send); 235 textColor = mContext.getColorStateList(R.color.remote_input_text); 236 hintColor = mContext.getColor(R.color.remote_input_hint); 237 deleteFgColor = textColor.getDefaultColor(); 238 try (TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ 239 com.android.internal.R.attr.colorSurfaceHighlight, 240 com.android.internal.R.attr.colorSurfaceVariant 241 })) { 242 editBgColor = ta.getColor(0, backgroundColor); 243 deleteBgColor = ta.getColor(1, Color.GRAY); 244 } 245 } 246 247 mEditText.setTextColor(textColor); 248 mEditText.setHintTextColor(hintColor); 249 if (mEditText.getTextCursorDrawable() != null) { 250 mEditText.getTextCursorDrawable().setColorFilter( 251 accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN); 252 } 253 mContentBackground.setColor(editBgColor); 254 mContentBackground.setStroke(stroke, accentColor); 255 mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor)); 256 mDeleteBg.setImageTintList(ColorStateList.valueOf(deleteBgColor)); 257 mSendButton.setImageTintList(accentColor); 258 mProgressBar.setProgressTintList(accentColor); 259 mProgressBar.setIndeterminateTintList(accentColor); 260 mProgressBar.setSecondaryProgressTintList(accentColor); 261 setBackgroundColor(backgroundColor); 262 } 263 264 @Override onFinishInflate()265 protected void onFinishInflate() { 266 super.onFinishInflate(); 267 268 mProgressBar = findViewById(R.id.remote_input_progress); 269 mSendButton = findViewById(R.id.remote_input_send); 270 mSendButton.setOnClickListener(this); 271 mContentBackground = (GradientDrawable) 272 mContext.getDrawable(R.drawable.remote_input_view_text_bg).mutate(); 273 mDelete = findViewById(R.id.remote_input_delete); 274 mDeleteBg = findViewById(R.id.remote_input_delete_bg); 275 mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN); 276 mDelete.setImageTintBlendMode(BlendMode.SRC_IN); 277 mDelete.setOnClickListener(v -> setAttachment(null)); 278 mContentView = findViewById(R.id.remote_input_content); 279 mContentView.setBackground(mContentBackground); 280 mEditText = findViewById(R.id.remote_input_text); 281 mEditText.setInnerFocusable(false); 282 // TextView initializes the spell checked when the view is attached to a window. 283 // This causes a couple of IPCs that can jank, especially during animations. 284 // By default the text view should be disabled, to avoid the unnecessary initialization. 285 mEditText.setEnabled(false); 286 mEditText.setWindowInsetsAnimationCallback( 287 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { 288 @NonNull 289 @Override 290 public WindowInsets onProgress(@NonNull WindowInsets insets, 291 @NonNull List<WindowInsetsAnimation> runningAnimations) { 292 return insets; 293 } 294 @Override 295 public void onEnd(@NonNull WindowInsetsAnimation animation) { 296 super.onEnd(animation); 297 if (animation.getTypeMask() == WindowInsets.Type.ime()) { 298 mEntry.mRemoteEditImeAnimatingAway = false; 299 WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets(); 300 if (editTextRootWindowInsets == null) { 301 Log.w(TAG, "onEnd called on detached view", new Exception()); 302 } 303 mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null 304 && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime()); 305 if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { 306 // Pass null to ensure all inputs are cleared for this entry b/227115380 307 mController.removeRemoteInput(mEntry, null); 308 } 309 } 310 } 311 }); 312 } 313 314 /** 315 * @deprecated TODO(b/193539698): views shouldn't have access to their controller, and places 316 * that need the controller shouldn't have access to the view 317 */ 318 @Deprecated setController(RemoteInputViewController controller)319 public void setController(RemoteInputViewController controller) { 320 mViewController = controller; 321 } 322 323 /** 324 * @deprecated TODO(b/193539698): views shouldn't have access to their controller, and places 325 * that need the controller shouldn't have access to the view 326 */ 327 @Deprecated getController()328 public RemoteInputViewController getController() { 329 return mViewController; 330 } 331 332 /** Clear the attachment, if present. */ clearAttachment()333 public void clearAttachment() { 334 setAttachment(null); 335 } 336 337 @VisibleForTesting setAttachment(ContentInfo item)338 protected void setAttachment(ContentInfo item) { 339 if (mEntry.remoteInputAttachment != null && mEntry.remoteInputAttachment != item) { 340 // We need to release permissions when sending the attachment to the target 341 // app or if it is deleted by the user. When sending to the target app, we 342 // can safely release permissions as soon as the call to 343 // `mController.grantInlineReplyUriPermission` is made (ie, after the grant 344 // to the target app has been created). 345 mEntry.remoteInputAttachment.releasePermissions(); 346 } 347 mEntry.remoteInputAttachment = item; 348 if (item != null) { 349 mEntry.remoteInputUri = item.getClip().getItemAt(0).getUri(); 350 mEntry.remoteInputMimeType = item.getClip().getDescription().getMimeType(0); 351 } 352 353 View attachment = findViewById(R.id.remote_input_content_container); 354 ImageView iconView = findViewById(R.id.remote_input_attachment_image); 355 iconView.setImageDrawable(null); 356 if (item == null) { 357 attachment.setVisibility(GONE); 358 return; 359 } 360 iconView.setImageURI(item.getClip().getItemAt(0).getUri()); 361 if (iconView.getDrawable() == null) { 362 attachment.setVisibility(GONE); 363 } else { 364 attachment.setVisibility(VISIBLE); 365 mUiEventLogger.logWithInstanceId( 366 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_ATTACH_IMAGE, 367 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 368 mEntry.getSbn().getInstanceId()); 369 } 370 updateSendButton(); 371 } 372 373 /** Show the "sending in-progress" UI. */ startSending()374 public void startSending() { 375 mEditText.setEnabled(false); 376 mSending = true; 377 mSendButton.setVisibility(INVISIBLE); 378 mProgressBar.setVisibility(VISIBLE); 379 mEditText.mShowImeOnInputConnection = false; 380 } 381 sendRemoteInput()382 private void sendRemoteInput() { 383 for (Runnable listener : new ArrayList<>(mOnSendListeners)) { 384 listener.run(); 385 } 386 } 387 getText()388 public CharSequence getText() { 389 return mEditText.getText(); 390 } 391 inflate(Context context, ViewGroup root, NotificationEntry entry, RemoteInputController controller)392 public static RemoteInputView inflate(Context context, ViewGroup root, 393 NotificationEntry entry, 394 RemoteInputController controller) { 395 RemoteInputView v = (RemoteInputView) 396 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); 397 v.mController = controller; 398 v.mEntry = entry; 399 UserHandle user = computeTextOperationUser(entry.getSbn().getUser()); 400 v.mEditText.mUser = user; 401 v.mEditText.setTextOperationUser(user); 402 v.setTag(VIEW_TAG); 403 404 return v; 405 } 406 407 @Override onClick(View v)408 public void onClick(View v) { 409 if (v == mSendButton) { 410 sendRemoteInput(); 411 } 412 } 413 414 @Override onTouchEvent(MotionEvent event)415 public boolean onTouchEvent(MotionEvent event) { 416 super.onTouchEvent(event); 417 418 // We never want for a touch to escape to an outer view or one we covered. 419 return true; 420 } 421 isAnimatingAppearance()422 public boolean isAnimatingAppearance() { 423 return mIsAnimatingAppearance; 424 } 425 426 @VisibleForTesting onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus)427 void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { 428 mController.removeRemoteInput(mEntry, mToken); 429 mEntry.remoteInputText = mEditText.getText(); 430 431 // During removal, we get reattached and lose focus. Not hiding in that 432 // case to prevent flicker. 433 if (!mRemoved) { 434 ViewGroup parent = (ViewGroup) getParent(); 435 if (animate && parent != null && mIsFocusAnimationFlagActive) { 436 437 ViewGroup grandParent = (ViewGroup) parent.getParent(); 438 ViewGroupOverlay overlay = parent.getOverlay(); 439 View actionsContainer = getActionsContainerLayout(); 440 int actionsContainerHeight = 441 actionsContainer != null ? actionsContainer.getHeight() : 0; 442 443 // After adding this RemoteInputView to the overlay of the parent (and thus removing 444 // it from the parent itself), the parent will shrink in height. This causes the 445 // overlay to be moved. To correct the position of the overlay we need to offset it. 446 int overlayOffsetY = actionsContainerHeight - getHeight(); 447 overlay.add(this); 448 if (grandParent != null) grandParent.setClipChildren(false); 449 450 Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); 451 View self = this; 452 animator.addListener(new AnimatorListenerAdapter() { 453 @Override 454 public void onAnimationEnd(Animator animation) { 455 overlay.remove(self); 456 parent.addView(self); 457 if (grandParent != null) grandParent.setClipChildren(true); 458 setVisibility(GONE); 459 if (mWrapper != null) { 460 mWrapper.setRemoteInputVisible(false); 461 } 462 if (doAfterDefocus != null) { 463 doAfterDefocus.run(); 464 } 465 } 466 }); 467 if (actionsContainer != null) actionsContainer.setAlpha(0f); 468 animator.start(); 469 470 } else if (animate && mRevealParams != null && mRevealParams.radius > 0) { 471 android.animation.Animator reveal = mRevealParams.createCircularHideAnimator(this); 472 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 473 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT); 474 reveal.addListener(new android.animation.AnimatorListenerAdapter() { 475 @Override 476 public void onAnimationEnd(android.animation.Animator animation) { 477 setVisibility(GONE); 478 if (mWrapper != null) { 479 mWrapper.setRemoteInputVisible(false); 480 } 481 } 482 }); 483 reveal.start(); 484 } else { 485 setVisibility(GONE); 486 if (doAfterDefocus != null) doAfterDefocus.run(); 487 if (mWrapper != null) { 488 mWrapper.setRemoteInputVisible(false); 489 } 490 } 491 } 492 493 if (logClose) { 494 mUiEventLogger.logWithInstanceId( 495 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_CLOSE, 496 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 497 mEntry.getSbn().getInstanceId()); 498 } 499 } 500 501 @VisibleForTesting setViewRootImpl(ViewRootImpl viewRoot)502 protected void setViewRootImpl(ViewRootImpl viewRoot) { 503 mTestableViewRootImpl = viewRoot; 504 } 505 506 @VisibleForTesting setEditTextReferenceToSelf()507 protected void setEditTextReferenceToSelf() { 508 mEditText.mRemoteInputView = this; 509 } 510 511 @Override onAttachedToWindow()512 protected void onAttachedToWindow() { 513 super.onAttachedToWindow(); 514 setEditTextReferenceToSelf(); 515 mEditText.setOnEditorActionListener(mEditorActionHandler); 516 mEditText.addTextChangedListener(mTextWatcher); 517 if (mEntry.getRow().isChangingPosition()) { 518 if (getVisibility() == VISIBLE && mEditText.isFocusable()) { 519 mEditText.requestFocus(); 520 } 521 } 522 } 523 524 @Override onDetachedFromWindow()525 protected void onDetachedFromWindow() { 526 super.onDetachedFromWindow(); 527 mEditText.removeTextChangedListener(mTextWatcher); 528 mEditText.setOnEditorActionListener(null); 529 mEditText.mRemoteInputView = null; 530 if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { 531 return; 532 } 533 mController.removeRemoteInput(mEntry, mToken); 534 mController.removeSpinning(mEntry.getKey(), mToken); 535 } 536 537 @Override getViewRootImpl()538 public ViewRootImpl getViewRootImpl() { 539 if (mTestableViewRootImpl != null) { 540 return mTestableViewRootImpl; 541 } 542 return super.getViewRootImpl(); 543 } 544 registerBackCallback()545 private void registerBackCallback() { 546 ViewRootImpl viewRoot = getViewRootImpl(); 547 if (viewRoot == null) { 548 if (DEBUG) { 549 Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback"); 550 } 551 return; 552 } 553 if (DEBUG) { 554 Log.d(TAG, "registering Predictive Back callback"); 555 } 556 viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 557 OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback); 558 } 559 unregisterBackCallback()560 private void unregisterBackCallback() { 561 ViewRootImpl viewRoot = getViewRootImpl(); 562 if (viewRoot == null) { 563 if (DEBUG) { 564 Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback"); 565 } 566 return; 567 } 568 if (DEBUG) { 569 Log.d(TAG, "unregistering Predictive Back callback"); 570 } 571 viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( 572 mEditText.mOnBackInvokedCallback); 573 } 574 575 @Override onVisibilityAggregated(boolean isVisible)576 public void onVisibilityAggregated(boolean isVisible) { 577 if (isVisible) { 578 registerBackCallback(); 579 } else { 580 unregisterBackCallback(); 581 } 582 super.onVisibilityAggregated(isVisible); 583 mEditText.setEnabled(isVisible && !mSending); 584 } 585 setHintText(CharSequence hintText)586 public void setHintText(CharSequence hintText) { 587 mEditText.setHint(hintText); 588 } 589 setSupportedMimeTypes(Collection<String> mimeTypes)590 public void setSupportedMimeTypes(Collection<String> mimeTypes) { 591 mEditText.setSupportedMimeTypes(mimeTypes); 592 } 593 594 /** Populates the text field of the remote input with the given content. */ setEditTextContent(@ullable CharSequence editTextContent)595 public void setEditTextContent(@Nullable CharSequence editTextContent) { 596 mEditText.setText(editTextContent); 597 } 598 599 /** 600 * Sets whether the feature flag for the revised inline reply animation is active or not. 601 * @param active 602 */ setIsFocusAnimationFlagActive(boolean active)603 public void setIsFocusAnimationFlagActive(boolean active) { 604 mIsFocusAnimationFlagActive = active; 605 } 606 607 /** 608 * Focuses the RemoteInputView and animates its appearance 609 */ focusAnimated()610 public void focusAnimated() { 611 if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE 612 && mRevealParams != null) { 613 android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); 614 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 615 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 616 animator.start(); 617 } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { 618 mIsAnimatingAppearance = true; 619 setAlpha(0f); 620 Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); 621 focusAnimator.addListener(new AnimatorListenerAdapter() { 622 @Override 623 public void onAnimationEnd(Animator animation, boolean isReverse) { 624 mIsAnimatingAppearance = false; 625 } 626 }); 627 focusAnimator.start(); 628 } 629 focus(); 630 } 631 computeTextOperationUser(UserHandle notificationUser)632 private static UserHandle computeTextOperationUser(UserHandle notificationUser) { 633 return UserHandle.ALL.equals(notificationUser) 634 ? UserHandle.of(ActivityManager.getCurrentUser()) : notificationUser; 635 } 636 focus()637 public void focus() { 638 mUiEventLogger.logWithInstanceId( 639 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN, 640 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 641 mEntry.getSbn().getInstanceId()); 642 643 setVisibility(VISIBLE); 644 if (mWrapper != null) { 645 mWrapper.setRemoteInputVisible(true); 646 } 647 mEditText.setInnerFocusable(true); 648 mEditText.mShowImeOnInputConnection = true; 649 mEditText.setText(mEntry.remoteInputText); 650 mEditText.setSelection(mEditText.length()); 651 mEditText.requestFocus(); 652 mController.addRemoteInput(mEntry, mToken); 653 setAttachment(mEntry.remoteInputAttachment); 654 655 updateSendButton(); 656 } 657 onNotificationUpdateOrReset()658 public void onNotificationUpdateOrReset() { 659 boolean sending = mProgressBar.getVisibility() == VISIBLE; 660 661 if (sending) { 662 // Update came in after we sent the reply, time to reset. 663 reset(); 664 } 665 666 if (isActive() && mWrapper != null) { 667 mWrapper.setRemoteInputVisible(true); 668 } 669 } 670 reset()671 private void reset() { 672 if (mIsFocusAnimationFlagActive) { 673 mProgressBar.setVisibility(INVISIBLE); 674 mResetting = true; 675 mSending = false; 676 onDefocus(true /* animate */, false /* logClose */, () -> { 677 mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); 678 mEditText.getText().clear(); 679 mEditText.setEnabled(isAggregatedVisible()); 680 mSendButton.setVisibility(VISIBLE); 681 mController.removeSpinning(mEntry.getKey(), mToken); 682 updateSendButton(); 683 setAttachment(null); 684 mResetting = false; 685 }); 686 return; 687 } 688 689 mResetting = true; 690 mSending = false; 691 mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); 692 693 mEditText.getText().clear(); 694 mEditText.setEnabled(isAggregatedVisible()); 695 mSendButton.setVisibility(VISIBLE); 696 mProgressBar.setVisibility(INVISIBLE); 697 mController.removeSpinning(mEntry.getKey(), mToken); 698 updateSendButton(); 699 onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); 700 setAttachment(null); 701 702 mResetting = false; 703 } 704 705 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)706 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 707 if (mResetting && child == mEditText) { 708 // Suppress text events if it happens during resetting. Ideally this would be 709 // suppressed by the text view not being shown, but that doesn't work here because it 710 // needs to stay visible for the animation. 711 return false; 712 } 713 return super.onRequestSendAccessibilityEvent(child, event); 714 } 715 updateSendButton()716 private void updateSendButton() { 717 mSendButton.setEnabled(mEditText.length() != 0 || mEntry.remoteInputAttachment != null); 718 } 719 close()720 public void close() { 721 mEditText.defocusIfNeeded(false /* animated */); 722 } 723 724 @Override onInterceptTouchEvent(MotionEvent ev)725 public boolean onInterceptTouchEvent(MotionEvent ev) { 726 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 727 mController.requestDisallowLongPressAndDismiss(); 728 } 729 return super.onInterceptTouchEvent(ev); 730 } 731 requestScrollTo()732 public boolean requestScrollTo() { 733 mController.lockScrollTo(mEntry); 734 return true; 735 } 736 isActive()737 public boolean isActive() { 738 return mEditText.isFocused() && mEditText.isEnabled(); 739 } 740 setRemoved()741 public void setRemoved() { 742 mRemoved = true; 743 } 744 setRevealParameters(@ullable RevealParams revealParams)745 public void setRevealParameters(@Nullable RevealParams revealParams) { 746 mRevealParams = revealParams; 747 } 748 749 @Override dispatchStartTemporaryDetach()750 public void dispatchStartTemporaryDetach() { 751 super.dispatchStartTemporaryDetach(); 752 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and 753 // won't lose IME focus. 754 final int iEditText = indexOfChild(mEditText); 755 if (iEditText != -1) { 756 detachViewFromParent(iEditText); 757 } 758 } 759 760 @Override dispatchFinishTemporaryDetach()761 public void dispatchFinishTemporaryDetach() { 762 if (isAttachedToWindow()) { 763 attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); 764 } else { 765 removeDetachedView(mEditText, false /* animate */); 766 } 767 super.dispatchFinishTemporaryDetach(); 768 } 769 setWrapper(NotificationViewWrapper wrapper)770 public void setWrapper(NotificationViewWrapper wrapper) { 771 mWrapper = wrapper; 772 } 773 774 /** 775 * Register a listener to be notified when this view's visibility changes. 776 * 777 * Specifically, the passed {@link Consumer} will receive {@code true} when 778 * {@link #getVisibility()} would return {@link View#VISIBLE}, and {@code false} it would return 779 * any other value. 780 */ addOnVisibilityChangedListener(Consumer<Boolean> listener)781 public void addOnVisibilityChangedListener(Consumer<Boolean> listener) { 782 mOnVisibilityChangedListeners.add(listener); 783 } 784 785 /** 786 * Unregister a listener previously registered via 787 * {@link #addOnVisibilityChangedListener(Consumer)}. 788 */ removeOnVisibilityChangedListener(Consumer<Boolean> listener)789 public void removeOnVisibilityChangedListener(Consumer<Boolean> listener) { 790 mOnVisibilityChangedListeners.remove(listener); 791 } 792 793 @Override onVisibilityChanged(View changedView, int visibility)794 protected void onVisibilityChanged(View changedView, int visibility) { 795 super.onVisibilityChanged(changedView, visibility); 796 if (changedView == this) { 797 for (Consumer<Boolean> listener : new ArrayList<>(mOnVisibilityChangedListeners)) { 798 listener.accept(visibility == VISIBLE); 799 } 800 // Hide soft-keyboard when the input view became invisible 801 // (i.e. The notification shade collapsed by pressing the home key) 802 if (visibility != VISIBLE && !mController.isRemoteInputActive()) { 803 mEditText.hideIme(); 804 } 805 } 806 } 807 isSending()808 public boolean isSending() { 809 return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken); 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(Runnable listener)834 public void addOnSendRemoteInputListener(Runnable listener) { 835 mOnSendListeners.add(listener); 836 } 837 838 /** Removes a previously-added listener for send events on this RemoteInputView */ removeOnSendRemoteInputListener(Runnable listener)839 public void removeOnSendRemoteInputListener(Runnable listener) { 840 mOnSendListeners.remove(listener); 841 } 842 843 @Override onLayout(boolean changed, int l, int t, int r, int b)844 protected void onLayout(boolean changed, int l, int t, int r, int b) { 845 super.onLayout(changed, l, t, r, b); 846 if (mIsFocusAnimationFlagActive) setPivotY(getMeasuredHeight()); 847 if (mContentBackgroundBounds != null) { 848 mContentBackground.setBounds(mContentBackgroundBounds); 849 } 850 } 851 852 /** 853 * @return action button container view (i.e. ViewGroup containing Reply button etc.) 854 */ getActionsContainerLayout()855 public View getActionsContainerLayout() { 856 ViewGroup parentView = (ViewGroup) getParent(); 857 if (parentView == null) return null; 858 return parentView.findViewById(com.android.internal.R.id.actions_container_layout); 859 } 860 861 /** 862 * Creates an animator for the focus animation. 863 * 864 * @param fadeOutView View that will be faded out during the focus animation. 865 */ getFocusAnimator(@ullable View fadeOutView)866 private Animator getFocusAnimator(@Nullable View fadeOutView) { 867 final AnimatorSet animatorSet = new AnimatorSet(); 868 869 final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); 870 alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); 871 alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 872 alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 873 874 ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f); 875 scaleAnimator.addUpdateListener(valueAnimator -> { 876 setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0); 877 }); 878 scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); 879 scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); 880 881 if (fadeOutView == null) { 882 animatorSet.playTogether(alphaAnimator, scaleAnimator); 883 } else { 884 final Animator fadeOutViewAlphaAnimator = 885 ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); 886 fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); 887 fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 888 animatorSet.addListener(new AnimatorListenerAdapter() { 889 @Override 890 public void onAnimationEnd(Animator animation, boolean isReverse) { 891 fadeOutView.setAlpha(1f); 892 } 893 }); 894 animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); 895 } 896 return animatorSet; 897 } 898 899 /** 900 * Creates an animator for the defocus animation. 901 * 902 * @param fadeInView View that will be faded in during the defocus animation. 903 * @param offsetY The RemoteInputView will be offset by offsetY during the animation 904 */ getDefocusAnimator(@ullable View fadeInView, int offsetY)905 private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { 906 final AnimatorSet animatorSet = new AnimatorSet(); 907 908 final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); 909 alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 910 alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); 911 alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 912 913 ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); 914 scaleAnimator.addUpdateListener(valueAnimator -> { 915 setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY); 916 }); 917 scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); 918 scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); 919 scaleAnimator.addListener(new AnimatorListenerAdapter() { 920 @Override 921 public void onAnimationEnd(Animator animation, boolean isReverse) { 922 setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */); 923 } 924 }); 925 926 if (fadeInView == null) { 927 animatorSet.playTogether(alphaAnimator, scaleAnimator); 928 } else { 929 fadeInView.forceHasOverlappingRendering(false); 930 Animator fadeInViewAlphaAnimator = 931 ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); 932 fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 933 fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 934 fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); 935 animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); 936 } 937 return animatorSet; 938 } 939 940 /** 941 * Sets affected view properties for a vertical scale animation 942 * 943 * @param scaleY desired vertical view scale 944 * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation 945 */ setFocusAnimationScaleY(float scaleY, int verticalOffset)946 private void setFocusAnimationScaleY(float scaleY, int verticalOffset) { 947 int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight()); 948 Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(), 949 mContentView.getHeight() - verticalBoundOffset); 950 mContentBackground.setBounds(contentBackgroundBounds); 951 mContentView.setBackground(mContentBackground); 952 if (scaleY == 1f) { 953 mContentBackgroundBounds = null; 954 } else { 955 mContentBackgroundBounds = contentBackgroundBounds; 956 } 957 setTranslationY(verticalBoundOffset + verticalOffset); 958 } 959 960 /** Handler for button click on send action in IME. */ 961 private class EditorActionHandler implements TextView.OnEditorActionListener { 962 963 @Override onEditorAction(TextView v, int actionId, KeyEvent event)964 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 965 final boolean isSoftImeEvent = event == null 966 && (actionId == EditorInfo.IME_ACTION_DONE 967 || actionId == EditorInfo.IME_ACTION_NEXT 968 || actionId == EditorInfo.IME_ACTION_SEND); 969 final boolean isKeyboardEnterKey = event != null 970 && KeyEvent.isConfirmKey(event.getKeyCode()) 971 && event.getAction() == KeyEvent.ACTION_DOWN; 972 973 if (isSoftImeEvent || isKeyboardEnterKey) { 974 if (mEditText.length() > 0 || mEntry.remoteInputAttachment != null) { 975 sendRemoteInput(); 976 } 977 // Consume action to prevent IME from closing. 978 return true; 979 } 980 return false; 981 } 982 } 983 984 /** Observes text change events and updates the visibility of the send button accordingly. */ 985 private class SendButtonTextWatcher implements TextWatcher { 986 987 @Override beforeTextChanged(CharSequence s, int start, int count, int after)988 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 989 990 @Override onTextChanged(CharSequence s, int start, int before, int count)991 public void onTextChanged(CharSequence s, int start, int before, int count) {} 992 993 @Override afterTextChanged(Editable s)994 public void afterTextChanged(Editable s) { 995 updateSendButton(); 996 } 997 } 998 999 /** 1000 * An EditText that changes appearance based on whether it's focusable and becomes 1001 * un-focusable whenever the user navigates away from it or it becomes invisible. 1002 */ 1003 public static class RemoteEditText extends EditText { 1004 1005 private final OnReceiveContentListener mOnReceiveContentListener = this::onReceiveContent; 1006 1007 private RemoteInputView mRemoteInputView; 1008 boolean mShowImeOnInputConnection; 1009 private LightBarController mLightBarController; 1010 private InputMethodManager mInputMethodManager; 1011 private ArraySet<String> mSupportedMimes = new ArraySet<>(); 1012 UserHandle mUser; 1013 RemoteEditText(Context context, AttributeSet attrs)1014 public RemoteEditText(Context context, AttributeSet attrs) { 1015 super(context, attrs); 1016 mLightBarController = Dependency.get(LightBarController.class); 1017 } 1018 setSupportedMimeTypes(@ullable Collection<String> mimeTypes)1019 void setSupportedMimeTypes(@Nullable Collection<String> mimeTypes) { 1020 String[] types = null; 1021 OnReceiveContentListener listener = null; 1022 if (mimeTypes != null && !mimeTypes.isEmpty()) { 1023 types = mimeTypes.toArray(new String[0]); 1024 listener = mOnReceiveContentListener; 1025 } 1026 setOnReceiveContentListener(types, listener); 1027 mSupportedMimes.clear(); 1028 mSupportedMimes.addAll(mimeTypes); 1029 } 1030 hideIme()1031 private void hideIme() { 1032 final WindowInsetsController insetsController = getWindowInsetsController(); 1033 if (insetsController != null) { 1034 insetsController.hide(WindowInsets.Type.ime()); 1035 } 1036 } 1037 defocusIfNeeded(boolean animate)1038 private void defocusIfNeeded(boolean animate) { 1039 if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition() 1040 || isTemporarilyDetached()) { 1041 if (isTemporarilyDetached()) { 1042 // We might get reattached but then the other one of HUN / expanded might steal 1043 // our focus, so we'll need to save our text here. 1044 if (mRemoteInputView != null) { 1045 mRemoteInputView.mEntry.remoteInputText = getText(); 1046 } 1047 } 1048 return; 1049 } 1050 if (isFocusable() && isEnabled()) { 1051 setInnerFocusable(false); 1052 if (mRemoteInputView != null) { 1053 mRemoteInputView 1054 .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); 1055 } 1056 mShowImeOnInputConnection = false; 1057 } 1058 } 1059 1060 @Override onVisibilityChanged(View changedView, int visibility)1061 protected void onVisibilityChanged(View changedView, int visibility) { 1062 super.onVisibilityChanged(changedView, visibility); 1063 1064 if (!isShown()) { 1065 defocusIfNeeded(false /* animate */); 1066 } 1067 } 1068 1069 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)1070 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1071 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1072 if (mRemoteInputView != null) { 1073 mRemoteInputView.onEditTextFocusChanged(this, focused); 1074 } 1075 if (!focused) { 1076 defocusIfNeeded(true /* animate */); 1077 } 1078 if (mRemoteInputView != null && !mRemoteInputView.mRemoved) { 1079 mLightBarController.setDirectReplying(focused); 1080 } 1081 } 1082 1083 @Override getFocusedRect(Rect r)1084 public void getFocusedRect(Rect r) { 1085 super.getFocusedRect(r); 1086 r.top = mScrollY; 1087 r.bottom = mScrollY + (mBottom - mTop); 1088 } 1089 1090 @Override requestRectangleOnScreen(Rect rectangle)1091 public boolean requestRectangleOnScreen(Rect rectangle) { 1092 return mRemoteInputView.requestScrollTo(); 1093 } 1094 1095 @Override onKeyDown(int keyCode, KeyEvent event)1096 public boolean onKeyDown(int keyCode, KeyEvent event) { 1097 if (keyCode == KeyEvent.KEYCODE_BACK) { 1098 // Eat the DOWN event here to prevent any default behavior. 1099 return true; 1100 } 1101 return super.onKeyDown(keyCode, event); 1102 } 1103 1104 private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { 1105 if (DEBUG) { 1106 Log.d(TAG, "Predictive Back Callback dispatched"); 1107 } 1108 respondToKeycodeBack(); 1109 }; 1110 respondToKeycodeBack()1111 private void respondToKeycodeBack() { 1112 defocusIfNeeded(true /* animate */); 1113 } 1114 1115 @Override onKeyUp(int keyCode, KeyEvent event)1116 public boolean onKeyUp(int keyCode, KeyEvent event) { 1117 if (keyCode == KeyEvent.KEYCODE_BACK) { 1118 respondToKeycodeBack(); 1119 return true; 1120 } 1121 return super.onKeyUp(keyCode, event); 1122 } 1123 1124 @Override onKeyPreIme(int keyCode, KeyEvent event)1125 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1126 // When BACK key is pressed, this method would be invoked twice. 1127 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && 1128 event.getAction() == KeyEvent.ACTION_UP) { 1129 defocusIfNeeded(true /* animate */); 1130 } 1131 return super.onKeyPreIme(keyCode, event); 1132 } 1133 1134 @Override onCheckIsTextEditor()1135 public boolean onCheckIsTextEditor() { 1136 // Stop being editable while we're being removed. During removal, we get reattached, 1137 // and editable views get their spellchecking state re-evaluated which is too costly 1138 // during the removal animation. 1139 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved; 1140 return !flyingOut && super.onCheckIsTextEditor(); 1141 } 1142 1143 @Override onCreateInputConnection(EditorInfo outAttrs)1144 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 1145 final InputConnection ic = super.onCreateInputConnection(outAttrs); 1146 Context userContext = null; 1147 try { 1148 userContext = mContext.createPackageContextAsUser( 1149 mContext.getPackageName(), 0, mUser); 1150 } catch (PackageManager.NameNotFoundException e) { 1151 Log.e(TAG, "Unable to create user context:" + e.getMessage(), e); 1152 } 1153 1154 if (mShowImeOnInputConnection && ic != null) { 1155 Context targetContext = userContext != null ? userContext : getContext(); 1156 mInputMethodManager = targetContext.getSystemService(InputMethodManager.class); 1157 if (mInputMethodManager != null) { 1158 // onCreateInputConnection is called by InputMethodManager in the middle of 1159 // setting up the connection to the IME; wait with requesting the IME until that 1160 // work has completed. 1161 post(new Runnable() { 1162 @Override 1163 public void run() { 1164 mInputMethodManager.viewClicked(RemoteEditText.this); 1165 mInputMethodManager.showSoftInput(RemoteEditText.this, 0); 1166 } 1167 }); 1168 } 1169 } 1170 1171 return ic; 1172 } 1173 1174 @Override onCommitCompletion(CompletionInfo text)1175 public void onCommitCompletion(CompletionInfo text) { 1176 clearComposingText(); 1177 setText(text.getText()); 1178 setSelection(getText().length()); 1179 } 1180 setInnerFocusable(boolean focusable)1181 void setInnerFocusable(boolean focusable) { 1182 setFocusableInTouchMode(focusable); 1183 setFocusable(focusable); 1184 setCursorVisible(focusable); 1185 1186 if (focusable) { 1187 requestFocus(); 1188 } 1189 } 1190 onReceiveContent(View view, ContentInfo payload)1191 private ContentInfo onReceiveContent(View view, ContentInfo payload) { 1192 Pair<ContentInfo, ContentInfo> split = 1193 payload.partition(item -> item.getUri() != null); 1194 ContentInfo uriItems = split.first; 1195 ContentInfo remainingItems = split.second; 1196 if (uriItems != null) { 1197 mRemoteInputView.setAttachment(uriItems); 1198 } 1199 return remainingItems; 1200 } 1201 1202 } 1203 1204 public static class RevealParams { 1205 final int centerX; 1206 final int centerY; 1207 final int radius; 1208 RevealParams(int centerX, int centerY, int radius)1209 public RevealParams(int centerX, int centerY, int radius) { 1210 this.centerX = centerX; 1211 this.centerY = centerY; 1212 this.radius = radius; 1213 } 1214 createCircularHideAnimator(View view)1215 android.animation.Animator createCircularHideAnimator(View view) { 1216 return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, radius, 0); 1217 } 1218 createCircularRevealAnimator(View view)1219 android.animation.Animator createCircularRevealAnimator(View view) { 1220 return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, 0, radius); 1221 } 1222 } 1223 } 1224