Read the high refresh rate blacklist from DeviceConfig.

When available, we should read the high refresh rate blacklist from
DeviceConfig, and use the device's configured list as a default /
fallback. This lets us change the blacklist in the field in case we find
additional compatibility issues, or one of the developers fixes their
app and asks us to remove them from the blacklist.

Fixes: 132338514
Test: atest WmTests:HighRefreshRateBlacklistTest
Exempt-From-Owner-Approval: TBR
Merged-In: I984dab60eeafff01758ac38c3db6056b33f662a6
Change-Id: I984dab60eeafff01758ac38c3db6056b33f662a6
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index e30ba38..1fd212d 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -336,6 +336,15 @@
         @TestApi
         String KEY_SYSTEM_GESTURES_EXCLUDED_BY_PRE_Q_STICKY_IMMERSIVE =
                 "system_gestures_excluded_by_pre_q_sticky_immersive";
+
+        /**
+         * Key for controlling which packages are explicitly blocked from running at refresh rates
+         * higher than 60hz.
+         *
+         * @see android.provider.DeviceConfig#NAMESPACE_WINDOW_MANAGER
+         * @hide
+         */
+        String KEY_HIGH_REFRESH_RATE_BLACKLIST = "high_refresh_rate_blacklist";
     }
 
     private static final Object sLock = new Object();
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 305c1a6..e44c5f7 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4208,4 +4208,8 @@
 
     <!-- Sharesheet: define a max number of targets per application for new shortcuts-based direct share introduced in Q -->
     <integer name="config_maxShortcutTargetsPerApp">3</integer>
+
+    <!-- The list of packages to automatically opt out of refresh rates higher than 60hz because
+         of known compatibility issues. -->
+    <string-array name="config_highRefreshRateBlacklist"></string-array>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0043826e..1743391 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3819,6 +3819,8 @@
   <java-symbol type="string" name="config_defaultSupervisionProfileOwnerComponent" />
   <java-symbol type="bool" name="config_inflateSignalStrength" />
 
+  <java-symbol type="array" name="config_highRefreshRateBlacklist" />
+
   <java-symbol type="drawable" name="android_logotype" />
   <java-symbol type="layout" name="platlogo_layout" />
 
diff --git a/services/core/java/com/android/server/wm/HighRefreshRateBlacklist.java b/services/core/java/com/android/server/wm/HighRefreshRateBlacklist.java
index c0f53b8..5726cb2 100644
--- a/services/core/java/com/android/server/wm/HighRefreshRateBlacklist.java
+++ b/services/core/java/com/android/server/wm/HighRefreshRateBlacklist.java
@@ -16,60 +16,102 @@
 
 package com.android.server.wm;
 
+import static android.provider.DeviceConfig.WindowManager.KEY_HIGH_REFRESH_RATE_BLACKLIST;
+
 import android.annotation.NonNull;
-import android.os.SystemProperties;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
 import android.util.ArraySet;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+
+import java.io.PrintWriter;
+import java.util.concurrent.Executor;
 
 /**
  * A Blacklist for packages that should force the display out of high refresh rate.
  */
 class HighRefreshRateBlacklist {
 
-    private static final String SYSPROP_KEY = "ro.window_manager.high_refresh_rate_blacklist";
-    private static final String SYSPROP_KEY_LENGTH_SUFFIX = "_length";
-    private static final String SYSPROP_KEY_ENTRY_SUFFIX = "_entry";
-    private static final int MAX_ENTRIES = 50;
+    private final ArraySet<String> mBlacklistedPackages = new ArraySet<>();
+    @NonNull
+    private final String[] mDefaultBlacklist;
+    private final Object mLock = new Object();
 
-    private ArraySet<String> mBlacklistedPackages = new ArraySet<>();
-
-    static HighRefreshRateBlacklist create() {
-        return new HighRefreshRateBlacklist(new SystemPropertyGetter() {
+    static HighRefreshRateBlacklist create(@NonNull Resources r) {
+        return new HighRefreshRateBlacklist(r, new DeviceConfigInterface() {
             @Override
-            public int getInt(String key, int def) {
-                return SystemProperties.getInt(key, def);
+            public @Nullable String getProperty(@NonNull String namespace, @NonNull String name) {
+                return DeviceConfig.getProperty(namespace, name);
             }
-
-            @Override
-            public String get(String key) {
-                return SystemProperties.get(key);
+            public void addOnPropertyChangedListener(@NonNull String namespace,
+                    @NonNull Executor executor,
+                    @NonNull DeviceConfig.OnPropertyChangedListener listener) {
+                DeviceConfig.addOnPropertyChangedListener(namespace, executor, listener);
             }
         });
     }
 
     @VisibleForTesting
-    HighRefreshRateBlacklist(SystemPropertyGetter propertyGetter) {
+    HighRefreshRateBlacklist(Resources r, DeviceConfigInterface deviceConfig) {
+        mDefaultBlacklist = r.getStringArray(R.array.config_highRefreshRateBlacklist);
+        deviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                BackgroundThread.getExecutor(), new OnPropertyChangedListener());
+        final String property = deviceConfig.getProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                KEY_HIGH_REFRESH_RATE_BLACKLIST);
+        updateBlacklist(property);
+    }
 
-        // Read and populate the blacklist
-        final int length = Math.min(
-                propertyGetter.getInt(SYSPROP_KEY + SYSPROP_KEY_LENGTH_SUFFIX, 0),
-                MAX_ENTRIES);
-        for (int i = 1; i <= length; i++) {
-            final String packageName = propertyGetter.get(
-                    SYSPROP_KEY + SYSPROP_KEY_ENTRY_SUFFIX + i);
-            if (!packageName.isEmpty()) {
-                mBlacklistedPackages.add(packageName);
+    private void updateBlacklist(@Nullable String property) {
+        synchronized (mLock) {
+            mBlacklistedPackages.clear();
+            if (property != null) {
+                String[] packages = property.split(",");
+                for (String pkg : packages) {
+                    String pkgName = pkg.trim();
+                    if (!pkgName.isEmpty()) {
+                        mBlacklistedPackages.add(pkgName);
+                    }
+                }
+            } else {
+                // If there's no config, or the config has been deleted, fallback to the device's
+                // default blacklist
+                for (String pkg : mDefaultBlacklist) {
+                    mBlacklistedPackages.add(pkg);
+                }
             }
         }
     }
 
     boolean isBlacklisted(String packageName) {
-        return mBlacklistedPackages.contains(packageName);
+        synchronized (mLock) {
+            return mBlacklistedPackages.contains(packageName);
+        }
+    }
+    void dump(PrintWriter pw) {
+        pw.println("High Refresh Rate Blacklist");
+        pw.println("  Packages:");
+        synchronized (mLock) {
+            for (String pkg : mBlacklistedPackages) {
+                pw.println("    " + pkg);
+            }
+        }
     }
 
-    interface SystemPropertyGetter {
-        int getInt(String key, int def);
-        @NonNull String get(String key);
+    interface DeviceConfigInterface {
+        @Nullable String getProperty(@NonNull String namespace, @NonNull String name);
+        void addOnPropertyChangedListener(@NonNull String namespace, @NonNull Executor executor,
+                @NonNull DeviceConfig.OnPropertyChangedListener listener);
+    }
+
+    private class OnPropertyChangedListener implements DeviceConfig.OnPropertyChangedListener {
+        public void onPropertyChanged(@NonNull String namespace, @NonNull String name,
+                @Nullable String value) {
+            updateBlacklist(value);
+        }
     }
 }
+
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 09ba2c8..f356b51 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -854,7 +854,7 @@
 
     final Configuration mTempConfiguration = new Configuration();
 
-    final HighRefreshRateBlacklist mHighRefreshRateBlacklist = HighRefreshRateBlacklist.create();
+    final HighRefreshRateBlacklist mHighRefreshRateBlacklist;
 
     // If true, only the core apps and services are being launched because the device
     // is in a special boot mode, such as being encrypted or waiting for a decryption password.
@@ -1141,6 +1141,8 @@
                 this, mInputManager, mActivityTaskManager, mH.getLooper());
         mDragDropController = new DragDropController(this, mH.getLooper());
 
+        mHighRefreshRateBlacklist = HighRefreshRateBlacklist.create(context.getResources());
+
         mSystemGestureExclusionLimitDp = Math.max(MIN_GESTURE_EXCLUSION_LIMIT_DP,
                 DeviceConfig.getInt(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                         KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP, 0));
@@ -5922,6 +5924,12 @@
         mRoot.dumpTokens(pw, dumpAll);
     }
 
+
+    private void dumpHighRefreshRateBlacklist(PrintWriter pw) {
+        pw.println("WINDOW MANAGER HIGH REFRESH RATE BLACKLIST (dumpsys window refresh)");
+        mHighRefreshRateBlacklist.dump(pw);
+    }
+
     private void dumpTraceStatus(PrintWriter pw) {
         pw.println("WINDOW MANAGER TRACE (dumpsys window trace)");
         pw.print(mWindowTracing.getStatus() + "\n");
@@ -6321,6 +6329,9 @@
             } else if ("trace".equals(cmd)) {
                 dumpTraceStatus(pw);
                 return;
+            } else if ("refresh".equals(cmd)) {
+                dumpHighRefreshRateBlacklist(pw);
+                return;
             } else {
                 // Dumping a single name?
                 if (!dumpWindows(pw, cmd, args, opti, dumpAll)) {
@@ -6376,6 +6387,10 @@
                 pw.println(separator);
             }
             dumpTraceStatus(pw);
+            if (dumpAll) {
+                pw.println(separator);
+            }
+            dumpHighRefreshRateBlacklist(pw);
         }
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/HighRefreshRateBlacklistTest.java b/services/tests/wmtests/src/com/android/server/wm/HighRefreshRateBlacklistTest.java
index 5586726..e02b69c4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/HighRefreshRateBlacklistTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/HighRefreshRateBlacklistTest.java
@@ -16,71 +16,153 @@
 
 package com.android.server.wm;
 
+import static android.provider.DeviceConfig.WindowManager.KEY_HIGH_REFRESH_RATE_BLACKLIST;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import android.content.res.Resources;
 import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+import android.util.Pair;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
-import com.android.server.wm.HighRefreshRateBlacklist.SystemPropertyGetter;
+import com.android.internal.R;
+import com.android.server.wm.HighRefreshRateBlacklist.DeviceConfigInterface;
 
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Build/Install/Run:
- *  atest WmTests:HighRefreshRateBlacklistTest
+ * atest WmTests:HighRefreshRateBlacklistTest
  */
 @SmallTest
 @Presubmit
-@FlakyTest
 public class HighRefreshRateBlacklistTest {
 
     @Test
-    public void testBlacklist() {
-        HighRefreshRateBlacklist blacklist = new HighRefreshRateBlacklist(
-                new SystemPropertyGetter() {
-
-                    @Override
-                    public int getInt(String key, int def) {
-                        if ("ro.window_manager.high_refresh_rate_blacklist_length".equals(key)) {
-                            return 2;
-                        }
-                        return def;
-                    }
-
-                    @Override
-                    public String get(String key) {
-                        if ("ro.window_manager.high_refresh_rate_blacklist_entry1".equals(key)) {
-                            return "com.android.sample1";
-                        }
-                        if ("ro.window_manager.high_refresh_rate_blacklist_entry2".equals(key)) {
-                            return "com.android.sample2";
-                        }
-                        return "";
-                    }
-                });
+    public void testDefaultBlacklist() {
+        final Resources r = createResources("com.android.sample1", "com.android.sample2");
+        HighRefreshRateBlacklist blacklist =
+                new HighRefreshRateBlacklist(r, new FakeDeviceConfigInterface());
         assertTrue(blacklist.isBlacklisted("com.android.sample1"));
         assertTrue(blacklist.isBlacklisted("com.android.sample2"));
         assertFalse(blacklist.isBlacklisted("com.android.sample3"));
     }
 
     @Test
-    public void testNoBlacklist() {
-        HighRefreshRateBlacklist blacklist = new HighRefreshRateBlacklist(
-                new SystemPropertyGetter() {
-
-                    @Override
-                    public int getInt(String key, int def) {
-                        return def;
-                    }
-
-                    @Override
-                    public String get(String key) {
-                        return "";
-                    }
-                });
+    public void testNoDefaultBlacklist() {
+        final Resources r = createResources();
+        HighRefreshRateBlacklist blacklist =
+                new HighRefreshRateBlacklist(r, new FakeDeviceConfigInterface());
         assertFalse(blacklist.isBlacklisted("com.android.sample1"));
     }
+
+    @Test
+    public void testDefaultBlacklistIsOverriddenByDeviceConfig() {
+        final Resources r = createResources("com.android.sample1");
+        final FakeDeviceConfigInterface config = new FakeDeviceConfigInterface();
+        config.setBlacklist("com.android.sample2,com.android.sample3");
+        HighRefreshRateBlacklist blacklist = new HighRefreshRateBlacklist(r, config);
+        assertFalse(blacklist.isBlacklisted("com.android.sample1"));
+        assertTrue(blacklist.isBlacklisted("com.android.sample2"));
+        assertTrue(blacklist.isBlacklisted("com.android.sample3"));
+    }
+
+    @Test
+    public void testDefaultBlacklistIsOverriddenByEmptyDeviceConfig() {
+        final Resources r = createResources("com.android.sample1");
+        final FakeDeviceConfigInterface config = new FakeDeviceConfigInterface();
+        config.setBlacklist("");
+        HighRefreshRateBlacklist blacklist = new HighRefreshRateBlacklist(r, config);
+        assertFalse(blacklist.isBlacklisted("com.android.sample1"));
+    }
+
+    @Test
+    public void testDefaultBlacklistIsOverriddenByDeviceConfigUpdate() {
+        final Resources r = createResources("com.android.sample1");
+        final FakeDeviceConfigInterface config = new FakeDeviceConfigInterface();
+        HighRefreshRateBlacklist blacklist = new HighRefreshRateBlacklist(r, config);
+
+        // First check that the default blacklist is in effect
+        assertTrue(blacklist.isBlacklisted("com.android.sample1"));
+        assertFalse(blacklist.isBlacklisted("com.android.sample2"));
+        assertFalse(blacklist.isBlacklisted("com.android.sample3"));
+
+        //  Then confirm that the DeviceConfig list has propagated and taken effect.
+        config.setBlacklist("com.android.sample2,com.android.sample3");
+        assertFalse(blacklist.isBlacklisted("com.android.sample1"));
+        assertTrue(blacklist.isBlacklisted("com.android.sample2"));
+        assertTrue(blacklist.isBlacklisted("com.android.sample3"));
+
+        //  Finally make sure we go back to the default list if the DeviceConfig gets deleted.
+        config.setBlacklist(null);
+        assertTrue(blacklist.isBlacklisted("com.android.sample1"));
+        assertFalse(blacklist.isBlacklisted("com.android.sample2"));
+        assertFalse(blacklist.isBlacklisted("com.android.sample3"));
+    }
+
+    private Resources createResources(String... defaultBlacklist) {
+        Resources r = mock(Resources.class);
+        when(r.getStringArray(R.array.config_highRefreshRateBlacklist))
+                .thenReturn(defaultBlacklist);
+        return r;
+    }
+
+
+    class FakeDeviceConfigInterface implements DeviceConfigInterface {
+        private List<Pair<DeviceConfig.OnPropertyChangedListener, Executor>> mListeners =
+                new ArrayList<>();
+        private String mBlacklist;
+
+        @Override
+        public String getProperty(String namespace, String name) {
+            if (!DeviceConfig.NAMESPACE_WINDOW_MANAGER.equals(namespace)
+                    || !KEY_HIGH_REFRESH_RATE_BLACKLIST.equals(name)) {
+                throw new IllegalArgumentException("Only things in NAMESPACE_WINDOW_MANAGER "
+                        + "supported.");
+            }
+            return mBlacklist;
+        }
+
+        @Override
+        public void addOnPropertyChangedListener(String namespace, Executor executor,
+                DeviceConfig.OnPropertyChangedListener listener) {
+
+            if (!DeviceConfig.NAMESPACE_WINDOW_MANAGER.equals(namespace)) {
+                throw new IllegalArgumentException("Only things in NAMESPACE_WINDOW_MANAGER "
+                        + "supported.");
+            }
+            mListeners.add(new Pair<>(listener, executor));
+        }
+
+        void setBlacklist(String blacklist) {
+            mBlacklist = blacklist;
+            CountDownLatch latch = new CountDownLatch(mListeners.size());
+            for (Pair<DeviceConfig.OnPropertyChangedListener, Executor> listenerInfo :
+                    mListeners) {
+                final Executor executor = listenerInfo.second;
+                final DeviceConfig.OnPropertyChangedListener listener = listenerInfo.first;
+                executor.execute(() -> {
+                    listener.onPropertyChanged(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                            KEY_HIGH_REFRESH_RATE_BLACKLIST, blacklist);
+                    latch.countDown();
+                });
+            }
+            try {
+                latch.await(10, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Failed to notify all blacklist listeners in time.", e);
+            }
+        }
+    }
 }