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*/);