RemoteInputView.java revision 56cf73a0f4704c2d1665ed0287fecea3cd871a51
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
17package com.android.systemui.statusbar.policy;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.app.Notification;
22import android.app.PendingIntent;
23import android.app.RemoteInput;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ShortcutManager;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.text.Editable;
31import android.text.TextWatcher;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.KeyEvent;
35import android.view.LayoutInflater;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewAnimationUtils;
39import android.view.ViewGroup;
40import android.view.ViewParent;
41import android.view.inputmethod.CompletionInfo;
42import android.view.inputmethod.EditorInfo;
43import android.view.inputmethod.InputConnection;
44import android.view.inputmethod.InputMethodManager;
45import android.widget.EditText;
46import android.widget.ImageButton;
47import android.widget.LinearLayout;
48import android.widget.ProgressBar;
49import android.widget.TextView;
50
51import com.android.internal.logging.MetricsLogger;
52import com.android.internal.logging.MetricsProto;
53import com.android.systemui.Interpolators;
54import com.android.systemui.R;
55import com.android.systemui.statusbar.ExpandableView;
56import com.android.systemui.statusbar.NotificationData;
57import com.android.systemui.statusbar.RemoteInputController;
58import com.android.systemui.statusbar.stack.ScrollContainer;
59import com.android.systemui.statusbar.stack.StackStateAnimator;
60
61/**
62 * Host for the remote input.
63 */
64public 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
89    public RemoteInputView(Context context, AttributeSet attrs) {
90        super(context, attrs);
91    }
92
93    @Override
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
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
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
178    public void onClick(View v) {
179        if (v == mSendButton) {
180            sendRemoteInput();
181        }
182    }
183
184    @Override
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
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
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
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
239    public void setPendingIntent(PendingIntent pendingIntent) {
240        mPendingIntent = pendingIntent;
241    }
242
243    public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
244        mRemoteInputs = remoteInputs;
245        mRemoteInput = remoteInput;
246        mEditText.setHint(mRemoteInput.getLabel());
247    }
248
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
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
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
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
293    private void updateSendButton() {
294        mSendButton.setEnabled(mEditText.getText().length() != 0);
295    }
296
297    @Override
298    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
299
300    @Override
301    public void onTextChanged(CharSequence s, int start, int before, int count) {}
302
303    @Override
304    public void afterTextChanged(Editable s) {
305        updateSendButton();
306    }
307
308    public void close() {
309        mEditText.defocusIfNeeded(false /* animated */);
310    }
311
312    @Override
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
324    public boolean requestScrollTo() {
325        findScrollContainer();
326        mScrollContainer.lockScrollTo(mScrollContainerChild);
327        return true;
328    }
329
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
350    public boolean isActive() {
351        return mEditText.isFocused();
352    }
353
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     */
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
403    public PendingIntent getPendingIntent() {
404        return mPendingIntent;
405    }
406
407    public void setRemoved() {
408        mRemoved = true;
409    }
410
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
427        public RemoteEditText(Context context, AttributeSet attrs) {
428            super(context, attrs);
429            mBackground = getBackground();
430        }
431
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
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
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
463        public void getFocusedRect(Rect r) {
464            super.getFocusedRect(r);
465            r.top = mScrollY;
466            r.bottom = mScrollY + (mBottom - mTop);
467        }
468
469        @Override
470        public boolean requestRectangleOnScreen(Rect rectangle) {
471            return mRemoteInputView.requestScrollTo();
472        }
473
474        @Override
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
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
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
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
525        public void onCommitCompletion(CompletionInfo text) {
526            clearComposingText();
527            setText(text.getText());
528            setSelection(getText().length());
529        }
530
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