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