Merge "Let SoftInputWindow handle BadTokenException"
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index acaaf07..d165889 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -62,7 +62,6 @@
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.WindowManager.BadTokenException;
 import android.view.animation.AnimationUtils;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
@@ -354,7 +353,6 @@
     SoftInputWindow mWindow;
     boolean mInitialized;
     boolean mWindowCreated;
-    boolean mWindowAdded;
     boolean mWindowVisible;
     boolean mWindowWasVisible;
     boolean mInShowWindow;
@@ -572,16 +570,7 @@
             if (DEBUG) Log.v(TAG, "showSoftInput()");
             boolean wasVis = isInputViewShown();
             if (dispatchOnShowInputRequested(flags, false)) {
-                try {
-                    showWindow(true);
-                } catch (BadTokenException e) {
-                    // We have ignored BadTokenException here since Jelly Bean MR-2 (API Level 18).
-                    // We could ignore BadTokenException in InputMethodService#showWindow() instead,
-                    // but it may break assumptions for those who override #showWindow() that we can
-                    // detect errors in #showWindow() by checking BadTokenException.
-                    // TODO: Investigate its feasibility.  Update JavaDoc of #showWindow() of
-                    // whether it's OK to override #showWindow() or not.
-                }
+                showWindow(true);
             }
             clearInsetOfPreviousIme();
             // If user uses hard keyboard, IME button should always be shown.
@@ -999,13 +988,7 @@
         mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
                 mInsetsComputer);
         doFinishInput();
-        if (mWindowAdded) {
-            // Disable exit animation for the current IME window
-            // to avoid the race condition between the exit and enter animations
-            // when the current IME is being switched to another one.
-            mWindow.getWindow().setWindowAnimations(0);
-            mWindow.dismiss();
-        }
+        mWindow.dismissForDestroyIfNecessary();
         if (mSettingsObserver != null) {
             mSettingsObserver.unregister();
             mSettingsObserver = null;
@@ -1792,7 +1775,6 @@
     public void showWindow(boolean showInput) {
         if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput
                 + " mShowInputRequested=" + mShowInputRequested
-                + " mWindowAdded=" + mWindowAdded
                 + " mWindowCreated=" + mWindowCreated
                 + " mWindowVisible=" + mWindowVisible
                 + " mInputStarted=" + mInputStarted
@@ -1802,27 +1784,12 @@
             Log.w(TAG, "Re-entrance in to showWindow");
             return;
         }
-        
-        try {
-            mWindowWasVisible = mWindowVisible;
-            mInShowWindow = true;
-            showWindowInner(showInput);
-        } catch (BadTokenException e) {
-            // BadTokenException is a normal consequence in certain situations, e.g., swapping IMEs
-            // while there is a DO_SHOW_SOFT_INPUT message in the IIMethodWrapper queue.
-            if (DEBUG) Log.v(TAG, "BadTokenException: IME is done.");
-            mWindowVisible = false;
-            mWindowAdded = false;
-            // Rethrow the exception to preserve the existing behavior.  Some IMEs may have directly
-            // called this method and relied on this exception for some clean-up tasks.
-            // TODO: Give developers a clear guideline of whether it's OK to call this method or
-            // InputMethodService#requestShowSelf(int) should always be used instead.
-            throw e;
-        } finally {
-            // TODO: Is it OK to set true when we get BadTokenException?
-            mWindowWasVisible = true;
-            mInShowWindow = false;
-        }
+
+        mWindowWasVisible = mWindowVisible;
+        mInShowWindow = true;
+        showWindowInner(showInput);
+        mWindowWasVisible = true;
+        mInShowWindow = false;
     }
 
     void showWindowInner(boolean showInput) {
@@ -1839,9 +1806,8 @@
         initialize();
         updateFullscreenMode();
         updateInputViewShown();
-        
-        if (!mWindowAdded || !mWindowCreated) {
-            mWindowAdded = true;
+
+        if (!mWindowCreated) {
             mWindowCreated = true;
             initialize();
             if (DEBUG) Log.v(TAG, "CALL: onCreateCandidatesView");
@@ -2867,8 +2833,7 @@
     @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         final Printer p = new PrintWriterPrinter(fout);
         p.println("Input method service state for " + this + ":");
-        p.println("  mWindowCreated=" + mWindowCreated
-                + " mWindowAdded=" + mWindowAdded);
+        p.println("  mWindowCreated=" + mWindowCreated);
         p.println("  mWindowVisible=" + mWindowVisible
                 + " mWindowWasVisible=" + mWindowWasVisible
                 + " mInShowWindow=" + mInShowWindow);
diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java
index 795117e..b4b8887 100644
--- a/core/java/android/inputmethodservice/SoftInputWindow.java
+++ b/core/java/android/inputmethodservice/SoftInputWindow.java
@@ -16,15 +16,22 @@
 
 package android.inputmethodservice;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
 import android.app.Dialog;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Debug;
 import android.os.IBinder;
+import android.util.Log;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.WindowManager;
 
+import java.lang.annotation.Retention;
+
 /**
  * A SoftInputWindow is a Dialog that is intended to be used for a top-level input
  * method window.  It will be displayed along the edge of the screen, moving
@@ -33,6 +40,9 @@
  * @hide
  */
 public class SoftInputWindow extends Dialog {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "SoftInputWindow";
+
     final String mName;
     final Callback mCallback;
     final KeyEvent.Callback mKeyEventCallback;
@@ -42,16 +52,65 @@
     final boolean mTakesFocus;
     private final Rect mBounds = new Rect();
 
+    @Retention(SOURCE)
+    @IntDef(value = {SoftInputWindowState.TOKEN_PENDING, SoftInputWindowState.TOKEN_SET,
+            SoftInputWindowState.SHOWN_AT_LEAST_ONCE, SoftInputWindowState.REJECTED_AT_LEAST_ONCE})
+    private @interface SoftInputWindowState {
+        /**
+         * The window token is not set yet.
+         */
+        int TOKEN_PENDING = 0;
+        /**
+         * The window token was set, but the window is not shown yet.
+         */
+        int TOKEN_SET = 1;
+        /**
+         * The window was shown at least once.
+         */
+        int SHOWN_AT_LEAST_ONCE = 2;
+        /**
+         * {@link android.view.WindowManager.BadTokenException} was sent when calling
+         * {@link Dialog#show()} at least once.
+         */
+        int REJECTED_AT_LEAST_ONCE = 3;
+        /**
+         * The window is considered destroyed.  Any incoming request should be ignored.
+         */
+        int DESTROYED = 4;
+    }
+
+    @SoftInputWindowState
+    private int mWindowState = SoftInputWindowState.TOKEN_PENDING;
+
     public interface Callback {
         public void onBackPressed();
     }
 
     public void setToken(IBinder token) {
-        WindowManager.LayoutParams lp = getWindow().getAttributes();
-        lp.token = token;
-        getWindow().setAttributes(lp);
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                // Normal scenario.  Nothing to worry about.
+                WindowManager.LayoutParams lp = getWindow().getAttributes();
+                lp.token = token;
+                getWindow().setAttributes(lp);
+                updateWindowState(SoftInputWindowState.TOKEN_SET);
+                return;
+            case SoftInputWindowState.TOKEN_SET:
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                throw new IllegalStateException("setToken can be called only once");
+            case SoftInputWindowState.DESTROYED:
+                // Just ignore.  Since there are multiple event queues from the token is issued
+                // in the system server to the timing when it arrives here, it can be delivered
+                // after the is already destroyed.  No one should be blamed because of such an
+                // unfortunate but possible scenario.
+                Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
+                return;
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
     }
-    
+
     /**
      * Create a SoftInputWindow that uses a custom style.
      * 
@@ -190,4 +249,109 @@
 
         getWindow().setFlags(windowSetFlags, windowModFlags);
     }
+
+    @Override
+    public final void show() {
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                throw new IllegalStateException("Window token is not set yet.");
+            case SoftInputWindowState.TOKEN_SET:
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                // Normal scenario.  Nothing to worry about.
+                try {
+                    super.show();
+                    updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
+                } catch (WindowManager.BadTokenException e) {
+                    // Just ignore this exception.  Since show() can be requested from other
+                    // components such as the system and there could be multiple event queues before
+                    // the request finally arrives here, the system may have already invalidated the
+                    // window token attached to our window.  In such a scenario, receiving
+                    // BadTokenException here is an expected behavior.  We just ignore it and update
+                    // the state so that we do not touch this window later.
+                    Log.i(TAG, "Probably the IME window token is already invalidated."
+                            + " show() does nothing.");
+                    updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
+                }
+                return;
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG, "Not trying to call show() because it was already rejected once.");
+                return;
+            case SoftInputWindowState.DESTROYED:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG, "Ignoring show() because the window is already destroyed.");
+                return;
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
+    }
+
+    final void dismissForDestroyIfNecessary() {
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+            case SoftInputWindowState.TOKEN_SET:
+                // nothing to do because the window has never been shown.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                // Disable exit animation for the current IME window
+                // to avoid the race condition between the exit and enter animations
+                // when the current IME is being switched to another one.
+                try {
+                    getWindow().setWindowAnimations(0);
+                    dismiss();
+                } catch (WindowManager.BadTokenException e) {
+                    // Just ignore this exception.  Since show() can be requested from other
+                    // components such as the system and there could be multiple event queues before
+                    // the request finally arrives here, the system may have already invalidated the
+                    // window token attached to our window.  In such a scenario, receiving
+                    // BadTokenException here is an expected behavior.  We just ignore it and update
+                    // the state so that we do not touch this window later.
+                    Log.i(TAG, "Probably the IME window token is already invalidated. "
+                            + "No need to dismiss it.");
+                }
+                // Either way, consider that the window is destroyed.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG,
+                        "Not trying to dismiss the window because it is most likely unnecessary.");
+                // Anyway, consider that the window is destroyed.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.DESTROYED:
+                throw new IllegalStateException(
+                        "dismissForDestroyIfNecessary can be called only once");
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
+    }
+
+    private void updateWindowState(@SoftInputWindowState int newState) {
+        if (DEBUG) {
+            if (mWindowState != newState) {
+                Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
+                        + stateToString(newState) + " @ " + Debug.getCaller());
+            }
+        }
+        mWindowState = newState;
+    }
+
+    private static String stateToString(@SoftInputWindowState int state) {
+        switch (state) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                return "TOKEN_PENDING";
+            case SoftInputWindowState.TOKEN_SET:
+                return "TOKEN_SET";
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                return "SHOWN_AT_LEAST_ONCE";
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                return "REJECTED_AT_LEAST_ONCE";
+            case SoftInputWindowState.DESTROYED:
+                return "DESTROYED";
+            default:
+                throw new IllegalStateException("Unknown state=" + state);
+        }
+    }
 }