Start using applyOverrideConfiguration

Allows us to fully work with WebViews! The problem
being that we have a VERY small window to call the
method before we have to fall back to the current
behavior.

The last place it can be called is attachBaseContext()
which is before onCreate(). That causes a problem since
we use the savedInstanceState to keep track of the current
local night mode, thus we don't actually know the
correct night mode to apply in attachBaseContext().

This means we have had to move to a static store for
any in-flight local night modes.

Also this CL required a tidy up of how we handle
Activities vs Dialogs. We now explicity keep ref to the
'host' and change behavior based on the type of host.
This allows us to remove all `instanceof` checks on
mOriginalWindowCallback and mContext, which were used
for the same thing, but is semantically different.

Test: included
BUG: 62477814

Change-Id: I0ec61c9e6109f86d26606992e5bcb58fb5bff88f
diff --git a/appcompat/api/1.1.0-alpha03.txt b/appcompat/api/1.1.0-alpha03.txt
index 19ac0fb..ae7a150 100644
--- a/appcompat/api/1.1.0-alpha03.txt
+++ b/appcompat/api/1.1.0-alpha03.txt
@@ -252,9 +252,10 @@
   public abstract class AppCompatDelegate {
     method public abstract void addContentView(android.view.View!, android.view.ViewGroup.LayoutParams!);
     method public abstract boolean applyDayNight();
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.app.Activity!, androidx.appcompat.app.AppCompatCallback!);
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.app.Dialog!, androidx.appcompat.app.AppCompatCallback!);
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.content.Context!, android.view.Window!, androidx.appcompat.app.AppCompatCallback!);
+    method public void attachBaseContext(android.content.Context!);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.app.Dialog, androidx.appcompat.app.AppCompatCallback?);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.view.Window, androidx.appcompat.app.AppCompatCallback?);
     method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
     method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
     method public static int getDefaultNightMode();
diff --git a/appcompat/api/current.txt b/appcompat/api/current.txt
index 19ac0fb..ae7a150 100644
--- a/appcompat/api/current.txt
+++ b/appcompat/api/current.txt
@@ -252,9 +252,10 @@
   public abstract class AppCompatDelegate {
     method public abstract void addContentView(android.view.View!, android.view.ViewGroup.LayoutParams!);
     method public abstract boolean applyDayNight();
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.app.Activity!, androidx.appcompat.app.AppCompatCallback!);
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.app.Dialog!, androidx.appcompat.app.AppCompatCallback!);
-    method public static androidx.appcompat.app.AppCompatDelegate! create(android.content.Context!, android.view.Window!, androidx.appcompat.app.AppCompatCallback!);
+    method public void attachBaseContext(android.content.Context!);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.app.Dialog, androidx.appcompat.app.AppCompatCallback?);
+    method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.view.Window, androidx.appcompat.app.AppCompatCallback?);
     method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
     method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
     method public static int getDefaultNightMode();
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java b/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
index b7bc2c9..0223129 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
@@ -33,6 +33,7 @@
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.res.Configuration;
+import android.webkit.WebView;
 
 import androidx.appcompat.test.R;
 import androidx.core.content.ContextCompat;
@@ -174,32 +175,31 @@
         final FakeTwilightManager twilightManager = new FakeTwilightManager();
         TwilightManager.setInstance(twilightManager);
 
-        NightModeActivity activity = mActivityTestRule.getActivity();
-
-        // Set MODE_NIGHT_AUTO so that we will change to night mode automatically
-        activity = setLocalNightModeAndWaitForRecreate(activity,
+        // Set MODE_NIGHT_AUTO_TIME so that we will change to night mode automatically
+        setLocalNightModeAndWaitForRecreate(mActivityTestRule.getActivity(),
                 AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
+
         // Verify that we're currently in day mode
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
 
-        final NightModeActivity toTest = activity;
         final CountDownLatch resumeCompleteLatch = new CountDownLatch(1);
 
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
+                final NightModeActivity activity = mActivityTestRule.getActivity();
                 final Instrumentation instrumentation =
                         InstrumentationRegistry.getInstrumentation();
                 // Now fool the Activity into thinking that it has gone into the background
-                instrumentation.callActivityOnPause(toTest);
-                instrumentation.callActivityOnStop(toTest);
+                instrumentation.callActivityOnPause(activity);
+                instrumentation.callActivityOnStop(activity);
 
                 // Now update the twilight manager while the Activity is in the 'background'
                 twilightManager.setIsNight(true);
 
                 // Now tell the Activity that it has gone into the foreground again
-                instrumentation.callActivityOnStart(toTest);
-                instrumentation.callActivityOnResume(toTest);
+                instrumentation.callActivityOnStart(activity);
+                instrumentation.callActivityOnResume(activity);
 
                 resumeCompleteLatch.countDown();
             }
@@ -265,6 +265,31 @@
         assertConfigurationNightModeEquals(Configuration.UI_MODE_NIGHT_YES, activity);
     }
 
+    @Test
+    public void testLoadingWebViewMaintainsConfiguration() throws Throwable {
+        // Set night mode and wait for the new Activity
+        final NightModeActivity activity = setLocalNightModeAndWaitForRecreate(
+                mActivityTestRule.getActivity(), AppCompatDelegate.MODE_NIGHT_YES);
+
+        // Assert that the context still has a night themed configuration
+        assertConfigurationNightModeEquals(
+                Configuration.UI_MODE_NIGHT_YES,
+                activity.getResources().getConfiguration());
+
+        // Now load a WebView into the Activity
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final WebView webView = new WebView(activity);
+            }
+        });
+
+        // Now assert that the context still has a night themed configuration
+        assertConfigurationNightModeEquals(
+                Configuration.UI_MODE_NIGHT_YES,
+                activity.getResources().getConfiguration());
+    }
+
     private static class FakeTwilightManager extends TwilightManager {
         private boolean mIsNight;
 
diff --git a/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java b/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
index 03e3f00..c34cc57 100644
--- a/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
+++ b/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
@@ -16,6 +16,7 @@
 
 package androidx.appcompat.app;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -67,6 +68,12 @@
     private Resources mResources;
 
     @Override
+    protected void attachBaseContext(Context newBase) {
+        super.attachBaseContext(newBase);
+        getDelegate().attachBaseContext(newBase);
+    }
+
+    @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         final AppCompatDelegate delegate = getDelegate();
         delegate.installViewFactory();
diff --git a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index f3e2ec5..a5b0e31 100644
--- a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -208,8 +208,10 @@
      *
      * @param callback An optional callback for AppCompat specific events
      */
-    public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
-        return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
+    @NonNull
+    public static AppCompatDelegate create(@NonNull Activity activity,
+            @Nullable AppCompatCallback callback) {
+        return new AppCompatDelegateImpl(activity, callback);
     }
 
     /**
@@ -217,8 +219,10 @@
      *
      * @param callback An optional callback for AppCompat specific events
      */
-    public static AppCompatDelegate create(Dialog dialog, AppCompatCallback callback) {
-        return new AppCompatDelegateImpl(dialog.getContext(), dialog.getWindow(), callback);
+    @NonNull
+    public static AppCompatDelegate create(@NonNull Dialog dialog,
+            @Nullable AppCompatCallback callback) {
+        return new AppCompatDelegateImpl(dialog, callback);
     }
 
     /**
@@ -227,8 +231,9 @@
      *
      * @param callback An optional callback for AppCompat specific events
      */
-    public static AppCompatDelegate create(Context context, Window window,
-            AppCompatCallback callback) {
+    @NonNull
+    public static AppCompatDelegate create(@NonNull Context context, @NonNull Window window,
+            @Nullable AppCompatCallback callback) {
         return new AppCompatDelegateImpl(context, window, callback);
     }
 
@@ -347,6 +352,12 @@
     public abstract void addContentView(View v, ViewGroup.LayoutParams lp);
 
     /**
+     * Should be called from {@link Activity#attachBaseContext(Context)}
+     */
+    public void attachBaseContext(Context context) {
+    }
+
+    /**
      * Should be called from {@link Activity#onTitleChanged(CharSequence, int)}}
      */
     public abstract void setTitle(@Nullable CharSequence title);
diff --git a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index f1d5a1f..8bb7de4 100644
--- a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -29,6 +29,7 @@
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
@@ -99,6 +100,7 @@
 import androidx.appcompat.widget.VectorEnabledTintResources;
 import androidx.appcompat.widget.ViewStubCompat;
 import androidx.appcompat.widget.ViewUtils;
+import androidx.collection.ArrayMap;
 import androidx.core.app.NavUtils;
 import androidx.core.view.KeyEventDispatcher;
 import androidx.core.view.LayoutInflaterCompat;
@@ -113,13 +115,19 @@
 import org.xmlpull.v1.XmlPullParser;
 
 import java.util.List;
+import java.util.Map;
 
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
 class AppCompatDelegateImpl extends AppCompatDelegate
         implements MenuBuilder.Callback, LayoutInflater.Factory2 {
 
+    private static final Map<Class<?>, Integer> sLocalNightModes = new ArrayMap<>();
+
     private static final boolean DEBUG = false;
     private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
-    private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode";
 
     private static final int[] sWindowBackgroundStyleable = {android.R.attr.windowBackground};
 
@@ -163,10 +171,10 @@
         }
     }
 
+    final Object mHost;
     final Context mContext;
-    final Window mWindow;
-    final Window.Callback mOriginalWindowCallback;
-    final Window.Callback mAppCompatWindowCallback;
+    Window mWindow;
+    private AppCompatWindowCallback mAppCompatWindowCallback;
     final AppCompatCallback mAppCompatCallback;
 
     ActionBar mActionBar;
@@ -214,6 +222,7 @@
 
     private boolean mLongPressBackDown;
 
+    private boolean mBaseContextAttached;
     private boolean mCreated;
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     boolean mIsDestroyed;
@@ -251,36 +260,76 @@
 
     private AppCompatViewInflater mAppCompatViewInflater;
 
+    AppCompatDelegateImpl(Activity activity, AppCompatCallback callback) {
+        this(activity, null, callback, activity);
+    }
+
+    AppCompatDelegateImpl(Dialog dialog, AppCompatCallback callback) {
+        this(dialog.getContext(), dialog.getWindow(), callback, dialog);
+    }
+
     AppCompatDelegateImpl(Context context, Window window, AppCompatCallback callback) {
+        this(context, window, callback, context);
+    }
+
+    private AppCompatDelegateImpl(Context context, Window window, AppCompatCallback callback,
+            Object host) {
         mContext = context;
-        mWindow = window;
         mAppCompatCallback = callback;
+        mHost = host;
 
-        mOriginalWindowCallback = mWindow.getCallback();
-        if (mOriginalWindowCallback instanceof AppCompatWindowCallback) {
-            throw new IllegalStateException(
-                    "AppCompat has already installed itself into the Window");
+        if (window != null) {
+            attachToWindow(window);
         }
-        mAppCompatWindowCallback = new AppCompatWindowCallback(mOriginalWindowCallback);
-        // Now install the new callback
-        mWindow.setCallback(mAppCompatWindowCallback);
 
-        final TintTypedArray a = TintTypedArray.obtainStyledAttributes(
-                context, null, sWindowBackgroundStyleable);
-        final Drawable winBg = a.getDrawableIfKnown(0);
-        if (winBg != null) {
-            mWindow.setBackgroundDrawable(winBg);
+        if (mLocalNightMode == MODE_NIGHT_UNSPECIFIED && mHost instanceof Dialog) {
+            final AppCompatActivity activity = tryUnwrapContext();
+            if (activity != null) {
+                // This code path is used to detect when this Delegate is a child Delegate from
+                // an Activity, primarily for Dialogs. Dialogs use the Activity as it's Context,
+                // so we want to make sure that the this 'child' delegate does not interfere
+                // with the Activity config. The simplest way to do that is to match the
+                // outer Activity's local night mode
+                mLocalNightMode = activity.getDelegate().getLocalNightMode();
+            }
         }
-        a.recycle();
+        if (mLocalNightMode == MODE_NIGHT_UNSPECIFIED) {
+            // Try and read the current night mode from our static store
+            final Integer value = sLocalNightModes.get(mHost.getClass());
+            if (value != null) {
+                mLocalNightMode = value;
+                // Finally remove the value
+                sLocalNightModes.remove(mHost.getClass());
+            }
+        }
+    }
+
+    @Override
+    public void attachBaseContext(Context context) {
+        applyDayNight();
+        mBaseContextAttached = true;
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (mOriginalWindowCallback instanceof Activity) {
+        // attachBaseContext will only be called from an Activity, so make sure we switch this for
+        // Dialogs, etc
+        mBaseContextAttached = true;
+
+        // We lazily fetch the Window for Activities, to allow DayNight to apply in
+        // attachBaseContext
+        if (mWindow == null && mHost instanceof Activity) {
+            attachToWindow(((Activity) mHost).getWindow());
+        }
+
+        if (mWindow == null) {
+            throw new IllegalStateException("We have not been given a Window");
+        }
+
+        if (mHost instanceof Activity) {
             String parentActivityName = null;
             try {
-                parentActivityName = NavUtils.getParentActivityName(
-                        (Activity) mOriginalWindowCallback);
+                parentActivityName = NavUtils.getParentActivityName((Activity) mHost);
             } catch (IllegalArgumentException iae) {
                 // Ignore in this case
             }
@@ -295,26 +344,16 @@
             }
         }
 
-        if (mLocalNightMode == MODE_NIGHT_UNSPECIFIED && mContext instanceof AppCompatActivity) {
-            final AppCompatDelegate delegate = ((AppCompatActivity) mContext).getDelegate();
-            if (delegate != this) {
-                // This code path is used to detect when this Delegate is a child Delegate from
-                // an Activity, primarily for Dialogs. Dialogs use the Activity as it's Context,
-                // so we want to make sure that the this 'child' delegate does not interfere
-                // with the Activity config. The simplest way to do that is to match the
-                // outer Activity's local night mode
-                mLocalNightMode = delegate.getLocalNightMode();
-            }
-        }
-        if (savedInstanceState != null && mLocalNightMode == MODE_NIGHT_UNSPECIFIED) {
-            // If we have a icicle and we haven't had a local night mode set yet, try and read
-            // it from the icicle
-            mLocalNightMode = savedInstanceState.getInt(KEY_LOCAL_NIGHT_MODE,
-                    MODE_NIGHT_UNSPECIFIED);
-        }
-
         applyDayNight();
 
+        final TintTypedArray a = TintTypedArray.obtainStyledAttributes(
+                mContext, null, sWindowBackgroundStyleable);
+        final Drawable winBg = a.getDrawableIfKnown(0);
+        if (winBg != null) {
+            mWindow.setBackgroundDrawable(winBg);
+        }
+        a.recycle();
+
         mCreated = true;
     }
 
@@ -347,11 +386,10 @@
             return;
         }
 
-        if (mOriginalWindowCallback instanceof Activity) {
-            mActionBar = new WindowDecorActionBar((Activity) mOriginalWindowCallback,
-                    mOverlayActionBar);
-        } else if (mOriginalWindowCallback instanceof Dialog) {
-            mActionBar = new WindowDecorActionBar((Dialog) mOriginalWindowCallback);
+        if (mHost instanceof Activity) {
+            mActionBar = new WindowDecorActionBar((Activity) mHost, mOverlayActionBar);
+        } else if (mHost instanceof Dialog) {
+            mActionBar = new WindowDecorActionBar((Dialog) mHost);
         }
         if (mActionBar != null) {
             mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
@@ -360,7 +398,7 @@
 
     @Override
     public void setSupportActionBar(Toolbar toolbar) {
-        if (!(mOriginalWindowCallback instanceof Activity)) {
+        if (!(mHost instanceof Activity)) {
             // Only Activities support custom Action Bars
             return;
         }
@@ -382,8 +420,8 @@
         }
 
         if (toolbar != null) {
-            final ToolbarActionBar tbab = new ToolbarActionBar(toolbar,
-                    ((Activity) mOriginalWindowCallback).getTitle(), mAppCompatWindowCallback);
+            final ToolbarActionBar tbab = new ToolbarActionBar(toolbar, getTitle(),
+                    mAppCompatWindowCallback);
             mActionBar = tbab;
             mWindow.setCallback(tbab.getWrappedWindowCallback());
         } else {
@@ -484,43 +522,43 @@
     @Override
     public void setContentView(View v) {
         ensureSubDecor();
-        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
         contentParent.removeAllViews();
         contentParent.addView(v);
-        mOriginalWindowCallback.onContentChanged();
+        mAppCompatWindowCallback.getWrapped().onContentChanged();
     }
 
     @Override
     public void setContentView(int resId) {
         ensureSubDecor();
-        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
         contentParent.removeAllViews();
         LayoutInflater.from(mContext).inflate(resId, contentParent);
-        mOriginalWindowCallback.onContentChanged();
+        mAppCompatWindowCallback.getWrapped().onContentChanged();
     }
 
     @Override
     public void setContentView(View v, ViewGroup.LayoutParams lp) {
         ensureSubDecor();
-        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
         contentParent.removeAllViews();
         contentParent.addView(v, lp);
-        mOriginalWindowCallback.onContentChanged();
+        mAppCompatWindowCallback.getWrapped().onContentChanged();
     }
 
     @Override
     public void addContentView(View v, ViewGroup.LayoutParams lp) {
         ensureSubDecor();
-        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
         contentParent.addView(v, lp);
-        mOriginalWindowCallback.onContentChanged();
+        mAppCompatWindowCallback.getWrapped().onContentChanged();
     }
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
         if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED) {
             // If we have a local night mode set, save it
-            outState.putInt(KEY_LOCAL_NIGHT_MODE, mLocalNightMode);
+            sLocalNightModes.put(mHost.getClass(), mLocalNightMode);
         }
     }
 
@@ -550,6 +588,24 @@
         mThemeResId = themeResId;
     }
 
+    private void attachToWindow(@NonNull Window window) {
+        if (mWindow != null) {
+            throw new IllegalStateException(
+                    "AppCompat has already installed itself into the Window");
+        }
+
+        final Window.Callback callback = window.getCallback();
+        if (callback instanceof AppCompatWindowCallback) {
+            throw new IllegalStateException(
+                    "AppCompat has already installed itself into the Window");
+        }
+        mAppCompatWindowCallback = new AppCompatWindowCallback(callback);
+        // Now install the new callback
+        window.setCallback(mAppCompatWindowCallback);
+
+        mWindow = window;
+    }
+
     private void ensureSubDecor() {
         if (!mSubDecorInstalled) {
             mSubDecor = createSubDecor();
@@ -884,8 +940,8 @@
 
     final CharSequence getTitle() {
         // If the original window callback is an Activity, we'll use its title
-        if (mOriginalWindowCallback instanceof Activity) {
-            return ((Activity) mOriginalWindowCallback).getTitle();
+        if (mHost instanceof Activity) {
+            return ((Activity) mHost).getTitle();
         }
         // Else, we'll return the title we have recorded ourselves
         return mTitle;
@@ -1199,8 +1255,7 @@
     boolean dispatchKeyEvent(KeyEvent event) {
         // Check AppCompatDialog directly since it isn't able to implement KeyEventDispatcher
         // while it is @hide.
-        if (mOriginalWindowCallback instanceof KeyEventDispatcher.Component
-                || mOriginalWindowCallback instanceof AppCompatDialog) {
+        if (mHost instanceof KeyEventDispatcher.Component || mHost instanceof AppCompatDialog) {
             View root = mWindow.getDecorView();
             if (root != null && KeyEventDispatcher.dispatchBeforeHierarchy(root, event)) {
                 return true;
@@ -1209,7 +1264,7 @@
 
         if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
             // If this is a MENU event, let the Activity have a go.
-            if (mOriginalWindowCallback.dispatchKeyEvent(event)) {
+            if (mAppCompatWindowCallback.getWrapped().dispatchKeyEvent(event)) {
                 return true;
             }
         }
@@ -1361,6 +1416,22 @@
         return onCreateView(null, name, context, attrs);
     }
 
+    @Nullable
+    private AppCompatActivity tryUnwrapContext() {
+        Context context = mContext;
+        while (context != null) {
+            if (context instanceof AppCompatActivity) {
+                return (AppCompatActivity) context;
+            }
+            if (context instanceof ContextWrapper) {
+                context = ((ContextWrapper) context).getBaseContext();
+            } else {
+                return null;
+            }
+        }
+        return null;
+    }
+
     private void openPanel(final PanelFeatureState st, KeyEvent event) {
         // Already open, return
         if (st.isOpen || mIsDestroyed) {
@@ -1814,7 +1885,7 @@
             // We need to be careful which callback we dispatch the call to. We can not dispatch
             // this to the Window's callback since that will call back into this method and cause a
             // crash. Instead we need to dispatch down to the original Activity/Dialog/etc.
-            mOriginalWindowCallback.onPanelClosed(featureId, menu);
+            mAppCompatWindowCallback.getWrapped().onPanelClosed(featureId, menu);
         }
     }
 
@@ -2111,11 +2182,9 @@
      */
     private boolean updateForNightMode(@ApplyableNightMode final int mode,
             final boolean allowRecreation) {
-        final Resources res = mContext.getResources();
-        final Configuration config = res.getConfiguration();
-        final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        boolean handled = false;
 
-        int newNightMode = currentNightMode;
+        int newNightMode;
         switch (mode) {
             case MODE_NIGHT_YES:
                 newNightMode = Configuration.UI_MODE_NIGHT_YES;
@@ -2123,6 +2192,7 @@
             case MODE_NIGHT_NO:
                 newNightMode = Configuration.UI_MODE_NIGHT_NO;
                 break;
+            default:
             case MODE_NIGHT_FOLLOW_SYSTEM:
                 // If we're following the system, we just use the system default from the
                 // application context
@@ -2133,67 +2203,97 @@
                 break;
         }
 
-        boolean handled = false;
+        final boolean activityHandlingUiMode = isActivityManifestHandlingUiMode();
 
-        if (currentNightMode != newNightMode) {
-            final boolean manifestHandlingUiMode = isActivityManifestHandlingUiMode();
-            final boolean shouldRecreateOnNightModeChange = allowRecreation
-                    && !manifestHandlingUiMode && mCreated && mContext instanceof Activity;
+        if (!activityHandlingUiMode && Build.VERSION.SDK_INT >= 17 && !mBaseContextAttached
+                && mHost instanceof android.view.ContextThemeWrapper) {
+            // If we're here then we can try and apply an override configuration on the Context.
+            final Configuration conf = new Configuration();
+            conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
 
-            if (shouldRecreateOnNightModeChange) {
+            try {
                 if (DEBUG) {
-                    Log.d(TAG, "updateForNightMode. Night mode changed, recreating Activity."
-                            + " Mode: " + mode);
+                    Log.d(TAG, "updateForNightMode. Applying override config");
                 }
-                // If we've already been created, we need to recreate the Activity for the
-                // mode to be applied
-                ((Activity) mContext).recreate();
-            } else if (!manifestHandlingUiMode) {
-                // If the Activity is not set to handle uiMode config changes we will
-                // update the Resources with a new Configuration with an updated UI Mode
-                final Configuration newConf = new Configuration(config);
-                newConf.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
-                res.updateConfiguration(newConf, res.getDisplayMetrics());
-
-                if (DEBUG) {
-                    Log.d(TAG, "updateForNightMode. Night mode changed, updated res config."
-                            + " Mode: " + mode);
-                }
-                // We may need to flush the Resources' drawable cache due to framework bugs.
-                if (Build.VERSION.SDK_INT < 26) {
-                    ResourcesFlusher.flush(res);
-                }
-
-                if (mThemeResId != 0) {
-                    // We need to re-apply the theme so that it reflected the new
-                    // configuration
-                    mContext.setTheme(mThemeResId);
-
-                    if (Build.VERSION.SDK_INT >= 23) {
-                        // On M+ setTheme only applies if the themeResId actually changes,
-                        // since we have no way to publicly check what the Theme's current
-                        // themeResId is, we just manually apply it anyway. Most of the time
-                        // this is what we need anyway (since the themeResId does not
-                        // often change)
-                        mContext.getTheme().applyStyle(mThemeResId, true);
-                    }
-                }
+                ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
+                handled = true;
+            } catch (IllegalStateException e) {
+                // applyOverrideConfiguration throws an IllegalStateException if it's resources
+                // have already been created. Since there's no way to check this beforehand we
+                // just have to try it and catch the exception
+                handled = false;
             }
-            handled = true;
-        } else {
-            if (DEBUG) {
-                Log.d(TAG, "applyNightMode() | Skipping. Night mode has not changed: " + mode);
+        }
+
+        if (!handled && !activityHandlingUiMode) {
+            final int currentNightMode = mContext.getResources().getConfiguration().uiMode
+                    & Configuration.UI_MODE_NIGHT_MASK;
+            if (currentNightMode != newNightMode) {
+                if (allowRecreation && (Build.VERSION.SDK_INT >= 17 || mCreated)
+                        && mHost instanceof Activity) {
+                    // If we're created and are an Activity, we can recreate() to apply
+                    // The SDK_INT check above is because applyOverrideConfiguration only exists on
+                    // API 17+, so we don't want to get into an loop of infinite recreations.
+                    // On < API 17 we need to use updateConfiguration before we're 'created'
+                    if (DEBUG) {
+                        Log.d(TAG, "updateForNightMode. Recreating Activity");
+                    }
+                    ((Activity) mHost).recreate();
+                    handled = true;
+                }
+                if (!handled) {
+                    // Else we need to use the updateConfiguration path
+                    if (DEBUG) {
+                        Log.d(TAG, "updateForNightMode. Updating resources config");
+                    }
+                    updateResourcesConfigurationForNightMode(newNightMode);
+                    handled = true;
+                }
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "updateForNightMode. Skipping. Night mode: " + mode);
+                }
             }
         }
 
         // Notify the activity of the night mode
-        if (mContext instanceof AppCompatActivity) {
-            ((AppCompatActivity) mContext).onNightModeChanged(mode);
+        if (handled && mHost instanceof AppCompatActivity) {
+            ((AppCompatActivity) mHost).onNightModeChanged(mode);
         }
 
         return handled;
     }
 
+    private void updateResourcesConfigurationForNightMode(final int uiModeNightModeValue) {
+        // If the Activity is not set to handle uiMode config changes we will
+        // update the Resources with a new Configuration with an updated UI Mode
+        final Resources res = mContext.getResources();
+        final Configuration conf = new Configuration();
+        conf.uiMode = uiModeNightModeValue
+                | (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+        res.updateConfiguration(conf, null);
+
+        // We may need to flush the Resources' drawable cache due to framework bugs.
+        if (Build.VERSION.SDK_INT < 26) {
+            ResourcesFlusher.flush(res);
+        }
+
+        if (mThemeResId != 0) {
+            // We need to re-apply the theme so that it reflected the new
+            // configuration
+            mContext.setTheme(mThemeResId);
+
+            if (Build.VERSION.SDK_INT >= 23) {
+                // On M+ setTheme only applies if the themeResId actually changes,
+                // since we have no way to publicly check what the Theme's current
+                // themeResId is, we just manually apply it anyway. Most of the time
+                // this is what we need anyway (since the themeResId does not
+                // often change)
+                mContext.getTheme().applyStyle(mThemeResId, true);
+            }
+        }
+    }
+
     /**
      * @hide
      */
@@ -2215,11 +2315,11 @@
     }
 
     private boolean isActivityManifestHandlingUiMode() {
-        if (!mActivityHandlesUiModeChecked && mContext instanceof Activity) {
+        if (!mActivityHandlesUiModeChecked && mHost instanceof Activity) {
             final PackageManager pm = mContext.getPackageManager();
             try {
                 final ActivityInfo info = pm.getActivityInfo(
-                        new ComponentName(mContext, mContext.getClass()), 0);
+                        new ComponentName(mContext, mHost.getClass()), 0);
                 mActivityHandlesUiMode = (info.configChanges & ActivityInfo.CONFIG_UI_MODE) != 0;
             } catch (PackageManager.NameNotFoundException e) {
                 // This shouldn't happen but let's not crash because of it, we'll just log and
diff --git a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDialog.java b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDialog.java
index e1539cb..18b09ef 100644
--- a/appcompat/src/main/java/androidx/appcompat/app/AppCompatDialog.java
+++ b/appcompat/src/main/java/androidx/appcompat/app/AppCompatDialog.java
@@ -56,11 +56,15 @@
     public AppCompatDialog(Context context, int theme) {
         super(context, getThemeResId(context, theme));
 
+        final AppCompatDelegate delegate = getDelegate();
+        // Make sure we provide the delegate with the current theme res id
+        delegate.onSetTheme(getThemeResId(context, theme));
+
         // This is a bit weird, but Dialog's are typically created and setup before being shown,
         // which means that we can't rely on onCreate() being called before a content view is set.
         // To workaround this, we call onCreate(null) in the ctor, and then again as usual in
         // onCreate().
-        getDelegate().onCreate(null);
+        delegate.onCreate(null);
     }
 
     protected AppCompatDialog(Context context, boolean cancelable,
diff --git a/appcompat/src/main/java/androidx/appcompat/view/WindowCallbackWrapper.java b/appcompat/src/main/java/androidx/appcompat/view/WindowCallbackWrapper.java
index b5bb8d56..05a241c 100644
--- a/appcompat/src/main/java/androidx/appcompat/view/WindowCallbackWrapper.java
+++ b/appcompat/src/main/java/androidx/appcompat/view/WindowCallbackWrapper.java
@@ -183,4 +183,8 @@
     public void onPointerCaptureChanged(boolean hasCapture) {
         mWrapped.onPointerCaptureChanged(hasCapture);
     }
+
+    public final Window.Callback getWrapped() {
+        return mWrapped;
+    }
 }