blob: 32d5cf587e0c7eb1b2186cfab4c145243ad7e969 [file] [log] [blame]
/*
* Copyright (C) 2021 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.server.app;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static android.content.Intent.EXTRA_REPLACING;
import static android.server.app.Flags.gameDefaultFrameRate;
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.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.GameManager;
import android.app.GameManager.GameMode;
import android.app.GameManagerInternal;
import android.app.GameModeConfiguration;
import android.app.GameModeInfo;
import android.app.GameState;
import android.app.IGameManagerService;
import android.app.IGameModeListener;
import android.app.IGameStateListener;
import android.app.StatsManager;
import android.app.UidObserver;
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.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
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;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.PermissionEnforcer;
import android.os.PowerManagerInternal;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemProperties;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.AttributeSet;
import android.util.KeyValueListParser;
import android.util.Slog;
import android.util.StatsEvent;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
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.BufferedWriter;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Service to manage game related features.
*
* <p>Game service is a core service that monitors, coordinates game related features,
* as well as collect metrics.</p>
*
* @hide
*/
public final class GameManagerService extends IGameManagerService.Stub {
public static final String TAG = "GameManagerService";
// event strings used for logging
private static final String EVENT_SET_GAME_MODE = "SET_GAME_MODE";
private static final String EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG =
"UPDATE_CUSTOM_GAME_MODE_CONFIG";
private static final String EVENT_RECEIVE_SHUTDOWN_INDENT = "RECEIVE_SHUTDOWN_INDENT";
private static final String EVENT_ON_USER_STARTING = "ON_USER_STARTING";
private static final String EVENT_ON_USER_SWITCHING = "ON_USER_SWITCHING";
private static final String EVENT_ON_USER_STOPPING = "ON_USER_STOPPING";
static final int WRITE_SETTINGS = 1;
static final int REMOVE_SETTINGS = 2;
static final int POPULATE_GAME_MODE_SETTINGS = 3;
static final int SET_GAME_STATE = 4;
static final int CANCEL_GAME_LOADING_MODE = 5;
static final int WRITE_GAME_MODE_INTERVENTION_LIST_FILE = 6;
static final int WRITE_DELAY_MILLIS = 10 * 1000; // 10 seconds
static final int LOADING_BOOST_MAX_DURATION = 5 * 1000; // 5 seconds
static final String PROPERTY_PERSISTENT_GFX_GAME_DEFAULT_FRAME_RATE_ENABLED =
"persist.graphics.game_default_frame_rate.enabled";
static final String PROPERTY_RO_SURFACEFLINGER_GAME_DEFAULT_FRAME_RATE =
"ro.surface_flinger.game_default_frame_rate_override";
private static final String PACKAGE_NAME_MSG_KEY = "packageName";
private static final String USER_ID_MSG_KEY = "userId";
private static final String GAME_MODE_INTERVENTION_LIST_FILE_NAME =
"game_mode_intervention.list";
private final Context mContext;
private final Object mLock = new Object();
private final Object mDeviceConfigLock = new Object();
private final Object mGameModeListenerLock = new Object();
private final Object mGameStateListenerLock = new Object();
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
final Handler mHandler;
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final PowerManagerInternal mPowerManagerInternal;
@VisibleForTesting
final AtomicFile mGameModeInterventionListFile;
private DeviceConfigListener mDeviceConfigListener;
@GuardedBy("mLock")
private final ArrayMap<Integer, GameManagerSettings> mSettings = new ArrayMap<>();
@GuardedBy("mDeviceConfigLock")
private final ArrayMap<String, GamePackageConfiguration> mConfigs = new ArrayMap<>();
// listener to caller uid map
@GuardedBy("mGameModeListenerLock")
private final ArrayMap<IGameModeListener, Integer> mGameModeListeners = new ArrayMap<>();
@GuardedBy("mGameStateListenerLock")
private final ArrayMap<IGameStateListener, Integer> mGameStateListeners = new ArrayMap<>();
@Nullable
private final GameServiceController mGameServiceController;
private final Object mUidObserverLock = new Object();
@VisibleForTesting
@Nullable
final MyUidObserver mUidObserver;
@GuardedBy("mUidObserverLock")
private final Set<Integer> mForegroundGameUids = new HashSet<>();
private final GameManagerServiceSystemPropertiesWrapper mSysProps;
private float mGameDefaultFrameRateValue;
@VisibleForTesting
static class Injector {
public GameManagerServiceSystemPropertiesWrapper createSystemPropertiesWrapper() {
return new GameManagerServiceSystemPropertiesWrapper() {
@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 GameManagerService(Context context) {
this(context, createServiceThread().getLooper());
}
GameManagerService(Context context, Looper looper) {
this(context, looper, Environment.getDataDirectory(), new Injector());
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
GameManagerService(Context context, Looper looper, File dataDir, Injector injector) {
super(PermissionEnforcer.fromContext(context));
mContext = context;
mHandler = new SettingsHandler(looper);
mPackageManager = mContext.getPackageManager();
mUserManager = mContext.getSystemService(UserManager.class);
mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
File systemDir = new File(dataDir, "system");
systemDir.mkdirs();
FileUtils.setPermissions(systemDir.toString(),
FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH,
-1, -1);
mGameModeInterventionListFile = new AtomicFile(new File(systemDir,
GAME_MODE_INTERVENTION_LIST_FILE_NAME));
FileUtils.setPermissions(mGameModeInterventionListFile.getBaseFile().getAbsolutePath(),
FileUtils.S_IRUSR | FileUtils.S_IWUSR
| FileUtils.S_IRGRP | FileUtils.S_IWGRP,
-1, -1);
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
mGameServiceController = new GameServiceController(
context, BackgroundThread.getExecutor(),
new GameServiceProviderSelectorImpl(
context.getResources(),
context.getPackageManager()),
new GameServiceProviderInstanceFactoryImpl(context));
} else {
mGameServiceController = null;
}
mUidObserver = new MyUidObserver();
try {
ActivityManager.getService().registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
ActivityManager.PROCESS_STATE_UNKNOWN, null);
} catch (RemoteException e) {
Slog.w(TAG, "Could not register UidObserver");
}
mSysProps = injector.createSystemPropertiesWrapper();
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver result) {
new GameManagerShellCommand().exec(this, in, out, err, args, callback, result);
}
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
writer.println("Permission Denial: can't dump GameManagerService from from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ " without permission " + android.Manifest.permission.DUMP);
return;
}
if (args == null || args.length == 0) {
writer.println("*Dump GameManagerService*");
dumpAllGameConfigs(writer);
}
}
private void dumpAllGameConfigs(PrintWriter pw) {
final int userId = ActivityManager.getCurrentUser();
String[] packageList = getInstalledGamePackageNames(userId);
for (final String packageName : packageList) {
pw.println(getInterventionList(packageName, userId));
}
}
class SettingsHandler extends Handler {
SettingsHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
doHandleMessage(msg);
}
void doHandleMessage(Message msg) {
switch (msg.what) {
case WRITE_SETTINGS: {
final int userId = (int) msg.obj;
if (userId < 0) {
Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId);
synchronized (mLock) {
removeEqualMessages(WRITE_SETTINGS, msg.obj);
}
break;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
synchronized (mLock) {
removeEqualMessages(WRITE_SETTINGS, msg.obj);
if (mSettings.containsKey(userId)) {
GameManagerSettings userSettings = mSettings.get(userId);
userSettings.writePersistentDataLocked();
}
}
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
break;
}
case REMOVE_SETTINGS: {
final int userId = (int) msg.obj;
if (userId < 0) {
Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId);
synchronized (mLock) {
removeEqualMessages(WRITE_SETTINGS, msg.obj);
removeEqualMessages(REMOVE_SETTINGS, msg.obj);
}
break;
}
synchronized (mLock) {
// Since the user was removed, ignore previous write message
// and do write here.
removeEqualMessages(WRITE_SETTINGS, msg.obj);
removeEqualMessages(REMOVE_SETTINGS, msg.obj);
if (mSettings.containsKey(userId)) {
final GameManagerSettings userSettings = mSettings.get(userId);
mSettings.remove(userId);
userSettings.writePersistentDataLocked();
}
}
break;
}
case POPULATE_GAME_MODE_SETTINGS: {
removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
final int userId = (int) msg.obj;
final String[] packageNames = getInstalledGamePackageNames(userId);
updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames);
break;
}
case SET_GAME_STATE: {
final GameState gameState = (GameState) msg.obj;
final boolean isLoading = gameState.isLoading();
final Bundle data = msg.getData();
final String packageName = data.getString(PACKAGE_NAME_MSG_KEY);
final int userId = data.getInt(USER_ID_MSG_KEY);
// Restrict to games only. Requires performance mode to be enabled.
final boolean boostEnabled =
getGameMode(packageName, userId) == GameManager.GAME_MODE_PERFORMANCE;
int uid;
try {
uid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (NameNotFoundException e) {
Slog.v(TAG, "Failed to get package metadata");
uid = -1;
}
FrameworkStatsLog.write(FrameworkStatsLog.GAME_STATE_CHANGED, packageName, uid,
boostEnabled, gameStateModeToStatsdGameState(gameState.getMode()),
isLoading, gameState.getLabel(), gameState.getQuality());
if (boostEnabled) {
if (mPowerManagerInternal == null) {
Slog.d(TAG, "Error setting loading mode for package " + packageName
+ " and userId " + userId);
break;
}
if (mHandler.hasMessages(CANCEL_GAME_LOADING_MODE)) {
mHandler.removeMessages(CANCEL_GAME_LOADING_MODE);
}
Slog.v(TAG, String.format(
"Game loading power mode %s (game state change isLoading=%b)",
isLoading ? "ON" : "OFF", isLoading));
mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, isLoading);
if (isLoading) {
int loadingBoostDuration = getLoadingBoostDuration(packageName, userId);
loadingBoostDuration = loadingBoostDuration > 0 ? loadingBoostDuration
: LOADING_BOOST_MAX_DURATION;
mHandler.sendMessageDelayed(
mHandler.obtainMessage(CANCEL_GAME_LOADING_MODE),
loadingBoostDuration);
}
}
synchronized (mGameStateListenerLock) {
for (IGameStateListener listener : mGameStateListeners.keySet()) {
try {
listener.onGameStateChanged(packageName, gameState, userId);
} catch (RemoteException ex) {
Slog.w(TAG, "Cannot notify game state change for listener added by "
+ mGameStateListeners.get(listener));
}
}
}
break;
}
case CANCEL_GAME_LOADING_MODE: {
Slog.v(TAG, "Game loading power mode OFF (loading boost ended)");
mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, false);
break;
}
case WRITE_GAME_MODE_INTERVENTION_LIST_FILE: {
final int userId = (int) msg.obj;
if (userId < 0) {
Slog.wtf(TAG, "Attempt to write setting for invalid user: " + userId);
synchronized (mLock) {
removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);
}
break;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);
writeGameModeInterventionsToFile(userId);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
break;
}
}
}
}
private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
DeviceConfigListener() {
super();
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_GAME_OVERLAY,
mContext.getMainExecutor(), this);
}
@Override
public void onPropertiesChanged(Properties properties) {
final String[] packageNames = properties.getKeyset().toArray(new String[0]);
Slog.v(TAG, "Device config changed for packages: " + Arrays.toString(packageNames));
updateConfigsForUser(ActivityManager.getCurrentUser(), true /*checkGamePackage*/,
packageNames);
}
@Override
public void finalize() {
DeviceConfig.removeOnPropertiesChangedListener(this);
}
}
/**
* Called by games to communicate the current state to the platform.
*
* @param packageName The client package name.
* @param gameState An object set to the current state.
* @param userId The user associated with this state.
*/
public void setGameState(String packageName, @NonNull GameState gameState,
@UserIdInt int userId) {
if (!isPackageGame(packageName, userId)) {
Slog.d(TAG, "No-op for attempt to set game state for non-game app: " + packageName);
// Restrict to games only.
return;
}
final Message msg = mHandler.obtainMessage(SET_GAME_STATE);
final Bundle data = new Bundle();
data.putString(PACKAGE_NAME_MSG_KEY, packageName);
data.putInt(USER_ID_MSG_KEY, userId);
msg.setData(data);
msg.obj = gameState;
mHandler.sendMessage(msg);
}
/**
* GamePackageConfiguration manages all game mode config details for its associated package.
*/
public static class GamePackageConfiguration {
public static final String TAG = "GameManagerService_GamePackageConfiguration";
/**
* Metadata that can be included in the app manifest to allow/disallow any window manager
* downscaling interventions. Default value is TRUE.
*/
public static final String METADATA_WM_ALLOW_DOWNSCALE =
"com.android.graphics.intervention.wm.allowDownscale";
/**
* Metadata that can be included in the app manifest to allow/disallow any ANGLE
* interventions. Default value is TRUE.
*/
public static final String METADATA_ANGLE_ALLOW_ANGLE =
"com.android.graphics.intervention.angle.allowAngle";
/**
* Metadata that needs to be included in the app manifest to OPT-IN to PERFORMANCE mode.
* This means the app will assume full responsibility for the experience provided by this
* mode and the system will enable no window manager downscaling.
* Default value is FALSE
*/
public static final String METADATA_PERFORMANCE_MODE_ENABLE =
"com.android.app.gamemode.performance.enabled";
/**
* Metadata that needs to be included in the app manifest to OPT-IN to BATTERY mode.
* This means the app will assume full responsibility for the experience provided by this
* mode and the system will enable no window manager downscaling.
* Default value is FALSE
*/
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 Object mModeConfigLock = new Object();
@GuardedBy("mModeConfigLock")
private final ArrayMap<Integer, GameModeConfiguration> mModeConfigs = new ArrayMap<>();
// if adding new properties or make any of the below overridable, the method
// copyAndApplyOverride should be updated accordingly
private boolean mPerfModeOverridden = false;
private boolean mBatteryModeOverridden = false;
private boolean mAllowDownscale = true;
private boolean mAllowAngle = true;
private boolean mAllowFpsOverride = true;
GamePackageConfiguration(String packageName) {
mPackageName = packageName;
}
GamePackageConfiguration(PackageManager packageManager, String packageName, int userId) {
mPackageName = packageName;
try {
final ApplicationInfo ai = packageManager.getApplicationInfoAsUser(packageName,
PackageManager.GET_META_DATA, userId);
if (!parseInterventionFromXml(packageManager, ai, packageName)
&& ai.metaData != null) {
mPerfModeOverridden = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);
mBatteryModeOverridden = 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);
}
} catch (NameNotFoundException e) {
// Not all packages are installed, hence ignore those that are not installed yet.
Slog.v(TAG, "Failed to get package metadata");
}
final String configString = DeviceConfig.getProperty(
DeviceConfig.NAMESPACE_GAME_OVERLAY, packageName);
if (configString != null) {
final String[] gameModeConfigStrings = configString.split(":");
for (String gameModeConfigString : gameModeConfigStrings) {
try {
final KeyValueListParser parser = new KeyValueListParser(',');
parser.setString(gameModeConfigString);
addModeConfig(new GameModeConfiguration(parser));
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Invalid config string");
}
}
}
}
private boolean parseInterventionFromXml(PackageManager packageManager, ApplicationInfo ai,
String packageName) {
boolean xmlFound = false;
try (XmlResourceParser parser = ai.loadXmlMetaData(packageManager,
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 = packageManager.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);
mPerfModeOverridden = array.getBoolean(
GameModeConfig_supportsPerformanceGameMode, false);
mBatteryModeOverridden = 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) {
// set flag back to default values when parsing fails
mPerfModeOverridden = false;
mBatteryModeOverridden = false;
mAllowDownscale = true;
mAllowAngle = true;
mAllowFpsOverride = true;
Slog.e(TAG, "Error while parsing XML meta-data for "
+ METADATA_GAME_MODE_CONFIG);
}
return xmlFound;
}
GameModeConfiguration getOrAddDefaultGameModeConfiguration(int gameMode) {
synchronized (mModeConfigLock) {
mModeConfigs.putIfAbsent(gameMode, new GameModeConfiguration(gameMode));
return mModeConfigs.get(gameMode);
}
}
// used to check if the override package config has any game mode config, if not, it's
// considered empty and safe to delete from settings
boolean hasActiveGameModeConfig() {
synchronized (mModeConfigLock) {
return !mModeConfigs.isEmpty();
}
}
/**
* GameModeConfiguration contains all the values for all the interventions associated with
* a game mode.
*/
public class GameModeConfiguration {
public static final String TAG = "GameManagerService_GameModeConfiguration";
public static final String MODE_KEY = "mode";
public static final String SCALING_KEY = "downscaleFactor";
public static final String FPS_KEY = "fps";
public static final String ANGLE_KEY = "useAngle";
public static final String LOADING_BOOST_KEY = "loadingBoost";
public static final float DEFAULT_SCALING = -1f;
public static final String DEFAULT_FPS = "";
public static final boolean DEFAULT_USE_ANGLE = false;
public static final int DEFAULT_LOADING_BOOST_DURATION = -1;
private final @GameMode int mGameMode;
private float mScaling = DEFAULT_SCALING;
private String mFps = DEFAULT_FPS;
private boolean mUseAngle;
private int mLoadingBoostDuration;
GameModeConfiguration(int gameMode) {
mGameMode = gameMode;
mUseAngle = DEFAULT_USE_ANGLE;
mLoadingBoostDuration = DEFAULT_LOADING_BOOST_DURATION;
}
GameModeConfiguration(KeyValueListParser parser) {
mGameMode = parser.getInt(MODE_KEY, GameManager.GAME_MODE_UNSUPPORTED);
// willGamePerformOptimizations() returns if an app will handle all of the changes
// necessary for a particular game mode. If so, the Android framework (i.e.
// GameManagerService) will not do anything for the app (like window scaling or
// using ANGLE).
mScaling = !mAllowDownscale || willGamePerformOptimizations(mGameMode)
? DEFAULT_SCALING : parser.getFloat(SCALING_KEY, DEFAULT_SCALING);
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
// - The Phenotype config has enabled it.
mUseAngle = mAllowAngle && !willGamePerformOptimizations(mGameMode)
&& parser.getBoolean(ANGLE_KEY, DEFAULT_USE_ANGLE);
mLoadingBoostDuration = willGamePerformOptimizations(mGameMode)
? DEFAULT_LOADING_BOOST_DURATION
: parser.getInt(LOADING_BOOST_KEY, DEFAULT_LOADING_BOOST_DURATION);
}
public int getGameMode() {
return mGameMode;
}
public synchronized float getScaling() {
return mScaling;
}
public synchronized int getFps() {
try {
final int fpsInt = Integer.parseInt(mFps);
return fpsInt;
} catch (NumberFormatException e) {
return 0;
}
}
synchronized String getFpsStr() {
return mFps;
}
public synchronized boolean getUseAngle() {
return mUseAngle;
}
public synchronized int getLoadingBoostDuration() {
return mLoadingBoostDuration;
}
public synchronized void setScaling(float scaling) {
mScaling = scaling;
}
public synchronized void setFpsStr(String fpsStr) {
mFps = fpsStr;
}
public synchronized void setUseAngle(boolean useAngle) {
mUseAngle = useAngle;
}
public synchronized void setLoadingBoostDuration(int loadingBoostDuration) {
mLoadingBoostDuration = loadingBoostDuration;
}
public boolean isActive() {
return (mGameMode == GameManager.GAME_MODE_STANDARD
|| mGameMode == GameManager.GAME_MODE_PERFORMANCE
|| mGameMode == GameManager.GAME_MODE_BATTERY
|| mGameMode == GameManager.GAME_MODE_CUSTOM)
&& !willGamePerformOptimizations(mGameMode);
}
android.app.GameModeConfiguration toPublicGameModeConfig() {
int fpsOverride;
try {
fpsOverride = Integer.parseInt(mFps);
} catch (NumberFormatException e) {
fpsOverride = 0;
}
// TODO(b/243448953): match to proper value in case of display change?
fpsOverride = fpsOverride > 0 ? fpsOverride
: android.app.GameModeConfiguration.FPS_OVERRIDE_NONE;
final float scaling = mScaling == DEFAULT_SCALING ? 1.0f : mScaling;
return new android.app.GameModeConfiguration.Builder()
.setScalingFactor(scaling)
.setFpsOverride(fpsOverride).build();
}
void updateFromPublicGameModeConfig(android.app.GameModeConfiguration config) {
mScaling = config.getScalingFactor();
mFps = String.valueOf(config.getFpsOverride());
}
/**
* @hide
*/
public String toString() {
return "[Game Mode:" + mGameMode + ",Scaling:" + mScaling + ",Use Angle:"
+ mUseAngle + ",Fps:" + mFps + ",Loading Boost Duration:"
+ mLoadingBoostDuration + "]";
}
}
public String getPackageName() {
return mPackageName;
}
/**
* Returns if the app will assume full responsibility for the experience provided by this
* mode. If True, the system will not perform any interventions for the app.
*
* @return True if the app package has specified in its metadata either:
* "com.android.app.gamemode.performance.enabled" or
* "com.android.app.gamemode.battery.enabled" with a value of "true"
*/
public boolean willGamePerformOptimizations(@GameMode int gameMode) {
return (mBatteryModeOverridden && gameMode == GameManager.GAME_MODE_BATTERY)
|| (mPerfModeOverridden && gameMode == GameManager.GAME_MODE_PERFORMANCE);
}
private int getAvailableGameModesBitfield() {
int field = modeToBitmask(GameManager.GAME_MODE_CUSTOM)
| modeToBitmask(GameManager.GAME_MODE_STANDARD);
synchronized (mModeConfigLock) {
for (final int mode : mModeConfigs.keySet()) {
field |= modeToBitmask(mode);
}
}
if (mBatteryModeOverridden) {
field |= modeToBitmask(GameManager.GAME_MODE_BATTERY);
}
if (mPerfModeOverridden) {
field |= modeToBitmask(GameManager.GAME_MODE_PERFORMANCE);
}
return field;
}
/**
* Get an array of a package's available game modes.
*/
public @GameMode int[] getAvailableGameModes() {
final int modesBitfield = getAvailableGameModesBitfield();
int[] modes = new int[Integer.bitCount(modesBitfield)];
int i = 0;
final int gameModeInHighestBit =
Integer.numberOfTrailingZeros(Integer.highestOneBit(modesBitfield));
for (int mode = 0; mode <= gameModeInHighestBit; ++mode) {
if (((modesBitfield >> mode) & 1) != 0) {
modes[i++] = mode;
}
}
return modes;
}
/**
* Get an array of a package's overridden game modes.
*/
public @GameMode int[] getOverriddenGameModes() {
if (mBatteryModeOverridden && mPerfModeOverridden) {
return new int[]{GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE};
} else if (mBatteryModeOverridden) {
return new int[]{GameManager.GAME_MODE_BATTERY};
} else if (mPerfModeOverridden) {
return new int[]{GameManager.GAME_MODE_PERFORMANCE};
} else {
return new int[]{};
}
}
/**
* 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) {
synchronized (mModeConfigLock) {
return mModeConfigs.get(gameMode);
}
}
/**
* Inserts a new GameModeConfiguration.
*/
public void addModeConfig(GameModeConfiguration config) {
if (config.isActive()) {
synchronized (mModeConfigLock) {
mModeConfigs.put(config.getGameMode(), config);
}
} else {
Slog.w(TAG, "Attempt to add inactive game mode config for "
+ mPackageName + ":" + config.toString());
}
}
/**
* Removes the GameModeConfiguration.
*/
public void removeModeConfig(int mode) {
synchronized (mModeConfigLock) {
mModeConfigs.remove(mode);
}
}
public boolean isActive() {
synchronized (mModeConfigLock) {
return mModeConfigs.size() > 0 || mBatteryModeOverridden || mPerfModeOverridden;
}
}
GamePackageConfiguration copyAndApplyOverride(GamePackageConfiguration overrideConfig) {
GamePackageConfiguration copy = new GamePackageConfiguration(mPackageName);
// if a game mode is overridden, we treat it with the highest priority and reset any
// overridden game modes so that interventions are always executed.
copy.mPerfModeOverridden = mPerfModeOverridden && !(overrideConfig != null
&& overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE)
!= null);
copy.mBatteryModeOverridden = mBatteryModeOverridden && !(overrideConfig != null
&& overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY)
!= null);
// if any game mode is overridden, we will consider all interventions forced-active,
// this can be done more granular by checking if a specific intervention is
// overridden under each game mode override, but only if necessary.
copy.mAllowDownscale = mAllowDownscale || overrideConfig != null;
copy.mAllowAngle = mAllowAngle || overrideConfig != null;
copy.mAllowFpsOverride = mAllowFpsOverride || overrideConfig != null;
if (overrideConfig != null) {
synchronized (copy.mModeConfigLock) {
synchronized (mModeConfigLock) {
for (Map.Entry<Integer, GameModeConfiguration> entry :
mModeConfigs.entrySet()) {
copy.mModeConfigs.put(entry.getKey(), entry.getValue());
}
}
synchronized (overrideConfig.mModeConfigLock) {
for (Map.Entry<Integer, GameModeConfiguration> entry :
overrideConfig.mModeConfigs.entrySet()) {
copy.mModeConfigs.put(entry.getKey(), entry.getValue());
}
}
}
}
return copy;
}
public String toString() {
synchronized (mModeConfigLock) {
return "[Name:" + mPackageName + " Modes: " + mModeConfigs.toString() + "]";
}
}
}
private final class LocalService extends GameManagerInternal {
@Override
public float getResolutionScalingFactor(String packageName, int userId) {
final int gameMode = getGameModeFromSettingsUnchecked(packageName, userId);
return getResolutionScalingFactorInternal(packageName, gameMode, userId);
}
}
/**
* SystemService lifecycle for GameService.
*
* @hide
*/
public static class Lifecycle extends SystemService {
private GameManagerService mService;
public Lifecycle(Context context) {
super(context);
mService = new GameManagerService(context);
}
@Override
public void onStart() {
publishBinderService(Context.GAME_SERVICE, mService);
mService.publishLocalService();
mService.registerDeviceConfigListener();
mService.registerPackageReceiver();
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_BOOT_COMPLETED) {
mService.onBootCompleted();
mService.registerStatsCallbacks();
}
}
@Override
public void onUserStarting(@NonNull TargetUser user) {
Slog.d(TAG, "Starting user " + user.getUserIdentifier());
mService.onUserStarting(user,
Environment.getDataSystemDeDirectory(user.getUserIdentifier()));
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
mService.onUserUnlocking(user);
}
@Override
public void onUserStopping(@NonNull TargetUser user) {
mService.onUserStopping(user);
}
@Override
public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
mService.onUserSwitching(from, to);
}
}
private boolean isValidPackageName(String packageName, int userId) {
try {
return mPackageManager.getPackageUidAsUser(packageName, userId)
== Binder.getCallingUid();
} catch (NameNotFoundException e) {
return false;
}
}
private void checkPermission(String permission) throws SecurityException {
if (mContext.checkCallingOrSelfPermission(permission)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Access denied to process: " + Binder.getCallingPid()
+ ", must have permission " + permission);
}
}
private @GameMode int[] getAvailableGameModesUnchecked(String packageName, int userId) {
final GamePackageConfiguration config = getConfig(packageName, userId);
if (config == null) {
return new int[]{GameManager.GAME_MODE_STANDARD, GameManager.GAME_MODE_CUSTOM};
}
return config.getAvailableGameModes();
}
private boolean isPackageGame(String packageName, @UserIdInt int userId) {
try {
final ApplicationInfo applicationInfo = mPackageManager
.getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
return applicationInfo.category == ApplicationInfo.CATEGORY_GAME;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 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, int userId)
throws SecurityException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
if (!isPackageGame(packageName, userId)) {
return new int[]{};
}
return getAvailableGameModesUnchecked(packageName, userId);
}
private @GameMode int getGameModeFromSettingsUnchecked(String packageName,
@UserIdInt int userId) {
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
Slog.d(TAG, "User ID '" + userId + "' does not have a Game Mode"
+ " selected for package: '" + packageName + "'");
return GameManager.GAME_MODE_STANDARD;
}
return mSettings.get(userId).getGameModeLocked(packageName);
}
}
/**
* Get the Game Mode for the package name.
* Verifies that the calling process is for the matching package UID or has
* {@link android.Manifest.permission#MANAGE_GAME_MODE}.
*/
@Override
public @GameMode int getGameMode(@NonNull String packageName, @UserIdInt int userId)
throws SecurityException {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, true, "getGameMode",
"com.android.server.app.GameManagerService");
// Restrict to games only.
if (!isPackageGame(packageName, userId)) {
// The game mode for applications that are not identified as game is always
// UNSUPPORTED. See {@link PackageManager#setApplicationCategoryHint(String, int)}
return GameManager.GAME_MODE_UNSUPPORTED;
}
// This function handles two types of queries:
// 1) A normal, non-privileged app querying its own Game Mode.
// 2) A privileged system service querying the Game Mode of another package.
// The least privileged case is a normal app performing a query, so check that first and
// return a value if the package name is valid. Next, check if the caller has the necessary
// permission and return a value. Do this check last, since it can throw an exception.
if (isValidPackageName(packageName, userId)) {
return getGameModeFromSettingsUnchecked(packageName, userId);
}
// Since the package name doesn't match, check the caller has the necessary permission.
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
return getGameModeFromSettingsUnchecked(packageName, userId);
}
/**
* Get the GameModeInfo for the package name.
* Verifies that the calling process is for the matching package UID or has
* {@link android.Manifest.permission#MANAGE_GAME_MODE}. If the package is not a game,
* null is always returned.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
@Nullable
public GameModeInfo getGameModeInfo(@NonNull String packageName, @UserIdInt int userId) {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, true, "getGameModeInfo",
"com.android.server.app.GameManagerService");
// Check the caller has the necessary permission.
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
if (!isPackageGame(packageName, userId)) {
return null;
}
final @GameMode int activeGameMode = getGameModeFromSettingsUnchecked(packageName, userId);
final GamePackageConfiguration config = getConfig(packageName, userId);
if (config != null) {
final @GameMode int[] overriddenGameModes = config.getOverriddenGameModes();
final @GameMode int[] availableGameModes = config.getAvailableGameModes();
GameModeInfo.Builder gameModeInfoBuilder = new GameModeInfo.Builder()
.setActiveGameMode(activeGameMode)
.setAvailableGameModes(availableGameModes)
.setOverriddenGameModes(overriddenGameModes)
.setDownscalingAllowed(config.mAllowDownscale)
.setFpsOverrideAllowed(config.mAllowFpsOverride);
for (int gameMode : availableGameModes) {
if (!config.willGamePerformOptimizations(gameMode)) {
GamePackageConfiguration.GameModeConfiguration gameModeConfig =
config.getGameModeConfiguration(gameMode);
if (gameModeConfig != null) {
gameModeInfoBuilder.setGameModeConfiguration(gameMode,
gameModeConfig.toPublicGameModeConfig());
}
}
}
return gameModeInfoBuilder.build();
} else {
return new GameModeInfo.Builder()
.setActiveGameMode(activeGameMode)
.setAvailableGameModes(getAvailableGameModesUnchecked(packageName, userId))
.build();
}
}
/**
* Sets the Game Mode for the package name.
* Verifies that the calling process has {@link android.Manifest.permission#MANAGE_GAME_MODE}.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void setGameMode(String packageName, @GameMode int gameMode, int userId)
throws SecurityException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
Slog.d(TAG, "No-op for attempt to set UNSUPPORTED mode for app: " + packageName);
return;
} else if (!isPackageGame(packageName, userId)) {
Slog.d(TAG, "No-op for attempt to set game mode for non-game app: " + packageName);
return;
}
int fromGameMode;
synchronized (mLock) {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, true, "setGameMode",
"com.android.server.app.GameManagerService");
if (!mSettings.containsKey(userId)) {
Slog.d(TAG, "Failed to set game mode for package " + packageName
+ " as user " + userId + " is not started");
return;
}
GameManagerSettings userSettings = mSettings.get(userId);
fromGameMode = userSettings.getGameModeLocked(packageName);
userSettings.setGameModeLocked(packageName, gameMode);
}
updateInterventions(packageName, gameMode, userId);
synchronized (mGameModeListenerLock) {
for (IGameModeListener listener : mGameModeListeners.keySet()) {
Binder.allowBlocking(listener.asBinder());
try {
listener.onGameModeChanged(packageName, fromGameMode, gameMode, userId);
} catch (RemoteException ex) {
Slog.w(TAG, "Cannot notify game mode change for listener added by "
+ mGameModeListeners.get(listener));
}
}
}
sendUserMessage(userId, WRITE_SETTINGS, EVENT_SET_GAME_MODE, WRITE_DELAY_MILLIS);
sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
EVENT_SET_GAME_MODE, 0 /*delayMillis*/);
int gameUid = -1;
try {
gameUid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (NameNotFoundException ex) {
Slog.d(TAG, "Cannot find the UID for package " + packageName + " under user " + userId);
}
FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CHANGED, gameUid,
Binder.getCallingUid(), gameModeToStatsdGameMode(fromGameMode),
gameModeToStatsdGameMode(gameMode));
}
/**
* Get if ANGLE is enabled for the package for the currently enabled game mode.
* Checks that the caller has {@link android.Manifest.permission#MANAGE_GAME_MODE}.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public @GameMode boolean isAngleEnabled(String packageName, int userId)
throws SecurityException {
final int gameMode = getGameMode(packageName, userId);
if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
return false;
}
final GamePackageConfiguration config;
synchronized (mDeviceConfigLock) {
config = mConfigs.get(packageName);
if (config == null) {
return false;
}
}
GamePackageConfiguration.GameModeConfiguration gameModeConfiguration =
config.getGameModeConfiguration(gameMode);
if (gameModeConfiguration == null) {
return false;
}
return gameModeConfiguration.getUseAngle();
}
/**
* If loading boost is applicable for the package for the currently enabled game mode, return
* the boost duration. If no configuration is available for the selected package or mode, the
* default is returned.
*/
public int getLoadingBoostDuration(String packageName, int userId)
throws SecurityException {
final int gameMode = getGameMode(packageName, userId);
if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
return -1;
}
final GamePackageConfiguration config;
synchronized (mDeviceConfigLock) {
config = mConfigs.get(packageName);
}
if (config == null) {
return -1;
}
GamePackageConfiguration.GameModeConfiguration gameModeConfiguration =
config.getGameModeConfiguration(gameMode);
if (gameModeConfiguration == null) {
return -1;
}
return gameModeConfiguration.getLoadingBoostDuration();
}
/**
* If loading boost is enabled, invoke it.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
@GameMode public void notifyGraphicsEnvironmentSetup(String packageName, int userId)
throws SecurityException {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, true, "notifyGraphicsEnvironmentSetup",
"com.android.server.app.GameManagerService");
if (!isValidPackageName(packageName, userId)) {
Slog.d(TAG, "No-op for attempt to notify graphics env setup for different package"
+ "than caller with uid: " + Binder.getCallingUid());
return;
}
final int gameMode = getGameMode(packageName, userId);
if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
Slog.d(TAG, "No-op for attempt to notify graphics env setup for non-game app: "
+ packageName);
return;
}
int loadingBoostDuration = getLoadingBoostDuration(packageName, userId);
if (loadingBoostDuration != -1) {
if (loadingBoostDuration == 0 || loadingBoostDuration > LOADING_BOOST_MAX_DURATION) {
loadingBoostDuration = LOADING_BOOST_MAX_DURATION;
}
if (mHandler.hasMessages(CANCEL_GAME_LOADING_MODE)) {
// The loading mode has already been set and is waiting to be unset. It is not
// required to set the mode again and we should replace the queued cancel
// instruction.
mHandler.removeMessages(CANCEL_GAME_LOADING_MODE);
} else {
Slog.v(TAG, "Game loading power mode ON (loading boost on game start)");
mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, true);
}
mHandler.sendMessageDelayed(
mHandler.obtainMessage(CANCEL_GAME_LOADING_MODE), loadingBoostDuration);
}
}
/**
* Sets the game service provider to a given package, meant for testing.
*
* <p>This setting persists until the next call or until the next reboot.
*
* <p>Checks that the caller has {@link android.Manifest.permission#SET_GAME_SERVICE}.
*/
@Override
@RequiresPermission(Manifest.permission.SET_GAME_SERVICE)
public void setGameServiceProvider(@Nullable String packageName) throws SecurityException {
checkPermission(Manifest.permission.SET_GAME_SERVICE);
if (mGameServiceController == null) {
return;
}
mGameServiceController.setGameServiceProvider(packageName);
}
/**
* Updates the resolution scaling factor for the package's target game mode and activates it.
*
* @param scalingFactor enable scaling override over any other compat scaling if positive,
* or disable the override otherwise
* @throws SecurityException if caller doesn't have
* {@link android.Manifest.permission#MANAGE_GAME_MODE}
* permission.
* @throws IllegalArgumentException if the user ID provided doesn't exist.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void updateResolutionScalingFactor(String packageName, int gameMode, float scalingFactor,
int userId) throws SecurityException, IllegalArgumentException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
throw new IllegalArgumentException("User " + userId + " wasn't started");
}
}
setGameModeConfigOverride(packageName, userId, gameMode, null /*fpsStr*/,
Float.toString(scalingFactor));
}
/**
* Gets the resolution scaling factor for the package's target game mode.
*
* @return scaling factor for the game mode if exists or negative value otherwise.
* @throws SecurityException if caller doesn't have
* {@link android.Manifest.permission#MANAGE_GAME_MODE}
* permission.
* @throws IllegalArgumentException if the user ID provided doesn't exist.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public float getResolutionScalingFactor(String packageName, int gameMode, int userId)
throws SecurityException, IllegalArgumentException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
throw new IllegalArgumentException("User " + userId + " wasn't started");
}
}
return getResolutionScalingFactorInternal(packageName, gameMode, userId);
}
float getResolutionScalingFactorInternal(String packageName, int gameMode, int userId) {
final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
if (packageConfig == null) {
return GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING;
}
final GamePackageConfiguration.GameModeConfiguration modeConfig =
packageConfig.getGameModeConfiguration(gameMode);
if (modeConfig != null) {
return modeConfig.getScaling();
}
return GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING;
}
/**
* Updates the config for the game's {@link GameManager#GAME_MODE_CUSTOM} mode.
*
* @throws SecurityException if caller doesn't have
* {@link android.Manifest.permission#MANAGE_GAME_MODE}
* permission.
* @throws IllegalArgumentException if the user ID provided doesn't exist.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void updateCustomGameModeConfiguration(String packageName,
GameModeConfiguration gameModeConfig, int userId)
throws SecurityException, IllegalArgumentException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
if (!isPackageGame(packageName, userId)) {
Slog.d(TAG, "No-op for attempt to update custom game mode for non-game app: "
+ packageName);
return;
}
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
throw new IllegalArgumentException("User " + userId + " wasn't started");
}
}
// TODO(b/243448953): add validation on gameModeConfig provided
// Adding game mode config override of the given package name
GamePackageConfiguration configOverride;
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
return;
}
final GameManagerSettings settings = mSettings.get(userId);
// look for the existing GamePackageConfiguration override
configOverride = settings.getConfigOverride(packageName);
if (configOverride == null) {
configOverride = new GamePackageConfiguration(packageName);
settings.setConfigOverride(packageName, configOverride);
}
}
GamePackageConfiguration.GameModeConfiguration internalConfig =
configOverride.getOrAddDefaultGameModeConfiguration(GameManager.GAME_MODE_CUSTOM);
final float scalingValueFrom = internalConfig.getScaling();
final int fpsValueFrom = internalConfig.getFps();
internalConfig.updateFromPublicGameModeConfig(gameModeConfig);
sendUserMessage(userId, WRITE_SETTINGS, EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG,
WRITE_DELAY_MILLIS);
sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG, WRITE_DELAY_MILLIS /*delayMillis*/);
final int gameMode = getGameMode(packageName, userId);
if (gameMode == GameManager.GAME_MODE_CUSTOM) {
updateInterventions(packageName, gameMode, userId);
}
Slog.i(TAG, "Updated custom game mode config for package: " + packageName
+ " with FPS=" + internalConfig.getFps() + ";Scaling="
+ internalConfig.getScaling() + " under user " + userId);
int gameUid = -1;
try {
gameUid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (NameNotFoundException ex) {
Slog.d(TAG, "Cannot find the UID for package " + packageName + " under user " + userId);
}
FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CONFIGURATION_CHANGED, gameUid,
Binder.getCallingUid(), gameModeToStatsdGameMode(GameManager.GAME_MODE_CUSTOM),
scalingValueFrom, gameModeConfig.getScalingFactor(),
fpsValueFrom, gameModeConfig.getFpsOverride());
}
/**
* Adds a game mode listener.
*
* @throws SecurityException if caller doesn't have
* {@link android.Manifest.permission#MANAGE_GAME_MODE}
* permission.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void addGameModeListener(@NonNull IGameModeListener listener) {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
try {
final IBinder listenerBinder = listener.asBinder();
listenerBinder.linkToDeath(new DeathRecipient() {
@Override public void binderDied() {
// TODO(b/258851194): add traces on binder death based listener removal
removeGameModeListenerUnchecked(listener);
listenerBinder.unlinkToDeath(this, 0 /*flags*/);
}
}, 0 /*flags*/);
synchronized (mGameModeListenerLock) {
mGameModeListeners.put(listener, Binder.getCallingUid());
}
} catch (RemoteException ex) {
Slog.e(TAG,
"Failed to link death recipient for IGameModeListener from caller "
+ Binder.getCallingUid() + ", abandoned its listener registration", ex);
}
}
/**
* Removes a game mode listener.
*
* @throws SecurityException if caller doesn't have
* {@link android.Manifest.permission#MANAGE_GAME_MODE}
* permission.
*/
@Override
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void removeGameModeListener(@NonNull IGameModeListener listener) {
// TODO(b/258851194): add traces on manual listener removal
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
removeGameModeListenerUnchecked(listener);
}
private void removeGameModeListenerUnchecked(IGameModeListener listener) {
synchronized (mGameModeListenerLock) {
mGameModeListeners.remove(listener);
}
}
/**
* Adds a game state listener.
*/
@Override
public void addGameStateListener(@NonNull IGameStateListener listener) {
try {
final IBinder listenerBinder = listener.asBinder();
listenerBinder.linkToDeath(new DeathRecipient() {
@Override public void binderDied() {
removeGameStateListenerUnchecked(listener);
listenerBinder.unlinkToDeath(this, 0 /*flags*/);
}
}, 0 /*flags*/);
synchronized (mGameStateListenerLock) {
mGameStateListeners.put(listener, Binder.getCallingUid());
}
} catch (RemoteException ex) {
Slog.e(TAG,
"Failed to link death recipient for IGameStateListener from caller "
+ Binder.getCallingUid() + ", abandoned its listener registration", ex);
}
}
/**
* Removes a game state listener.
*/
@Override
public void removeGameStateListener(@NonNull IGameStateListener listener) {
removeGameStateListenerUnchecked(listener);
}
private void removeGameStateListenerUnchecked(IGameStateListener listener) {
synchronized (mGameStateListenerLock) {
mGameStateListeners.remove(listener);
}
}
/**
* Notified when boot is completed.
*/
@VisibleForTesting
void onBootCompleted() {
Slog.d(TAG, "onBootCompleted");
if (mGameServiceController != null) {
mGameServiceController.onBootComplete();
}
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
synchronized (mLock) {
// Note that the max wait time of broadcast is 10s (see
// {@ShutdownThread#MAX_BROADCAST_TIMEMAX_BROADCAST_TIME}) currently so
// this can be optional only if we have message delay plus processing
// time significant smaller to prevent data loss.
for (Map.Entry<Integer, GameManagerSettings> entry : mSettings.entrySet()) {
final int userId = entry.getKey();
sendUserMessage(userId, WRITE_SETTINGS,
EVENT_RECEIVE_SHUTDOWN_INDENT, 0 /*delayMillis*/);
sendUserMessage(userId,
WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
EVENT_RECEIVE_SHUTDOWN_INDENT,
0 /*delayMillis*/);
}
}
}
}
}, new IntentFilter(Intent.ACTION_SHUTDOWN));
Slog.v(TAG, "Game loading power mode OFF (game manager service start/restart)");
mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, false);
Slog.v(TAG, "Game power mode OFF (game manager service start/restart)");
mPowerManagerInternal.setPowerMode(Mode.GAME, false);
mGameDefaultFrameRateValue = (float) mSysProps.getInt(
PROPERTY_RO_SURFACEFLINGER_GAME_DEFAULT_FRAME_RATE, 60);
Slog.v(TAG, "Game Default Frame Rate : " + mGameDefaultFrameRateValue);
}
private void sendUserMessage(int userId, int what, String eventForLog, int delayMillis) {
Message msg = mHandler.obtainMessage(what, userId);
if (!mHandler.sendMessageDelayed(msg, delayMillis)) {
Slog.e(TAG, "Failed to send user message " + what + " on " + eventForLog);
}
}
void onUserStarting(@NonNull TargetUser user, File settingDataDir) {
final int userId = user.getUserIdentifier();
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
GameManagerSettings userSettings = new GameManagerSettings(settingDataDir);
mSettings.put(userId, userSettings);
userSettings.readPersistentDataLocked();
}
}
sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_STARTING,
0 /*delayMillis*/);
if (mGameServiceController != null) {
mGameServiceController.notifyUserStarted(user);
}
}
void onUserUnlocking(@NonNull TargetUser user) {
if (mGameServiceController != null) {
mGameServiceController.notifyUserUnlocking(user);
}
}
void onUserStopping(TargetUser user) {
final int userId = user.getUserIdentifier();
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
return;
}
sendUserMessage(userId, REMOVE_SETTINGS, EVENT_ON_USER_STOPPING, 0 /*delayMillis*/);
}
if (mGameServiceController != null) {
mGameServiceController.notifyUserStopped(user);
}
}
void onUserSwitching(TargetUser from, TargetUser to) {
final int toUserId = to.getUserIdentifier();
// we want to re-populate the setting when switching user as the device config may have
// changed, which will only update for the previous user, see
// DeviceConfigListener#onPropertiesChanged.
sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_SWITCHING,
0 /*delayMillis*/);
if (mGameServiceController != null) {
mGameServiceController.notifyNewForegroundUser(to);
}
}
/**
* Remove frame rate override due to mode switch
*/
private void resetFps(String packageName, @UserIdInt int userId) {
try {
final float fps = 0.0f;
final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
setGameModeFrameRateOverride(uid, fps);
} catch (PackageManager.NameNotFoundException e) {
return;
}
}
private static int modeToBitmask(@GameMode int gameMode) {
return (1 << gameMode);
}
private boolean bitFieldContainsModeBitmask(int bitField, @GameMode int gameMode) {
return (bitField & modeToBitmask(gameMode)) != 0;
}
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
private void updateUseAngle(String packageName, @GameMode int gameMode) {
// TODO (b/188475576): Nothing to do yet. Remove if it's still empty when we're ready to
// ship.
}
private void updateFps(GamePackageConfiguration packageConfig, String packageName,
@GameMode int gameMode, @UserIdInt int userId) {
final GamePackageConfiguration.GameModeConfiguration modeConfig =
packageConfig.getGameModeConfiguration(gameMode);
if (modeConfig == null) {
Slog.d(TAG, "Game mode " + gameMode + " not found for " + packageName);
return;
}
try {
final float fps = modeConfig.getFps();
final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
setGameModeFrameRateOverride(uid, fps);
} catch (PackageManager.NameNotFoundException e) {
return;
}
}
private void updateInterventions(String packageName,
@GameMode int gameMode, @UserIdInt int userId) {
final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
if (gameMode == GameManager.GAME_MODE_STANDARD
|| gameMode == GameManager.GAME_MODE_UNSUPPORTED || packageConfig == null
|| packageConfig.willGamePerformOptimizations(gameMode)
|| packageConfig.getGameModeConfiguration(gameMode) == null) {
resetFps(packageName, userId);
// resolution scaling does not need to be reset as it's now read dynamically on game
// restart, see #getResolutionScalingFactor and CompatModePackages#getCompatScale.
// TODO: reset Angle intervention here once implemented
if (packageConfig == null) {
Slog.v(TAG, "Package configuration not found for " + packageName);
return;
}
} else {
updateFps(packageConfig, packageName, gameMode, userId);
}
updateUseAngle(packageName, gameMode);
}
/**
* Set the Game Mode Configuration override.
* Update the config if exists, create one if not.
*/
@VisibleForTesting
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void setGameModeConfigOverride(String packageName, @UserIdInt int userId,
@GameMode int gameMode, String fpsStr, String scaling) throws SecurityException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
int gameUid = -1;
try {
gameUid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (NameNotFoundException ex) {
Slog.d(TAG, "Cannot find the UID for package " + packageName + " under user " + userId);
}
GamePackageConfiguration pkgConfig = getConfig(packageName, userId);
if (pkgConfig != null && pkgConfig.getGameModeConfiguration(gameMode) != null) {
final GamePackageConfiguration.GameModeConfiguration currentModeConfig =
pkgConfig.getGameModeConfiguration(gameMode);
FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CONFIGURATION_CHANGED, gameUid,
Binder.getCallingUid(), gameModeToStatsdGameMode(gameMode),
currentModeConfig.getScaling() /* fromScaling */,
scaling == null ? currentModeConfig.getScaling()
: Float.parseFloat(scaling) /* toScaling */,
currentModeConfig.getFps() /* fromFps */,
fpsStr == null ? currentModeConfig.getFps()
: Integer.parseInt(fpsStr)) /* toFps */;
} else {
FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CONFIGURATION_CHANGED, gameUid,
Binder.getCallingUid(), gameModeToStatsdGameMode(gameMode),
GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING /* fromScaling*/,
scaling == null ? GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING
: Float.parseFloat(scaling) /* toScaling */,
0 /* fromFps */,
fpsStr == null ? 0 : Integer.parseInt(fpsStr) /* toFps */);
}
// Adding game mode config override of the given package name
GamePackageConfiguration configOverride;
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
return;
}
final GameManagerSettings settings = mSettings.get(userId);
// look for the existing GamePackageConfiguration override
configOverride = settings.getConfigOverride(packageName);
if (configOverride == null) {
configOverride = new GamePackageConfiguration(packageName);
settings.setConfigOverride(packageName, configOverride);
}
}
// modify GameModeConfiguration intervention settings
GamePackageConfiguration.GameModeConfiguration modeConfigOverride =
configOverride.getOrAddDefaultGameModeConfiguration(gameMode);
if (fpsStr != null) {
modeConfigOverride.setFpsStr(fpsStr);
} else {
modeConfigOverride.setFpsStr(
GamePackageConfiguration.GameModeConfiguration.DEFAULT_FPS);
}
if (scaling != null) {
modeConfigOverride.setScaling(Float.parseFloat(scaling));
}
Slog.i(TAG, "Package Name: " + packageName
+ " FPS: " + String.valueOf(modeConfigOverride.getFps())
+ " Scaling: " + modeConfigOverride.getScaling());
setGameMode(packageName, gameMode, userId);
}
/**
* Reset the overridden gameModeConfiguration of the given mode.
* Remove the config override if game mode is not specified.
*/
@VisibleForTesting
@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
public void resetGameModeConfigOverride(String packageName, @UserIdInt int userId,
@GameMode int gameModeToReset) throws SecurityException {
checkPermission(Manifest.permission.MANAGE_GAME_MODE);
// resets GamePackageConfiguration of a given packageName.
// If a gameMode is specified, only reset the GameModeConfiguration of the gameMode.
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
return;
}
final GameManagerSettings settings = mSettings.get(userId);
if (gameModeToReset != -1) {
final GamePackageConfiguration configOverride = settings.getConfigOverride(
packageName);
if (configOverride == null) {
return;
}
final int modesBitfield = configOverride.getAvailableGameModesBitfield();
if (!bitFieldContainsModeBitmask(modesBitfield, gameModeToReset)) {
return;
}
configOverride.removeModeConfig(gameModeToReset);
if (!configOverride.hasActiveGameModeConfig()) {
settings.removeConfigOverride(packageName);
}
} else {
settings.removeConfigOverride(packageName);
}
}
// Make sure after resetting the game mode is still supported.
// If not, set the game mode to standard
int gameMode = getGameMode(packageName, userId);
final GamePackageConfiguration config = getConfig(packageName, userId);
final int newGameMode = getNewGameMode(gameMode, config);
if (gameMode != newGameMode) {
setGameMode(packageName, GameManager.GAME_MODE_STANDARD, userId);
return;
}
setGameMode(packageName, gameMode, userId);
}
private int getNewGameMode(int gameMode, GamePackageConfiguration config) {
int newGameMode = gameMode;
if (config != null) {
int modesBitfield = config.getAvailableGameModesBitfield();
// Remove UNSUPPORTED to simplify the logic here, since we really just
// want to check if we support selectable game modes
modesBitfield &= ~modeToBitmask(GameManager.GAME_MODE_UNSUPPORTED);
if (!bitFieldContainsModeBitmask(modesBitfield, gameMode)) {
// always default to STANDARD if there is no mode config
newGameMode = GameManager.GAME_MODE_STANDARD;
}
} else {
// always default to STANDARD if there is no package config
newGameMode = GameManager.GAME_MODE_STANDARD;
}
return newGameMode;
}
/**
* Returns the string listing all the interventions currently set to a game.
*/
@RequiresPermission(Manifest.permission.QUERY_ALL_PACKAGES)
public String getInterventionList(String packageName, int userId) {
checkPermission(Manifest.permission.QUERY_ALL_PACKAGES);
final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
final StringBuilder listStrSb = new StringBuilder();
if (packageConfig == null) {
listStrSb.append("\n No intervention found for package ")
.append(packageName);
return listStrSb.toString();
}
listStrSb.append("\n")
.append(packageConfig.toString());
return listStrSb.toString();
}
/**
* @hide
*/
@VisibleForTesting
void updateConfigsForUser(@UserIdInt int userId, boolean checkGamePackage,
String... packageNames) {
if (checkGamePackage) {
packageNames = Arrays.stream(packageNames).filter(
p -> isPackageGame(p, userId)).toArray(String[]::new);
}
try {
synchronized (mDeviceConfigLock) {
for (final String packageName : packageNames) {
final GamePackageConfiguration config =
new GamePackageConfiguration(mPackageManager, packageName, userId);
if (config.isActive()) {
Slog.v(TAG, "Adding config: " + config.toString());
mConfigs.put(packageName, config);
} else {
Slog.v(TAG, "Inactive package config for "
+ config.getPackageName() + ":" + config.toString());
mConfigs.remove(packageName);
}
}
}
synchronized (mLock) {
if (!mSettings.containsKey(userId)) {
return;
}
}
for (final String packageName : packageNames) {
int gameMode = getGameMode(packageName, userId);
// Make sure the user settings and package configs don't conflict.
// I.e. the user setting is set to a mode that no longer available due to
// config/manifest changes.
// Most of the time we won't have to change anything.
GamePackageConfiguration config = null;
synchronized (mDeviceConfigLock) {
config = mConfigs.get(packageName);
}
final int newGameMode = getNewGameMode(gameMode, config);
if (newGameMode != gameMode) {
setGameMode(packageName, newGameMode, userId);
} else {
// Make sure we handle the case when the interventions are changed while
// the game mode remains the same. We call only updateInterventions() here.
updateInterventions(packageName, gameMode, userId);
}
}
sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
"UPDATE_CONFIGS_FOR_USERS", 0 /*delayMillis*/);
} catch (Exception e) {
Slog.e(TAG, "Failed to update configs for user " + userId + ": " + e);
}
}
/*
Write the interventions and mode of each game to file /system/data/game_mode_intervention.list
Each line will contain the information of each game, separated by tab.
The format of the output is:
<package name> <UID> <current mode> <game mode 1> <interventions> <game mode 2> <interventions>
For example:
com.android.app1 1425 1 2 angle=0,scaling=1.0,fps=60 3 angle=1,scaling=0.5,fps=30
*/
private void writeGameModeInterventionsToFile(@UserIdInt int userId) {
FileOutputStream fileOutputStream = null;
BufferedWriter bufferedWriter;
try {
fileOutputStream = mGameModeInterventionListFile.startWrite();
bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream,
Charset.defaultCharset()));
final StringBuilder sb = new StringBuilder();
final List<String> installedGamesList = getInstalledGamePackageNamesByAllUsers(userId);
for (final String packageName : installedGamesList) {
GamePackageConfiguration packageConfig = getConfig(packageName, userId);
if (packageConfig == null) {
continue;
}
sb.append(packageName);
sb.append("\t");
sb.append(mPackageManager.getPackageUidAsUser(packageName, userId));
sb.append("\t");
sb.append(getGameMode(packageName, userId));
sb.append("\t");
final int[] modes = packageConfig.getAvailableGameModes();
for (int mode : modes) {
final GamePackageConfiguration.GameModeConfiguration gameModeConfiguration =
packageConfig.getGameModeConfiguration(mode);
if (gameModeConfiguration == null) {
continue;
}
sb.append(mode);
sb.append("\t");
final int useAngle = gameModeConfiguration.getUseAngle() ? 1 : 0;
sb.append(TextUtils.formatSimple("angle=%d", useAngle));
sb.append(",");
final float scaling = gameModeConfiguration.getScaling();
sb.append("scaling=");
sb.append(scaling);
sb.append(",");
final int fps = gameModeConfiguration.getFps();
sb.append(TextUtils.formatSimple("fps=%d", fps));
sb.append("\t");
}
sb.append("\n");
}
bufferedWriter.append(sb);
bufferedWriter.flush();
FileUtils.sync(fileOutputStream);
mGameModeInterventionListFile.finishWrite(fileOutputStream);
} catch (Exception e) {
mGameModeInterventionListFile.failWrite(fileOutputStream);
Slog.wtf(TAG, "Failed to write game_mode_intervention.list, exception " + e);
}
return;
}
private int[] getAllUserIds(@UserIdInt int currentUserId) {
final List<UserInfo> users = mUserManager.getUsers();
int[] userIds = new int[users.size()];
for (int i = 0; i < userIds.length; ++i) {
userIds[i] = users.get(i).id;
}
if (currentUserId != -1) {
userIds = ArrayUtils.appendInt(userIds, currentUserId);
}
return userIds;
}
private String[] getInstalledGamePackageNames(@UserIdInt int userId) {
final List<PackageInfo> packages =
mPackageManager.getInstalledPackagesAsUser(0, userId);
return packages.stream().filter(e -> e.applicationInfo != null && e.applicationInfo.category
== ApplicationInfo.CATEGORY_GAME)
.map(e -> e.packageName)
.toArray(String[]::new);
}
private List<String> getInstalledGamePackageNamesByAllUsers(@UserIdInt int currentUserId) {
HashSet<String> packageSet = new HashSet<>();
final int[] userIds = getAllUserIds(currentUserId);
for (int userId : userIds) {
packageSet.addAll(Arrays.asList(getInstalledGamePackageNames(userId)));
}
return new ArrayList<>(packageSet);
}
/**
* @hide
*/
public GamePackageConfiguration getConfig(String packageName, int userId) {
GamePackageConfiguration overrideConfig = null;
GamePackageConfiguration config;
synchronized (mDeviceConfigLock) {
config = mConfigs.get(packageName);
}
synchronized (mLock) {
if (mSettings.containsKey(userId)) {
overrideConfig = mSettings.get(userId).getConfigOverride(packageName);
}
}
if (overrideConfig == null || config == null) {
return overrideConfig == null ? config : overrideConfig;
}
return config.copyAndApplyOverride(overrideConfig);
}
private void registerPackageReceiver() {
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(ACTION_PACKAGE_ADDED);
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 int userId = getSendingUserId();
if (userId != ActivityManager.getCurrentUser()) {
return;
}
final String packageName = data.getSchemeSpecificPart();
try {
final ApplicationInfo applicationInfo = mPackageManager
.getApplicationInfoAsUser(
packageName, PackageManager.MATCH_ALL, userId);
if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
return;
}
} catch (NameNotFoundException e) {
// Ignore the exception.
}
switch (intent.getAction()) {
case ACTION_PACKAGE_ADDED:
updateConfigsForUser(userId, true /*checkGamePackage*/, packageName);
break;
case ACTION_PACKAGE_REMOVED:
if (!intent.getBooleanExtra(EXTRA_REPLACING, false)) {
synchronized (mDeviceConfigLock) {
mConfigs.remove(packageName);
}
synchronized (mLock) {
if (mSettings.containsKey(userId)) {
mSettings.get(userId).removeGame(packageName);
}
sendUserMessage(userId, WRITE_SETTINGS,
Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);
sendUserMessage(userId,
WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);
}
}
break;
default:
// do nothing
break;
}
} catch (NullPointerException e) {
Slog.e(TAG, "Failed to get package name for new package");
}
}
};
mContext.registerReceiverForAllUsers(packageReceiver, packageFilter,
/* broadcastPermission= */ null, /* scheduler= */ null);
}
private void registerDeviceConfigListener() {
mDeviceConfigListener = new DeviceConfigListener();
}
private void publishLocalService() {
LocalServices.addService(GameManagerInternal.class, new LocalService());
}
private void registerStatsCallbacks() {
final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
statsManager.setPullAtomCallback(
FrameworkStatsLog.GAME_MODE_INFO,
null, // use default PullAtomMetadata values
DIRECT_EXECUTOR,
this::onPullAtom);
statsManager.setPullAtomCallback(
FrameworkStatsLog.GAME_MODE_CONFIGURATION,
null, // use default PullAtomMetadata values
DIRECT_EXECUTOR,
this::onPullAtom);
statsManager.setPullAtomCallback(
FrameworkStatsLog.GAME_MODE_LISTENER,
null, // use default PullAtomMetadata values
DIRECT_EXECUTOR,
this::onPullAtom);
}
private int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) {
if (atomTag == FrameworkStatsLog.GAME_MODE_INFO
|| atomTag == FrameworkStatsLog.GAME_MODE_CONFIGURATION) {
int userId = ActivityManager.getCurrentUser();
Set<String> packages;
synchronized (mDeviceConfigLock) {
packages = mConfigs.keySet();
}
for (String p : packages) {
GamePackageConfiguration config = getConfig(p, userId);
if (config == null) {
continue;
}
int uid = -1;
try {
uid = mPackageManager.getPackageUidAsUser(p, userId);
} catch (NameNotFoundException ex) {
Slog.d(TAG,
"Cannot find UID for package " + p + " under user handle id " + userId);
}
if (atomTag == FrameworkStatsLog.GAME_MODE_INFO) {
data.add(
FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.GAME_MODE_INFO, uid,
gameModesToStatsdGameModes(config.getOverriddenGameModes()),
gameModesToStatsdGameModes(config.getAvailableGameModes())));
} else if (atomTag == FrameworkStatsLog.GAME_MODE_CONFIGURATION) {
for (int gameMode : config.getAvailableGameModes()) {
GamePackageConfiguration.GameModeConfiguration modeConfig =
config.getGameModeConfiguration(gameMode);
if (modeConfig != null) {
data.add(FrameworkStatsLog.buildStatsEvent(
FrameworkStatsLog.GAME_MODE_CONFIGURATION, uid,
gameModeToStatsdGameMode(gameMode), modeConfig.getFps(),
modeConfig.getScaling()));
}
}
}
}
} else if (atomTag == FrameworkStatsLog.GAME_MODE_LISTENER) {
synchronized (mGameModeListenerLock) {
data.add(FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.GAME_MODE_LISTENER,
mGameModeListeners.size()));
}
}
return android.app.StatsManager.PULL_SUCCESS;
}
private static int[] gameModesToStatsdGameModes(int[] modes) {
if (modes == null) {
return null;
}
int[] statsdModes = new int[modes.length];
int i = 0;
for (int mode : modes) {
statsdModes[i++] = gameModeToStatsdGameMode(mode);
}
return statsdModes;
}
private static int gameModeToStatsdGameMode(int mode) {
switch (mode) {
case GameManager.GAME_MODE_BATTERY:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_BATTERY;
case GameManager.GAME_MODE_PERFORMANCE:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_PERFORMANCE;
case GameManager.GAME_MODE_CUSTOM:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_CUSTOM;
case GameManager.GAME_MODE_STANDARD:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_STANDARD;
case GameManager.GAME_MODE_UNSUPPORTED:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_UNSUPPORTED;
default:
return FrameworkStatsLog.GAME_MODE_CONFIGURATION__GAME_MODE__GAME_MODE_UNSPECIFIED;
}
}
private static int gameStateModeToStatsdGameState(int mode) {
switch (mode) {
case GameState.MODE_NONE:
return FrameworkStatsLog.GAME_STATE_CHANGED__STATE__MODE_NONE;
case GameState.MODE_GAMEPLAY_INTERRUPTIBLE:
return FrameworkStatsLog.GAME_STATE_CHANGED__STATE__MODE_GAMEPLAY_INTERRUPTIBLE;
case GameState.MODE_GAMEPLAY_UNINTERRUPTIBLE:
return FrameworkStatsLog.GAME_STATE_CHANGED__STATE__MODE_GAMEPLAY_UNINTERRUPTIBLE;
case GameState.MODE_CONTENT:
return FrameworkStatsLog.GAME_STATE_CHANGED__STATE__MODE_CONTENT;
case GameState.MODE_UNKNOWN:
default:
return FrameworkStatsLog.GAME_STATE_CHANGED__STATE__MODE_UNKNOWN;
}
}
private static ServiceThread createServiceThread() {
ServiceThread handlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
handlerThread.start();
return handlerThread;
}
@VisibleForTesting
void setGameModeFrameRateOverride(int uid, float frameRate) {
nativeSetGameModeFrameRateOverride(uid, frameRate);
}
@VisibleForTesting
void setGameDefaultFrameRateOverride(int uid, float frameRate) {
Slog.v(TAG, "setDefaultFrameRateOverride : " + uid + " , " + frameRate);
nativeSetGameDefaultFrameRateOverride(uid, frameRate);
}
private float getGameDefaultFrameRate() {
final boolean isGameDefaultFrameRateEnabled;
float gameDefaultFrameRate = 0.0f;
synchronized (mLock) {
isGameDefaultFrameRateEnabled =
mSysProps.getBoolean(
PROPERTY_PERSISTENT_GFX_GAME_DEFAULT_FRAME_RATE_ENABLED, true);
}
if (gameDefaultFrameRate()) {
gameDefaultFrameRate = isGameDefaultFrameRateEnabled
? mGameDefaultFrameRateValue : 0.0f;
}
return gameDefaultFrameRate;
}
@Override
@EnforcePermission(Manifest.permission.MANAGE_GAME_MODE)
public void toggleGameDefaultFrameRate(boolean isEnabled) {
toggleGameDefaultFrameRate_enforcePermission();
if (gameDefaultFrameRate()) {
Slog.v(TAG, "toggleGameDefaultFrameRate : " + isEnabled);
this.toggleGameDefaultFrameRateUnchecked(isEnabled);
}
}
private void toggleGameDefaultFrameRateUnchecked(boolean isEnabled) {
// Update system properties.
// Here we only need to immediately update games that are in the foreground.
// We will update game default frame rate when a game comes into foreground in
// MyUidObserver.
synchronized (mLock) {
if (isEnabled) {
mSysProps.set(
PROPERTY_PERSISTENT_GFX_GAME_DEFAULT_FRAME_RATE_ENABLED, "true");
} else {
mSysProps.set(
PROPERTY_PERSISTENT_GFX_GAME_DEFAULT_FRAME_RATE_ENABLED, "false");
}
}
// Update all foreground games' frame rate.
synchronized (mUidObserverLock) {
for (int uid : mForegroundGameUids) {
setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate());
}
}
}
/**
* load dynamic library for frame rate overriding JNI calls
*/
private static native void nativeSetGameModeFrameRateOverride(int uid, float frameRate);
private static native void nativeSetGameDefaultFrameRateOverride(int uid, float frameRate);
final class MyUidObserver extends UidObserver {
@Override
public void onUidGone(int uid, boolean disabled) {
synchronized (mUidObserverLock) {
disableGameMode(uid);
}
}
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
synchronized (mUidObserverLock) {
if (procState != ActivityManager.PROCESS_STATE_TOP) {
disableGameMode(uid);
return;
}
final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
if (packages == null || packages.length == 0) {
return;
}
final int userId = mContext.getUserId();
if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) {
return;
}
if (mForegroundGameUids.isEmpty()) {
Slog.v(TAG, "Game power mode ON (process state was changed to foreground)");
mPowerManagerInternal.setPowerMode(Mode.GAME, true);
}
setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate());
mForegroundGameUids.add(uid);
}
}
private void disableGameMode(int uid) {
synchronized (mUidObserverLock) {
if (!mForegroundGameUids.contains(uid)) {
return;
}
mForegroundGameUids.remove(uid);
if (!mForegroundGameUids.isEmpty()) {
return;
}
Slog.v(TAG,
"Game power mode OFF (process remomved or state changed to background)");
mPowerManagerInternal.setPowerMode(Mode.GAME, false);
}
}
}
}