/*
 * 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_CHANGED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;

import static com.android.server.wm.CompatModePackages.DOWNSCALED;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_30;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_35;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_40;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_45;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_50;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_55;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_60;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_65;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_70;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_75;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_80;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_85;
import static com.android.server.wm.CompatModePackages.DOWNSCALE_90;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.GameManager;
import android.app.GameManager.GameMode;
import android.app.IGameManagerService;
import android.app.compat.PackageOverride;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.ArrayMap;
import android.util.KeyValueListParser;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.compat.CompatibilityOverrideConfig;
import com.android.internal.compat.IPlatformCompat;
import com.android.server.ServiceThread;
import com.android.server.SystemService;
import com.android.server.SystemService.TargetUser;

import java.io.FileDescriptor;
import java.util.List;

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

    private static final boolean DEBUG = false;

    static final int WRITE_SETTINGS = 1;
    static final int REMOVE_SETTINGS = 2;
    static final int POPULATE_GAME_MODE_SETTINGS = 3;
    static final int WRITE_SETTINGS_DELAY = 10 * 1000;  // 10 seconds
    static final PackageOverride COMPAT_ENABLED = new PackageOverride.Builder().setEnabled(true)
            .build();
    static final PackageOverride COMPAT_DISABLED = new PackageOverride.Builder().setEnabled(false)
            .build();

    private final Context mContext;
    private final Object mLock = new Object();
    private final Object mDeviceConfigLock = new Object();
    private final Handler mHandler;
    private final PackageManager mPackageManager;
    private final IPlatformCompat mPlatformCompat;
    private DeviceConfigListener mDeviceConfigListener;
    @GuardedBy("mLock")
    private final ArrayMap<Integer, GameManagerSettings> mSettings = new ArrayMap<>();
    @GuardedBy("mDeviceConfigLock")
    private final ArrayMap<String, GamePackageConfiguration> mConfigs = new ArrayMap<>();

    public GameManagerService(Context context) {
        this(context, createServiceThread().getLooper());
    }

    GameManagerService(Context context, Looper looper) {
        mContext = context;
        mHandler = new SettingsHandler(looper);
        mPackageManager = mContext.getPackageManager();
        mPlatformCompat = IPlatformCompat.Stub.asInterface(
                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
    }

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

    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) {
                            removeMessages(WRITE_SETTINGS, msg.obj);
                        }
                        break;
                    }

                    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
                    synchronized (mLock) {
                        removeMessages(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) {
                            removeMessages(WRITE_SETTINGS, msg.obj);
                            removeMessages(REMOVE_SETTINGS, msg.obj);
                        }
                        break;
                    }

                    synchronized (mLock) {
                        // Since the user was removed, ignore previous write message
                        // and do write here.
                        removeMessages(WRITE_SETTINGS, msg.obj);
                        removeMessages(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: {
                    // Scan all game packages and re-enforce the configured compat mode overrides
                    // as the DeviceConfig may have be wiped/since last reboot and we can't risk
                    // having overrides configured for packages that no longer have any DeviceConfig
                    // and thus any way to escape compat mode.
                    removeMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
                    final int userId = (int) msg.obj;
                    final String[] packageNames = getInstalledGamePackageNames(userId);
                    updateConfigsForUser(userId, packageNames);
                    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]);
            updateConfigsForUser(ActivityManager.getCurrentUser(), packageNames);
        }

        @Override
        public void finalize() {
            DeviceConfig.removeOnPropertiesChangedListener(this);
        }
    }

    // Turn the raw string to the corresponding CompatChange id.
    static long getCompatChangeId(String raw) {
        switch (raw) {
            case "0.3":
                return DOWNSCALE_30;
            case "0.35":
                return DOWNSCALE_35;
            case "0.4":
                return DOWNSCALE_40;
            case "0.45":
                return DOWNSCALE_45;
            case "0.5":
                return DOWNSCALE_50;
            case "0.55":
                return DOWNSCALE_55;
            case "0.6":
                return DOWNSCALE_60;
            case "0.65":
                return DOWNSCALE_65;
            case "0.7":
                return DOWNSCALE_70;
            case "0.75":
                return DOWNSCALE_75;
            case "0.8":
                return DOWNSCALE_80;
            case "0.85":
                return DOWNSCALE_85;
            case "0.9":
                return DOWNSCALE_90;
        }
        return 0;
    }

    /**
     * GamePackageConfiguration manages all game mode config details for its associated package.
     */
    @VisibleForTesting
    public 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";

        private final String mPackageName;
        private final ArrayMap<Integer, GameModeConfiguration> mModeConfigs;
        private boolean mPerfModeOptedIn;
        private boolean mBatteryModeOptedIn;
        private boolean mAllowDownscale;
        private boolean mAllowAngle;

        GamePackageConfiguration(String packageName, int userId) {
            mPackageName = packageName;
            mModeConfigs = new ArrayMap<>();
            try {
                final ApplicationInfo ai = mPackageManager.getApplicationInfoAsUser(packageName,
                        PackageManager.GET_META_DATA, userId);
                if (ai.metaData != null) {
                    mPerfModeOptedIn = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);
                    mBatteryModeOptedIn = 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);
                } else {
                    mPerfModeOptedIn = false;
                    mBatteryModeOptedIn = false;
                    mAllowDownscale = true;
                    mAllowAngle = true;
                }
            } catch (PackageManager.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");
                    }
                }
            }
        }

        /**
         * GameModeConfiguration contains all the values for all the interventions associated with
         * a game mode.
         */
        @VisibleForTesting
        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 DEFAULT_SCALING = "1.0";
            public static final String ANGLE_KEY = "useAngle";

            private final @GameMode int mGameMode;
            private final String mScaling;
            private final boolean mUseAngle;

            GameModeConfiguration(KeyValueListParser parser) {
                mGameMode = parser.getInt(MODE_KEY, GameManager.GAME_MODE_UNSUPPORTED);
                // isGameModeOptedIn() 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.getString(SCALING_KEY, DEFAULT_SCALING);
                // 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, false);
            }

            public int getGameMode() {
                return mGameMode;
            }

            public String getScaling() {
                return mScaling;
            }

            public boolean getUseAngle() {
                return mUseAngle;
            }

            public boolean isValid() {
                return (mGameMode == GameManager.GAME_MODE_PERFORMANCE
                        || mGameMode == GameManager.GAME_MODE_BATTERY)
                        && (!mAllowDownscale || getCompatChangeId() != 0);
            }

            /**
             * @hide
             */
            public String toString() {
                return "[Game Mode:" + mGameMode + ",Scaling:" + mScaling + ",Use Angle:"
                        + mUseAngle + "]";
            }

            /**
             * Get the corresponding compat change id for the current scaling string.
             */
            public long getCompatChangeId() {
                return GameManagerService.getCompatChangeId(mScaling);
            }
        }

        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 (mBatteryModeOptedIn && gameMode == GameManager.GAME_MODE_BATTERY)
                    || (mPerfModeOptedIn && gameMode == GameManager.GAME_MODE_PERFORMANCE);
        }

        private int getAvailableGameModesBitfield() {
            int field = 0;
            for (final int mode : mModeConfigs.keySet()) {
                field |= modeToBitmask(mode);
            }
            if (mBatteryModeOptedIn) {
                field |= modeToBitmask(GameManager.GAME_MODE_BATTERY);
            }
            if (mPerfModeOptedIn) {
                field |= modeToBitmask(GameManager.GAME_MODE_PERFORMANCE);
            }
            // The lowest bit is reserved for UNSUPPORTED, STANDARD is supported if we support any
            // other mode.
            if (field > 1) {
                field |= modeToBitmask(GameManager.GAME_MODE_STANDARD);
            } else {
                field |= modeToBitmask(GameManager.GAME_MODE_UNSUPPORTED);
            }
            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 a GameModeConfiguration for a given game mode.
         *
         * @return The package's GameModeConfiguration for the provided mode or null if absent
         */
        public GameModeConfiguration getGameModeConfiguration(@GameMode int gameMode) {
            return mModeConfigs.get(gameMode);
        }

        /**
         * Insert a new GameModeConfiguration
         */
        public void addModeConfig(GameModeConfiguration config) {
            if (config.isValid()) {
                mModeConfigs.put(config.getGameMode(), config);
            } else {
                Slog.w(TAG, "Invalid game mode config for "
                        + mPackageName + ":" + config.toString());
            }
        }

        public boolean isValid() {
            return mModeConfigs.size() > 0 || mBatteryModeOptedIn || mPerfModeOptedIn;
        }

        public String toString() {
            return "[Name:" + mPackageName + " Modes: " + mModeConfigs.toString() + "]";
        }
    }

    /**
     * SystemService lifecycle for GameService.
     *
     * @hide
     */
    public static class Lifecycle extends SystemService {
        private GameManagerService mService;

        public Lifecycle(Context context) {
            super(context);
        }

        @Override
        public void onStart() {
            mService = new GameManagerService(getContext());
            publishBinderService(Context.GAME_SERVICE, mService);
            mService.registerDeviceConfigListener();
            mService.registerPackageReceiver();
        }

        @Override
        public void onBootPhase(int phase) {
            if (phase == PHASE_BOOT_COMPLETED) {
                mService.onBootCompleted();
            }
        }

        @Override
        public void onUserStarting(@NonNull TargetUser user) {
            mService.onUserStarting(user.getUserIdentifier());
        }

        @Override
        public void onUserStopping(@NonNull TargetUser user) {
            mService.onUserStopping(user.getUserIdentifier());
        }

        @Override
        public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
            mService.onUserSwitching(from, to.getUserIdentifier());
        }
    }

    private boolean isValidPackageName(String packageName, int userId) {
        try {
            return mPackageManager.getPackageUidAsUser(packageName, userId)
                    == Binder.getCallingUid();
        } catch (PackageManager.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);
        }
    }

    /**
     * Get an array of game modes available for a given package.
     * Checks that the caller has {@link android.Manifest.permission#MANAGE_GAME_MODE}.
     */
    @Override
    @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
    public @GameMode int[] getAvailableGameModes(String packageName) throws SecurityException {
        checkPermission(Manifest.permission.MANAGE_GAME_MODE);
        synchronized (mDeviceConfigLock) {
            final GamePackageConfiguration config = mConfigs.get(packageName);
            if (config == null) {
                return new int[]{GameManager.GAME_MODE_UNSUPPORTED};
            }
            return config.getAvailableGameModes();
        }
    }

    private @GameMode int getGameModeFromSettings(String packageName, int userId) {
        synchronized (mLock) {
            if (!mSettings.containsKey(userId)) {
                Slog.w(TAG, "User ID '" + userId + "' does not have a Game Mode"
                        + " selected for package: '" + packageName + "'");
                return GameManager.GAME_MODE_UNSUPPORTED;
            }

            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(String packageName, int userId)
            throws SecurityException {
        userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
                Binder.getCallingUid(), userId, false, true, "getGameMode",
                "com.android.server.app.GameManagerService");

        // Restrict to games only.
        try {
            final ApplicationInfo applicationInfo = mPackageManager
                    .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
            if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
                // 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;
            }
        } catch (PackageManager.NameNotFoundException e) {
            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 getGameModeFromSettings(packageName, userId);
        }

        // Since the package name doesn't match, check the caller has the necessary permission.
        checkPermission(Manifest.permission.MANAGE_GAME_MODE);
        return getGameModeFromSettings(packageName, userId);
    }

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

        // Restrict to games only.
        try {
            final ApplicationInfo applicationInfo = mPackageManager
                    .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
            if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
                // Ignore attempt to set the game mode for applications that are not identified
                // as game. See {@link PackageManager#setApplicationCategoryHint(String, int)}
                return;
            }
        } catch (PackageManager.NameNotFoundException e) {
            return;
        }

        synchronized (mLock) {
            userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
                    Binder.getCallingUid(), userId, false, true, "setGameMode",
                    "com.android.server.app.GameManagerService");

            if (!mSettings.containsKey(userId)) {
                return;
            }
            GameManagerSettings userSettings = mSettings.get(userId);
            userSettings.setGameModeLocked(packageName, gameMode);
            final Message msg = mHandler.obtainMessage(WRITE_SETTINGS);
            msg.obj = userId;
            if (!mHandler.hasEqualMessages(WRITE_SETTINGS, userId)) {
                mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY);
            }
        }
        updateInterventions(packageName, 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 getAngleEnabled(String packageName, int userId)
            throws SecurityException {
        final int gameMode = getGameMode(packageName, userId);
        if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
            return false;
        }

        synchronized (mDeviceConfigLock) {
            final GamePackageConfiguration config = mConfigs.get(packageName);
            if (config == null) {
                return false;
            }
            GamePackageConfiguration.GameModeConfiguration gameModeConfiguration =
                    config.getGameModeConfiguration(gameMode);
            if (gameModeConfiguration == null) {
                return false;
            }
            return gameModeConfiguration.getUseAngle();
        }
    }

    /**
     * Notified when boot is completed.
     */
    @VisibleForTesting
    void onBootCompleted() {
        Slog.d(TAG, "onBootCompleted");
    }

    void onUserStarting(int userId) {
        synchronized (mLock) {
            if (!mSettings.containsKey(userId)) {
                GameManagerSettings userSettings =
                        new GameManagerSettings(Environment.getDataSystemDeDirectory(userId));
                mSettings.put(userId, userSettings);
                userSettings.readPersistentDataLocked();
            }
        }
        final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS);
        msg.obj = userId;
        mHandler.sendMessage(msg);
    }

    void onUserStopping(int userId) {
        synchronized (mLock) {
            if (!mSettings.containsKey(userId)) {
                return;
            }
            final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS);
            msg.obj = userId;
            mHandler.sendMessage(msg);
        }
    }

    void onUserSwitching(TargetUser from, int toUserId) {
        if (from != null) {
            synchronized (mLock) {
                final int fromUserId = from.getUserIdentifier();
                if (mSettings.containsKey(fromUserId)) {
                    final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS);
                    msg.obj = fromUserId;
                    mHandler.sendMessage(msg);
                }
            }
        }
        final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS);
        msg.obj = toUserId;
        mHandler.sendMessage(msg);
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public void disableCompatScale(String packageName) {
        final long uid = Binder.clearCallingIdentity();
        try {
            Slog.i(TAG, "Disabling downscale for " + packageName);
            final ArrayMap<Long, PackageOverride> overrides = new ArrayMap<>();
            overrides.put(DOWNSCALED, COMPAT_DISABLED);
            final CompatibilityOverrideConfig changeConfig = new CompatibilityOverrideConfig(
                    overrides);
            try {
                mPlatformCompat.putOverridesOnReleaseBuilds(changeConfig, packageName);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call IPlatformCompat#putOverridesOnReleaseBuilds", e);
            }
        } finally {
            Binder.restoreCallingIdentity(uid);
        }
    }

    private void enableCompatScale(String packageName, long scaleId) {
        final long uid = Binder.clearCallingIdentity();
        try {
            Slog.i(TAG, "Enabling downscale: " + scaleId + " for " + packageName);
            final ArrayMap<Long, PackageOverride> overrides = new ArrayMap<>();
            overrides.put(DOWNSCALED, COMPAT_ENABLED);
            overrides.put(DOWNSCALE_30, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_35, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_40, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_45, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_50, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_55, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_60, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_65, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_70, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_75, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_80, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_85, COMPAT_DISABLED);
            overrides.put(DOWNSCALE_90, COMPAT_DISABLED);
            overrides.put(scaleId, COMPAT_ENABLED);
            final CompatibilityOverrideConfig changeConfig = new CompatibilityOverrideConfig(
                    overrides);
            try {
                mPlatformCompat.putOverridesOnReleaseBuilds(changeConfig, packageName);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call IPlatformCompat#putOverridesOnReleaseBuilds", e);
            }
        } finally {
            Binder.restoreCallingIdentity(uid);
        }
    }

    private void updateCompatModeDownscale(String packageName, @GameMode int gameMode) {
        synchronized (mDeviceConfigLock) {
            if (gameMode == GameManager.GAME_MODE_STANDARD
                    || gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
                disableCompatScale(packageName);
                return;
            }
            final GamePackageConfiguration packageConfig = mConfigs.get(packageName);
            if (packageConfig == null) {
                disableCompatScale(packageName);
                Slog.v(TAG, "Package configuration not found for " + packageName);
                return;
            }
            if (DEBUG) {
                Slog.v(TAG, dumpDeviceConfigs());
            }
            if (packageConfig.willGamePerformOptimizations(gameMode)) {
                disableCompatScale(packageName);
                return;
            }
            final GamePackageConfiguration.GameModeConfiguration modeConfig =
                    packageConfig.getGameModeConfiguration(gameMode);
            if (modeConfig == null) {
                Slog.i(TAG, "Game mode " + gameMode + " not found for " + packageName);
                return;
            }
            long scaleId = modeConfig.getCompatChangeId();
            if (scaleId == 0) {
                Slog.w(TAG, "Invalid downscaling change id " + scaleId + " for "
                        + packageName);
                return;
            }

            enableCompatScale(packageName, scaleId);
        }
    }

    private 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 updateInterventions(String packageName, @GameMode int gameMode) {
        updateCompatModeDownscale(packageName, gameMode);
        updateUseAngle(packageName, gameMode);
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public void updateConfigsForUser(int userId, String ...packageNames) {
        try {
            synchronized (mDeviceConfigLock) {
                for (final String packageName : packageNames) {
                    final GamePackageConfiguration config =
                            new GamePackageConfiguration(packageName, userId);
                    if (config.isValid()) {
                        if (DEBUG) {
                            Slog.i(TAG, "Adding config: " + config.toString());
                        }
                        mConfigs.put(packageName, config);
                    } else {
                        Slog.w(TAG, "Invalid package config for "
                                + config.getPackageName() + ":" + config.toString());
                        mConfigs.remove(packageName);
                    }
                }
            }
            for (final String packageName : packageNames) {
                if (mSettings.containsKey(userId)) {
                    int gameMode = getGameMode(packageName, userId);
                    int newGameMode = gameMode;
                    // 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;
                    synchronized (mDeviceConfigLock) {
                        config = mConfigs.get(packageName);
                    }
                    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)) {
                            if (bitFieldContainsModeBitmask(modesBitfield,
                                    GameManager.GAME_MODE_STANDARD)) {
                                // If the current set mode isn't supported, but we support STANDARD,
                                // then set the mode to STANDARD.
                                newGameMode = GameManager.GAME_MODE_STANDARD;
                            } else {
                                // If we don't support any game modes, then set to UNSUPPORTED
                                newGameMode = GameManager.GAME_MODE_UNSUPPORTED;
                            }
                        }
                    } else if (gameMode != GameManager.GAME_MODE_UNSUPPORTED) {
                        // If we have no config for the package, but the configured mode is not
                        // UNSUPPORTED, then set to UNSUPPORTED
                        newGameMode = GameManager.GAME_MODE_UNSUPPORTED;
                    }
                    if (newGameMode != gameMode) {
                        setGameMode(packageName, newGameMode, userId);
                    }
                    updateInterventions(packageName, gameMode);
                }
            }
        } catch (Exception e) {
            Slog.e(TAG, "Failed to update compat modes for user " + userId + ": " + e);
        }
    }

    private String[] getInstalledGamePackageNames(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);
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public GamePackageConfiguration getConfig(String packageName) {
        return mConfigs.get(packageName);
    }

    private void registerPackageReceiver() {
        final IntentFilter packageFilter = new IntentFilter();
        packageFilter.addAction(ACTION_PACKAGE_ADDED);
        packageFilter.addAction(ACTION_PACKAGE_CHANGED);
        packageFilter.addAction(ACTION_PACKAGE_REMOVED);
        packageFilter.addDataScheme("package");
        final BroadcastReceiver packageReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
                final Uri data = intent.getData();
                try {
                    final 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 (PackageManager.NameNotFoundException e) {
                        // Ignore the exception.
                    }
                    switch (intent.getAction()) {
                        case ACTION_PACKAGE_ADDED:
                        case ACTION_PACKAGE_CHANGED:
                            updateConfigsForUser(userId, packageName);
                            break;
                        case ACTION_PACKAGE_REMOVED:
                            disableCompatScale(packageName);
                            synchronized (mDeviceConfigLock) {
                                mConfigs.remove(packageName);
                            }
                            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 String dumpDeviceConfigs() {
        StringBuilder out = new StringBuilder();
        for (String key : mConfigs.keySet()) {
            out.append("[\nName: ").append(key)
                    .append("\nConfig: ").append(mConfigs.get(key).toString()).append("\n]");
        }
        return out.toString();
    }

    private static ServiceThread createServiceThread() {
        ServiceThread handlerThread = new ServiceThread(TAG,
                Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
        handlerThread.start();
        return handlerThread;
    }
}
