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