OSDN Git Service

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