Home | History | Annotate | Download | only in policy
      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.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;
     51 
     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;
     62 
     63 /**
     64  * Host for the remote input.
     65  */
     66 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
     67 
     68     private static final String TAG = "RemoteInput";
     69 
     70     // A marker object that let's us easily find views of this class.
     71     public static final Object VIEW_TAG = new Object();
     72 
     73     public final Object mToken = new Object();
     74 
     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;
     82 
     83     private NotificationData.Entry mEntry;
     84 
     85     private ScrollContainer mScrollContainer;
     86     private View mScrollContainerChild;
     87     private boolean mRemoved;
     88 
     89     private int mRevealCx;
     90     private int mRevealCy;
     91     private int mRevealR;
     92 
     93     private boolean mResetting;
     94     private NotificationViewWrapper mWrapper;
     95 
     96     public RemoteInputView(Context context, AttributeSet attrs) {
     97         super(context, attrs);
     98     }
     99 
    100     @Override
    101     protected void onFinishInflate() {
    102         super.onFinishInflate();
    103 
    104         mProgressBar = findViewById(R.id.remote_input_progress);
    105 
    106         mSendButton = findViewById(R.id.remote_input_send);
    107         mSendButton.setOnClickListener(this);
    108 
    109         mEditText = (RemoteEditText) getChildAt(0);
    110         mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
    111             @Override
    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;
    120 
    121                 if (isSoftImeEvent || isKeyboardEnterKey) {
    122                     if (mEditText.length() > 0) {
    123                         sendRemoteInput();
    124                     }
    125                     // Consume action to prevent IME from closing.
    126                     return true;
    127                 }
    128                 return false;
    129             }
    130         });
    131         mEditText.addTextChangedListener(this);
    132         mEditText.setInnerFocusable(false);
    133         mEditText.mRemoteInputView = this;
    134     }
    135 
    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,
    141                 results);
    142 
    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);
    151 
    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());
    160 
    161         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
    162                 mEntry.notification.getPackageName());
    163         try {
    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());
    169         }
    170     }
    171 
    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;
    178         v.mEntry = entry;
    179         v.setTag(VIEW_TAG);
    180 
    181         return v;
    182     }
    183 
    184     @Override
    185     public void onClick(View v) {
    186         if (v == mSendButton) {
    187             sendRemoteInput();
    188         }
    189     }
    190 
    191     @Override
    192     public boolean onTouchEvent(MotionEvent event) {
    193         super.onTouchEvent(event);
    194 
    195         // We never want for a touch to escape to an outer view or one we covered.
    196         return true;
    197     }
    198 
    199     private void onDefocus(boolean animate) {
    200         mController.removeRemoteInput(mEntry, mToken);
    201         mEntry.remoteInputText = mEditText.getText();
    202 
    203         // During removal, we get reattached and lose focus. Not hiding in that
    204         // case to prevent flicker.
    205         if (!mRemoved) {
    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() {
    212                     @Override
    213                     public void onAnimationEnd(Animator animation) {
    214                         setVisibility(INVISIBLE);
    215                         if (mWrapper != null) {
    216                             mWrapper.setRemoteInputVisible(false);
    217                         }
    218                     }
    219                 });
    220                 reveal.start();
    221             } else {
    222                 setVisibility(INVISIBLE);
    223                 if (mWrapper != null) {
    224                     mWrapper.setRemoteInputVisible(false);
    225                 }
    226             }
    227         }
    228         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
    229                 mEntry.notification.getPackageName());
    230     }
    231 
    232     @Override
    233     protected void onAttachedToWindow() {
    234         super.onAttachedToWindow();
    235         if (mEntry.row.isChangingPosition()) {
    236             if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
    237                 mEditText.requestFocus();
    238             }
    239         }
    240     }
    241 
    242     @Override
    243     protected void onDetachedFromWindow() {
    244         super.onDetachedFromWindow();
    245         if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
    246             return;
    247         }
    248         mController.removeRemoteInput(mEntry, mToken);
    249         mController.removeSpinning(mEntry.key, mToken);
    250     }
    251 
    252     public void setPendingIntent(PendingIntent pendingIntent) {
    253         mPendingIntent = pendingIntent;
    254     }
    255 
    256     public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
    257         mRemoteInputs = remoteInputs;
    258         mRemoteInput = remoteInput;
    259         mEditText.setHint(mRemoteInput.getLabel());
    260     }
    261 
    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);
    268             animator.start();
    269         }
    270         focus();
    271     }
    272 
    273     public void focus() {
    274         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
    275                 mEntry.notification.getPackageName());
    276 
    277         setVisibility(VISIBLE);
    278         if (mWrapper != null) {
    279             mWrapper.setRemoteInputVisible(true);
    280         }
    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();
    287         updateSendButton();
    288     }
    289 
    290     public void onNotificationUpdateOrReset() {
    291         boolean sending = mProgressBar.getVisibility() == VISIBLE;
    292 
    293         if (sending) {
    294             // Update came in after we sent the reply, time to reset.
    295             reset();
    296         }
    297 
    298         if (isActive() && mWrapper != null) {
    299             mWrapper.setRemoteInputVisible(true);
    300         }
    301     }
    302 
    303     private void reset() {
    304         mResetting = true;
    305 
    306         mEditText.getText().clear();
    307         mEditText.setEnabled(true);
    308         mSendButton.setVisibility(VISIBLE);
    309         mProgressBar.setVisibility(INVISIBLE);
    310         mController.removeSpinning(mEntry.key, mToken);
    311         updateSendButton();
    312         onDefocus(false /* animate */);
    313 
    314         mResetting = false;
    315     }
    316 
    317     @Override
    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.
    323             return false;
    324         }
    325         return super.onRequestSendAccessibilityEvent(child, event);
    326     }
    327 
    328     private void updateSendButton() {
    329         mSendButton.setEnabled(mEditText.getText().length() != 0);
    330     }
    331 
    332     @Override
    333     public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    334 
    335     @Override
    336     public void onTextChanged(CharSequence s, int start, int before, int count) {}
    337 
    338     @Override
    339     public void afterTextChanged(Editable s) {
    340         updateSendButton();
    341     }
    342 
    343     public void close() {
    344         mEditText.defocusIfNeeded(false /* animated */);
    345     }
    346 
    347     @Override
    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();
    354             }
    355         }
    356         return super.onInterceptTouchEvent(ev);
    357     }
    358 
    359     public boolean requestScrollTo() {
    360         findScrollContainer();
    361         mScrollContainer.lockScrollTo(mScrollContainerChild);
    362         return true;
    363     }
    364 
    365     private void findScrollContainer() {
    366         if (mScrollContainer == null) {
    367             mScrollContainerChild = null;
    368             ViewParent p = this;
    369             while (p != null) {
    370                 if (mScrollContainerChild == null && p instanceof ExpandableView) {
    371                     mScrollContainerChild = (View) p;
    372                 }
    373                 if (p.getParent() instanceof ScrollContainer) {
    374                     mScrollContainer = (ScrollContainer) p.getParent();
    375                     if (mScrollContainerChild == null) {
    376                         mScrollContainerChild = (View) p;
    377                     }
    378                     break;
    379                 }
    380                 p = p.getParent();
    381             }
    382         }
    383     }
    384 
    385     public boolean isActive() {
    386         return mEditText.isFocused() && mEditText.isEnabled();
    387     }
    388 
    389     public void stealFocusFrom(RemoteInputView other) {
    390         other.close();
    391         setPendingIntent(other.mPendingIntent);
    392         setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
    393         setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
    394         focus();
    395     }
    396 
    397     /**
    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
    400      *
    401      * @return true if a matching action was found, false otherwise
    402      */
    403     public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
    404         if (mPendingIntent == null || actions == null) {
    405             return false;
    406         }
    407         Intent current = mPendingIntent.getIntent();
    408         if (current == null) {
    409             return false;
    410         }
    411 
    412         for (Notification.Action a : actions) {
    413             RemoteInput[] inputs = a.getRemoteInputs();
    414             if (a.actionIntent == null || inputs == null) {
    415                 continue;
    416             }
    417             Intent candidate = a.actionIntent.getIntent();
    418             if (!current.filterEquals(candidate)) {
    419                 continue;
    420             }
    421 
    422             RemoteInput input = null;
    423             for (RemoteInput i : inputs) {
    424                 if (i.getAllowFreeFormInput()) {
    425                     input = i;
    426                 }
    427             }
    428             if (input == null) {
    429                 continue;
    430             }
    431             setPendingIntent(a.actionIntent);
    432             setRemoteInput(inputs, input);
    433             return true;
    434         }
    435         return false;
    436     }
    437 
    438     public PendingIntent getPendingIntent() {
    439         return mPendingIntent;
    440     }
    441 
    442     public void setRemoved() {
    443         mRemoved = true;
    444     }
    445 
    446     public void setRevealParameters(int cx, int cy, int r) {
    447         mRevealCx = cx;
    448         mRevealCy = cy;
    449         mRevealR = r;
    450     }
    451 
    452     @Override
    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);
    458     }
    459 
    460     @Override
    461     public void dispatchFinishTemporaryDetach() {
    462         if (isAttachedToWindow()) {
    463             attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
    464         } else {
    465             removeDetachedView(mEditText, false /* animate */);
    466         }
    467         super.dispatchFinishTemporaryDetach();
    468     }
    469 
    470     public void setWrapper(NotificationViewWrapper wrapper) {
    471         mWrapper = wrapper;
    472     }
    473 
    474     /**
    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.
    477      */
    478     public static class RemoteEditText extends EditText {
    479 
    480         private final Drawable mBackground;
    481         private RemoteInputView mRemoteInputView;
    482         boolean mShowImeOnInputConnection;
    483 
    484         public RemoteEditText(Context context, AttributeSet attrs) {
    485             super(context, attrs);
    486             mBackground = getBackground();
    487         }
    488 
    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();
    497                     }
    498                 }
    499                 return;
    500             }
    501             if (isFocusable() && isEnabled()) {
    502                 setInnerFocusable(false);
    503                 if (mRemoteInputView != null) {
    504                     mRemoteInputView.onDefocus(animate);
    505                 }
    506                 mShowImeOnInputConnection = false;
    507             }
    508         }
    509 
    510         @Override
    511         protected void onVisibilityChanged(View changedView, int visibility) {
    512             super.onVisibilityChanged(changedView, visibility);
    513 
    514             if (!isShown()) {
    515                 defocusIfNeeded(false /* animate */);
    516             }
    517         }
    518 
    519         @Override
    520         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
    521             super.onFocusChanged(focused, direction, previouslyFocusedRect);
    522             if (!focused) {
    523                 defocusIfNeeded(true /* animate */);
    524             }
    525         }
    526 
    527         @Override
    528         public void getFocusedRect(Rect r) {
    529             super.getFocusedRect(r);
    530             r.top = mScrollY;
    531             r.bottom = mScrollY + (mBottom - mTop);
    532         }
    533 
    534         @Override
    535         public boolean requestRectangleOnScreen(Rect rectangle) {
    536             return mRemoteInputView.requestScrollTo();
    537         }
    538 
    539         @Override
    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.
    543                 return true;
    544             }
    545             return super.onKeyDown(keyCode, event);
    546         }
    547 
    548         @Override
    549         public boolean onKeyUp(int keyCode, KeyEvent event) {
    550             if (keyCode == KeyEvent.KEYCODE_BACK) {
    551                 defocusIfNeeded(true /* animate */);
    552                 return true;
    553             }
    554             return super.onKeyUp(keyCode, event);
    555         }
    556 
    557         @Override
    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();
    564         }
    565 
    566         @Override
    567         public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    568             final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
    569 
    570             if (mShowImeOnInputConnection && inputConnection != null) {
    571                 final InputMethodManager imm = InputMethodManager.getInstance();
    572                 if (imm != null) {
    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() {
    577                         @Override
    578                         public void run() {
    579                             imm.viewClicked(RemoteEditText.this);
    580                             imm.showSoftInput(RemoteEditText.this, 0);
    581                         }
    582                     });
    583                 }
    584             }
    585 
    586             return inputConnection;
    587         }
    588 
    589         @Override
    590         public void onCommitCompletion(CompletionInfo text) {
    591             clearComposingText();
    592             setText(text.getText());
    593             setSelection(getText().length());
    594         }
    595 
    596         void setInnerFocusable(boolean focusable) {
    597             setFocusableInTouchMode(focusable);
    598             setFocusable(focusable);
    599             setCursorVisible(focusable);
    600 
    601             if (focusable) {
    602                 requestFocus();
    603                 setBackground(mBackground);
    604             } else {
    605                 setBackground(null);
    606             }
    607 
    608         }
    609     }
    610 }
    611