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.ActivityManager;
22 import android.app.Notification;
23 import android.app.PendingIntent;
24 import android.app.RemoteInput;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ShortcutManager;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 import android.os.Bundle;
31 import android.os.UserHandle;
32 import android.text.Editable;
33 import android.text.InputType;
34 import android.text.TextWatcher;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.view.KeyEvent;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewAnimationUtils;
42 import android.view.ViewGroup;
43 import android.view.ViewParent;
44 import android.view.accessibility.AccessibilityEvent;
45 import android.view.inputmethod.CompletionInfo;
46 import android.view.inputmethod.EditorInfo;
47 import android.view.inputmethod.InputConnection;
48 import android.view.inputmethod.InputMethodManager;
49 import android.widget.EditText;
50 import android.widget.ImageButton;
51 import android.widget.LinearLayout;
52 import android.widget.ProgressBar;
53 import android.widget.TextView;
55 import com.android.internal.logging.MetricsLogger;
56 import com.android.internal.logging.MetricsProto;
57 import com.android.systemui.Interpolators;
58 import com.android.systemui.R;
59 import com.android.systemui.statusbar.ExpandableView;
60 import com.android.systemui.statusbar.NotificationData;
61 import com.android.systemui.statusbar.RemoteInputController;
62 import com.android.systemui.statusbar.stack.ScrollContainer;
63 import com.android.systemui.statusbar.stack.StackStateAnimator;
66 * Host for the remote input.
68 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
70 private static final String TAG = "RemoteInput";
72 // A marker object that let's us easily find views of this class.
73 public static final Object VIEW_TAG = new Object();
75 public final Object mToken = new Object();
77 private RemoteEditText mEditText;
78 private ImageButton mSendButton;
79 private ProgressBar mProgressBar;
80 private PendingIntent mPendingIntent;
81 private RemoteInput[] mRemoteInputs;
82 private RemoteInput mRemoteInput;
83 private RemoteInputController mController;
85 private NotificationData.Entry mEntry;
87 private ScrollContainer mScrollContainer;
88 private View mScrollContainerChild;
89 private boolean mRemoved;
91 private int mRevealCx;
92 private int mRevealCy;
95 private boolean mResetting;
97 public RemoteInputView(Context context, AttributeSet attrs) {
98 super(context, attrs);
102 protected void onFinishInflate() {
103 super.onFinishInflate();
105 mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress);
107 mSendButton = (ImageButton) findViewById(R.id.remote_input_send);
108 mSendButton.setOnClickListener(this);
110 mEditText = (RemoteEditText) getChildAt(0);
111 mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
113 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
114 final boolean isSoftImeEvent = event == null
115 && (actionId == EditorInfo.IME_ACTION_DONE
116 || actionId == EditorInfo.IME_ACTION_NEXT
117 || actionId == EditorInfo.IME_ACTION_SEND);
118 final boolean isKeyboardEnterKey = event != null
119 && KeyEvent.isConfirmKey(event.getKeyCode())
120 && event.getAction() == KeyEvent.ACTION_DOWN;
122 if (isSoftImeEvent || isKeyboardEnterKey) {
123 if (mEditText.length() > 0) {
126 // Consume action to prevent IME from closing.
132 mEditText.addTextChangedListener(this);
133 mEditText.setInnerFocusable(false);
134 mEditText.mRemoteInputView = this;
137 private void sendRemoteInput() {
138 Bundle results = new Bundle();
139 results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
140 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
141 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
144 mEditText.setEnabled(false);
145 mSendButton.setVisibility(INVISIBLE);
146 mProgressBar.setVisibility(VISIBLE);
147 mEntry.remoteInputText = mEditText.getText();
148 mController.addSpinning(mEntry.key, mToken);
149 mController.removeRemoteInput(mEntry, mToken);
150 mEditText.mShowImeOnInputConnection = false;
151 mController.remoteInputSent(mEntry);
153 // Tell ShortcutManager that this package has been "activated". ShortcutManager
154 // will reset the throttling for this package.
155 // Strictly speaking, the intent receiver may be different from the notification publisher,
156 // but that's an edge case, and also because we can't always know which package will receive
157 // an intent, so we just reset for the publisher.
158 getContext().getSystemService(ShortcutManager.class).onApplicationActive(
159 mEntry.notification.getPackageName(),
160 mEntry.notification.getUser().getIdentifier());
162 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
163 mEntry.notification.getPackageName());
165 mPendingIntent.send(mContext, 0, fillInIntent);
166 } catch (PendingIntent.CanceledException e) {
167 Log.i(TAG, "Unable to send remote input result", e);
168 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
169 mEntry.notification.getPackageName());
173 public static RemoteInputView inflate(Context context, ViewGroup root,
174 NotificationData.Entry entry,
175 RemoteInputController controller) {
176 RemoteInputView v = (RemoteInputView)
177 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
178 v.mController = controller;
180 v.mEditText.setRestrictedAcrossUser(true);
187 public void onClick(View v) {
188 if (v == mSendButton) {
194 public boolean onTouchEvent(MotionEvent event) {
195 super.onTouchEvent(event);
197 // We never want for a touch to escape to an outer view or one we covered.
201 private void onDefocus(boolean animate) {
202 mController.removeRemoteInput(mEntry, mToken);
203 mEntry.remoteInputText = mEditText.getText();
205 // During removal, we get reattached and lose focus. Not hiding in that
206 // case to prevent flicker.
208 if (animate && mRevealR > 0) {
209 Animator reveal = ViewAnimationUtils.createCircularReveal(
210 this, mRevealCx, mRevealCy, mRevealR, 0);
211 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
212 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
213 reveal.addListener(new AnimatorListenerAdapter() {
215 public void onAnimationEnd(Animator animation) {
216 setVisibility(INVISIBLE);
221 setVisibility(INVISIBLE);
224 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
225 mEntry.notification.getPackageName());
229 protected void onAttachedToWindow() {
230 super.onAttachedToWindow();
231 if (mEntry.row.isChangingPosition()) {
232 if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
233 mEditText.requestFocus();
239 protected void onDetachedFromWindow() {
240 super.onDetachedFromWindow();
241 if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
244 mController.removeRemoteInput(mEntry, mToken);
245 mController.removeSpinning(mEntry.key, mToken);
248 public void setPendingIntent(PendingIntent pendingIntent) {
249 mPendingIntent = pendingIntent;
252 public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
253 mRemoteInputs = remoteInputs;
254 mRemoteInput = remoteInput;
255 mEditText.setHint(mRemoteInput.getLabel());
258 public void focusAnimated() {
259 if (getVisibility() != VISIBLE) {
260 Animator animator = ViewAnimationUtils.createCircularReveal(
261 this, mRevealCx, mRevealCy, 0, mRevealR);
262 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
263 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
269 public void focus() {
270 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
271 mEntry.notification.getPackageName());
273 setVisibility(VISIBLE);
274 mController.addRemoteInput(mEntry, mToken);
276 // Disable suggestions on non-owner (secondary) user.
277 // SpellCheckerService of primary user runs on secondary as well which shows
278 // "Add to dictionary" dialog on the primary user. (See b/123232892)
279 // Note: this doesn't affect work-profile users on P or older versions.
280 if (UserHandle.myUserId() != ActivityManager.getCurrentUser()) {
281 mEditText.setInputType(
282 mEditText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
285 mEditText.setInnerFocusable(true);
286 mEditText.mShowImeOnInputConnection = true;
287 mEditText.setText(mEntry.remoteInputText);
288 mEditText.setSelection(mEditText.getText().length());
289 mEditText.requestFocus();
293 public void onNotificationUpdateOrReset() {
294 boolean sending = mProgressBar.getVisibility() == VISIBLE;
297 // Update came in after we sent the reply, time to reset.
302 private void reset() {
305 mEditText.getText().clear();
306 mEditText.setEnabled(true);
307 mSendButton.setVisibility(VISIBLE);
308 mProgressBar.setVisibility(INVISIBLE);
309 mController.removeSpinning(mEntry.key, mToken);
311 onDefocus(false /* animate */);
317 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
318 if (mResetting && child == mEditText) {
319 // Suppress text events if it happens during resetting. Ideally this would be
320 // suppressed by the text view not being shown, but that doesn't work here because it
321 // needs to stay visible for the animation.
324 return super.onRequestSendAccessibilityEvent(child, event);
327 private void updateSendButton() {
328 mSendButton.setEnabled(mEditText.getText().length() != 0);
332 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
335 public void onTextChanged(CharSequence s, int start, int before, int count) {}
338 public void afterTextChanged(Editable s) {
342 public void close() {
343 mEditText.defocusIfNeeded(false /* animated */);
347 public boolean onInterceptTouchEvent(MotionEvent ev) {
348 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
349 findScrollContainer();
350 if (mScrollContainer != null) {
351 mScrollContainer.requestDisallowLongPress();
352 mScrollContainer.requestDisallowDismiss();
355 return super.onInterceptTouchEvent(ev);
358 public boolean requestScrollTo() {
359 findScrollContainer();
360 mScrollContainer.lockScrollTo(mScrollContainerChild);
364 private void findScrollContainer() {
365 if (mScrollContainer == null) {
366 mScrollContainerChild = null;
369 if (mScrollContainerChild == null && p instanceof ExpandableView) {
370 mScrollContainerChild = (View) p;
372 if (p.getParent() instanceof ScrollContainer) {
373 mScrollContainer = (ScrollContainer) p.getParent();
374 if (mScrollContainerChild == null) {
375 mScrollContainerChild = (View) p;
384 public boolean isActive() {
385 return mEditText.isFocused() && mEditText.isEnabled();
388 public void stealFocusFrom(RemoteInputView other) {
390 setPendingIntent(other.mPendingIntent);
391 setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
392 setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
397 * Tries to find an action in {@param actions} that matches the current pending intent
398 * of this view and updates its state to that of the found action
400 * @return true if a matching action was found, false otherwise
402 public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
403 if (mPendingIntent == null || actions == null) {
406 Intent current = mPendingIntent.getIntent();
407 if (current == null) {
411 for (Notification.Action a : actions) {
412 RemoteInput[] inputs = a.getRemoteInputs();
413 if (a.actionIntent == null || inputs == null) {
416 Intent candidate = a.actionIntent.getIntent();
417 if (!current.filterEquals(candidate)) {
421 RemoteInput input = null;
422 for (RemoteInput i : inputs) {
423 if (i.getAllowFreeFormInput()) {
430 setPendingIntent(a.actionIntent);
431 setRemoteInput(inputs, input);
437 public PendingIntent getPendingIntent() {
438 return mPendingIntent;
441 public void setRemoved() {
445 public void setRevealParameters(int cx, int cy, int r) {
452 public void dispatchStartTemporaryDetach() {
453 super.dispatchStartTemporaryDetach();
454 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
455 // won't lose IME focus.
456 detachViewFromParent(mEditText);
460 public void dispatchFinishTemporaryDetach() {
461 if (isAttachedToWindow()) {
462 attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
464 removeDetachedView(mEditText, false /* animate */);
466 super.dispatchFinishTemporaryDetach();
470 * An EditText that changes appearance based on whether it's focusable and becomes
471 * un-focusable whenever the user navigates away from it or it becomes invisible.
473 public static class RemoteEditText extends EditText {
475 private final Drawable mBackground;
476 private RemoteInputView mRemoteInputView;
477 boolean mShowImeOnInputConnection;
479 public RemoteEditText(Context context, AttributeSet attrs) {
480 super(context, attrs);
481 mBackground = getBackground();
484 private void defocusIfNeeded(boolean animate) {
485 if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()
486 || isTemporarilyDetached()) {
487 if (isTemporarilyDetached()) {
488 // We might get reattached but then the other one of HUN / expanded might steal
489 // our focus, so we'll need to save our text here.
490 if (mRemoteInputView != null) {
491 mRemoteInputView.mEntry.remoteInputText = getText();
496 if (isFocusable() && isEnabled()) {
497 setInnerFocusable(false);
498 if (mRemoteInputView != null) {
499 mRemoteInputView.onDefocus(animate);
501 mShowImeOnInputConnection = false;
506 protected void onVisibilityChanged(View changedView, int visibility) {
507 super.onVisibilityChanged(changedView, visibility);
510 defocusIfNeeded(false /* animate */);
515 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
516 super.onFocusChanged(focused, direction, previouslyFocusedRect);
518 defocusIfNeeded(true /* animate */);
523 public void getFocusedRect(Rect r) {
524 super.getFocusedRect(r);
526 r.bottom = mScrollY + (mBottom - mTop);
530 public boolean requestRectangleOnScreen(Rect rectangle) {
531 return mRemoteInputView.requestScrollTo();
535 public boolean onKeyDown(int keyCode, KeyEvent event) {
536 if (keyCode == KeyEvent.KEYCODE_BACK) {
537 // Eat the DOWN event here to prevent any default behavior.
540 return super.onKeyDown(keyCode, event);
544 public boolean onKeyUp(int keyCode, KeyEvent event) {
545 if (keyCode == KeyEvent.KEYCODE_BACK) {
546 defocusIfNeeded(true /* animate */);
549 return super.onKeyUp(keyCode, event);
553 public boolean onCheckIsTextEditor() {
554 // Stop being editable while we're being removed. During removal, we get reattached,
555 // and editable views get their spellchecking state re-evaluated which is too costly
556 // during the removal animation.
557 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
558 return !flyingOut && super.onCheckIsTextEditor();
562 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
563 final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
565 if (mShowImeOnInputConnection && inputConnection != null) {
566 final InputMethodManager imm = InputMethodManager.getInstance();
568 // onCreateInputConnection is called by InputMethodManager in the middle of
569 // setting up the connection to the IME; wait with requesting the IME until that
570 // work has completed.
571 post(new Runnable() {
574 imm.viewClicked(RemoteEditText.this);
575 imm.showSoftInput(RemoteEditText.this, 0);
581 return inputConnection;
585 public void onCommitCompletion(CompletionInfo text) {
586 clearComposingText();
587 setText(text.getText());
588 setSelection(getText().length());
591 void setInnerFocusable(boolean focusable) {
592 setFocusableInTouchMode(focusable);
593 setFocusable(focusable);
594 setCursorVisible(focusable);
598 setBackground(mBackground);