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