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