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