Fix night mode for config-based color upconversions

This works around an issue in the platform where
night-configured color resources which are converted
to ColorDrawables are not purged from the Resources
cache. Fixed by manually forcing the resources instance
to purge its caches when the night mode changes.

BUG: 30132023

Change-Id: I68128bd752d7c8451940631df336154246e921ae
diff --git a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV14.java b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV14.java
index 3279666..631fc35 100644
--- a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV14.java
+++ b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV14.java
@@ -30,6 +30,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.view.SupportActionModeWrapper;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ActionMode;
 import android.view.Window;
@@ -38,6 +39,8 @@
 
     private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode";
 
+    private static final boolean FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE = true;
+
     @NightMode
     private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED;
     private boolean mApplyDayNightCalled;
@@ -205,10 +208,25 @@
                 if (DEBUG) {
                     Log.d(TAG, "applyNightMode() | Night mode changed, updating configuration");
                 }
-                final Configuration newConf = new Configuration(conf);
-                newConf.uiMode = newNightMode
-                        | (newConf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
-                res.updateConfiguration(newConf, null);
+                final Configuration config = new Configuration(conf);
+                final DisplayMetrics metrics = res.getDisplayMetrics();
+                final float originalFontScale = config.fontScale;
+
+                // Update the UI Mode to reflect the new night mode
+                config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+                if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) {
+                    // Set a fake font scale value to flush any resource caches
+                    config.fontScale = originalFontScale * 2;
+                }
+                // Now update the configuration
+                res.updateConfiguration(config, metrics);
+
+                if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) {
+                    // If we're flushing the resources cache, revert back to the original
+                    // font scale value
+                    config.fontScale = originalFontScale;
+                    res.updateConfiguration(config, metrics);
+                }
             }
             return true;
         } else {
@@ -233,7 +251,7 @@
 
     private boolean shouldRecreateOnNightModeChange() {
         if (mApplyDayNightCalled && mContext instanceof Activity) {
-            // If we've already appliedDayNight() (via setTheme), we need to check if the
+            // If we've already applyDayNight() (via setTheme), we need to check if the
             // Activity has configChanges set to handle uiMode changes
             final PackageManager pm = mContext.getPackageManager();
             try {
diff --git a/v7/appcompat/tests/res/drawable/test_night_color_conversion_background.xml b/v7/appcompat/tests/res/drawable/test_night_color_conversion_background.xml
new file mode 100644
index 0000000..dd32112
--- /dev/null
+++ b/v7/appcompat/tests/res/drawable/test_night_color_conversion_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@color/color_sky" />
+</selector>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/layout/activity_night_mode.xml b/v7/appcompat/tests/res/layout/activity_night_mode.xml
index 8f3463d..5cd8563 100644
--- a/v7/appcompat/tests/res/layout/activity_night_mode.xml
+++ b/v7/appcompat/tests/res/layout/activity_night_mode.xml
@@ -25,4 +25,10 @@
         android:layout_height="wrap_content"
         android:text="@string/night_mode" />
 
+    <View
+        android:id="@+id/view_background"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
+        android:background="@drawable/test_night_color_conversion_background" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/values-night/colors.xml b/v7/appcompat/tests/res/values-night/colors.xml
new file mode 100644
index 0000000..cf0fc9f
--- /dev/null
+++ b/v7/appcompat/tests/res/values-night/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <color name="color_sky">@color/color_sky_night</color>
+</resources>
diff --git a/v7/appcompat/tests/res/values/colors.xml b/v7/appcompat/tests/res/values/colors.xml
index 1f6fab2..8ec92b7 100644
--- a/v7/appcompat/tests/res/values/colors.xml
+++ b/v7/appcompat/tests/res/values/colors.xml
@@ -31,4 +31,9 @@
 
     <color name="emerald_translucent_default">#8020A060</color>
     <color name="emerald_translucent_disabled">#8070C090</color>
+
+    <color name="color_sky_day">#90F0FF</color>
+    <color name="color_sky_night">#3050CF</color>
+    <color name="color_sky">@color/color_sky_day</color>
+
 </resources>
diff --git a/v7/appcompat/tests/src/android/support/v7/app/NightModeActivity.java b/v7/appcompat/tests/src/android/support/v7/app/NightModeActivity.java
index a2d038b..a52d26b 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/NightModeActivity.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/NightModeActivity.java
@@ -20,8 +20,31 @@
 import android.support.v7.testutils.BaseTestActivity;
 
 public class NightModeActivity extends BaseTestActivity {
+
+    /**
+     * Warning, gross hack here. Since night mode uses recreate(), we need a way to be able to
+     * grab the top activity. The test runner only keeps reference to the original Activity which
+     * is no good for these tests. Fixed by keeping a static reference to the 'top' instance, and
+     * updating it in onResume and onPause. I said it was gross.
+     */
+    static NightModeActivity TOP_ACTIVITY = null;
+
     @Override
     protected int getContentViewLayoutResId() {
         return R.layout.activity_night_mode;
     }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        TOP_ACTIVITY = this;
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (TOP_ACTIVITY == this) {
+            TOP_ACTIVITY = null;
+        }
+    }
 }
diff --git a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
index 6ea0077d..a3d91d2 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
@@ -20,12 +20,12 @@
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.support.v7.app.NightModeActivity.TOP_ACTIVITY;
+import static android.support.v7.testutils.TestUtils.setLocalNightModeAndWaitForRecreate;
+import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
 
 import static org.junit.Assert.assertFalse;
 
-import android.app.Instrumentation;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SdkSuppress;
 import android.support.v7.appcompat.test.R;
 import android.test.suitebuilder.annotation.MediumTest;
@@ -53,26 +53,39 @@
 
     @Test
     public void testLocalDayNightModeRecreatesActivity() {
-        final NightModeActivity activity = getActivity();
-
         // Verify first that we're in day mode
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
 
         // Now force the local night mode to be yes (aka night mode)
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                activity.getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
-            }
-        });
-        instrumentation.waitForIdleSync();
+        setLocalNightModeAndWaitForRecreate(getActivity(), AppCompatDelegate.MODE_NIGHT_YES);
 
         // Now check the text has changed, signifying that night resources are being used
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_NIGHT)));
     }
 
     @Test
+    public void testColorConvertedDrawableChangesWithNightMode() {
+        final NightModeActivity activity = getActivity();
+        final int dayColor = activity.getResources().getColor(R.color.color_sky_day);
+        final int nightColor = activity.getResources().getColor(R.color.color_sky_night);
+
+        // Loop through and switching from day to night and vice-versa multiple times. It needs
+        // to be looped since the issue is with drawable caching, therefore we need to prime the
+        // cache for the issue to happen
+        for (int i = 0; i < 5; i++) {
+            // First force it to not be night mode
+            setLocalNightModeAndWaitForRecreate(TOP_ACTIVITY, AppCompatDelegate.MODE_NIGHT_NO);
+            // ... and verify first that we're in day mode
+            onView(withId(R.id.view_background)).check(matches(isBackground(dayColor)));
+
+            // Now force the local night mode to be yes (aka night mode)
+            setLocalNightModeAndWaitForRecreate(TOP_ACTIVITY, AppCompatDelegate.MODE_NIGHT_YES);
+            // ... and verify first that we're in night mode
+            onView(withId(R.id.view_background)).check(matches(isBackground(nightColor)));
+        }
+    }
+
+    @Test
     public void testNightModeAutoRecreatesOnTimeChange() {
         // Create a fake TwilightManager and set it as the app instance
         final FakeTwilightManager twilightManager = new FakeTwilightManager();
@@ -85,21 +98,13 @@
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
 
         // Now set MODE_NIGHT_AUTO so that we will change to night mode automatically
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                // Force the local night mode to be auto
-                delegate.setLocalNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
-            }
-        });
-        instrumentation.waitForIdleSync();
+        setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
 
         // Assert that the original Activity has not been destroyed yet
         assertFalse(activity.isDestroyed());
 
         // Now update the fake twilight manager to be in night and trigger a fake 'time' change
-        instrumentation.runOnMainSync(new Runnable() {
+        getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
                 twilightManager.setIsNight(true);
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
index b904127..37a3732 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
@@ -16,6 +16,7 @@
 
 package android.support.v7.testutils;
 
+import android.app.Instrumentation;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -24,10 +25,14 @@
 import android.os.SystemClock;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
 import android.support.v4.util.Pair;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.app.AppCompatDelegate;
 import android.support.v7.widget.TintTypedArray;
 import android.view.View;
 import android.view.ViewParent;
+
 import junit.framework.Assert;
 
 import java.util.ArrayList;
@@ -249,4 +254,16 @@
             a.recycle();
         }
     }
+
+    public static void setLocalNightModeAndWaitForRecreate(final AppCompatActivity activity,
+            @AppCompatDelegate.NightMode final int nightMode) {
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        instrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                activity.getDelegate().setLocalNightMode(nightMode);
+            }
+        });
+        instrumentation.waitForIdleSync();
+    }
 }
\ No newline at end of file