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