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 android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.annotation.Nullable; 22 import android.app.ActivityManager; 23 import android.app.Notification; 24 import android.app.PendingIntent; 25 import android.app.RemoteInput; 26 import android.content.ClipDescription; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ShortcutManager; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.ServiceManager; 36 import android.os.SystemClock; 37 import android.os.UserHandle; 38 import android.text.Editable; 39 import android.text.SpannedString; 40 import android.text.TextWatcher; 41 import android.util.AttributeSet; 42 import android.util.Log; 43 import android.view.KeyEvent; 44 import android.view.LayoutInflater; 45 import android.view.MotionEvent; 46 import android.view.View; 47 import android.view.ViewAnimationUtils; 48 import android.view.ViewGroup; 49 import android.view.accessibility.AccessibilityEvent; 50 import android.view.inputmethod.CompletionInfo; 51 import android.view.inputmethod.EditorInfo; 52 import android.view.inputmethod.InputConnection; 53 import android.view.inputmethod.InputMethodManager; 54 import android.widget.EditText; 55 import android.widget.ImageButton; 56 import android.widget.LinearLayout; 57 import android.widget.ProgressBar; 58 import android.widget.TextView; 59 60 import androidx.core.view.inputmethod.InputConnectionCompat; 61 import androidx.core.view.inputmethod.InputContentInfoCompat; 62 63 import com.android.internal.logging.MetricsLogger; 64 import com.android.internal.logging.nano.MetricsProto; 65 import com.android.internal.statusbar.IStatusBarService; 66 import com.android.systemui.Dependency; 67 import com.android.systemui.Interpolators; 68 import com.android.systemui.R; 69 import com.android.systemui.statusbar.RemoteInputController; 70 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 71 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; 72 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 73 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 74 import com.android.systemui.statusbar.phone.LightBarController; 75 76 import java.util.HashMap; 77 import java.util.function.Consumer; 78 79 /** 80 * Host for the remote input. 81 */ 82 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher { 83 84 private static final String TAG = "RemoteInput"; 85 86 // A marker object that let's us easily find views of this class. 87 public static final Object VIEW_TAG = new Object(); 88 89 public final Object mToken = new Object(); 90 91 private RemoteEditText mEditText; 92 private ImageButton mSendButton; 93 private ProgressBar mProgressBar; 94 private PendingIntent mPendingIntent; 95 private RemoteInput[] mRemoteInputs; 96 private RemoteInput mRemoteInput; 97 private RemoteInputController mController; 98 private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; 99 100 private IStatusBarService mStatusBarManagerService; 101 102 private NotificationEntry mEntry; 103 104 private boolean mRemoved; 105 106 private int mRevealCx; 107 private int mRevealCy; 108 private int mRevealR; 109 110 private boolean mResetting; 111 private NotificationViewWrapper mWrapper; 112 private Consumer<Boolean> mOnVisibilityChangedListener; 113 RemoteInputView(Context context, AttributeSet attrs)114 public RemoteInputView(Context context, AttributeSet attrs) { 115 super(context, attrs); 116 mRemoteInputQuickSettingsDisabler = Dependency.get(RemoteInputQuickSettingsDisabler.class); 117 mStatusBarManagerService = IStatusBarService.Stub.asInterface( 118 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 119 } 120 121 @Override onFinishInflate()122 protected void onFinishInflate() { 123 super.onFinishInflate(); 124 125 mProgressBar = findViewById(R.id.remote_input_progress); 126 127 mSendButton = findViewById(R.id.remote_input_send); 128 mSendButton.setOnClickListener(this); 129 130 mEditText = (RemoteEditText) getChildAt(0); 131 mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { 132 @Override 133 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 134 final boolean isSoftImeEvent = event == null 135 && (actionId == EditorInfo.IME_ACTION_DONE 136 || actionId == EditorInfo.IME_ACTION_NEXT 137 || actionId == EditorInfo.IME_ACTION_SEND); 138 final boolean isKeyboardEnterKey = event != null 139 && KeyEvent.isConfirmKey(event.getKeyCode()) 140 && event.getAction() == KeyEvent.ACTION_DOWN; 141 142 if (isSoftImeEvent || isKeyboardEnterKey) { 143 if (mEditText.length() > 0) { 144 sendRemoteInput(prepareRemoteInputFromText()); 145 } 146 // Consume action to prevent IME from closing. 147 return true; 148 } 149 return false; 150 } 151 }); 152 mEditText.addTextChangedListener(this); 153 mEditText.setInnerFocusable(false); 154 mEditText.mRemoteInputView = this; 155 } 156 prepareRemoteInputFromText()157 protected Intent prepareRemoteInputFromText() { 158 Bundle results = new Bundle(); 159 results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); 160 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 161 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, 162 results); 163 164 mEntry.remoteInputText = mEditText.getText(); 165 mEntry.remoteInputUri = null; 166 mEntry.remoteInputMimeType = null; 167 168 if (mEntry.editedSuggestionInfo == null) { 169 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT); 170 } else { 171 RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE); 172 } 173 174 return fillInIntent; 175 } 176 prepareRemoteInputFromData(String contentType, Uri data)177 protected Intent prepareRemoteInputFromData(String contentType, Uri data) { 178 HashMap<String, Uri> results = new HashMap<>(); 179 results.put(contentType, data); 180 mController.grantInlineReplyUriPermission(mEntry.getSbn(), data); 181 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 182 RemoteInput.addDataResultToIntent(mRemoteInput, fillInIntent, results); 183 184 mEntry.remoteInputText = mContext.getString(R.string.remote_input_image_insertion_text); 185 mEntry.remoteInputMimeType = contentType; 186 mEntry.remoteInputUri = data; 187 188 return fillInIntent; 189 } 190 sendRemoteInput(Intent intent)191 private void sendRemoteInput(Intent intent) { 192 mEditText.setEnabled(false); 193 mSendButton.setVisibility(INVISIBLE); 194 mProgressBar.setVisibility(VISIBLE); 195 mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime(); 196 mController.addSpinning(mEntry.getKey(), mToken); 197 mController.removeRemoteInput(mEntry, mToken); 198 mEditText.mShowImeOnInputConnection = false; 199 mController.remoteInputSent(mEntry); 200 mEntry.setHasSentReply(); 201 202 // Tell ShortcutManager that this package has been "activated". ShortcutManager 203 // will reset the throttling for this package. 204 // Strictly speaking, the intent receiver may be different from the notification publisher, 205 // but that's an edge case, and also because we can't always know which package will receive 206 // an intent, so we just reset for the publisher. 207 getContext().getSystemService(ShortcutManager.class).onApplicationActive( 208 mEntry.getSbn().getPackageName(), 209 mEntry.getSbn().getUser().getIdentifier()); 210 211 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND, 212 mEntry.getSbn().getPackageName()); 213 try { 214 mPendingIntent.send(mContext, 0, intent); 215 } catch (PendingIntent.CanceledException e) { 216 Log.i(TAG, "Unable to send remote input result", e); 217 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL, 218 mEntry.getSbn().getPackageName()); 219 } 220 } 221 getText()222 public CharSequence getText() { 223 return mEditText.getText(); 224 } 225 inflate(Context context, ViewGroup root, NotificationEntry entry, RemoteInputController controller)226 public static RemoteInputView inflate(Context context, ViewGroup root, 227 NotificationEntry entry, 228 RemoteInputController controller) { 229 RemoteInputView v = (RemoteInputView) 230 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); 231 v.mController = controller; 232 v.mEntry = entry; 233 UserHandle user = computeTextOperationUser(entry.getSbn().getUser()); 234 v.mEditText.mUser = user; 235 v.mEditText.setTextOperationUser(user); 236 v.setTag(VIEW_TAG); 237 238 return v; 239 } 240 241 @Override onClick(View v)242 public void onClick(View v) { 243 if (v == mSendButton) { 244 sendRemoteInput(prepareRemoteInputFromText()); 245 } 246 } 247 248 @Override onTouchEvent(MotionEvent event)249 public boolean onTouchEvent(MotionEvent event) { 250 super.onTouchEvent(event); 251 252 // We never want for a touch to escape to an outer view or one we covered. 253 return true; 254 } 255 onDefocus(boolean animate)256 private void onDefocus(boolean animate) { 257 mController.removeRemoteInput(mEntry, mToken); 258 mEntry.remoteInputText = mEditText.getText(); 259 260 // During removal, we get reattached and lose focus. Not hiding in that 261 // case to prevent flicker. 262 if (!mRemoved) { 263 if (animate && mRevealR > 0) { 264 Animator reveal = ViewAnimationUtils.createCircularReveal( 265 this, mRevealCx, mRevealCy, mRevealR, 0); 266 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 267 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT); 268 reveal.addListener(new AnimatorListenerAdapter() { 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 setVisibility(INVISIBLE); 272 if (mWrapper != null) { 273 mWrapper.setRemoteInputVisible(false); 274 } 275 } 276 }); 277 reveal.start(); 278 } else { 279 setVisibility(INVISIBLE); 280 if (mWrapper != null) { 281 mWrapper.setRemoteInputVisible(false); 282 } 283 } 284 } 285 286 mRemoteInputQuickSettingsDisabler.setRemoteInputActive(false); 287 288 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE, 289 mEntry.getSbn().getPackageName()); 290 } 291 292 @Override onAttachedToWindow()293 protected void onAttachedToWindow() { 294 super.onAttachedToWindow(); 295 if (mEntry.getRow().isChangingPosition()) { 296 if (getVisibility() == VISIBLE && mEditText.isFocusable()) { 297 mEditText.requestFocus(); 298 } 299 } 300 } 301 302 @Override onDetachedFromWindow()303 protected void onDetachedFromWindow() { 304 super.onDetachedFromWindow(); 305 if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { 306 return; 307 } 308 mController.removeRemoteInput(mEntry, mToken); 309 mController.removeSpinning(mEntry.getKey(), mToken); 310 } 311 setPendingIntent(PendingIntent pendingIntent)312 public void setPendingIntent(PendingIntent pendingIntent) { 313 mPendingIntent = pendingIntent; 314 } 315 316 /** 317 * Sets the remote input for this view. 318 * 319 * @param remoteInputs The remote inputs that need to be sent to the app. 320 * @param remoteInput The remote input that needs to be activated. 321 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or 322 * {@code null} if the user is not editing a smart reply. 323 */ setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput, @Nullable EditedSuggestionInfo editedSuggestionInfo)324 public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput, 325 @Nullable EditedSuggestionInfo editedSuggestionInfo) { 326 mRemoteInputs = remoteInputs; 327 mRemoteInput = remoteInput; 328 mEditText.setHint(mRemoteInput.getLabel()); 329 330 mEntry.editedSuggestionInfo = editedSuggestionInfo; 331 if (editedSuggestionInfo != null) { 332 mEntry.remoteInputText = editedSuggestionInfo.originalText; 333 } 334 } 335 focusAnimated()336 public void focusAnimated() { 337 if (getVisibility() != VISIBLE) { 338 Animator animator = ViewAnimationUtils.createCircularReveal( 339 this, mRevealCx, mRevealCy, 0, mRevealR); 340 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 341 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 342 animator.start(); 343 } 344 focus(); 345 } 346 computeTextOperationUser(UserHandle notificationUser)347 private static UserHandle computeTextOperationUser(UserHandle notificationUser) { 348 return UserHandle.ALL.equals(notificationUser) 349 ? UserHandle.of(ActivityManager.getCurrentUser()) : notificationUser; 350 } 351 focus()352 public void focus() { 353 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN, 354 mEntry.getSbn().getPackageName()); 355 356 setVisibility(VISIBLE); 357 if (mWrapper != null) { 358 mWrapper.setRemoteInputVisible(true); 359 } 360 mEditText.setInnerFocusable(true); 361 mEditText.mShowImeOnInputConnection = true; 362 mEditText.setText(mEntry.remoteInputText); 363 mEditText.setSelection(mEditText.getText().length()); 364 mEditText.requestFocus(); 365 mController.addRemoteInput(mEntry, mToken); 366 367 mRemoteInputQuickSettingsDisabler.setRemoteInputActive(true); 368 369 updateSendButton(); 370 } 371 onNotificationUpdateOrReset()372 public void onNotificationUpdateOrReset() { 373 boolean sending = mProgressBar.getVisibility() == VISIBLE; 374 375 if (sending) { 376 // Update came in after we sent the reply, time to reset. 377 reset(); 378 } 379 380 if (isActive() && mWrapper != null) { 381 mWrapper.setRemoteInputVisible(true); 382 } 383 } 384 reset()385 private void reset() { 386 mResetting = true; 387 mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); 388 389 mEditText.getText().clear(); 390 mEditText.setEnabled(true); 391 mSendButton.setVisibility(VISIBLE); 392 mProgressBar.setVisibility(INVISIBLE); 393 mController.removeSpinning(mEntry.getKey(), mToken); 394 updateSendButton(); 395 onDefocus(false /* animate */); 396 397 mResetting = false; 398 } 399 400 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)401 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 402 if (mResetting && child == mEditText) { 403 // Suppress text events if it happens during resetting. Ideally this would be 404 // suppressed by the text view not being shown, but that doesn't work here because it 405 // needs to stay visible for the animation. 406 return false; 407 } 408 return super.onRequestSendAccessibilityEvent(child, event); 409 } 410 updateSendButton()411 private void updateSendButton() { 412 mSendButton.setEnabled(mEditText.getText().length() != 0); 413 } 414 415 @Override beforeTextChanged(CharSequence s, int start, int count, int after)416 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 417 418 @Override onTextChanged(CharSequence s, int start, int before, int count)419 public void onTextChanged(CharSequence s, int start, int before, int count) {} 420 421 @Override afterTextChanged(Editable s)422 public void afterTextChanged(Editable s) { 423 updateSendButton(); 424 } 425 close()426 public void close() { 427 mEditText.defocusIfNeeded(false /* animated */); 428 } 429 430 @Override onInterceptTouchEvent(MotionEvent ev)431 public boolean onInterceptTouchEvent(MotionEvent ev) { 432 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 433 mController.requestDisallowLongPressAndDismiss(); 434 } 435 return super.onInterceptTouchEvent(ev); 436 } 437 requestScrollTo()438 public boolean requestScrollTo() { 439 mController.lockScrollTo(mEntry); 440 return true; 441 } 442 isActive()443 public boolean isActive() { 444 return mEditText.isFocused() && mEditText.isEnabled(); 445 } 446 stealFocusFrom(RemoteInputView other)447 public void stealFocusFrom(RemoteInputView other) { 448 other.close(); 449 setPendingIntent(other.mPendingIntent); 450 setRemoteInput(other.mRemoteInputs, other.mRemoteInput, mEntry.editedSuggestionInfo); 451 setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR); 452 focus(); 453 } 454 455 /** 456 * Tries to find an action in {@param actions} that matches the current pending intent 457 * of this view and updates its state to that of the found action 458 * 459 * @return true if a matching action was found, false otherwise 460 */ updatePendingIntentFromActions(Notification.Action[] actions)461 public boolean updatePendingIntentFromActions(Notification.Action[] actions) { 462 if (mPendingIntent == null || actions == null) { 463 return false; 464 } 465 Intent current = mPendingIntent.getIntent(); 466 if (current == null) { 467 return false; 468 } 469 470 for (Notification.Action a : actions) { 471 RemoteInput[] inputs = a.getRemoteInputs(); 472 if (a.actionIntent == null || inputs == null) { 473 continue; 474 } 475 Intent candidate = a.actionIntent.getIntent(); 476 if (!current.filterEquals(candidate)) { 477 continue; 478 } 479 480 RemoteInput input = null; 481 for (RemoteInput i : inputs) { 482 if (i.getAllowFreeFormInput()) { 483 input = i; 484 } 485 } 486 if (input == null) { 487 continue; 488 } 489 setPendingIntent(a.actionIntent); 490 setRemoteInput(inputs, input, null /* editedSuggestionInfo*/); 491 return true; 492 } 493 return false; 494 } 495 getPendingIntent()496 public PendingIntent getPendingIntent() { 497 return mPendingIntent; 498 } 499 setRemoved()500 public void setRemoved() { 501 mRemoved = true; 502 } 503 setRevealParameters(int cx, int cy, int r)504 public void setRevealParameters(int cx, int cy, int r) { 505 mRevealCx = cx; 506 mRevealCy = cy; 507 mRevealR = r; 508 } 509 510 @Override dispatchStartTemporaryDetach()511 public void dispatchStartTemporaryDetach() { 512 super.dispatchStartTemporaryDetach(); 513 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and 514 // won't lose IME focus. 515 detachViewFromParent(mEditText); 516 } 517 518 @Override dispatchFinishTemporaryDetach()519 public void dispatchFinishTemporaryDetach() { 520 if (isAttachedToWindow()) { 521 attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); 522 } else { 523 removeDetachedView(mEditText, false /* animate */); 524 } 525 super.dispatchFinishTemporaryDetach(); 526 } 527 setWrapper(NotificationViewWrapper wrapper)528 public void setWrapper(NotificationViewWrapper wrapper) { 529 mWrapper = wrapper; 530 } 531 setOnVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener)532 public void setOnVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { 533 mOnVisibilityChangedListener = visibilityChangedListener; 534 } 535 536 @Override onVisibilityChanged(View changedView, int visibility)537 protected void onVisibilityChanged(View changedView, int visibility) { 538 super.onVisibilityChanged(changedView, visibility); 539 if (changedView == this && mOnVisibilityChangedListener != null) { 540 mOnVisibilityChangedListener.accept(visibility == VISIBLE); 541 } 542 } 543 isSending()544 public boolean isSending() { 545 return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken); 546 } 547 548 /** 549 * An EditText that changes appearance based on whether it's focusable and becomes 550 * un-focusable whenever the user navigates away from it or it becomes invisible. 551 */ 552 public static class RemoteEditText extends EditText { 553 554 private final Drawable mBackground; 555 private RemoteInputView mRemoteInputView; 556 boolean mShowImeOnInputConnection; 557 private LightBarController mLightBarController; 558 UserHandle mUser; 559 RemoteEditText(Context context, AttributeSet attrs)560 public RemoteEditText(Context context, AttributeSet attrs) { 561 super(context, attrs); 562 mBackground = getBackground(); 563 mLightBarController = Dependency.get(LightBarController.class); 564 } 565 defocusIfNeeded(boolean animate)566 private void defocusIfNeeded(boolean animate) { 567 if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition() 568 || isTemporarilyDetached()) { 569 if (isTemporarilyDetached()) { 570 // We might get reattached but then the other one of HUN / expanded might steal 571 // our focus, so we'll need to save our text here. 572 if (mRemoteInputView != null) { 573 mRemoteInputView.mEntry.remoteInputText = getText(); 574 } 575 } 576 return; 577 } 578 if (isFocusable() && isEnabled()) { 579 setInnerFocusable(false); 580 if (mRemoteInputView != null) { 581 mRemoteInputView.onDefocus(animate); 582 } 583 mShowImeOnInputConnection = false; 584 } 585 } 586 587 @Override onVisibilityChanged(View changedView, int visibility)588 protected void onVisibilityChanged(View changedView, int visibility) { 589 super.onVisibilityChanged(changedView, visibility); 590 591 if (!isShown()) { 592 defocusIfNeeded(false /* animate */); 593 } 594 } 595 596 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)597 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 598 super.onFocusChanged(focused, direction, previouslyFocusedRect); 599 if (!focused) { 600 defocusIfNeeded(true /* animate */); 601 } 602 if (!mRemoteInputView.mRemoved) { 603 mLightBarController.setDirectReplying(focused); 604 } 605 } 606 607 @Override getFocusedRect(Rect r)608 public void getFocusedRect(Rect r) { 609 super.getFocusedRect(r); 610 r.top = mScrollY; 611 r.bottom = mScrollY + (mBottom - mTop); 612 } 613 614 @Override requestRectangleOnScreen(Rect rectangle)615 public boolean requestRectangleOnScreen(Rect rectangle) { 616 return mRemoteInputView.requestScrollTo(); 617 } 618 619 @Override onKeyDown(int keyCode, KeyEvent event)620 public boolean onKeyDown(int keyCode, KeyEvent event) { 621 if (keyCode == KeyEvent.KEYCODE_BACK) { 622 // Eat the DOWN event here to prevent any default behavior. 623 return true; 624 } 625 return super.onKeyDown(keyCode, event); 626 } 627 628 @Override onKeyUp(int keyCode, KeyEvent event)629 public boolean onKeyUp(int keyCode, KeyEvent event) { 630 if (keyCode == KeyEvent.KEYCODE_BACK) { 631 defocusIfNeeded(true /* animate */); 632 return true; 633 } 634 return super.onKeyUp(keyCode, event); 635 } 636 637 @Override onKeyPreIme(int keyCode, KeyEvent event)638 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 639 // When BACK key is pressed, this method would be invoked twice. 640 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && 641 event.getAction() == KeyEvent.ACTION_UP) { 642 defocusIfNeeded(true /* animate */); 643 } 644 return super.onKeyPreIme(keyCode, event); 645 } 646 647 @Override onCheckIsTextEditor()648 public boolean onCheckIsTextEditor() { 649 // Stop being editable while we're being removed. During removal, we get reattached, 650 // and editable views get their spellchecking state re-evaluated which is too costly 651 // during the removal animation. 652 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved; 653 return !flyingOut && super.onCheckIsTextEditor(); 654 } 655 656 @Override onCreateInputConnection(EditorInfo outAttrs)657 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 658 // TODO: Pass RemoteInput data types to allow image insertion. 659 // String[] allowedDataTypes = mRemoteInputView.mRemoteInput.getAllowedDataTypes() 660 // .toArray(new String[0]); 661 // EditorInfoCompat.setContentMimeTypes(outAttrs, allowedDataTypes); 662 final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); 663 664 final InputConnectionCompat.OnCommitContentListener callback = 665 new InputConnectionCompat.OnCommitContentListener() { 666 @Override 667 public boolean onCommitContent( 668 InputContentInfoCompat inputContentInfoCompat, int i, 669 Bundle bundle) { 670 Uri contentUri = inputContentInfoCompat.getContentUri(); 671 ClipDescription description = inputContentInfoCompat.getDescription(); 672 String mimeType = null; 673 if (description != null && description.getMimeTypeCount() > 0) { 674 mimeType = description.getMimeType(0); 675 } 676 if (mimeType != null) { 677 Intent dataIntent = mRemoteInputView.prepareRemoteInputFromData( 678 mimeType, contentUri); 679 mRemoteInputView.sendRemoteInput(dataIntent); 680 } 681 return true; 682 } 683 }; 684 685 InputConnection ic = inputConnection == null ? null : 686 InputConnectionCompat.createWrapper(inputConnection, outAttrs, callback); 687 688 Context userContext = null; 689 try { 690 userContext = mContext.createPackageContextAsUser( 691 mContext.getPackageName(), 0, mUser); 692 } catch (PackageManager.NameNotFoundException e) { 693 Log.e(TAG, "Unable to create user context:" + e.getMessage(), e); 694 } 695 696 if (mShowImeOnInputConnection && ic != null) { 697 Context targetContext = userContext != null ? userContext : getContext(); 698 final InputMethodManager imm = 699 targetContext.getSystemService(InputMethodManager.class); 700 if (imm != null) { 701 // onCreateInputConnection is called by InputMethodManager in the middle of 702 // setting up the connection to the IME; wait with requesting the IME until that 703 // work has completed. 704 post(new Runnable() { 705 @Override 706 public void run() { 707 imm.viewClicked(RemoteEditText.this); 708 imm.showSoftInput(RemoteEditText.this, 0); 709 } 710 }); 711 } 712 } 713 714 return ic; 715 } 716 717 @Override onCommitCompletion(CompletionInfo text)718 public void onCommitCompletion(CompletionInfo text) { 719 clearComposingText(); 720 setText(text.getText()); 721 setSelection(getText().length()); 722 } 723 setInnerFocusable(boolean focusable)724 void setInnerFocusable(boolean focusable) { 725 setFocusableInTouchMode(focusable); 726 setFocusable(focusable); 727 setCursorVisible(focusable); 728 729 if (focusable) { 730 requestFocus(); 731 setBackground(mBackground); 732 } else { 733 setBackground(null); 734 } 735 736 } 737 } 738 } 739