From afe0d0e3c7b4adb8fd5cfe98d2375a4c7bd9f1dd Mon Sep 17 00:00:00 2001 From: Andy Yu Date: Wed, 30 Mar 2022 16:57:31 -0700 Subject: [PATCH] Add writing function to intervention list Create function to write game mode and game intervention information to a file in known location. In the file we write all the game packages installed by all users on the device. We update/re-write the file when: 1. The user is switched 2. A game mode is changed 3. New device config is pushed and updated 4. Game mode/intervention is overriden via adb shell cmd Bug: 219543620 Test: atest GameManagerServiceTests Change-Id: I290f9b72d4654c7308797686a5e37d5887369d8d --- .../server/app/GameManagerService.java | 184 +++++++++++++++++- .../server/app/GameManagerServiceTests.java | 160 ++++++++++++++- 2 files changed, 341 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index 3a789741842fd..dd73cbed06d90 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -60,6 +60,7 @@ 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; @@ -68,6 +69,7 @@ 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.Looper; import android.os.Message; @@ -77,9 +79,12 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; +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; @@ -90,6 +95,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.compat.CompatibilityOverrideConfig; import com.android.internal.compat.IPlatformCompat; 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; @@ -99,9 +105,17 @@ 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; /** @@ -122,6 +136,7 @@ public final class GameManagerService extends IGameManagerService.Stub { 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_SETTINGS_DELAY = 10 * 1000; // 10 seconds static final int LOADING_BOOST_MAX_DURATION = 5 * 1000; // 5 seconds @@ -131,6 +146,8 @@ public final class GameManagerService extends IGameManagerService.Stub { .build(); 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(); @@ -138,8 +155,12 @@ public final class GameManagerService extends IGameManagerService.Stub { private final Object mOverrideConfigLock = new Object(); private final Handler mHandler; private final PackageManager mPackageManager; + private final UserManager mUserManager; private final IPlatformCompat mPlatformCompat; private final PowerManagerInternal mPowerManagerInternal; + private final File mSystemDir; + @VisibleForTesting + final AtomicFile mGameModeInterventionListFile; private DeviceConfigListener mDeviceConfigListener; @GuardedBy("mLock") private final ArrayMap mSettings = new ArrayMap<>(); @@ -158,9 +179,53 @@ public final class GameManagerService extends IGameManagerService.Stub { mContext = context; mHandler = new SettingsHandler(looper); mPackageManager = mContext.getPackageManager(); + mUserManager = mContext.getSystemService(UserManager.class); mPlatformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); + mSystemDir = new File(Environment.getDataDirectory(), "system"); + mSystemDir.mkdirs(); + FileUtils.setPermissions(mSystemDir.toString(), + FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, + -1, -1); + mGameModeInterventionListFile = new AtomicFile(new File(mSystemDir, + 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; + } + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + GameManagerService(Context context, Looper looper, File dataDir) { + mContext = context; + mHandler = new SettingsHandler(looper); + mPackageManager = mContext.getPackageManager(); + mUserManager = mContext.getSystemService(UserManager.class); + mPlatformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); + mSystemDir = new File(dataDir, "system"); + mSystemDir.mkdirs(); + FileUtils.setPermissions(mSystemDir.toString(), + FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, + -1, -1); + mGameModeInterventionListFile = new AtomicFile(new File(mSystemDir, + 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(), @@ -306,6 +371,22 @@ public final class GameManagerService extends IGameManagerService.Stub { 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) { + removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null); + } + break; + } + + Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); + removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null); + writeGameModeInterventionsToFile(userId); + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + break; + } } } } @@ -955,6 +1036,11 @@ public final class GameManagerService extends IGameManagerService.Stub { } } updateInterventions(packageName, gameMode, userId); + final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE); + msg.obj = userId; + if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) { + mHandler.sendMessage(msg); + } } /** @@ -1520,15 +1606,98 @@ public final class GameManagerService extends IGameManagerService.Stub { 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); } - updateInterventions(packageName, gameMode, userId); } } catch (Exception e) { Slog.e(TAG, "Failed to update compat modes for user " + userId + ": " + e); } + + final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE); + msg.obj = userId; + if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) { + mHandler.sendMessage(msg); + } } - private String[] getInstalledGamePackageNames(int userId) { + /* + 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: + + 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 installedGamesList = getInstalledGamePackageNamesByAllUsers(userId); + for (final String packageName : installedGamesList) { + GamePackageConfiguration packageConfig = getConfig(packageName); + 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 String 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 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 packages = mPackageManager.getInstalledPackagesAsUser(0, userId); return packages.stream().filter(e -> e.applicationInfo != null && e.applicationInfo.category @@ -1537,6 +1706,17 @@ public final class GameManagerService extends IGameManagerService.Stub { .toArray(String[]::new); } + private List getInstalledGamePackageNamesByAllUsers(@UserIdInt int currentUserId) { + HashSet packageSet = new HashSet<>(); + + final int[] userIds = getAllUserIds(currentUserId); + for (int userId : userIds) { + packageSet.addAll(Arrays.asList(getInstalledGamePackageNames(userId))); + } + + return new ArrayList<>(packageSet); + } + /** * @hide */ diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 09565b42e5158..d675b0aa4973a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -19,6 +19,7 @@ package com.android.server.app; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -49,6 +50,7 @@ import android.content.res.XmlResourceParser; import android.hardware.power.Mode; import android.os.Bundle; import android.os.PowerManagerInternal; +import android.os.UserManager; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; @@ -69,6 +71,8 @@ import org.mockito.Mock; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import java.io.File; +import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -79,7 +83,7 @@ import java.util.function.Supplier; @Presubmit public class GameManagerServiceTests { @Mock MockContext mMockContext; - private static final String TAG = "GameServiceTests"; + private static final String TAG = "GameManagerServiceTests"; private static final String PACKAGE_NAME_INVALID = "com.android.app"; private static final int USER_ID_1 = 1001; private static final int USER_ID_2 = 1002; @@ -91,6 +95,8 @@ public class GameManagerServiceTests { private PackageManager mMockPackageManager; @Mock private PowerManagerInternal mMockPowerManager; + @Mock + private UserManager mMockUserManager; // Stolen from ConnectivityServiceTest.MockContext class MockContext extends ContextWrapper { @@ -150,6 +156,15 @@ public class GameManagerServiceTests { public PackageManager getPackageManager() { return mMockPackageManager; } + + @Override + public Object getSystemService(String name) { + switch (name) { + case Context.USER_SERVICE: + return mMockUserManager; + } + throw new UnsupportedOperationException("Couldn't find system service: " + name); + } } @Before @@ -198,6 +213,19 @@ public class GameManagerServiceTests { mTestLooper.dispatchAll(); } + private void switchUser(GameManagerService gameManagerService, int from, int to) { + UserInfo userInfoFrom = new UserInfo(from, "name", 0); + UserInfo userInfoTo = new UserInfo(to, "name", 0); + gameManagerService.onUserSwitching(/* from */ new SystemService.TargetUser(userInfoFrom), + /* to */ new SystemService.TargetUser(userInfoTo)); + mTestLooper.dispatchAll(); + } + + private void mockManageUsersGranted() { + mMockContext.setPermission(Manifest.permission.MANAGE_USERS, + PackageManager.PERMISSION_GRANTED); + } + private void mockModifyGameModeGranted() { mMockContext.setPermission(Manifest.permission.MANAGE_GAME_MODE, PackageManager.PERMISSION_GRANTED); @@ -819,6 +847,7 @@ public class GameManagerServiceTests { GameManagerService gameManagerService = new GameManagerService( mMockContext, mTestLooper.getLooper()); startUser(gameManagerService, USER_ID_1); + gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, GameManager.GAME_MODE_PERFORMANCE, "120", "0.3"); gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, @@ -1246,4 +1275,133 @@ public class GameManagerServiceTests { public void testSetGameStateNotLoading() { setGameState(false); } + + private List readGameModeInterventionList() throws Exception { + final File interventionFile = new File(InstrumentationRegistry.getContext().getFilesDir(), + "system/game_mode_intervention.list"); + assertNotNull(interventionFile); + List output = Files.readAllLines(interventionFile.toPath()); + return output; + } + + private void mockInterventionListForMultipleUsers() { + final String[] packageNames = new String[] {"com.android.app0", + "com.android.app1", "com.android.app2"}; + + final ApplicationInfo[] applicationInfos = new ApplicationInfo[3]; + final PackageInfo[] pis = new PackageInfo[3]; + for (int i = 0; i < 3; ++i) { + applicationInfos[i] = new ApplicationInfo(); + applicationInfos[i].category = ApplicationInfo.CATEGORY_GAME; + applicationInfos[i].packageName = packageNames[i]; + + pis[i] = new PackageInfo(); + pis[i].packageName = packageNames[i]; + pis[i].applicationInfo = applicationInfos[i]; + } + + final List userOnePackages = new ArrayList<>(); + final List userTwoPackages = new ArrayList<>(); + userOnePackages.add(pis[1]); + userTwoPackages.add(pis[0]); + userTwoPackages.add(pis[2]); + + final List userInfos = new ArrayList<>(2); + userInfos.add(new UserInfo()); + userInfos.add(new UserInfo()); + userInfos.get(0).id = USER_ID_1; + userInfos.get(1).id = USER_ID_2; + + when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), eq(USER_ID_1))) + .thenReturn(userOnePackages); + when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), eq(USER_ID_2))) + .thenReturn(userTwoPackages); + when(mMockUserManager.getUsers()).thenReturn(userInfos); + } + + @Test + public void testVerifyInterventionList() throws Exception { + mockDeviceConfigAll(); + mockInterventionListForMultipleUsers(); + mockManageUsersGranted(); + mockModifyGameModeGranted(); + final Context context = InstrumentationRegistry.getContext(); + GameManagerService gameManagerService = + new GameManagerService(mMockContext, + mTestLooper.getLooper(), + context.getFilesDir()); + startUser(gameManagerService, USER_ID_1); + startUser(gameManagerService, USER_ID_2); + + gameManagerService.setGameModeConfigOverride("com.android.app0", USER_ID_2, + GameManager.GAME_MODE_PERFORMANCE, "120", "0.6"); + gameManagerService.setGameModeConfigOverride("com.android.app2", USER_ID_2, + GameManager.GAME_MODE_BATTERY, "60", "0.5"); + mTestLooper.dispatchAll(); + + /* Expected fileOutput (order may vary) + com.android.app2 0 2 angle=0,scaling=0.5,fps=90 3 angle=0,scaling=0.5,fps=60 + com.android.app1 1 2 angle=0,scaling=0.5,fps=90 3 angle=0,scaling=0.7,fps=30 + com.android.app0 0 2 angle=0,scaling=0.6,fps=120 3 angle=0,scaling=0.7,fps=30 + + The current game mode would only be set to non-zero if the current user have that game + installed. + */ + + List fileOutput = readGameModeInterventionList(); + assertEquals(fileOutput.size(), 3); + + String[] splitLine = fileOutput.get(0).split("\\s+"); + assertEquals(splitLine[0], "com.android.app2"); + assertEquals(splitLine[2], "3"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.5,fps=60"); + splitLine = fileOutput.get(1).split("\\s+"); + assertEquals(splitLine[0], "com.android.app1"); + assertEquals(splitLine[2], "0"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30"); + splitLine = fileOutput.get(2).split("\\s+"); + assertEquals(splitLine[0], "com.android.app0"); + assertEquals(splitLine[2], "2"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.6,fps=120"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30"); + + switchUser(gameManagerService, USER_ID_2, USER_ID_1); + gameManagerService.setGameMode("com.android.app1", + GameManager.GAME_MODE_BATTERY, USER_ID_1); + mTestLooper.dispatchAll(); + + fileOutput = readGameModeInterventionList(); + assertEquals(fileOutput.size(), 3); + + splitLine = fileOutput.get(0).split("\\s+"); + assertEquals(splitLine[0], "com.android.app2"); + assertEquals(splitLine[2], "0"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.5,fps=60"); + splitLine = fileOutput.get(1).split("\\s+"); + assertEquals(splitLine[0], "com.android.app1"); + assertEquals(splitLine[2], "3"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30"); + splitLine = fileOutput.get(2).split("\\s+"); + assertEquals(splitLine[0], "com.android.app0"); + assertEquals(splitLine[2], "0"); + assertEquals(splitLine[3], "2"); + assertEquals(splitLine[4], "angle=0,scaling=0.6,fps=120"); + assertEquals(splitLine[5], "3"); + assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30"); + + } }