diff --git a/core/java/android/app/GameManager.java b/core/java/android/app/GameManager.java
index 47de04078b457..5964f71d28db9 100644
--- a/core/java/android/app/GameManager.java
+++ b/core/java/android/app/GameManager.java
@@ -135,4 +135,20 @@ public final class GameManager {
throw e.rethrowFromSystemServer();
}
}
+ /**
+ * Returns a list of supported game modes for a given package.
+ *
+ * The caller must have {@link android.Manifest.permission#MANAGE_GAME_MODE}.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
+ public @GameMode int[] getAvailableGameModes(@NonNull String packageName) {
+ try {
+ return mService.getAvailableGameModes(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
}
diff --git a/core/java/android/app/IGameManagerService.aidl b/core/java/android/app/IGameManagerService.aidl
index c8e1478a9930e..4bf8a3f77bca8 100644
--- a/core/java/android/app/IGameManagerService.aidl
+++ b/core/java/android/app/IGameManagerService.aidl
@@ -22,4 +22,5 @@ package android.app;
interface IGameManagerService {
int getGameMode(String packageName, int userId);
void setGameMode(String packageName, int gameMode, int userId);
+ int[] getAvailableGameModes(String packageName);
}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 33f59f893b335..a539df78eaedc 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5564,6 +5564,7 @@
@hide
Not for use by third-party applications.
-->
+
mSettings = new ArrayMap<>();
+ @GuardedBy("mDeviceConfigLock")
+ private final ArrayMap mConfigs = new ArrayMap<>();
public GameManagerService(Context context) {
this(context, createServiceThread().getLooper());
@@ -76,7 +107,9 @@ public final class GameManagerService extends IGameManagerService.Stub {
GameManagerService(Context context, Looper looper) {
mContext = context;
mHandler = new SettingsHandler(looper);
- mPackageManager = context.getPackageManager();
+ mPackageManager = mContext.getPackageManager();
+ mPlatformCompat = IPlatformCompat.Stub.asInterface(
+ ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
}
@Override
@@ -143,10 +176,186 @@ public final class GameManagerService extends IGameManagerService.Stub {
}
break;
}
+ case POPULATE_GAME_MODE_SETTINGS: {
+ removeMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
+ loadDeviceConfigLocked();
+ break;
+ }
}
}
}
+ private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+
+ DeviceConfigListener() {
+ super();
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_GAME_OVERLAY,
+ mContext.getMainExecutor(), this);
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ synchronized (mDeviceConfigLock) {
+ for (String key : properties.getKeyset()) {
+ try {
+ // Check if the package is installed before caching it.
+ final String packageName = keyToPackageName(key);
+ mPackageManager.getPackageInfo(packageName, 0);
+ final GamePackageConfiguration config =
+ GamePackageConfiguration.fromProperties(key, properties);
+ if (config.isValid()) {
+ putConfig(config);
+ } else {
+ // This means that we received a bad config, or the config was deleted.
+ Slog.i(TAG, "Removing config for: " + packageName);
+ mConfigs.remove(packageName);
+ disableCompatScale(packageName);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ if (DEBUG) {
+ Slog.v(TAG, "Package name not found", e);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void finalize() {
+ DeviceConfig.removeOnPropertiesChangedListener(this);
+ }
+ }
+
+ private static class GameModeConfiguration {
+ public static final String TAG = "GameManagerService_GameModeConfiguration";
+ public static final String MODE_KEY = "mode";
+ public static final String SCALING_KEY = "downscaleFactor";
+
+ private final @GameMode int mGameMode;
+ private final String mScaling;
+
+ private GameModeConfiguration(@NonNull int gameMode,
+ @NonNull String scaling) {
+ mGameMode = gameMode;
+ mScaling = scaling;
+ }
+
+ public static GameModeConfiguration fromKeyValueListParser(KeyValueListParser parser) {
+ return new GameModeConfiguration(
+ parser.getInt(MODE_KEY, GameManager.GAME_MODE_UNSUPPORTED),
+ parser.getString(SCALING_KEY, "1.0")
+ );
+ }
+
+ public int getGameMode() {
+ return mGameMode;
+ }
+
+ public String getScaling() {
+ return mScaling;
+ }
+
+ public boolean isValid() {
+ return (mGameMode == GameManager.GAME_MODE_PERFORMANCE
+ || mGameMode == GameManager.GAME_MODE_BATTERY) && getCompatChangeId() != 0;
+ }
+
+ public String toString() {
+ return "[Game Mode:" + mGameMode + ",Scaling:" + mScaling + "]";
+ }
+
+ public long getCompatChangeId() {
+ switch (mScaling) {
+ case "0.5":
+ return DOWNSCALE_50;
+ case "0.6":
+ return DOWNSCALE_60;
+ case "0.7":
+ return DOWNSCALE_70;
+ case "0.8":
+ return DOWNSCALE_80;
+ case "0.9":
+ return DOWNSCALE_90;
+ }
+ return 0;
+ }
+ }
+
+ private static class GamePackageConfiguration {
+ public static final String TAG = "GameManagerService_GamePackageConfiguration";
+
+ private final String mPackageName;
+ private final ArrayMap mModeConfigs;
+
+ private GamePackageConfiguration(String keyName) {
+ mPackageName = keyToPackageName(keyName);
+ mModeConfigs = new ArrayMap<>();
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public @GameMode int[] getAvailableGameModes() {
+ if (mModeConfigs.keySet().size() > 0) {
+ return mModeConfigs.keySet().stream()
+ .mapToInt(Integer::intValue).toArray();
+ }
+ return new int[]{GameManager.GAME_MODE_UNSUPPORTED};
+ }
+
+ /**
+ * Get a GameModeConfiguration for a given game mode.
+ *
+ * @return The package's GameModeConfiguration for the provided mode or null if absent
+ */
+ public GameModeConfiguration getGameModeConfiguration(@GameMode int gameMode) {
+ return mModeConfigs.get(gameMode);
+ }
+
+ /**
+ * Insert a new GameModeConfiguration
+ */
+ public void addModeConfig(GameModeConfiguration config) {
+ if (config.isValid()) {
+ mModeConfigs.put(config.getGameMode(), config);
+ } else {
+ Slog.w(TAG, "Invalid game mode config for "
+ + mPackageName + ":" + config.toString());
+ }
+ }
+
+ /**
+ * Create a new instance from a package name and DeviceConfig.Properties instance
+ */
+ public static GamePackageConfiguration fromProperties(String key,
+ Properties properties) {
+ final GamePackageConfiguration packageConfig = new GamePackageConfiguration(key);
+ final String configString = properties.getString(key, "");
+ final String[] gameModeConfigStrings = configString.split(":");
+ for (String gameModeConfigString : gameModeConfigStrings) {
+ try {
+ final KeyValueListParser parser = new KeyValueListParser(',');
+ parser.setString(gameModeConfigString);
+ final GameModeConfiguration config =
+ GameModeConfiguration.fromKeyValueListParser(parser);
+ packageConfig.addModeConfig(config);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Invalid config string");
+ }
+ }
+ return packageConfig;
+ }
+
+ public boolean isValid() {
+ return mModeConfigs.size() > 0;
+ }
+
+ public String toString() {
+ return "[Name:" + mPackageName + " Modes: " + mModeConfigs.toString() + "]";
+ }
+ }
+
/**
* SystemService lifecycle for GameService.
*
@@ -163,6 +372,8 @@ public final class GameManagerService extends IGameManagerService.Stub {
public void onStart() {
mService = new GameManagerService(getContext());
publishBinderService(Context.GAME_SERVICE, mService);
+ mService.registerDeviceConfigListener();
+ mService.registerPackageReceiver();
}
@Override
@@ -200,10 +411,27 @@ public final class GameManagerService extends IGameManagerService.Stub {
}
}
+ /**
+ * Get an array of game modes available for a given package.
+ * Checks that the caller has {@link android.Manifest.permission#MANAGE_GAME_MODE}.
+ */
+ @Override
+ @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
+ public @GameMode int[] getAvailableGameModes(String packageName) throws SecurityException {
+ checkPermission(Manifest.permission.MANAGE_GAME_MODE);
+ synchronized (mDeviceConfigLock) {
+ final GamePackageConfiguration config = mConfigs.get(packageName);
+ if (config == null) {
+ return new int[]{GameManager.GAME_MODE_UNSUPPORTED};
+ }
+ return config.getAvailableGameModes();
+ }
+ }
+
private @GameMode int getGameModeFromSettings(String packageName, int userId) {
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
- Log.w(TAG, "User ID '" + userId + "' does not have a Game Mode"
+ Slog.w(TAG, "User ID '" + userId + "' does not have a Game Mode"
+ " selected for package: '" + packageName + "'");
return GameManager.GAME_MODE_UNSUPPORTED;
}
@@ -229,7 +457,7 @@ public final class GameManagerService extends IGameManagerService.Stub {
final ApplicationInfo applicationInfo = mPackageManager
.getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
- Log.e(TAG, "Ignoring attempt to get the Game Mode for '" + packageName
+ Slog.e(TAG, "Ignoring attempt to get the Game Mode for '" + packageName
+ "' which is not categorized as a game: applicationInfo.flags = "
+ applicationInfo.flags + ", category = " + applicationInfo.category);
return GameManager.GAME_MODE_UNSUPPORTED;
@@ -269,7 +497,7 @@ public final class GameManagerService extends IGameManagerService.Stub {
final ApplicationInfo applicationInfo = mPackageManager
.getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
- Log.e(TAG, "Ignoring attempt to set the Game Mode for '" + packageName
+ Slog.e(TAG, "Ignoring attempt to set the Game Mode for '" + packageName
+ "' which is not categorized as a game: applicationInfo.flags = "
+ applicationInfo.flags + ", category = " + applicationInfo.category);
return;
@@ -295,6 +523,7 @@ public final class GameManagerService extends IGameManagerService.Stub {
mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY);
}
}
+ updateCompatModeDownscale(packageName, gameMode);
}
/**
@@ -303,6 +532,8 @@ public final class GameManagerService extends IGameManagerService.Stub {
@VisibleForTesting
void onBootCompleted() {
Slog.d(TAG, "onBootCompleted");
+ final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS);
+ mHandler.sendMessage(msg);
}
void onUserStarting(int userId) {
@@ -329,6 +560,187 @@ public final class GameManagerService extends IGameManagerService.Stub {
}
}
+ private void loadDeviceConfigLocked() {
+ final List packages = mPackageManager.getInstalledPackages(0);
+ final String[] packageNames = packages.stream().map(e -> packageNameToKey(e.packageName))
+ .toArray(String[]::new);
+ synchronized (mDeviceConfigLock) {
+ final Properties properties = DeviceConfig.getProperties(
+ DeviceConfig.NAMESPACE_GAME_OVERLAY, packageNames);
+ for (String key : properties.getKeyset()) {
+ final GamePackageConfiguration config =
+ GamePackageConfiguration.fromProperties(key, properties);
+ putConfig(config);
+ }
+ }
+ }
+
+ private void disableCompatScale(String packageName) {
+ final long uid = Binder.clearCallingIdentity();
+ try {
+ final HashSet disabledSet = new HashSet<>();
+ disabledSet.add(DOWNSCALED);
+ final CompatibilityChangeConfig changeConfig = new CompatibilityChangeConfig(
+ new Compatibility.ChangeConfig(new HashSet<>(), disabledSet));
+ // TODO: switch to new API provided by aosp/1599153 once merged
+ try {
+ mPlatformCompat.setOverridesForTest(changeConfig, packageName);
+ } catch (SecurityException e) {
+ Slog.e(TAG, "Missing compat override permission", e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to call IPlatformCompat#setOverridesForTest", e);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(uid);
+ }
+ }
+
+ private void enableCompatScale(String packageName, long scaleId) {
+ final long uid = Binder.clearCallingIdentity();
+ try {
+ final HashSet disabledSet = new HashSet<>();
+ final HashSet enabledSet = new HashSet<>();
+ disabledSet.add(DOWNSCALE_50);
+ disabledSet.add(DOWNSCALE_60);
+ disabledSet.add(DOWNSCALE_70);
+ disabledSet.add(DOWNSCALE_80);
+ disabledSet.add(DOWNSCALE_90);
+ disabledSet.remove(scaleId);
+ enabledSet.add(DOWNSCALED);
+ enabledSet.add(scaleId);
+ final CompatibilityChangeConfig changeConfig = new CompatibilityChangeConfig(
+ new Compatibility.ChangeConfig(enabledSet, disabledSet));
+ // TODO: switch to new API provided by aosp/1599153 once merged
+ try {
+ mPlatformCompat.setOverridesForTest(changeConfig, packageName);
+ } catch (SecurityException e) {
+ Slog.e(TAG, "Missing compat override permission", e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to call IPlatformCompat#setOverridesForTest", e);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(uid);
+ }
+ }
+
+ private void updateCompatModeDownscale(String packageName, @GameMode int gameMode) {
+ synchronized (mDeviceConfigLock) {
+ if (gameMode == GameManager.GAME_MODE_STANDARD
+ || gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
+ disableCompatScale(packageName);
+ Slog.v(TAG, "Disabling downscale");
+ return;
+ }
+ if (DEBUG) {
+ Slog.v(TAG, dumpDeviceConfigs());
+ }
+ final GamePackageConfiguration packageConfig = mConfigs.get(packageName);
+ if (packageConfig == null) {
+ Slog.w(TAG, "Package configuration not found for " + packageName);
+ return;
+ }
+ final GameModeConfiguration modeConfig = packageConfig.getGameModeConfiguration(
+ gameMode);
+ if (modeConfig == null) {
+ Slog.w(TAG, "Game mode " + gameMode + " not found for " + packageName);
+ return;
+ }
+ long scaleId = modeConfig.getCompatChangeId();
+ if (scaleId == 0) {
+ Slog.w(TAG, "Invalid downscaling change id " + scaleId + " for "
+ + packageName);
+ return;
+ }
+ Slog.i(TAG, "Enabling downscale: " + scaleId + " for " + packageName);
+ enableCompatScale(packageName, scaleId);
+ }
+ }
+
+ private void putConfig(GamePackageConfiguration config) {
+ if (config.isValid()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Adding config: " + config.toString());
+ }
+ mConfigs.put(config.getPackageName(), config);
+ } else {
+ Slog.w(TAG, "Invalid package config for "
+ + config.getPackageName() + ":" + config.toString());
+ }
+ }
+
+ private void registerPackageReceiver() {
+ final IntentFilter packageFilter = new IntentFilter();
+ packageFilter.addAction(ACTION_PACKAGE_ADDED);
+ packageFilter.addAction(ACTION_PACKAGE_CHANGED);
+ packageFilter.addAction(ACTION_PACKAGE_REMOVED);
+ packageFilter.addDataScheme("package");
+ final BroadcastReceiver packageReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
+ final Uri data = intent.getData();
+ try {
+ final String packageName = data.getSchemeSpecificPart();
+ switch (intent.getAction()) {
+ case ACTION_PACKAGE_ADDED:
+ case ACTION_PACKAGE_CHANGED:
+ synchronized (mDeviceConfigLock) {
+ Properties properties = DeviceConfig.getProperties(
+ DeviceConfig.NAMESPACE_GAME_OVERLAY,
+ packageNameToKey(packageName));
+ for (String key : properties.getKeyset()) {
+ GamePackageConfiguration config =
+ GamePackageConfiguration.fromProperties(key,
+ properties);
+ putConfig(config);
+ }
+ }
+ break;
+ case ACTION_PACKAGE_REMOVED:
+ disableCompatScale(packageName);
+ mConfigs.remove(packageName);
+ break;
+ default:
+ // do nothing
+ break;
+ }
+ } catch (NullPointerException e) {
+ Slog.e(TAG, "Failed to get package name for new package", e);
+ }
+ }
+ };
+ mContext.registerReceiver(packageReceiver, packageFilter);
+ }
+
+ private void registerDeviceConfigListener() {
+ mDeviceConfigListener = new DeviceConfigListener();
+ }
+
+ /**
+ * Valid package name characters are [a-zA-Z0-9_] with a '.' delimiter. Policy keys can only use
+ * [a-zA-Z0-9_] so we must handle periods. We do this by appending a '_' to any existing
+ * sequence of '_', then we replace all '.' chars with '_';
+ */
+ private static String packageNameToKey(String name) {
+ return name.replaceAll("(_+)", "_$1").replaceAll("\\.", "_");
+ }
+
+ /**
+ * Replace the last '_' in a sequence with '.' (this can be one or more chars), then replace the
+ * resulting special case '_.' with just '_' to get the original package name.
+ */
+ private static String keyToPackageName(String key) {
+ return key.replaceAll("(_)(?!\\1)", ".").replaceAll("_\\.", "_");
+ }
+
+ private String dumpDeviceConfigs() {
+ StringBuilder out = new StringBuilder();
+ for (String key : mConfigs.keySet()) {
+ out.append("[\nName: ").append(key)
+ .append("\nConfig: ").append(mConfigs.get(key).toString()).append("\n]");
+ }
+ return out.toString();
+ }
+
private static ServiceThread createServiceThread() {
ServiceThread handlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);