OSDN Git Service

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