Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2017 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 androidx.slice.widget;
     18 
     19 import android.animation.Animator;
     20 import android.app.PendingIntent;
     21 import android.app.RemoteInput;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.graphics.Rect;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Build;
     27 import android.os.Bundle;
     28 import android.text.Editable;
     29 import android.text.TextWatcher;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.view.KeyEvent;
     33 import android.view.LayoutInflater;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewAnimationUtils;
     37 import android.view.ViewGroup;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.view.inputmethod.CompletionInfo;
     40 import android.view.inputmethod.EditorInfo;
     41 import android.view.inputmethod.InputConnection;
     42 import android.view.inputmethod.InputMethodManager;
     43 import android.widget.EditText;
     44 import android.widget.ImageButton;
     45 import android.widget.LinearLayout;
     46 import android.widget.ProgressBar;
     47 import android.widget.TextView;
     48 import android.widget.Toast;
     49 
     50 import androidx.annotation.RequiresApi;
     51 import androidx.annotation.RestrictTo;
     52 import androidx.core.content.ContextCompat;
     53 import androidx.slice.SliceItem;
     54 import androidx.slice.view.R;
     55 
     56 /**
     57  * Host for the remote input.
     58  *
     59  * @hide
     60  */
     61 // TODO this should be unified with SystemUI RemoteInputView (b/67527720)
     62 @RestrictTo(RestrictTo.Scope.LIBRARY)
     63 @RequiresApi(21)
     64 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
     65 
     66     private static final String TAG = "RemoteInput";
     67 
     68     /**
     69      * A marker object that let's us easily find views of this class.
     70      */
     71     public static final Object VIEW_TAG = new Object();
     72 
     73     private RemoteEditText mEditText;
     74     private ImageButton mSendButton;
     75     private ProgressBar mProgressBar;
     76     private SliceItem mAction;
     77     private RemoteInput[] mRemoteInputs;
     78     private RemoteInput mRemoteInput;
     79 
     80     private int mRevealCx;
     81     private int mRevealCy;
     82     private int mRevealR;
     83     private boolean mResetting;
     84 
     85     public RemoteInputView(Context context, AttributeSet attrs) {
     86         super(context, attrs);
     87     }
     88 
     89     @Override
     90     protected void onFinishInflate() {
     91         super.onFinishInflate();
     92 
     93         mProgressBar = findViewById(R.id.remote_input_progress);
     94         mSendButton = findViewById(R.id.remote_input_send);
     95         mSendButton.setOnClickListener(this);
     96 
     97         mEditText = (RemoteEditText) getChildAt(0);
     98         mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
     99             @Override
    100             public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    101                 final boolean isSoftImeEvent = event == null
    102                         && (actionId == EditorInfo.IME_ACTION_DONE
    103                                 || actionId == EditorInfo.IME_ACTION_NEXT
    104                                 || actionId == EditorInfo.IME_ACTION_SEND);
    105                 final boolean isKeyboardEnterKey = event != null
    106                         && isConfirmKey(event.getKeyCode())
    107                         && event.getAction() == KeyEvent.ACTION_DOWN;
    108 
    109                 if (isSoftImeEvent || isKeyboardEnterKey) {
    110                     if (mEditText.length() > 0) {
    111                         sendRemoteInput();
    112                     }
    113                     // Consume action to prevent IME from closing.
    114                     return true;
    115                 }
    116                 return false;
    117             }
    118         });
    119         mEditText.addTextChangedListener(this);
    120         mEditText.setInnerFocusable(false);
    121         mEditText.mRemoteInputView = this;
    122     }
    123 
    124     private void sendRemoteInput() {
    125         Bundle results = new Bundle();
    126         results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
    127         Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    128         RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
    129                 results);
    130 
    131         mEditText.setEnabled(false);
    132         mSendButton.setVisibility(INVISIBLE);
    133         mProgressBar.setVisibility(VISIBLE);
    134         mEditText.mShowImeOnInputConnection = false;
    135 
    136         // TODO: Figure out API for telling the system about slice interaction.
    137         // Tell ShortcutManager that this package has been "activated".  ShortcutManager
    138         // will reset the throttling for this package.
    139         // Strictly speaking, the intent receiver may be different from the intent creator,
    140         // but that's an edge case, and also because we can't always know which package will receive
    141         // an intent, so we just reset for the creator.
    142         //getContext().getSystemService(ShortcutManager.class).onApplicationActive(
    143         //        mAction.getCreatorPackage(),
    144         //        getContext().getUserId());
    145 
    146         try {
    147             mAction.fireAction(getContext(), fillInIntent);
    148             reset();
    149         } catch (PendingIntent.CanceledException e) {
    150             Log.i(TAG, "Unable to send remote input result", e);
    151             Toast.makeText(getContext(), "Failure sending pending intent for inline reply :(",
    152                     Toast.LENGTH_SHORT).show();
    153             reset();
    154         }
    155     }
    156 
    157     /**
    158      * Creates a remote input view.
    159      */
    160     public static RemoteInputView inflate(Context context, ViewGroup root) {
    161         RemoteInputView v = (RemoteInputView) LayoutInflater.from(context).inflate(
    162                 R.layout.abc_slice_remote_input, root, false);
    163         v.setTag(VIEW_TAG);
    164         return v;
    165     }
    166 
    167     @Override
    168     public void onClick(View v) {
    169         if (v == mSendButton) {
    170             sendRemoteInput();
    171         }
    172     }
    173 
    174     @Override
    175     public boolean onTouchEvent(MotionEvent event) {
    176         super.onTouchEvent(event);
    177 
    178         // We never want for a touch to escape to an outer view or one we covered.
    179         return true;
    180     }
    181 
    182     private void onDefocus() {
    183         setVisibility(INVISIBLE);
    184     }
    185 
    186     /**
    187      * Set the pending intent for remote input.
    188      */
    189     public void setAction(SliceItem action) {
    190         mAction = action;
    191     }
    192 
    193     /**
    194      * Set the remote inputs for this view.
    195      */
    196     public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
    197         mRemoteInputs = remoteInputs;
    198         mRemoteInput = remoteInput;
    199         mEditText.setHint(mRemoteInput.getLabel());
    200     }
    201 
    202     /**
    203      * Focuses the remote input view.
    204      */
    205     public void focusAnimated() {
    206         if (getVisibility() != VISIBLE) {
    207             Animator animator = ViewAnimationUtils.createCircularReveal(
    208                     this, mRevealCx, mRevealCy, 0, mRevealR);
    209             animator.setDuration(200);
    210             animator.start();
    211         }
    212         focus();
    213     }
    214 
    215     private void focus() {
    216         setVisibility(VISIBLE);
    217         mEditText.setInnerFocusable(true);
    218         mEditText.mShowImeOnInputConnection = true;
    219         mEditText.setSelection(mEditText.getText().length());
    220         mEditText.requestFocus();
    221         updateSendButton();
    222     }
    223 
    224     private void reset() {
    225         mResetting = true;
    226 
    227         mEditText.getText().clear();
    228         mEditText.setEnabled(true);
    229         mSendButton.setVisibility(VISIBLE);
    230         mProgressBar.setVisibility(INVISIBLE);
    231         updateSendButton();
    232         onDefocus();
    233 
    234         mResetting = false;
    235     }
    236 
    237     @Override
    238     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
    239         if (mResetting && child == mEditText) {
    240             // Suppress text events if it happens during resetting. Ideally this would be
    241             // suppressed by the text view not being shown, but that doesn't work here because it
    242             // needs to stay visible for the animation.
    243             return false;
    244         }
    245         return super.onRequestSendAccessibilityEvent(child, event);
    246     }
    247 
    248     private void updateSendButton() {
    249         mSendButton.setEnabled(mEditText.getText().length() != 0);
    250     }
    251 
    252     @Override
    253     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    254     }
    255 
    256     @Override
    257     public void onTextChanged(CharSequence s, int start, int before, int count) {
    258     }
    259 
    260     @Override
    261     public void afterTextChanged(Editable s) {
    262         updateSendButton();
    263     }
    264 
    265     /**
    266      * @hide
    267      */
    268     @RestrictTo(RestrictTo.Scope.LIBRARY)
    269     public void setRevealParameters(int cx, int cy, int r) {
    270         mRevealCx = cx;
    271         mRevealCy = cy;
    272         mRevealR = r;
    273     }
    274 
    275     @Override
    276     public void dispatchStartTemporaryDetach() {
    277         super.dispatchStartTemporaryDetach();
    278         // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
    279         // won't lose IME focus.
    280         detachViewFromParent(mEditText);
    281     }
    282 
    283     @Override
    284     public void dispatchFinishTemporaryDetach() {
    285         if (isAttachedToWindow()) {
    286             attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
    287         } else {
    288             removeDetachedView(mEditText, false /* animate */);
    289         }
    290         super.dispatchFinishTemporaryDetach();
    291     }
    292 
    293     /**
    294      * An EditText that changes appearance based on whether it's focusable and becomes un-focusable
    295      * whenever the user navigates away from it or it becomes invisible.
    296      */
    297     public static class RemoteEditText extends EditText {
    298 
    299         private final Drawable mBackground;
    300         private RemoteInputView mRemoteInputView;
    301         boolean mShowImeOnInputConnection;
    302 
    303         public RemoteEditText(Context context, AttributeSet attrs) {
    304             super(context, attrs);
    305             mBackground = getBackground();
    306         }
    307 
    308         private void defocusIfNeeded(boolean animate) {
    309             if (mRemoteInputView != null || isTemporarilyDetachedCompat()) {
    310                 if (isTemporarilyDetachedCompat()) {
    311                     // We might get reattached but then the other one of HUN / expanded might steal
    312                     // our focus, so we'll need to save our text here.
    313                 }
    314                 return;
    315             }
    316             if (isFocusable() && isEnabled()) {
    317                 setInnerFocusable(false);
    318                 if (mRemoteInputView != null) {
    319                     mRemoteInputView.onDefocus();
    320                 }
    321                 mShowImeOnInputConnection = false;
    322             }
    323         }
    324 
    325         private boolean isTemporarilyDetachedCompat() {
    326             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    327                 return isTemporarilyDetached();
    328             }
    329             return false;
    330         }
    331 
    332         @Override
    333         protected void onVisibilityChanged(View changedView, int visibility) {
    334             super.onVisibilityChanged(changedView, visibility);
    335 
    336             if (!isShown()) {
    337                 defocusIfNeeded(false /* animate */);
    338             }
    339         }
    340 
    341         @Override
    342         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
    343             super.onFocusChanged(focused, direction, previouslyFocusedRect);
    344             if (!focused) {
    345                 defocusIfNeeded(true /* animate */);
    346             }
    347         }
    348 
    349         @Override
    350         public void getFocusedRect(Rect r) {
    351             super.getFocusedRect(r);
    352             r.top = getScrollY();
    353             r.bottom = getScrollY() + (getBottom() - getTop());
    354         }
    355 
    356         @Override
    357         public boolean onKeyDown(int keyCode, KeyEvent event) {
    358             if (keyCode == KeyEvent.KEYCODE_BACK) {
    359                 // Eat the DOWN event here to prevent any default behavior.
    360                 return true;
    361             }
    362             return super.onKeyDown(keyCode, event);
    363         }
    364 
    365         @Override
    366         public boolean onKeyUp(int keyCode, KeyEvent event) {
    367             if (keyCode == KeyEvent.KEYCODE_BACK) {
    368                 defocusIfNeeded(true /* animate */);
    369                 return true;
    370             }
    371             return super.onKeyUp(keyCode, event);
    372         }
    373 
    374         @Override
    375         public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    376             final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
    377 
    378             if (mShowImeOnInputConnection && inputConnection != null) {
    379                 final InputMethodManager imm = ContextCompat.getSystemService(getContext(),
    380                         InputMethodManager.class);
    381                 if (imm != null) {
    382                     // onCreateInputConnection is called by InputMethodManager in the middle of
    383                     // setting up the connection to the IME; wait with requesting the IME until that
    384                     // work has completed.
    385                     post(new Runnable() {
    386                         @Override
    387                         public void run() {
    388                             imm.viewClicked(RemoteEditText.this);
    389                             imm.showSoftInput(RemoteEditText.this, 0);
    390                         }
    391                     });
    392                 }
    393             }
    394 
    395             return inputConnection;
    396         }
    397 
    398         @Override
    399         public void onCommitCompletion(CompletionInfo text) {
    400             clearComposingText();
    401             setText(text.getText());
    402             setSelection(getText().length());
    403         }
    404 
    405         void setInnerFocusable(boolean focusable) {
    406             setFocusableInTouchMode(focusable);
    407             setFocusable(focusable);
    408             setCursorVisible(focusable);
    409 
    410             if (focusable) {
    411                 requestFocus();
    412                 setBackground(mBackground);
    413             } else {
    414                 setBackground(null);
    415             }
    416 
    417         }
    418     }
    419 
    420     /** Whether key will, by default, trigger a click on the focused view.
    421      * @hide
    422      */
    423     @RestrictTo(RestrictTo.Scope.LIBRARY)
    424     public static final boolean isConfirmKey(int keyCode) {
    425         switch (keyCode) {
    426             case KeyEvent.KEYCODE_DPAD_CENTER:
    427             case KeyEvent.KEYCODE_ENTER:
    428             case KeyEvent.KEYCODE_SPACE:
    429             case KeyEvent.KEYCODE_NUMPAD_ENTER:
    430                 return true;
    431             default:
    432                 return false;
    433         }
    434     }
    435 }
    436