Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit afe0d0e3 authored by Andy Yu's avatar Andy Yu
Browse files

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
parent 83f04c01
Loading
Loading
Loading
Loading
+182 −2
Original line number Diff line number Diff line
@@ -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<Integer, GameManagerSettings> 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);
                }
            }
        } 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);
        }
    }

    /*
     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);
                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 String[] getInstalledGamePackageNames(int userId) {
    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
@@ -1537,6 +1706,17 @@ public final class GameManagerService extends IGameManagerService.Stub {
                .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
     */
+159 −1
Original line number Diff line number Diff line
@@ -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<String> readGameModeInterventionList() throws Exception {
        final File interventionFile = new File(InstrumentationRegistry.getContext().getFilesDir(),
                "system/game_mode_intervention.list");
        assertNotNull(interventionFile);
        List<String> 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<PackageInfo> userOnePackages = new ArrayList<>();
        final List<PackageInfo> userTwoPackages = new ArrayList<>();
        userOnePackages.add(pis[1]);
        userTwoPackages.add(pis[0]);
        userTwoPackages.add(pis[2]);

        final List<UserInfo> 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 <UID>   0   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.5,fps=60
         com.android.app1 <UID>   1   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.7,fps=30
         com.android.app0 <UID>   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<String> 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");

    }
}