diff --git a/core/api/current.txt b/core/api/current.txt index 41e75909493d0..b672baab56a65 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -326,6 +326,9 @@ package android { field public static final int allowClearUserData = 16842757; // 0x1010005 field public static final int allowClickWhenDisabled = 16844312; // 0x1010618 field public static final int allowEmbedded = 16843765; // 0x10103f5 + field public static final int allowGameAngleDriver; + field public static final int allowGameDownscaling; + field public static final int allowGameFpsOverride; field public static final int allowNativeHeapPointerTagging = 16844306; // 0x1010612 field public static final int allowParallelSyncs = 16843570; // 0x1010332 field public static final int allowSingleTap = 16843353; // 0x1010259 @@ -1435,10 +1438,12 @@ package android { field public static final int summaryOn = 16843247; // 0x10101ef field public static final int supportedTypes; field public static final int supportsAssist = 16844016; // 0x10104f0 + field public static final int supportsBatteryGameMode; field public static final int supportsInlineSuggestions = 16844301; // 0x101060d field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1 field public static final int supportsLocalInteraction = 16844047; // 0x101050f field public static final int supportsMultipleDisplays = 16844182; // 0x1010596 + field public static final int supportsPerformanceGameMode; field public static final int supportsPictureInPicture = 16844023; // 0x10104f7 field public static final int supportsRtl = 16843695; // 0x10103af field public static final int supportsStylusHandwriting; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 52a180b3c9e07..1d3254449ac68 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -278,6 +278,7 @@ package android.app { } public final class GameManager { + method @RequiresPermission(android.Manifest.permission.MANAGE_GAME_MODE) public boolean isAngleEnabled(@NonNull String); method public void setGameServiceProvider(@Nullable String); } diff --git a/core/java/android/app/GameManager.java b/core/java/android/app/GameManager.java index 289b3486b7fe3..040399ecb83be 100644 --- a/core/java/android/app/GameManager.java +++ b/core/java/android/app/GameManager.java @@ -181,14 +181,18 @@ public final class GameManager { /** * Returns if ANGLE is enabled for a given package and user ID. *

+ * ANGLE (Almost Native Graphics Layer Engine) can translate OpenGL ES commands to Vulkan + * commands. Enabling ANGLE may improve the performance and/or reduce the power consumption of + * applications. * The caller must have {@link android.Manifest.permission#MANAGE_GAME_MODE}. * * @hide */ + @TestApi @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE) - public @GameMode boolean getAngleEnabled(@NonNull String packageName) { + public @GameMode boolean isAngleEnabled(@NonNull String packageName) { try { - return mService.getAngleEnabled(packageName, mContext.getUserId()); + return mService.isAngleEnabled(packageName, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/IGameManagerService.aidl b/core/java/android/app/IGameManagerService.aidl index 3ea0767661bbd..7035ac078334c 100644 --- a/core/java/android/app/IGameManagerService.aidl +++ b/core/java/android/app/IGameManagerService.aidl @@ -26,7 +26,7 @@ interface IGameManagerService { int getGameMode(String packageName, int userId); void setGameMode(String packageName, int gameMode, int userId); int[] getAvailableGameModes(String packageName); - boolean getAngleEnabled(String packageName, int userId); + boolean isAngleEnabled(String packageName, int userId); void setGameState(String packageName, in GameState gameState, int userId); GameModeInfo getGameModeInfo(String packageName, int userId); void setGameServiceProvider(String packageName); diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index aa4b83a5c3616..0c3514fce76e3 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -165,7 +165,7 @@ public class GraphicsEnvironment { private boolean isAngleEnabledByGameMode(Context context, String packageName) { try { final boolean gameModeEnabledAngle = - (mGameManager != null) && mGameManager.getAngleEnabled(packageName); + (mGameManager != null) && mGameManager.isAngleEnabled(packageName); Log.v(TAG, "ANGLE GameManagerService for " + packageName + ": " + gameModeEnabledAngle); return gameModeEnabledAngle; } catch (SecurityException e) { diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 04e29890568a9..afe0f1bf00019 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8875,6 +8875,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index 3c9d29d77bbe6..551773e3e8343 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -20,6 +20,11 @@ import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_CHANGED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static com.android.internal.R.styleable.GameModeConfig_allowGameAngleDriver; +import static com.android.internal.R.styleable.GameModeConfig_allowGameDownscaling; +import static com.android.internal.R.styleable.GameModeConfig_allowGameFpsOverride; +import static com.android.internal.R.styleable.GameModeConfig_supportsBatteryGameMode; +import static com.android.internal.R.styleable.GameModeConfig_supportsPerformanceGameMode; import static com.android.server.wm.CompatModePackages.DOWNSCALED; import static com.android.server.wm.CompatModePackages.DOWNSCALE_30; import static com.android.server.wm.CompatModePackages.DOWNSCALE_35; @@ -54,6 +59,10 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; import android.hardware.power.Mode; import android.net.Uri; import android.os.Binder; @@ -71,8 +80,10 @@ import android.os.ShellCallback; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.util.ArrayMap; +import android.util.AttributeSet; import android.util.KeyValueListParser; import android.util.Slog; +import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -84,7 +95,11 @@ import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.SystemService.TargetUser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.util.List; @@ -425,12 +440,20 @@ public final class GameManagerService extends IGameManagerService.Stub { public static final String METADATA_BATTERY_MODE_ENABLE = "com.android.app.gamemode.battery.enabled"; + /** + * Metadata that allows a game to specify all intervention information with an XML file in + * the application field. + */ + public static final String METADATA_GAME_MODE_CONFIG = "android.game_mode_config"; + + private static final String GAME_MODE_CONFIG_NODE_NAME = "game-mode-config"; private final String mPackageName; private final ArrayMap mModeConfigs; private boolean mPerfModeOptedIn; private boolean mBatteryModeOptedIn; private boolean mAllowDownscale; private boolean mAllowAngle; + private boolean mAllowFpsOverride; GamePackageConfiguration(String packageName, int userId) { mPackageName = packageName; @@ -438,18 +461,21 @@ public final class GameManagerService extends IGameManagerService.Stub { try { final ApplicationInfo ai = mPackageManager.getApplicationInfoAsUser(packageName, PackageManager.GET_META_DATA, userId); - if (ai.metaData != null) { - mPerfModeOptedIn = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE); - mBatteryModeOptedIn = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE); - mAllowDownscale = ai.metaData.getBoolean(METADATA_WM_ALLOW_DOWNSCALE, true); - mAllowAngle = ai.metaData.getBoolean(METADATA_ANGLE_ALLOW_ANGLE, true); - } else { - mPerfModeOptedIn = false; - mBatteryModeOptedIn = false; - mAllowDownscale = true; - mAllowAngle = true; + if (!parseInterventionFromXml(ai, packageName)) { + if (ai.metaData != null) { + mPerfModeOptedIn = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE); + mBatteryModeOptedIn = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE); + mAllowDownscale = ai.metaData.getBoolean(METADATA_WM_ALLOW_DOWNSCALE, true); + mAllowAngle = ai.metaData.getBoolean(METADATA_ANGLE_ALLOW_ANGLE, true); + } else { + mPerfModeOptedIn = false; + mBatteryModeOptedIn = false; + mAllowDownscale = true; + mAllowAngle = true; + mAllowFpsOverride = true; + } } - } catch (PackageManager.NameNotFoundException e) { + } catch (NameNotFoundException e) { // Not all packages are installed, hence ignore those that are not installed yet. Slog.v(TAG, "Failed to get package metadata"); } @@ -469,6 +495,53 @@ public final class GameManagerService extends IGameManagerService.Stub { } } + private boolean parseInterventionFromXml(ApplicationInfo ai, String packageName) { + boolean xmlFound = false; + try (XmlResourceParser parser = ai.loadXmlMetaData(mPackageManager, + METADATA_GAME_MODE_CONFIG)) { + if (parser == null) { + Slog.v(TAG, "No " + METADATA_GAME_MODE_CONFIG + + " meta-data found for package " + mPackageName); + } else { + xmlFound = true; + final Resources resources = mPackageManager.getResourcesForApplication( + packageName); + final AttributeSet attributeSet = Xml.asAttributeSet(parser); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Do nothing + } + + boolean isStartingTagGameModeConfig = + GAME_MODE_CONFIG_NODE_NAME.equals(parser.getName()); + if (!isStartingTagGameModeConfig) { + Slog.w(TAG, "Meta-data does not start with " + + GAME_MODE_CONFIG_NODE_NAME + + " tag"); + } else { + final TypedArray array = resources.obtainAttributes(attributeSet, + com.android.internal.R.styleable.GameModeConfig); + mPerfModeOptedIn = array.getBoolean( + GameModeConfig_supportsPerformanceGameMode, false); + mBatteryModeOptedIn = array.getBoolean( + GameModeConfig_supportsBatteryGameMode, + false); + mAllowDownscale = array.getBoolean(GameModeConfig_allowGameDownscaling, + true); + mAllowAngle = array.getBoolean(GameModeConfig_allowGameAngleDriver, true); + mAllowFpsOverride = array.getBoolean(GameModeConfig_allowGameFpsOverride, + true); + array.recycle(); + } + } + } catch (NameNotFoundException | XmlPullParserException | IOException ex) { + Slog.e(TAG, "Error while parsing XML meta-data for " + + METADATA_GAME_MODE_CONFIG); + } + return xmlFound; + } + /** * GameModeConfiguration contains all the values for all the interventions associated with * a game mode. @@ -497,7 +570,8 @@ public final class GameManagerService extends IGameManagerService.Stub { mScaling = !mAllowDownscale || willGamePerformOptimizations(mGameMode) ? DEFAULT_SCALING : parser.getString(SCALING_KEY, DEFAULT_SCALING); - mFps = parser.getString(FPS_KEY, DEFAULT_FPS); + mFps = mAllowFpsOverride && !willGamePerformOptimizations(mGameMode) + ? parser.getString(FPS_KEY, DEFAULT_FPS) : DEFAULT_FPS; // We only want to use ANGLE if: // - We're allowed to use ANGLE (the app hasn't opted out via the manifest) AND // - The app has not opted in to performing the work itself AND @@ -691,7 +765,7 @@ public final class GameManagerService extends IGameManagerService.Stub { try { return mPackageManager.getPackageUidAsUser(packageName, userId) == Binder.getCallingUid(); - } catch (PackageManager.NameNotFoundException e) { + } catch (NameNotFoundException e) { return false; } } @@ -855,7 +929,7 @@ public final class GameManagerService extends IGameManagerService.Stub { */ @Override @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE) - public @GameMode boolean getAngleEnabled(String packageName, int userId) + public @GameMode boolean isAngleEnabled(String packageName, int userId) throws SecurityException { final int gameMode = getGameMode(packageName, userId); if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) { @@ -1413,7 +1487,7 @@ public final class GameManagerService extends IGameManagerService.Stub { if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) { return; } - } catch (PackageManager.NameNotFoundException e) { + } catch (NameNotFoundException e) { // Ignore the exception. } switch (intent.getAction()) { diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml b/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml new file mode 100644 index 0000000000000..eb154518c911a --- /dev/null +++ b/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml b/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml new file mode 100644 index 0000000000000..65b7467b80f5a --- /dev/null +++ b/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 44b81d4d4100c..d2358a08624d1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -26,6 +26,8 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -41,6 +43,9 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.hardware.power.Mode; import android.os.Bundle; import android.os.PowerManagerInternal; @@ -159,11 +164,17 @@ public class GameManagerServiceTests { mPackageName = mMockContext.getPackageName(); final ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.category = ApplicationInfo.CATEGORY_GAME; + applicationInfo.packageName = mPackageName; final PackageInfo pi = new PackageInfo(); pi.packageName = mPackageName; pi.applicationInfo = applicationInfo; final List packages = new ArrayList<>(); packages.add(pi); + + final Resources resources = + InstrumentationRegistry.getInstrumentation().getContext().getResources(); + when(mMockPackageManager.getResourcesForApplication(anyString())) + .thenReturn(resources); when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt())) .thenReturn(packages); when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) @@ -322,6 +333,46 @@ public class GameManagerServiceTests { .thenReturn(applicationInfo); } + private void mockInterventionsEnabledFromXml() throws Exception { + final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser( + mPackageName, PackageManager.GET_META_DATA, USER_ID_1); + Bundle metaDataBundle = new Bundle(); + final int resId = 123; + metaDataBundle.putInt( + GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId); + applicationInfo.metaData = metaDataBundle; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + seedGameManagerServiceMetaDataFromFile(mPackageName, resId, + "res/xml/gama_manager_service_metadata_config_enabled.xml"); + } + + private void mockInterventionsDisabledFromXml() throws Exception { + final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser( + mPackageName, PackageManager.GET_META_DATA, USER_ID_1); + Bundle metaDataBundle = new Bundle(); + final int resId = 123; + metaDataBundle.putInt( + GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId); + applicationInfo.metaData = metaDataBundle; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + seedGameManagerServiceMetaDataFromFile(mPackageName, resId, + "res/xml/gama_manager_service_metadata_config_disabled.xml"); + } + + + private void seedGameManagerServiceMetaDataFromFile(String packageName, int resId, + String fileName) + throws Exception { + AssetManager assetManager = + InstrumentationRegistry.getInstrumentation().getContext().getAssets(); + XmlResourceParser xmlResourceParser = + assetManager.openXmlResourceParser(fileName); + when(mMockPackageManager.getXml(eq(packageName), eq(resId), any())) + .thenReturn(xmlResourceParser); + } + /** * By default game mode is not supported. */ @@ -511,8 +562,8 @@ public class GameManagerServiceTests { gameManagerService.getConfig(mPackageName); assertEquals(config.getGameModeConfiguration(gameMode).getUseAngle(), angleEnabled); - // Validate GameManagerService.getAngleEnabled() returns the correct value. - assertEquals(gameManagerService.getAngleEnabled(mPackageName, USER_ID_1), angleEnabled); + // Validate GameManagerService.isAngleEnabled() returns the correct value. + assertEquals(gameManagerService.isAngleEnabled(mPackageName, USER_ID_1), angleEnabled); } private void checkFps(GameManagerService gameManagerService, int gameMode, int fps) { @@ -523,7 +574,7 @@ public class GameManagerServiceTests { } GameManagerService.GamePackageConfiguration config = gameManagerService.getConfig(mPackageName); - assertEquals(config.getGameModeConfiguration(gameMode).getFps(), fps); + assertEquals(fps, config.getGameModeConfiguration(gameMode).getFps()); } /** @@ -904,6 +955,36 @@ public class GameManagerServiceTests { checkAngleEnabled(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, true); } + @Test + public void testGameModeConfigAllowFpsTrue() throws Exception { + mockDeviceConfigAll(); + mockModifyGameModeGranted(); + mockInterventionsEnabledFromXml(); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); + startUser(gameManagerService, USER_ID_1); + GameManagerService.GamePackageConfiguration config = + gameManagerService.getConfig(mPackageName); + assertEquals(90, + config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE).getFps()); + assertEquals(30, config.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY).getFps()); + } + + @Test + public void testGameModeConfigAllowFpsFalse() throws Exception { + mockDeviceConfigAll(); + mockModifyGameModeGranted(); + mockInterventionsDisabledFromXml(); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); + startUser(gameManagerService, USER_ID_1); + GameManagerService.GamePackageConfiguration config = + gameManagerService.getConfig(mPackageName); + assertEquals(0, + config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE).getFps()); + assertEquals(0, config.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY).getFps()); + } + @Test public void testInterventionFps() throws Exception { mockDeviceConfigAll();