Merge "Integrate DeviceConfig with GameManagerService" into sc-dev

This commit is contained in:
Peiyong Lin
2021-03-25 01:09:41 +00:00
committed by Android (Google) Code Review
4 changed files with 436 additions and 6 deletions

View File

@@ -135,4 +135,20 @@ public final class GameManager {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns a list of supported game modes for a given package.
* <p>
* 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();
}
}
}

View File

@@ -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);
}

View File

@@ -5564,6 +5564,7 @@
@hide <p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"
android:protectionLevel="signature|privileged" />
<uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
<!-- Allows input events to be monitored. Very dangerous! @hide -->
<permission android:name="android.permission.MONITOR_INPUT"

View File

@@ -16,6 +16,17 @@
package com.android.server.app;
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.server.wm.CompatModePackages.DOWNSCALED;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_50;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_60;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_70;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_80;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_90;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
@@ -23,27 +34,41 @@ import android.app.ActivityManager;
import android.app.GameManager;
import android.app.GameManager.GameMode;
import android.app.IGameManagerService;
import android.compat.Compatibility;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.ArrayMap;
import android.util.Log;
import android.util.KeyValueListParser;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.IPlatformCompat;
import com.android.server.ServiceThread;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.util.HashSet;
import java.util.List;
/**
* Service to manage game related features.
@@ -60,14 +85,20 @@ public final class GameManagerService extends IGameManagerService.Stub {
static final int WRITE_SETTINGS = 1;
static final int REMOVE_SETTINGS = 2;
static final int POPULATE_GAME_MODE_SETTINGS = 3;
static final int WRITE_SETTINGS_DELAY = 10 * 1000; // 10 seconds
private final Context mContext;
private final PackageManager mPackageManager;
private final Object mLock = new Object();
private final Object mDeviceConfigLock = new Object();
private final Handler mHandler;
private final PackageManager mPackageManager;
private final IPlatformCompat mPlatformCompat;
private DeviceConfigListener mDeviceConfigListener;
@GuardedBy("mLock")
private final ArrayMap<Integer, GameManagerSettings> mSettings = new ArrayMap<>();
@GuardedBy("mDeviceConfigLock")
private final ArrayMap<String, GamePackageConfiguration> 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<Integer, GameModeConfiguration> 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<PackageInfo> 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<Long> 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<Long> disabledSet = new HashSet<>();
final HashSet<Long> 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*/);