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.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.stack.ScrollContainer;
     60 import com.android.systemui.statusbar.stack.StackStateAnimator;
     61 
     62 /**
     63  * Host for the remote input.
     64  */
     65 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
     66 
     67     private static final String TAG = "RemoteInput";
     68 
     69     // A marker object that let's us easily find views of this class.
     70     public static final Object VIEW_TAG = new Object();
     71 
     72     public final Object mToken = new Object();
     73 
     74     private RemoteEditText mEditText;
     75     private ImageButton mSendButton;
     76     private ProgressBar mProgressBar;
     77     private PendingIntent mPendingIntent;
     78     private RemoteInput[] mRemoteInputs;
     79     private RemoteInput mRemoteInput;
     80     private RemoteInputController mController;
     81 
     82     private NotificationData.Entry mEntry;
     83 
     84     private ScrollContainer mScrollContainer;
     85     private View mScrollContainerChild;
     86     private boolean mRemoved;
     87 
     88     private int mRevealCx;
     89     private int mRevealCy;
     90     private int mRevealR;
     91 
     92     private boolean mResetting;
     93 
     94     public RemoteInputView(Context context, AttributeSet attrs) {
     95         super(context, attrs);
     96     }
     97 
     98     @Override
     99     protected void onFinishInflate() {
    100         super.onFinishInflate();
    101 
    102         mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress);
    103 
    104         mSendButton = (ImageButton) findViewById(R.id.remote_input_send);
    105         mSendButton.setOnClickListener(this);
    106 
    107         mEditText = (RemoteEditText) getChildAt(0);
    108         mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
    109             @Override
    110             public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    111                 final boolean isSoftImeEvent = event == null
    112                         && (actionId == EditorInfo.IME_ACTION_DONE
    113                         || actionId == EditorInfo.IME_ACTION_NEXT
    114                         || actionId == EditorInfo.IME_ACTION_SEND);
    115                 final boolean isKeyboardEnterKey = event != null
    116                         && KeyEvent.isConfirmKey(event.getKeyCode())
    117                         && event.getAction() == KeyEvent.ACTION_DOWN;
    118 
    119                 if (isSoftImeEvent || isKeyboardEnterKey) {
    120                     if (mEditText.length() > 0) {
    121                         sendRemoteInput();
    122                     }
    123                     // Consume action to prevent IME from closing.
    124                     return true;
    125                 }
    126                 return false;
    127             }
    128         });
    129         mEditText.addTextChangedListener(this);
    130         mEditText.setInnerFocusable(false);
    131         mEditText.mRemoteInputView = this;
    132     }
    133 
    134     private void sendRemoteInput() {
    135         Bundle results = new Bundle();
    136         results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
    137         Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    138         RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
    139                 results);
    140 
    141         mEditText.setEnabled(false);
    142         mSendButton.setVisibility(INVISIBLE);
    143         mProgressBar.setVisibility(VISIBLE);
    144         mEntry.remoteInputText = mEditText.getText();
    145         mController.addSpinning(mEntry.key, mToken);
    146         mController.removeRemoteInput(mEntry, mToken);
    147         mEditText.mShowImeOnInputConnection = false;
    148         mController.remoteInputSent(mEntry);
    149 
    150         // Tell ShortcutManager that this package has been "activated".  ShortcutManager
    151         // will reset the throttling for this package.
    152         // Strictly speaking, the intent receiver may be different from the notification publisher,
    153         // but that's an edge case, and also because we can't always know which package will receive
    154         // an intent, so we just reset for the publisher.
    155         getContext().getSystemService(ShortcutManager.class).onApplicationActive(
    156                 mEntry.notification.getPackageName(),
    157                 mEntry.notification.getUser().getIdentifier());
    158 
    159         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
    160                 mEntry.notification.getPackageName());
    161         try {
    162             mPendingIntent.send(mContext, 0, fillInIntent);
    163         } catch (PendingIntent.CanceledException e) {
    164             Log.i(TAG, "Unable to send remote input result", e);
    165             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
    166                     mEntry.notification.getPackageName());
    167         }
    168     }
    169 
    170     public static RemoteInputView inflate(Context context, ViewGroup root,
    171             NotificationData.Entry entry,
    172             RemoteInputController controller) {
    173         RemoteInputView v = (RemoteInputView)
    174                 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
    175         v.mController = controller;
    176         v.mEntry = entry;
    177         v.setTag(VIEW_TAG);
    178 
    179         return v;
    180     }
    181 
    182     @Override
    183     public void onClick(View v) {
    184         if (v == mSendButton) {
    185             sendRemoteInput();
    186         }
    187     }
    188 
    189     @Override
    190     public boolean onTouchEvent(MotionEvent event) {
    191         super.onTouchEvent(event);
    192 
    193         // We never want for a touch to escape to an outer view or one we covered.
    194         return true;
    195     }
    196 
    197     private void onDefocus(boolean animate) {
    198         mController.removeRemoteInput(mEntry, mToken);
    199         mEntry.remoteInputText = mEditText.getText();
    200 
    201         // During removal, we get reattached and lose focus. Not hiding in that
    202         // case to prevent flicker.
    203         if (!mRemoved) {
    204             if (animate && mRevealR > 0) {
    205                 Animator reveal = ViewAnimationUtils.createCircularReveal(
    206                         this, mRevealCx, mRevealCy, mRevealR, 0);
    207                 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
    208                 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
    209                 reveal.addListener(new AnimatorListenerAdapter() {
    210                     @Override
    211                     public void onAnimationEnd(Animator animation) {
    212                         setVisibility(INVISIBLE);
    213                     }
    214                 });
    215                 reveal.start();
    216             } else {
    217                 setVisibility(INVISIBLE);
    218             }
    219         }
    220         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
    221                 mEntry.notification.getPackageName());
    222     }
    223 
    224     @Override
    225     protected void onAttachedToWindow() {
    226         super.onAttachedToWindow();
    227         if (mEntry.row.isChangingPosition()) {
    228             if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
    229                 mEditText.requestFocus();
    230             }
    231         }
    232     }
    233 
    234     @Override
    235     protected void onDetachedFromWindow() {
    236         super.onDetachedFromWindow();
    237         if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) {
    238             return;
    239         }
    240         mController.removeRemoteInput(mEntry, mToken);
    241         mController.removeSpinning(mEntry.key, mToken);
    242     }
    243 
    244     public void setPendingIntent(PendingIntent pendingIntent) {
    245         mPendingIntent = pendingIntent;
    246     }
    247 
    248     public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
    249         mRemoteInputs = remoteInputs;
    250         mRemoteInput = remoteInput;
    251         mEditText.setHint(mRemoteInput.getLabel());
    252     }
    253 
    254     public void focusAnimated() {
    255         if (getVisibility() != VISIBLE) {
    256             Animator animator = ViewAnimationUtils.createCircularReveal(
    257                     this, mRevealCx, mRevealCy, 0, mRevealR);
    258             animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
    259             animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
    260             animator.start();
    261         }
    262         focus();
    263     }
    264 
    265     public void focus() {
    266         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
    267                 mEntry.notification.getPackageName());
    268 
    269         setVisibility(VISIBLE);
    270         mController.addRemoteInput(mEntry, mToken);
    271         mEditText.setInnerFocusable(true);
    272         mEditText.mShowImeOnInputConnection = true;
    273         mEditText.setText(mEntry.remoteInputText);
    274         mEditText.setSelection(mEditText.getText().length());
    275         mEditText.requestFocus();
    276         updateSendButton();
    277     }
    278 
    279     public void onNotificationUpdateOrReset() {
    280         boolean sending = mProgressBar.getVisibility() == VISIBLE;
    281 
    282         if (sending) {
    283             // Update came in after we sent the reply, time to reset.
    284             reset();
    285         }
    286     }
    287 
    288     private void reset() {
    289         mResetting = true;
    290 
    291         mEditText.getText().clear();
    292         mEditText.setEnabled(true);
    293         mSendButton.setVisibility(VISIBLE);
    294         mProgressBar.setVisibility(INVISIBLE);
    295         mController.removeSpinning(mEntry.key, mToken);
    296         updateSendButton();
    297         onDefocus(false /* animate */);
    298 
    299         mResetting = false;
    300     }
    301 
    302     @Override
    303     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
    304         if (mResetting && child == mEditText) {
    305             // Suppress text events if it happens during resetting. Ideally this would be
    306             // suppressed by the text view not being shown, but that doesn't work here because it
    307             // needs to stay visible for the animation.
    308             return false;
    309         }
    310         return super.onRequestSendAccessibilityEvent(child, event);
    311     }
    312 
    313     private void updateSendButton() {
    314         mSendButton.setEnabled(mEditText.getText().length() != 0);
    315     }
    316 
    317     @Override
    318     public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    319 
    320     @Override
    321     public void onTextChanged(CharSequence s, int start, int before, int count) {}
    322 
    323     @Override
    324     public void afterTextChanged(Editable s) {
    325         updateSendButton();
    326     }
    327 
    328     public void close() {
    329         mEditText.defocusIfNeeded(false /* animated */);
    330     }
    331 
    332     @Override
    333     public boolean onInterceptTouchEvent(MotionEvent ev) {
    334         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    335             findScrollContainer();
    336             if (mScrollContainer != null) {
    337                 mScrollContainer.requestDisallowLongPress();
    338                 mScrollContainer.requestDisallowDismiss();
    339             }
    340         }
    341         return super.onInterceptTouchEvent(ev);
    342     }
    343 
    344     public boolean requestScrollTo() {
    345         findScrollContainer();
    346         mScrollContainer.lockScrollTo(mScrollContainerChild);
    347         return true;
    348     }
    349 
    350     private void findScrollContainer() {
    351         if (mScrollContainer == null) {
    352             mScrollContainerChild = null;
    353             ViewParent p = this;
    354             while (p != null) {
    355                 if (mScrollContainerChild == null && p instanceof ExpandableView) {
    356                     mScrollContainerChild = (View) p;
    357                 }
    358                 if (p.getParent() instanceof ScrollContainer) {
    359                     mScrollContainer = (ScrollContainer) p.getParent();
    360                     if (mScrollContainerChild == null) {
    361                         mScrollContainerChild = (View) p;
    362                     }
    363                     break;
    364                 }
    365                 p = p.getParent();
    366             }
    367         }
    368     }
    369 
    370     public boolean isActive() {
    371         return mEditText.isFocused() && mEditText.isEnabled();
    372     }
    373 
    374     public void stealFocusFrom(RemoteInputView other) {
    375         other.close();
    376         setPendingIntent(other.mPendingIntent);
    377         setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
    378         setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
    379         focus();
    380     }
    381 
    382     /**
    383      * Tries to find an action in {@param actions} that matches the current pending intent
    384      * of this view and updates its state to that of the found action
    385      *
    386      * @return true if a matching action was found, false otherwise
    387      */
    388     public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
    389         if (mPendingIntent == null || actions == null) {
    390             return false;
    391         }
    392         Intent current = mPendingIntent.getIntent();
    393         if (current == null) {
    394             return false;
    395         }
    396 
    397         for (Notification.Action a : actions) {
    398             RemoteInput[] inputs = a.getRemoteInputs();
    399             if (a.actionIntent == null || inputs == null) {
    400                 continue;
    401             }
    402             Intent candidate = a.actionIntent.getIntent();
    403             if (!current.filterEquals(candidate)) {
    404                 continue;
    405             }
    406 
    407             RemoteInput input = null;
    408             for (RemoteInput i : inputs) {
    409                 if (i.getAllowFreeFormInput()) {
    410                     input = i;
    411                 }
    412             }
    413             if (input == null) {
    414                 continue;
    415             }
    416             setPendingIntent(a.actionIntent);
    417             setRemoteInput(inputs, input);
    418             return true;
    419         }
    420         return false;
    421     }
    422 
    423     public PendingIntent getPendingIntent() {
    424         return mPendingIntent;
    425     }
    426 
    427     public void setRemoved() {
    428         mRemoved = true;
    429     }
    430 
    431     public void setRevealParameters(int cx, int cy, int r) {
    432         mRevealCx = cx;
    433         mRevealCy = cy;
    434         mRevealR = r;
    435     }
    436 
    437     @Override
    438     public void dispatchStartTemporaryDetach() {
    439         super.dispatchStartTemporaryDetach();
    440         // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
    441         // won't lose IME focus.
    442         detachViewFromParent(mEditText);
    443     }
    444 
    445     @Override
    446     public void dispatchFinishTemporaryDetach() {
    447         if (isAttachedToWindow()) {
    448             attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
    449         } else {
    450             removeDetachedView(mEditText, false /* animate */);
    451         }
    452         super.dispatchFinishTemporaryDetach();
    453     }
    454 
    455     /**
    456      * An EditText that changes appearance based on whether it's focusable and becomes
    457      * un-focusable whenever the user navigates away from it or it becomes invisible.
    458      */
    459     public static class RemoteEditText extends EditText {
    460 
    461         private final Drawable mBackground;
    462         private RemoteInputView mRemoteInputView;
    463         boolean mShowImeOnInputConnection;
    464 
    465         public RemoteEditText(Context context, AttributeSet attrs) {
    466             super(context, attrs);
    467             mBackground = getBackground();
    468         }
    469 
    470         private void defocusIfNeeded(boolean animate) {
    471             if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()
    472                     || isTemporarilyDetached()) {
    473                 if (isTemporarilyDetached()) {
    474                     // We might get reattached but then the other one of HUN / expanded might steal
    475                     // our focus, so we'll need to save our text here.
    476                     if (mRemoteInputView != null) {
    477                         mRemoteInputView.mEntry.remoteInputText = getText();
    478                     }
    479                 }
    480                 return;
    481             }
    482             if (isFocusable() && isEnabled()) {
    483                 setInnerFocusable(false);
    484                 if (mRemoteInputView != null) {
    485                     mRemoteInputView.onDefocus(animate);
    486                 }
    487                 mShowImeOnInputConnection = false;
    488             }
    489         }
    490 
    491         @Override
    492         protected void onVisibilityChanged(View changedView, int visibility) {
    493             super.onVisibilityChanged(changedView, visibility);
    494 
    495             if (!isShown()) {
    496                 defocusIfNeeded(false /* animate */);
    497             }
    498         }
    499 
    500         @Override
    501         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
    502             super.onFocusChanged(focused, direction, previouslyFocusedRect);
    503             if (!focused) {
    504                 defocusIfNeeded(true /* animate */);
    505             }
    506         }
    507 
    508         @Override
    509         public void getFocusedRect(Rect r) {
    510             super.getFocusedRect(r);
    511             r.top = mScrollY;
    512             r.bottom = mScrollY + (mBottom - mTop);
    513         }
    514 
    515         @Override
    516         public boolean requestRectangleOnScreen(Rect rectangle) {
    517             return mRemoteInputView.requestScrollTo();
    518         }
    519 
    520         @Override
    521         public boolean onKeyDown(int keyCode, KeyEvent event) {
    522             if (keyCode == KeyEvent.KEYCODE_BACK) {
    523                 // Eat the DOWN event here to prevent any default behavior.
    524                 return true;
    525             }
    526             return super.onKeyDown(keyCode, event);
    527         }
    528 
    529         @Override
    530         public boolean onKeyUp(int keyCode, KeyEvent event) {
    531             if (keyCode == KeyEvent.KEYCODE_BACK) {
    532                 defocusIfNeeded(true /* animate */);
    533                 return true;
    534             }
    535             return super.onKeyUp(keyCode, event);
    536         }
    537 
    538         @Override
    539         public boolean onCheckIsTextEditor() {
    540             // Stop being editable while we're being removed. During removal, we get reattached,
    541             // and editable views get their spellchecking state re-evaluated which is too costly
    542             // during the removal animation.
    543             boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
    544             return !flyingOut && super.onCheckIsTextEditor();
    545         }
    546 
    547         @Override
    548         public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    549             final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
    550 
    551             if (mShowImeOnInputConnection && inputConnection != null) {
    552                 final InputMethodManager imm = InputMethodManager.getInstance();
    553                 if (imm != null) {
    554                     // onCreateInputConnection is called by InputMethodManager in the middle of
    555                     // setting up the connection to the IME; wait with requesting the IME until that
    556                     // work has completed.
    557                     post(new Runnable() {
    558                         @Override
    559                         public void run() {
    560                             imm.viewClicked(RemoteEditText.this);
    561                             imm.showSoftInput(RemoteEditText.this, 0);
    562                         }
    563                     });
    564                 }
    565             }
    566 
    567             return inputConnection;
    568         }
    569 
    570         @Override
    571         public void onCommitCompletion(CompletionInfo text) {
    572             clearComposingText();
    573             setText(text.getText());
    574             setSelection(getText().length());
    575         }
    576 
    577         void setInnerFocusable(boolean focusable) {
    578             setFocusableInTouchMode(focusable);
    579             setFocusable(focusable);
    580             setCursorVisible(focusable);
    581 
    582             if (focusable) {
    583                 requestFocus();
    584                 setBackground(mBackground);
    585             } else {
    586                 setBackground(null);
    587             }
    588 
    589         }
    590     }
    591 }
    592