2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.policy;
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.app.Notification;
22 import android.app.PendingIntent;
23 import android.app.RemoteInput;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.ShortcutManager;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.text.Editable;
31 import android.text.TextWatcher;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.KeyEvent;
35 import android.view.LayoutInflater;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewAnimationUtils;
39 import android.view.ViewGroup;
40 import android.view.ViewParent;
41 import android.view.accessibility.AccessibilityEvent;
42 import android.view.inputmethod.CompletionInfo;
43 import android.view.inputmethod.EditorInfo;
44 import android.view.inputmethod.InputConnection;
45 import android.view.inputmethod.InputMethodManager;
46 import android.widget.EditText;
47 import android.widget.ImageButton;
48 import android.widget.LinearLayout;
49 import android.widget.ProgressBar;
50 import android.widget.TextView;
52 import com.android.internal.logging.MetricsLogger;
53 import com.android.internal.logging.nano.MetricsProto;
54 import com.android.systemui.Interpolators;
55 import com.android.systemui.R;
56 import com.android.systemui.statusbar.ExpandableView;
57 import com.android.systemui.statusbar.NotificationData;
58 import com.android.systemui.statusbar.RemoteInputController;
59 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
60 import com.android.systemui.statusbar.stack.ScrollContainer;
61 import com.android.systemui.statusbar.stack.StackStateAnimator;
64 * Host for the remote input.
66 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
68 private static final String TAG = "RemoteInput";
70 // A marker object that let's us easily find views of this class.
71 public static final Object VIEW_TAG = new Object();
73 public final Object mToken = new Object();
75 private RemoteEditText mEditText;
76 private ImageButton mSendButton;
77 private ProgressBar mProgressBar;
78 private PendingIntent mPendingIntent;
79 private RemoteInput[] mRemoteInputs;
80 private RemoteInput mRemoteInput;
81 private RemoteInputController mController;
83 private NotificationData.Entry mEntry;
85 private ScrollContainer mScrollContainer;
86 private View mScrollContainerChild;
87 private boolean mRemoved;
89 private int mRevealCx;
90 private int mRevealCy;
93 private boolean mResetting;
94 private NotificationViewWrapper mWrapper;
96 public RemoteInputView(Context context, AttributeSet attrs) {
97 super(context, attrs);
101 protected void onFinishInflate() {
102 super.onFinishInflate();
104 mProgressBar = findViewById(R.id.remote_input_progress);
106 mSendButton = findViewById(R.id.remote_input_send);
107 mSendButton.setOnClickListener(this);
109 mEditText = (RemoteEditText) getChildAt(0);
110 mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
112 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
113 final boolean isSoftImeEvent = event == null
114 && (actionId == EditorInfo.IME_ACTION_DONE
115 || actionId == EditorInfo.IME_ACTION_NEXT
116 || actionId == EditorInfo.IME_ACTION_SEND);
117 final boolean isKeyboardEnterKey = event != null
118 && KeyEvent.isConfirmKey(event.getKeyCode())
119 && event.getAction() == KeyEvent.ACTION_DOWN;
121 if (isSoftImeEvent || isKeyboardEnterKey) {
122 if (mEditText.length() > 0) {
125 // Consume action to prevent IME from closing.
131 mEditText.addTextChangedListener(this);
132 mEditText.setInnerFocusable(false);
133 mEditText.mRemoteInputView = this;
136 private void sendRemoteInput() {
137 Bundle results = new Bundle();
138 results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
139 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
140 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
143 mEditText.setEnabled(false);
144 mSendButton.setVisibility(INVISIBLE);
145 mProgressBar.setVisibility(VISIBLE);
146 mEntry.remoteInputText = mEditText.getText();
147 mController.addSpinning(mEntry.key, mToken);
148 mController.removeRemoteInput(mEntry, mToken);
149 mEditText.mShowImeOnInputConnection = false;
150 mController.remoteInputSent(mEntry);
152 // Tell ShortcutManager that this package has been "activated". ShortcutManager
153 // will reset the throttling for this package.
154 // Strictly speaking, the intent receiver may be different from the notification publisher,
155 // but that's an edge case, and also because we can't always know which package will receive
156 // an intent, so we just reset for the publisher.
157 getContext().getSystemService(ShortcutManager.class).onApplicationActive(
158 mEntry.notification.getPackageName(),
159 mEntry.notification.getUser().getIdentifier());
161 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
162 mEntry.notification.getPackageName());
164 mPendingIntent.send(mContext, 0, fillInIntent);
165 } catch (PendingIntent.CanceledException e) {
166 Log.i(TAG, "Unable to send remote input result", e);
167 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
168 mEntry.notification.getPackageName());
172 public static RemoteInputView inflate(Context context, ViewGroup root,
173 NotificationData.Entry entry,
174 RemoteInputController controller) {
175 RemoteInputView v = (RemoteInputView)
176 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
177 v.mController = controller;
185 public void onClick(View v) {
186 if (v == mSendButton) {
192 public boolean onTouchEvent(MotionEvent event) {
193 super.onTouchEvent(event);
195 // We never want for a touch to escape to an outer view or one we covered.
199 private void onDefocus(boolean animate) {
200 mController.removeRemoteInput(mEntry, mToken);
201 mEntry.remoteInputText = mEditText.getText();
203 // During removal, we get reattached and lose focus. Not hiding in that
204 // case to prevent flicker.
206 if (animate && mRevealR > 0) {
207 Animator reveal = ViewAnimationUtils.createCircularReveal(
208 this, mRevealCx, mRevealCy, mRevealR, 0);
209 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
210 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
211 reveal.addListener(new AnimatorListenerAdapter() {
213 public void onAnimationEnd(Animator animation) {
214 setVisibility(INVISIBLE);
215 if (mWrapper != null) {
216 mWrapper.setRemoteInputVisible(false);
222 setVisibility(INVISIBLE);
223 if (mWrapper != null) {
224 mWrapper.setRemoteInputVisible(false);
228 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
229 mEntry.notification.getPackageName());
233 protected void onAttachedToWindow() {
234 super.onAttachedToWindow();
235 if (mEntry.row.isChangingPosition()) {
236 if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
237 mEditText.requestFocus();
243 protected void onDetachedFromWindow() {
244 super.onDetachedFromWindow();
245 if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
248 mController.removeRemoteInput(mEntry, mToken);
249 mController.removeSpinning(mEntry.key, mToken);
252 public void setPendingIntent(PendingIntent pendingIntent) {
253 mPendingIntent = pendingIntent;
256 public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
257 mRemoteInputs = remoteInputs;
258 mRemoteInput = remoteInput;
259 mEditText.setHint(mRemoteInput.getLabel());
262 public void focusAnimated() {
263 if (getVisibility() != VISIBLE) {
264 Animator animator = ViewAnimationUtils.createCircularReveal(
265 this, mRevealCx, mRevealCy, 0, mRevealR);
266 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
267 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
273 public void focus() {
274 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
275 mEntry.notification.getPackageName());
277 setVisibility(VISIBLE);
278 if (mWrapper != null) {
279 mWrapper.setRemoteInputVisible(true);
281 mController.addRemoteInput(mEntry, mToken);
282 mEditText.setInnerFocusable(true);
283 mEditText.mShowImeOnInputConnection = true;
284 mEditText.setText(mEntry.remoteInputText);
285 mEditText.setSelection(mEditText.getText().length());
286 mEditText.requestFocus();
290 public void onNotificationUpdateOrReset() {
291 boolean sending = mProgressBar.getVisibility() == VISIBLE;
294 // Update came in after we sent the reply, time to reset.
298 if (isActive() && mWrapper != null) {
299 mWrapper.setRemoteInputVisible(true);
303 private void reset() {
306 mEditText.getText().clear();
307 mEditText.setEnabled(true);
308 mSendButton.setVisibility(VISIBLE);
309 mProgressBar.setVisibility(INVISIBLE);
310 mController.removeSpinning(mEntry.key, mToken);
312 onDefocus(false /* animate */);
318 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
319 if (mResetting && child == mEditText) {
320 // Suppress text events if it happens during resetting. Ideally this would be
321 // suppressed by the text view not being shown, but that doesn't work here because it
322 // needs to stay visible for the animation.
325 return super.onRequestSendAccessibilityEvent(child, event);
328 private void updateSendButton() {
329 mSendButton.setEnabled(mEditText.getText().length() != 0);
333 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
336 public void onTextChanged(CharSequence s, int start, int before, int count) {}
339 public void afterTextChanged(Editable s) {
343 public void close() {
344 mEditText.defocusIfNeeded(false /* animated */);
348 public boolean onInterceptTouchEvent(MotionEvent ev) {
349 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
350 findScrollContainer();
351 if (mScrollContainer != null) {
352 mScrollContainer.requestDisallowLongPress();
353 mScrollContainer.requestDisallowDismiss();
356 return super.onInterceptTouchEvent(ev);
359 public boolean requestScrollTo() {
360 findScrollContainer();
361 mScrollContainer.lockScrollTo(mScrollContainerChild);
365 private void findScrollContainer() {
366 if (mScrollContainer == null) {
367 mScrollContainerChild = null;
370 if (mScrollContainerChild == null && p instanceof ExpandableView) {
371 mScrollContainerChild = (View) p;
373 if (p.getParent() instanceof ScrollContainer) {
374 mScrollContainer = (ScrollContainer) p.getParent();
375 if (mScrollContainerChild == null) {
376 mScrollContainerChild = (View) p;
385 public boolean isActive() {
386 return mEditText.isFocused() && mEditText.isEnabled();
389 public void stealFocusFrom(RemoteInputView other) {
391 setPendingIntent(other.mPendingIntent);
392 setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
393 setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
398 * Tries to find an action in {@param actions} that matches the current pending intent
399 * of this view and updates its state to that of the found action
401 * @return true if a matching action was found, false otherwise
403 public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
404 if (mPendingIntent == null || actions == null) {
407 Intent current = mPendingIntent.getIntent();
408 if (current == null) {
412 for (Notification.Action a : actions) {
413 RemoteInput[] inputs = a.getRemoteInputs();
414 if (a.actionIntent == null || inputs == null) {
417 Intent candidate = a.actionIntent.getIntent();
418 if (!current.filterEquals(candidate)) {
422 RemoteInput input = null;
423 for (RemoteInput i : inputs) {
424 if (i.getAllowFreeFormInput()) {
431 setPendingIntent(a.actionIntent);
432 setRemoteInput(inputs, input);
438 public PendingIntent getPendingIntent() {
439 return mPendingIntent;
442 public void setRemoved() {
446 public void setRevealParameters(int cx, int cy, int r) {
453 public void dispatchStartTemporaryDetach() {
454 super.dispatchStartTemporaryDetach();
455 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
456 // won't lose IME focus.
457 detachViewFromParent(mEditText);
461 public void dispatchFinishTemporaryDetach() {
462 if (isAttachedToWindow()) {
463 attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
465 removeDetachedView(mEditText, false /* animate */);
467 super.dispatchFinishTemporaryDetach();
470 public void setWrapper(NotificationViewWrapper wrapper) {
475 * An EditText that changes appearance based on whether it's focusable and becomes
476 * un-focusable whenever the user navigates away from it or it becomes invisible.
478 public static class RemoteEditText extends EditText {
480 private final Drawable mBackground;
481 private RemoteInputView mRemoteInputView;
482 boolean mShowImeOnInputConnection;
484 public RemoteEditText(Context context, AttributeSet attrs) {
485 super(context, attrs);
486 mBackground = getBackground();
489 private void defocusIfNeeded(boolean animate) {
490 if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()
491 || isTemporarilyDetached()) {
492 if (isTemporarilyDetached()) {
493 // We might get reattached but then the other one of HUN / expanded might steal
494 // our focus, so we'll need to save our text here.
495 if (mRemoteInputView != null) {
496 mRemoteInputView.mEntry.remoteInputText = getText();
501 if (isFocusable() && isEnabled()) {
502 setInnerFocusable(false);
503 if (mRemoteInputView != null) {
504 mRemoteInputView.onDefocus(animate);
506 mShowImeOnInputConnection = false;
511 protected void onVisibilityChanged(View changedView, int visibility) {
512 super.onVisibilityChanged(changedView, visibility);
515 defocusIfNeeded(false /* animate */);
520 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
521 super.onFocusChanged(focused, direction, previouslyFocusedRect);
523 defocusIfNeeded(true /* animate */);
528 public void getFocusedRect(Rect r) {
529 super.getFocusedRect(r);
531 r.bottom = mScrollY + (mBottom - mTop);
535 public boolean requestRectangleOnScreen(Rect rectangle) {
536 return mRemoteInputView.requestScrollTo();
540 public boolean onKeyDown(int keyCode, KeyEvent event) {
541 if (keyCode == KeyEvent.KEYCODE_BACK) {
542 // Eat the DOWN event here to prevent any default behavior.
545 return super.onKeyDown(keyCode, event);
549 public boolean onKeyUp(int keyCode, KeyEvent event) {
550 if (keyCode == KeyEvent.KEYCODE_BACK) {
551 defocusIfNeeded(true /* animate */);
554 return super.onKeyUp(keyCode, event);
558 public boolean onCheckIsTextEditor() {
559 // Stop being editable while we're being removed. During removal, we get reattached,
560 // and editable views get their spellchecking state re-evaluated which is too costly
561 // during the removal animation.
562 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
563 return !flyingOut && super.onCheckIsTextEditor();
567 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
568 final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
570 if (mShowImeOnInputConnection && inputConnection != null) {
571 final InputMethodManager imm = InputMethodManager.getInstance();
573 // onCreateInputConnection is called by InputMethodManager in the middle of
574 // setting up the connection to the IME; wait with requesting the IME until that
575 // work has completed.
576 post(new Runnable() {
579 imm.viewClicked(RemoteEditText.this);
580 imm.showSoftInput(RemoteEditText.this, 0);
586 return inputConnection;
590 public void onCommitCompletion(CompletionInfo text) {
591 clearComposingText();
592 setText(text.getText());
593 setSelection(getText().length());
596 void setInnerFocusable(boolean focusable) {
597 setFocusableInTouchMode(focusable);
598 setFocusable(focusable);
599 setCursorVisible(focusable);
603 setBackground(mBackground);