RemoteInputView.java revision 0bd8a4b29bf92a901855d889c53186383dd2c5e7
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.PendingIntent;
20import android.app.RemoteInput;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.os.Bundle;
26import android.text.Editable;
27import android.text.TextWatcher;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.LayoutInflater;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.ViewParent;
36import android.view.inputmethod.CompletionInfo;
37import android.view.inputmethod.EditorInfo;
38import android.view.inputmethod.InputConnection;
39import android.view.inputmethod.InputMethodManager;
40import android.widget.EditText;
41import android.widget.ImageButton;
42import android.widget.LinearLayout;
43import android.widget.ProgressBar;
44import android.widget.TextView;
45
46import com.android.systemui.R;
47import com.android.systemui.statusbar.NotificationData;
48import com.android.systemui.statusbar.RemoteInputController;
49import com.android.systemui.statusbar.stack.LongPressCancelable;
50
51/**
52 * Host for the remote input.
53 */
54public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
55
56    private static final String TAG = "RemoteInput";
57
58    // A marker object that let's us easily find views of this class.
59    public static final Object VIEW_TAG = new Object();
60
61    private RemoteEditText mEditText;
62    private ImageButton mSendButton;
63    private ProgressBar mProgressBar;
64    private PendingIntent mPendingIntent;
65    private RemoteInput[] mRemoteInputs;
66    private RemoteInput mRemoteInput;
67    private RemoteInputController mController;
68
69    private NotificationData.Entry mEntry;
70    private LongPressCancelable mLongPressCancelable;
71
72    public RemoteInputView(Context context, AttributeSet attrs) {
73        super(context, attrs);
74    }
75
76    @Override
77    protected void onFinishInflate() {
78        super.onFinishInflate();
79
80        mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress);
81
82        mSendButton = (ImageButton) findViewById(R.id.remote_input_send);
83        mSendButton.setOnClickListener(this);
84
85        mEditText = (RemoteEditText) getChildAt(0);
86        mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
87            @Override
88            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
89
90                // Check if this was the result of hitting the enter key
91                final boolean isSoftImeEvent = event == null
92                        && (actionId == EditorInfo.IME_ACTION_DONE
93                        || actionId == EditorInfo.IME_ACTION_NEXT
94                        || actionId == EditorInfo.IME_ACTION_SEND);
95                final boolean isKeyboardEnterKey = event != null
96                        && KeyEvent.isConfirmKey(event.getKeyCode())
97                        && event.getAction() == KeyEvent.ACTION_DOWN;
98
99                if (isSoftImeEvent || isKeyboardEnterKey) {
100                    sendRemoteInput();
101                    return true;
102                }
103                return false;
104            }
105        });
106        mEditText.setOnClickListener(this);
107        mEditText.addTextChangedListener(this);
108        mEditText.setInnerFocusable(false);
109        mEditText.mDefocusListener = this;
110    }
111
112    private void sendRemoteInput() {
113        Bundle results = new Bundle();
114        results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
115        Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
116        RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
117                results);
118
119        mEditText.setEnabled(false);
120        mSendButton.setVisibility(INVISIBLE);
121        mProgressBar.setVisibility(VISIBLE);
122        mController.removeRemoteInput(mEntry);
123        mEditText.mShowImeOnInputConnection = false;
124
125        try {
126            mPendingIntent.send(mContext, 0, fillInIntent);
127        } catch (PendingIntent.CanceledException e) {
128            Log.i(TAG, "Unable to send remote input result", e);
129        }
130    }
131
132    public static RemoteInputView inflate(Context context, ViewGroup root,
133            NotificationData.Entry entry,
134            RemoteInputController controller) {
135        RemoteInputView v = (RemoteInputView)
136                LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
137        v.mController = controller;
138        v.mEntry = entry;
139        v.setTag(VIEW_TAG);
140
141        return v;
142    }
143
144    @Override
145    public void onClick(View v) {
146        if (v == mEditText) {
147            if (!mEditText.isFocusable()) {
148                focus();
149            }
150        } else if (v == mSendButton) {
151            sendRemoteInput();
152        }
153    }
154
155    public void onDefocus() {
156        mController.removeRemoteInput(mEntry);
157        mEntry.remoteInputText = mEditText.getText();
158        setVisibility(INVISIBLE);
159    }
160
161    @Override
162    protected void onAttachedToWindow() {
163        super.onAttachedToWindow();
164        if (mEntry.row.isChangingPosition()) {
165            if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
166                mEditText.requestFocus();
167            }
168        }
169    }
170
171    @Override
172    protected void onDetachedFromWindow() {
173        super.onDetachedFromWindow();
174        if (mEntry.row.isChangingPosition()) {
175            return;
176        }
177        mController.removeRemoteInput(mEntry);
178    }
179
180    public void setPendingIntent(PendingIntent pendingIntent) {
181        mPendingIntent = pendingIntent;
182    }
183
184    public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
185        mRemoteInputs = remoteInputs;
186        mRemoteInput = remoteInput;
187        mEditText.setHint(mRemoteInput.getLabel());
188    }
189
190    public void focus() {
191        mController.addRemoteInput(mEntry);
192        mEditText.setInnerFocusable(true);
193        mEditText.mShowImeOnInputConnection = true;
194        mEditText.setText(mEntry.remoteInputText);
195        mEditText.setSelection(mEditText.getText().length());
196        mEditText.requestFocus();
197        updateSendButton();
198    }
199
200    public void onNotificationUpdate() {
201        boolean sending = mProgressBar.getVisibility() == VISIBLE;
202
203        if (sending) {
204            // Update came in after we sent the reply, time to reset.
205            reset();
206        }
207    }
208
209    private void reset() {
210        mEditText.getText().clear();
211        mEditText.setEnabled(true);
212        mSendButton.setVisibility(VISIBLE);
213        mProgressBar.setVisibility(INVISIBLE);
214        updateSendButton();
215        onDefocus();
216    }
217
218    private void updateSendButton() {
219        mSendButton.setEnabled(mEditText.getText().length() != 0);
220    }
221
222    @Override
223    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
224
225    @Override
226    public void onTextChanged(CharSequence s, int start, int before, int count) {}
227
228    @Override
229    public void afterTextChanged(Editable s) {
230        updateSendButton();
231    }
232
233    public void close() {
234        mEditText.defocusIfNeeded();
235    }
236
237    @Override
238    public boolean onInterceptTouchEvent(MotionEvent ev) {
239        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
240            if (mLongPressCancelable == null) {
241                ViewParent p = getParent();
242                while (p != null) {
243                    if (p instanceof LongPressCancelable) {
244                        mLongPressCancelable = (LongPressCancelable) p;
245                        break;
246                    }
247                    p = p.getParent();
248                }
249            }
250            if (mLongPressCancelable != null) {
251                mLongPressCancelable.requestDisallowLongPress();
252            }
253        }
254        return super.onInterceptTouchEvent(ev);
255    }
256
257    /**
258     * An EditText that changes appearance based on whether it's focusable and becomes
259     * un-focusable whenever the user navigates away from it or it becomes invisible.
260     */
261    public static class RemoteEditText extends EditText {
262
263        private final Drawable mBackground;
264        private RemoteInputView mDefocusListener;
265        boolean mShowImeOnInputConnection;
266
267        public RemoteEditText(Context context, AttributeSet attrs) {
268            super(context, attrs);
269            mBackground = getBackground();
270        }
271
272        private void defocusIfNeeded() {
273            if (mDefocusListener.mEntry.row.isChangingPosition()) {
274                return;
275            }
276            if (isFocusable() && isEnabled()) {
277                setInnerFocusable(false);
278                if (mDefocusListener != null) {
279                    mDefocusListener.onDefocus();
280                }
281                mShowImeOnInputConnection = false;
282            }
283        }
284
285        @Override
286        protected void onVisibilityChanged(View changedView, int visibility) {
287            super.onVisibilityChanged(changedView, visibility);
288
289            if (!isShown()) {
290                defocusIfNeeded();
291            }
292        }
293
294        @Override
295        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
296            super.onFocusChanged(focused, direction, previouslyFocusedRect);
297            if (!focused) {
298                defocusIfNeeded();
299            }
300        }
301
302        @Override
303        public boolean requestRectangleOnScreen(Rect r) {
304            r.top = mScrollY;
305            r.bottom = mScrollY + (mBottom - mTop);
306            return super.requestRectangleOnScreen(r);
307        }
308
309        @Override
310        public void getFocusedRect(Rect r) {
311            super.getFocusedRect(r);
312            r.top = mScrollY;
313            r.bottom = mScrollY + (mBottom - mTop);
314        }
315
316        @Override
317        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
318            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
319                defocusIfNeeded();
320                final InputMethodManager imm = InputMethodManager.getInstance();
321                imm.hideSoftInputFromWindow(getWindowToken(), 0);
322                return true;
323            }
324            return super.onKeyPreIme(keyCode, event);
325        }
326
327        @Override
328        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
329            final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
330
331            if (mShowImeOnInputConnection && inputConnection != null) {
332                final InputMethodManager imm = InputMethodManager.getInstance();
333                if (imm != null) {
334                    // onCreateInputConnection is called by InputMethodManager in the middle of
335                    // setting up the connection to the IME; wait with requesting the IME until that
336                    // work has completed.
337                    post(new Runnable() {
338                        @Override
339                        public void run() {
340                            imm.viewClicked(RemoteEditText.this);
341                            imm.showSoftInput(RemoteEditText.this, 0);
342                        }
343                    });
344                }
345            }
346
347            return inputConnection;
348        }
349
350        @Override
351        public void onCommitCompletion(CompletionInfo text) {
352            clearComposingText();
353            setText(text.getText());
354            setSelection(getText().length());
355        }
356
357        void setInnerFocusable(boolean focusable) {
358            setFocusableInTouchMode(focusable);
359            setFocusable(focusable);
360            setCursorVisible(focusable);
361
362            if (focusable) {
363                requestFocus();
364                setBackground(mBackground);
365            } else {
366                setBackground(null);
367            }
368
369        }
370    }
371}
372