Add Settings switch to disable Game default frame rate

This patch adds a new toggle under Developer settings. It defaults
to off, meaning game default frame rate is not disabled. Users
can choose to togge it on to disable game default frame rate.

When a user toggles this switch, it calls to GameManagerService
to update the frame rate of games that are currently in the
foreground and coming games.

screenshots:
https://screenshot.googleplex.com/8jTWyNBhJm7zC4x
https://screenshot.googleplex.com/5junmXtuHnRxyL2

Bug: 286084594
Bug: 306266471
Test: m; flash
Test: atest
SettingsRoboTests:GameDefaultFrameRatePReferenceControllerTest
Change-Id: Ide843f61e57e244d6e1fc30f93b2358b2bcb655b
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 19927a2..0303b17 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -139,6 +139,7 @@
     <uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
     <uses-permission android:name="android.permission.ACCESS_GPU_SERVICE" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.MANAGE_GAME_MODE" />
 
     <application
             android:name=".SettingsApplication"
diff --git a/aconfig/settings_development_flag_declarations.aconfig b/aconfig/settings_development_flag_declarations.aconfig
index c23a38f..e12bccc 100644
--- a/aconfig/settings_development_flag_declarations.aconfig
+++ b/aconfig/settings_development_flag_declarations.aconfig
@@ -6,6 +6,13 @@
 # flags with 'development' to prevent naming collision.
 
 flag {
+    name: "development_game_default_frame_rate"
+    namespace: "game"
+    description: "This flag guards the new behavior with the addition of Game Default Frame Rate feature."
+    bug: "286084594"
+}
+
+flag {
   name: "development_hdr_sdr_ratio"
   namespace: "core_graphics"
   description: "Shows hdr/sdr dev opton on the development options page from aconfig"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 01f2525..780303d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2627,8 +2627,12 @@
     <string name="display_white_balance_summary"></string>
     <!-- Display settings screen, setting option name to change Fold setting -->
     <string name="fold_lock_behavior_title">Continue using apps on fold</string>
+    <!-- Display settings screen, game default frame rate settings title [CHAR LIMIT=30] -->
+    <string name="disable_game_default_frame_rate_title">Disable default frame rate for games</string>
+    <!-- Display settings screen, game default frame rate settings summary [CHAR LIMIT=NONE] -->
+    <string name="disable_game_default_frame_rate_summary">Disable limiting the maximum frame rate for games at <xliff:g id="frame_rate" example="60">%1$d</xliff:g> Hz.</string>
     <!-- Display settings screen, peak refresh rate settings title [CHAR LIMIT=30] -->
-    <string name="peak_refresh_rate_title">Smooth Display</string>
+    <string name="peak_refresh_rate_title">Smooth display</string>
     <!-- Display settings screen, peak refresh rate settings summary [CHAR LIMIT=NONE] -->
     <string name="peak_refresh_rate_summary">Automatically raises the refresh rate up to <xliff:g name="refresh_rate" example="120">%1$d</xliff:g> Hz for some content. Increases battery usage.</string>
     <!-- Display developer settings: Force to the highest refresh rate [CHAR LIMIT=NONE] -->
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index e1ccad8..fb5f419 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -255,6 +255,11 @@
             android:title="@string/enable_angle_as_system_driver"
             android:summary="@string/enable_angle_as_system_driver_summary" />
 
+        <SwitchPreferenceCompat
+            android:key="disable_game_default_frame_rate"
+            android:title="@string/disable_game_default_frame_rate_title"
+            android:summary="@string/disable_game_default_frame_rate_summary"/>
+
         <Preference
             android:key="graphics_driver_dashboard"
             android:title="@string/graphics_driver_dashboard_title"
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index a483f9f..8b7f443 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -688,6 +688,7 @@
         controllers.add(new HardwareLayersUpdatesPreferenceController(context));
         controllers.add(new DebugGpuOverdrawPreferenceController(context));
         controllers.add(new DebugNonRectClipOperationsPreferenceController(context));
+        controllers.add(new GameDefaultFrameRatePreferenceController(context));
         controllers.add(new ForceDarkPreferenceController(context));
         controllers.add(new EnableBlursPreferenceController(context));
         controllers.add(new ForceMSAAPreferenceController(context));
diff --git a/src/com/android/settings/development/DevelopmentSystemPropertiesWrapper.java b/src/com/android/settings/development/DevelopmentSystemPropertiesWrapper.java
new file mode 100644
index 0000000..e8a64d2
--- /dev/null
+++ b/src/com/android/settings/development/DevelopmentSystemPropertiesWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.settings.development;
+
+import android.annotation.NonNull;
+import android.os.SystemProperties;
+/**
+ * Wrapper interface to access {@link SystemProperties}.
+ *
+ * @hide
+ */
+
+public interface DevelopmentSystemPropertiesWrapper {
+    /**
+     * Get the String value for the given {@code key}.
+     *
+     * @param key the key to lookup
+     * @param def the default value in case the property is not set or empty
+     * @return if the {@code key} isn't found, return {@code def} if it isn't null, or an empty
+     * string otherwise
+     */
+    @NonNull
+    String get(@NonNull String key, @NonNull String def);
+    /**
+     * Set the value for the given {@code key} to {@code val}.
+     *
+     * @throws IllegalArgumentException if the {@code val} exceeds 91 characters
+     * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+     * SELinux. libc will log the underlying reason.
+     */
+    void set(@NonNull String key, @NonNull String val);
+
+    /**
+     * Get the Integer value for the given {@code key}.
+     *
+     * @param key the key to lookup
+     * @param def the default value in case the property is not set or empty
+     * @return if the {@code key} isn't found, return {@code def} if it isn't null, not parsable
+     * or an empty string otherwise
+     */
+    @NonNull
+    int getInt(@NonNull String key, @NonNull int def);
+
+    /**
+     * Get the boolean value for the given {@code key}.
+     *
+     * @param key the key to lookup
+     * @param def the default value in case the property is not set or empty
+     * @return if the {@code key} isn't found, return {@code def}.
+     */
+    boolean getBoolean(@NonNull String key, @NonNull boolean def);
+}
diff --git a/src/com/android/settings/development/GameDefaultFrameRatePreferenceController.java b/src/com/android/settings/development/GameDefaultFrameRatePreferenceController.java
new file mode 100644
index 0000000..00001fb
--- /dev/null
+++ b/src/com/android/settings/development/GameDefaultFrameRatePreferenceController.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.settings.development;
+
+
+import android.app.IGameManagerService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.R;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.flags.Flags;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+public class GameDefaultFrameRatePreferenceController extends DeveloperOptionsPreferenceController
+        implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin  {
+    private static final String TAG = "GameDefFrameRatePrefCtr";
+    private static final String DISABLE_GAME_DEFAULT_FRAME_RATE_KEY =
+            "disable_game_default_frame_rate";
+    private final IGameManagerService mGameManagerService;
+    static final String PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED =
+            "debug.graphics.game_default_frame_rate.disabled";
+
+    private final DevelopmentSystemPropertiesWrapper mSysProps;
+    private int mGameDefaultFrameRateValue;
+
+    @VisibleForTesting
+    static class Injector {
+        public DevelopmentSystemPropertiesWrapper createSystemPropertiesWrapper() {
+            return new DevelopmentSystemPropertiesWrapper() {
+                @Override
+                public String get(String key, String def) {
+                    return SystemProperties.get(key, def);
+                }
+                @Override
+                public boolean getBoolean(String key, boolean def) {
+                    return SystemProperties.getBoolean(key, def);
+                }
+
+                @Override
+                public int getInt(String key, int def) {
+                    return SystemProperties.getInt(key, def);
+                }
+
+                @Override
+                public void set(String key, String val) {
+                    SystemProperties.set(key, val);
+                }
+            };
+        }
+    }
+
+    public GameDefaultFrameRatePreferenceController(Context context) {
+        super(context);
+        mGameManagerService = IGameManagerService.Stub.asInterface(
+                ServiceManager.getService(Context.GAME_SERVICE));
+
+        mSysProps = new Injector().createSystemPropertiesWrapper();
+
+        mGameDefaultFrameRateValue = mSysProps.getInt(
+                "ro.surface_flinger.game_default_frame_rate_override", 60);
+    }
+
+    @VisibleForTesting
+    GameDefaultFrameRatePreferenceController(Context context,
+                                             IGameManagerService gameManagerService,
+                                             Injector injector) {
+        super(context);
+        mGameManagerService = gameManagerService;
+        mSysProps = injector.createSystemPropertiesWrapper();
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return DISABLE_GAME_DEFAULT_FRAME_RATE_KEY;
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        final boolean isDisabled = (Boolean) newValue;
+        try {
+            mGameManagerService.toggleGameDefaultFrameRate(!isDisabled);
+            updateGameDefaultPreferenceSetting();
+        } catch (RemoteException e) {
+            // intentional no-op
+        }
+        return true;
+    }
+
+    private void updateGameDefaultPreferenceSetting() {
+        final boolean isDisabled =
+                mSysProps.getBoolean(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED,
+                        false);
+        ((TwoStatePreference) mPreference).setChecked(isDisabled);
+        mPreference.setSummary(mContext.getString(
+                R.string.disable_game_default_frame_rate_summary,
+                mGameDefaultFrameRateValue));
+    }
+    @Override
+    public void updateState(Preference preference) {
+        super.updateState(preference);
+        updateGameDefaultPreferenceSetting();
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return Flags.developmentGameDefaultFrameRate();
+    }
+
+    @Override
+    protected void onDeveloperOptionsSwitchDisabled() {
+        super.onDeveloperOptionsSwitchDisabled();
+        final TwoStatePreference preference = (TwoStatePreference) mPreference;
+        if (preference.isChecked()) {
+            // When the developer option is disabled, we should set everything
+            // to off, that is, enabling game default frame rate.
+            try {
+                mGameManagerService.toggleGameDefaultFrameRate(true);
+            } catch (RemoteException e) {
+                // intentional no-op
+            }
+        }
+        preference.setChecked(false);
+    }
+
+}
diff --git a/src/com/android/settings/development/OWNERS b/src/com/android/settings/development/OWNERS
index 6443afe..09a4914 100644
--- a/src/com/android/settings/development/OWNERS
+++ b/src/com/android/settings/development/OWNERS
@@ -1,3 +1,6 @@
+# GameDefaultFrameRatePreferenceController
+per-file GameDefaultFrameRatePreferenceController.java=file:platform/frameworks/base:/GAME_MANAGER_OWNERS
+
 # ShowHdrSdrRatioPreferenceController
 per-file ShowHdrSdrRatioPreferenceController.java=file:platform/frameworks/native:/services/surfaceflinger/OWNERS
 
diff --git a/tests/robotests/src/com/android/settings/development/GameDefaultFrameRatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/GameDefaultFrameRatePreferenceControllerTest.java
new file mode 100644
index 0000000..d433905
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/GameDefaultFrameRatePreferenceControllerTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.settings.development;
+
+import static com.android.settings.development.GameDefaultFrameRatePreferenceController.Injector;
+import static com.android.settings.development.GameDefaultFrameRatePreferenceController.PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.IGameManagerService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.preference.PreferenceScreen;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class GameDefaultFrameRatePreferenceControllerTest {
+    @Mock
+    private Context mContext;
+    @Mock
+    private PreferenceScreen mScreen;
+    @Mock
+    private TwoStatePreference mPreference;
+    @Mock
+    private IGameManagerService mGameManagerService;
+    @Mock
+    private DevelopmentSystemPropertiesWrapper mSysPropsMock;
+
+    private GameDefaultFrameRatePreferenceController mController;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mController = new GameDefaultFrameRatePreferenceController(mContext, mGameManagerService,
+                new Injector(){
+                    @Override
+                    public DevelopmentSystemPropertiesWrapper createSystemPropertiesWrapper() {
+                        return mSysPropsMock;
+                    }
+                });
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+        mController.displayPreference(mScreen);
+    }
+
+    @Test
+    public void onPreferenceChange_settingEnabled_shouldChecked() throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        assertTrue(mController.isAvailable());
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(true);
+
+        mController.onPreferenceChange(mPreference, true /* new value */);
+        verify(mPreference).setChecked(true);
+    }
+
+    @Test
+    public void onPreferenceChange_settingDisabled_shouldUnchecked() throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        assertTrue(mController.isAvailable());
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(false);
+        mController.onPreferenceChange(mPreference, false /* new value */);
+        verify(mPreference).setChecked(false);
+    }
+
+    @Test
+    public void updateState_settingEnabled_shouldChecked() throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        assertTrue(mController.isAvailable());
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(true);
+        mController.updateState(mPreference);
+        verify(mPreference).setChecked(true);
+    }
+
+    @Test
+    public void updateState_settingDisabled_shouldUnchecked() throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        assertTrue(mController.isAvailable());
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(false);
+        mController.updateState(mPreference);
+        verify(mPreference).setChecked(false);
+    }
+
+    @Test
+    public void settingNotAvailable_flagsOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        mController = new GameDefaultFrameRatePreferenceController(
+                mContext, mGameManagerService, new Injector());
+        assertFalse(mController.isAvailable());
+    }
+
+    @Test
+    public void settingAvailable_flagsOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        mController = new GameDefaultFrameRatePreferenceController(
+                mContext, mGameManagerService, new Injector());
+        assertTrue(mController.isAvailable());
+    }
+
+    @Test
+    public void onDeveloperOptionsSwitchDisabled_preferenceUnchecked_shouldNotTurnOffPreference()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(false);
+        assertTrue(mController.isAvailable());
+        when(mPreference.isChecked()).thenReturn(false);
+        mController.onDeveloperOptionsSwitchDisabled();
+
+        verify(mPreference).setChecked(false);
+        verify(mPreference).setEnabled(false);
+    }
+
+    @Test
+    public void onDeveloperOptionsSwitchDisabled_preferenceChecked_shouldTurnOffPreference()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DEVELOPMENT_GAME_DEFAULT_FRAME_RATE);
+        when(mSysPropsMock.getBoolean(
+                ArgumentMatchers.eq(PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED),
+                ArgumentMatchers.eq(false)))
+                .thenReturn(true);
+        assertTrue(mController.isAvailable());
+
+        when(mPreference.isChecked()).thenReturn(true);
+        mController.onDeveloperOptionsSwitchDisabled();
+
+        verify(mPreference).setChecked(false);
+        verify(mPreference).setEnabled(false);
+    }
+}