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