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