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.app.Notification;
20 import android.app.PendingIntent;
21 import android.app.RemoteInput;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.os.Bundle;
27 import android.text.Editable;
28 import android.text.TextWatcher;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.KeyEvent;
32 import android.view.LayoutInflater;
33 import android.view.MotionEvent;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ViewParent;
37 import android.view.inputmethod.CompletionInfo;
38 import android.view.inputmethod.EditorInfo;
39 import android.view.inputmethod.InputConnection;
40 import android.view.inputmethod.InputMethodManager;
41 import android.widget.EditText;
42 import android.widget.ImageButton;
43 import android.widget.LinearLayout;
44 import android.widget.ProgressBar;
45 import android.widget.TextView;
46 
47 import com.android.internal.logging.MetricsLogger;
48 import com.android.internal.logging.MetricsProto;
49 import com.android.systemui.R;
50 import com.android.systemui.statusbar.ExpandableView;
51 import com.android.systemui.statusbar.NotificationData;
52 import com.android.systemui.statusbar.RemoteInputController;
53 import com.android.systemui.statusbar.stack.ScrollContainer;
54 
55 /**
56  * Host for the remote input.
57  */
58 public 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 
RemoteInputView(Context context, AttributeSet attrs)79     public RemoteInputView(Context context, AttributeSet attrs) {
80         super(context, attrs);
81     }
82 
83     @Override
onFinishInflate()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 
sendRemoteInput()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 
inflate(Context context, ViewGroup root, NotificationData.Entry entry, RemoteInputController controller)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
onClick(View v)159     public void onClick(View v) {
160         if (v == mSendButton) {
161             sendRemoteInput();
162         }
163     }
164 
165     @Override
onTouchEvent(MotionEvent event)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 
onDefocus()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
onAttachedToWindow()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
onDetachedFromWindow()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 
setPendingIntent(PendingIntent pendingIntent)206     public void setPendingIntent(PendingIntent pendingIntent) {
207         mPendingIntent = pendingIntent;
208     }
209 
setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput)210     public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
211         mRemoteInputs = remoteInputs;
212         mRemoteInput = remoteInput;
213         mEditText.setHint(mRemoteInput.getLabel());
214     }
215 
focus()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 
onNotificationUpdateOrReset()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 
reset()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 
updateSendButton()249     private void updateSendButton() {
250         mSendButton.setEnabled(mEditText.getText().length() != 0);
251     }
252 
253     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)254     public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
255 
256     @Override
onTextChanged(CharSequence s, int start, int before, int count)257     public void onTextChanged(CharSequence s, int start, int before, int count) {}
258 
259     @Override
afterTextChanged(Editable s)260     public void afterTextChanged(Editable s) {
261         updateSendButton();
262     }
263 
close()264     public void close() {
265         mEditText.defocusIfNeeded();
266     }
267 
268     @Override
onInterceptTouchEvent(MotionEvent ev)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 
requestScrollTo()280     public boolean requestScrollTo() {
281         findScrollContainer();
282         mScrollContainer.lockScrollTo(mScrollContainerChild);
283         return true;
284     }
285 
findScrollContainer()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 
isActive()306     public boolean isActive() {
307         return mEditText.isFocused();
308     }
309 
stealFocusFrom(RemoteInputView other)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      */
updatePendingIntentFromActions(Notification.Action[] actions)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 
getPendingIntent()359     public PendingIntent getPendingIntent() {
360         return mPendingIntent;
361     }
362 
setRemoved()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 
RemoteEditText(Context context, AttributeSet attrs)377         public RemoteEditText(Context context, AttributeSet attrs) {
378             super(context, attrs);
379             mBackground = getBackground();
380         }
381 
defocusIfNeeded()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
onVisibilityChanged(View changedView, int visibility)396         protected void onVisibilityChanged(View changedView, int visibility) {
397             super.onVisibilityChanged(changedView, visibility);
398 
399             if (!isShown()) {
400                 defocusIfNeeded();
401             }
402         }
403 
404         @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)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
getFocusedRect(Rect r)413         public void getFocusedRect(Rect r) {
414             super.getFocusedRect(r);
415             r.top = mScrollY;
416             r.bottom = mScrollY + (mBottom - mTop);
417         }
418 
419         @Override
requestRectangleOnScreen(Rect rectangle)420         public boolean requestRectangleOnScreen(Rect rectangle) {
421             return mRemoteInputView.requestScrollTo();
422         }
423 
424         @Override
onKeyPreIme(int keyCode, KeyEvent event)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
onCheckIsTextEditor()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
onCreateInputConnection(EditorInfo outAttrs)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
onCommitCompletion(CompletionInfo text)468         public void onCommitCompletion(CompletionInfo text) {
469             clearComposingText();
470             setText(text.getText());
471             setSelection(getText().length());
472         }
473 
setInnerFocusable(boolean focusable)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