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