Home | History | Annotate | Download | only in inputmethodservice
      1 /*
      2  * Copyright (C) 2007-2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package android.inputmethodservice;
     18 
     19 import static java.lang.annotation.RetentionPolicy.SOURCE;
     20 
     21 import android.annotation.IntDef;
     22 import android.app.Dialog;
     23 import android.content.Context;
     24 import android.graphics.Rect;
     25 import android.os.Debug;
     26 import android.os.IBinder;
     27 import android.util.Log;
     28 import android.view.Gravity;
     29 import android.view.KeyEvent;
     30 import android.view.MotionEvent;
     31 import android.view.WindowManager;
     32 
     33 import java.lang.annotation.Retention;
     34 
     35 /**
     36  * A SoftInputWindow is a Dialog that is intended to be used for a top-level input
     37  * method window.  It will be displayed along the edge of the screen, moving
     38  * the application user interface away from it so that the focused item is
     39  * always visible.
     40  * @hide
     41  */
     42 public class SoftInputWindow extends Dialog {
     43     private static final boolean DEBUG = false;
     44     private static final String TAG = "SoftInputWindow";
     45 
     46     final String mName;
     47     final Callback mCallback;
     48     final KeyEvent.Callback mKeyEventCallback;
     49     final KeyEvent.DispatcherState mDispatcherState;
     50     final int mWindowType;
     51     final int mGravity;
     52     final boolean mTakesFocus;
     53     private final Rect mBounds = new Rect();
     54 
     55     @Retention(SOURCE)
     56     @IntDef(value = {SoftInputWindowState.TOKEN_PENDING, SoftInputWindowState.TOKEN_SET,
     57             SoftInputWindowState.SHOWN_AT_LEAST_ONCE, SoftInputWindowState.REJECTED_AT_LEAST_ONCE})
     58     private @interface SoftInputWindowState {
     59         /**
     60          * The window token is not set yet.
     61          */
     62         int TOKEN_PENDING = 0;
     63         /**
     64          * The window token was set, but the window is not shown yet.
     65          */
     66         int TOKEN_SET = 1;
     67         /**
     68          * The window was shown at least once.
     69          */
     70         int SHOWN_AT_LEAST_ONCE = 2;
     71         /**
     72          * {@link android.view.WindowManager.BadTokenException} was sent when calling
     73          * {@link Dialog#show()} at least once.
     74          */
     75         int REJECTED_AT_LEAST_ONCE = 3;
     76         /**
     77          * The window is considered destroyed.  Any incoming request should be ignored.
     78          */
     79         int DESTROYED = 4;
     80     }
     81 
     82     @SoftInputWindowState
     83     private int mWindowState = SoftInputWindowState.TOKEN_PENDING;
     84 
     85     public interface Callback {
     86         public void onBackPressed();
     87     }
     88 
     89     public void setToken(IBinder token) {
     90         switch (mWindowState) {
     91             case SoftInputWindowState.TOKEN_PENDING:
     92                 // Normal scenario.  Nothing to worry about.
     93                 WindowManager.LayoutParams lp = getWindow().getAttributes();
     94                 lp.token = token;
     95                 getWindow().setAttributes(lp);
     96                 updateWindowState(SoftInputWindowState.TOKEN_SET);
     97                 return;
     98             case SoftInputWindowState.TOKEN_SET:
     99             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
    100             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
    101                 throw new IllegalStateException("setToken can be called only once");
    102             case SoftInputWindowState.DESTROYED:
    103                 // Just ignore.  Since there are multiple event queues from the token is issued
    104                 // in the system server to the timing when it arrives here, it can be delivered
    105                 // after the is already destroyed.  No one should be blamed because of such an
    106                 // unfortunate but possible scenario.
    107                 Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
    108                 return;
    109             default:
    110                 throw new IllegalStateException("Unexpected state=" + mWindowState);
    111         }
    112     }
    113 
    114     /**
    115      * Create a SoftInputWindow that uses a custom style.
    116      *
    117      * @param context The Context in which the DockWindow should run. In
    118      *        particular, it uses the window manager and theme from this context
    119      *        to present its UI.
    120      * @param theme A style resource describing the theme to use for the window.
    121      *        See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style
    122      *        and Theme Resources</a> for more information about defining and
    123      *        using styles. This theme is applied on top of the current theme in
    124      *        <var>context</var>. If 0, the default dialog theme will be used.
    125      */
    126     public SoftInputWindow(Context context, String name, int theme, Callback callback,
    127             KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState,
    128             int windowType, int gravity, boolean takesFocus) {
    129         super(context, theme);
    130         mName = name;
    131         mCallback = callback;
    132         mKeyEventCallback = keyEventCallback;
    133         mDispatcherState = dispatcherState;
    134         mWindowType = windowType;
    135         mGravity = gravity;
    136         mTakesFocus = takesFocus;
    137         initDockWindow();
    138     }
    139 
    140     @Override
    141     public void onWindowFocusChanged(boolean hasFocus) {
    142         super.onWindowFocusChanged(hasFocus);
    143         mDispatcherState.reset();
    144     }
    145 
    146     @Override
    147     public boolean dispatchTouchEvent(MotionEvent ev) {
    148         getWindow().getDecorView().getHitRect(mBounds);
    149 
    150         if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top,
    151                 mBounds.right - 1, mBounds.bottom - 1)) {
    152             return super.dispatchTouchEvent(ev);
    153         } else {
    154             MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top,
    155                     mBounds.right - 1, mBounds.bottom - 1);
    156             boolean handled = super.dispatchTouchEvent(temp);
    157             temp.recycle();
    158             return handled;
    159         }
    160     }
    161 
    162     /**
    163      * Set which boundary of the screen the DockWindow sticks to.
    164      *
    165      * @param gravity The boundary of the screen to stick. See {@link
    166      *        android.view.Gravity.LEFT}, {@link android.view.Gravity.TOP},
    167      *        {@link android.view.Gravity.BOTTOM}, {@link
    168      *        android.view.Gravity.RIGHT}.
    169      */
    170     public void setGravity(int gravity) {
    171         WindowManager.LayoutParams lp = getWindow().getAttributes();
    172         lp.gravity = gravity;
    173         updateWidthHeight(lp);
    174         getWindow().setAttributes(lp);
    175     }
    176 
    177     public int getGravity() {
    178         return getWindow().getAttributes().gravity;
    179     }
    180 
    181     private void updateWidthHeight(WindowManager.LayoutParams lp) {
    182         if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) {
    183             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
    184             lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
    185         } else {
    186             lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
    187             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
    188         }
    189     }
    190 
    191     public boolean onKeyDown(int keyCode, KeyEvent event) {
    192         if (mKeyEventCallback != null && mKeyEventCallback.onKeyDown(keyCode, event)) {
    193             return true;
    194         }
    195         return super.onKeyDown(keyCode, event);
    196     }
    197 
    198     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    199         if (mKeyEventCallback != null && mKeyEventCallback.onKeyLongPress(keyCode, event)) {
    200             return true;
    201         }
    202         return super.onKeyLongPress(keyCode, event);
    203     }
    204 
    205     public boolean onKeyUp(int keyCode, KeyEvent event) {
    206         if (mKeyEventCallback != null && mKeyEventCallback.onKeyUp(keyCode, event)) {
    207             return true;
    208         }
    209         return super.onKeyUp(keyCode, event);
    210     }
    211 
    212     public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
    213         if (mKeyEventCallback != null && mKeyEventCallback.onKeyMultiple(keyCode, count, event)) {
    214             return true;
    215         }
    216         return super.onKeyMultiple(keyCode, count, event);
    217     }
    218 
    219     public void onBackPressed() {
    220         if (mCallback != null) {
    221             mCallback.onBackPressed();
    222         } else {
    223             super.onBackPressed();
    224         }
    225     }
    226 
    227     private void initDockWindow() {
    228         WindowManager.LayoutParams lp = getWindow().getAttributes();
    229 
    230         lp.type = mWindowType;
    231         lp.setTitle(mName);
    232 
    233         lp.gravity = mGravity;
    234         updateWidthHeight(lp);
    235 
    236         getWindow().setAttributes(lp);
    237 
    238         int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
    239         int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
    240                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
    241                 WindowManager.LayoutParams.FLAG_DIM_BEHIND;
    242 
    243         if (!mTakesFocus) {
    244             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    245         } else {
    246             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
    247             windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
    248         }
    249 
    250         getWindow().setFlags(windowSetFlags, windowModFlags);
    251     }
    252 
    253     @Override
    254     public final void show() {
    255         switch (mWindowState) {
    256             case SoftInputWindowState.TOKEN_PENDING:
    257                 throw new IllegalStateException("Window token is not set yet.");
    258             case SoftInputWindowState.TOKEN_SET:
    259             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
    260                 // Normal scenario.  Nothing to worry about.
    261                 try {
    262                     super.show();
    263                     updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
    264                 } catch (WindowManager.BadTokenException e) {
    265                     // Just ignore this exception.  Since show() can be requested from other
    266                     // components such as the system and there could be multiple event queues before
    267                     // the request finally arrives here, the system may have already invalidated the
    268                     // window token attached to our window.  In such a scenario, receiving
    269                     // BadTokenException here is an expected behavior.  We just ignore it and update
    270                     // the state so that we do not touch this window later.
    271                     Log.i(TAG, "Probably the IME window token is already invalidated."
    272                             + " show() does nothing.");
    273                     updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
    274                 }
    275                 return;
    276             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
    277                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
    278                 Log.i(TAG, "Not trying to call show() because it was already rejected once.");
    279                 return;
    280             case SoftInputWindowState.DESTROYED:
    281                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
    282                 Log.i(TAG, "Ignoring show() because the window is already destroyed.");
    283                 return;
    284             default:
    285                 throw new IllegalStateException("Unexpected state=" + mWindowState);
    286         }
    287     }
    288 
    289     final void dismissForDestroyIfNecessary() {
    290         switch (mWindowState) {
    291             case SoftInputWindowState.TOKEN_PENDING:
    292             case SoftInputWindowState.TOKEN_SET:
    293                 // nothing to do because the window has never been shown.
    294                 updateWindowState(SoftInputWindowState.DESTROYED);
    295                 return;
    296             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
    297                 // Disable exit animation for the current IME window
    298                 // to avoid the race condition between the exit and enter animations
    299                 // when the current IME is being switched to another one.
    300                 try {
    301                     getWindow().setWindowAnimations(0);
    302                     dismiss();
    303                 } catch (WindowManager.BadTokenException e) {
    304                     // Just ignore this exception.  Since show() can be requested from other
    305                     // components such as the system and there could be multiple event queues before
    306                     // the request finally arrives here, the system may have already invalidated the
    307                     // window token attached to our window.  In such a scenario, receiving
    308                     // BadTokenException here is an expected behavior.  We just ignore it and update
    309                     // the state so that we do not touch this window later.
    310                     Log.i(TAG, "Probably the IME window token is already invalidated. "
    311                             + "No need to dismiss it.");
    312                 }
    313                 // Either way, consider that the window is destroyed.
    314                 updateWindowState(SoftInputWindowState.DESTROYED);
    315                 return;
    316             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
    317                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
    318                 Log.i(TAG,
    319                         "Not trying to dismiss the window because it is most likely unnecessary.");
    320                 // Anyway, consider that the window is destroyed.
    321                 updateWindowState(SoftInputWindowState.DESTROYED);
    322                 return;
    323             case SoftInputWindowState.DESTROYED:
    324                 throw new IllegalStateException(
    325                         "dismissForDestroyIfNecessary can be called only once");
    326             default:
    327                 throw new IllegalStateException("Unexpected state=" + mWindowState);
    328         }
    329     }
    330 
    331     private void updateWindowState(@SoftInputWindowState int newState) {
    332         if (DEBUG) {
    333             if (mWindowState != newState) {
    334                 Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
    335                         + stateToString(newState) + " @ " + Debug.getCaller());
    336             }
    337         }
    338         mWindowState = newState;
    339     }
    340 
    341     private static String stateToString(@SoftInputWindowState int state) {
    342         switch (state) {
    343             case SoftInputWindowState.TOKEN_PENDING:
    344                 return "TOKEN_PENDING";
    345             case SoftInputWindowState.TOKEN_SET:
    346                 return "TOKEN_SET";
    347             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
    348                 return "SHOWN_AT_LEAST_ONCE";
    349             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
    350                 return "REJECTED_AT_LEAST_ONCE";
    351             case SoftInputWindowState.DESTROYED:
    352                 return "DESTROYED";
    353             default:
    354                 throw new IllegalStateException("Unknown state=" + state);
    355         }
    356     }
    357 }
    358