Fix Snackbar#show() not working after rotation

Caused by us not handling the window detach
event, and thus not updating our state to reflect
the fact.

BUG: 24256478
Change-Id: Ie0065dedd27fefd202cc1f6bcf8016809bf32eae
diff --git a/design/api/current.txt b/design/api/current.txt
index 5523b5e..f3731dd 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -254,6 +254,7 @@
     method public int getDuration();
     method public android.view.View getView();
     method public boolean isShown();
+    method public boolean isShownOrQueued();
     method public static android.support.design.widget.Snackbar make(android.view.View, java.lang.CharSequence, int);
     method public static android.support.design.widget.Snackbar make(android.view.View, int, int);
     method public android.support.design.widget.Snackbar setAction(int, android.view.View.OnClickListener);
diff --git a/design/src/android/support/design/widget/Snackbar.java b/design/src/android/support/design/widget/Snackbar.java
index 0fccafe..af300f1 100644
--- a/design/src/android/support/design/widget/Snackbar.java
+++ b/design/src/android/support/design/widget/Snackbar.java
@@ -169,20 +169,21 @@
         });
     }
 
-    private final ViewGroup mParent;
+    private final ViewGroup mTargetParent;
     private final Context mContext;
     private final SnackbarLayout mView;
     private int mDuration;
     private Callback mCallback;
 
     private Snackbar(ViewGroup parent) {
-        mParent = parent;
+        mTargetParent = parent;
         mContext = parent.getContext();
 
         ThemeUtils.checkAppCompatTheme(mContext);
 
         LayoutInflater inflater = LayoutInflater.from(mContext);
-        mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
+        mView = (SnackbarLayout) inflater.inflate(
+                R.layout.design_layout_snackbar, mTargetParent, false);
     }
 
     /**
@@ -403,10 +404,18 @@
     }
 
     /**
-     * Return whether this Snackbar is currently being shown.
+     * Return whether this {@link Snackbar} is currently being shown.
      */
     public boolean isShown() {
-        return mView.isShown();
+        return SnackbarManager.getInstance().isCurrent(mManagerCallback);
+    }
+
+    /**
+     * Returns whether this {@link Snackbar} is currently being shown, or is queued to be
+     * shown next.
+     */
+    public boolean isShownOrQueued() {
+        return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
     }
 
     private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@@ -456,9 +465,30 @@
                 ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
             }
 
-            mParent.addView(mView);
+            mTargetParent.addView(mView);
         }
 
+        mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {}
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+                if (isShownOrQueued()) {
+                    // If we haven't already been dismissed then this event is coming from a
+                    // non-user initiated action. Hence we need to make sure that we callback
+                    // and keep our state up to date. We need to post the call since removeView()
+                    // will call through to onDetachedFromWindow and thus overflow.
+                    sHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            onViewHidden(Callback.DISMISS_EVENT_MANUAL);
+                        }
+                    });
+                }
+            }
+        });
+
         if (ViewCompat.isLaidOut(mView)) {
             // If the view is already laid out, animate it now
             animateViewIn();
@@ -563,8 +593,11 @@
     }
 
     private void onViewHidden(int event) {
-        // First remove the view from the parent
-        mParent.removeView(mView);
+        // First remove the view from the parent (if attached)
+        final ViewParent parent = mView.getParent();
+        if (parent instanceof ViewGroup) {
+            ((ViewGroup) parent).removeView(mView);
+        }
         // Now call the dismiss listener (if available)
         if (mCallback != null) {
             mCallback.onDismissed(this, event);
@@ -602,10 +635,16 @@
         private int mMaxInlineActionWidth;
 
         interface OnLayoutChangeListener {
-            public void onLayoutChange(View view, int left, int top, int right, int bottom);
+            void onLayoutChange(View view, int left, int top, int right, int bottom);
+        }
+
+        interface OnAttachStateChangeListener {
+            void onViewAttachedToWindow(View v);
+            void onViewDetachedFromWindow(View v);
         }
 
         private OnLayoutChangeListener mOnLayoutChangeListener;
+        private OnAttachStateChangeListener mOnAttachStateChangeListener;
 
         public SnackbarLayout(Context context) {
             this(context, null);
@@ -712,10 +751,30 @@
             }
         }
 
+        @Override
+        protected void onAttachedToWindow() {
+            super.onAttachedToWindow();
+            if (mOnAttachStateChangeListener != null) {
+                mOnAttachStateChangeListener.onViewAttachedToWindow(this);
+            }
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            super.onDetachedFromWindow();
+            if (mOnAttachStateChangeListener != null) {
+                mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
+            }
+        }
+
         void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
             mOnLayoutChangeListener = onLayoutChangeListener;
         }
 
+        void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
+            mOnAttachStateChangeListener = listener;
+        }
+
         private boolean updateViewsWithinLayout(final int orientation,
                 final int messagePadTop, final int messagePadBottom) {
             boolean changed = false;
diff --git a/design/src/android/support/design/widget/SnackbarManager.java b/design/src/android/support/design/widget/SnackbarManager.java
index 253ee92..be2f024 100644
--- a/design/src/android/support/design/widget/SnackbarManager.java
+++ b/design/src/android/support/design/widget/SnackbarManager.java
@@ -69,7 +69,7 @@
 
     public void show(int duration, Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 // Means that the callback is already in the queue. We'll just update the duration
                 mCurrentSnackbar.duration = duration;
 
@@ -78,7 +78,7 @@
                 mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
                 scheduleTimeoutLocked(mCurrentSnackbar);
                 return;
-            } else if (isNextSnackbar(callback)) {
+            } else if (isNextSnackbarLocked(callback)) {
                 // We'll just update the duration
                 mNextSnackbar.duration = duration;
             } else {
@@ -101,9 +101,9 @@
 
     public void dismiss(Callback callback, int event) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 cancelSnackbarLocked(mCurrentSnackbar, event);
-            } else if (isNextSnackbar(callback)) {
+            } else if (isNextSnackbarLocked(callback)) {
                 cancelSnackbarLocked(mNextSnackbar, event);
             }
         }
@@ -115,7 +115,7 @@
      */
     public void onDismissed(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 // If the callback is from a Snackbar currently show, remove it and show a new one
                 mCurrentSnackbar = null;
                 if (mNextSnackbar != null) {
@@ -131,7 +131,7 @@
      */
     public void onShown(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 scheduleTimeoutLocked(mCurrentSnackbar);
             }
         }
@@ -139,7 +139,7 @@
 
     public void cancelTimeout(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
             }
         }
@@ -147,12 +147,24 @@
 
     public void restoreTimeout(Callback callback) {
         synchronized (mLock) {
-            if (isCurrentSnackbar(callback)) {
+            if (isCurrentSnackbarLocked(callback)) {
                 scheduleTimeoutLocked(mCurrentSnackbar);
             }
         }
     }
 
+    public boolean isCurrent(Callback callback) {
+        synchronized (mLock) {
+            return isCurrentSnackbarLocked(callback);
+        }
+    }
+
+    public boolean isCurrentOrNext(Callback callback) {
+        synchronized (mLock) {
+            return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
+        }
+    }
+
     private static class SnackbarRecord {
         private final WeakReference<Callback> callback;
         private int duration;
@@ -191,11 +203,11 @@
         return false;
     }
 
-    private boolean isCurrentSnackbar(Callback callback) {
+    private boolean isCurrentSnackbarLocked(Callback callback) {
         return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
     }
 
-    private boolean isNextSnackbar(Callback callback) {
+    private boolean isNextSnackbarLocked(Callback callback) {
         return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
     }