1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.app; 18 19 import android.annotation.NonNull; 20 import android.app.ActivityManager; 21 import android.app.GameManager; 22 import android.app.IGameManagerService; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.os.RemoteException; 27 import android.os.ServiceManager; 28 import android.os.ServiceManager.ServiceNotFoundException; 29 import android.os.ShellCommand; 30 31 import java.io.PrintWriter; 32 import java.util.Locale; 33 import java.util.StringJoiner; 34 35 /** 36 * ShellCommands for GameManagerService. 37 * 38 * Use with {@code adb shell cmd game ...}. 39 */ 40 public class GameManagerShellCommand extends ShellCommand { 41 private static final String STANDARD_MODE_STR = "standard"; 42 private static final String STANDARD_MODE_NUM = "1"; 43 private static final String PERFORMANCE_MODE_STR = "performance"; 44 private static final String PERFORMANCE_MODE_NUM = "2"; 45 private static final String BATTERY_MODE_STR = "battery"; 46 private static final String BATTERY_MODE_NUM = "3"; 47 private static final String CUSTOM_MODE_STR = "custom"; 48 private static final String CUSTOM_MODE_NUM = "4"; 49 private static final String UNSUPPORTED_MODE_STR = "unsupported"; 50 private static final String UNSUPPORTED_MODE_NUM = String.valueOf( 51 GameManager.GAME_MODE_UNSUPPORTED); 52 53 private PackageManager mPackageManager; 54 GameManagerShellCommand(@onNull PackageManager packageManager)55 public GameManagerShellCommand(@NonNull PackageManager packageManager) { 56 mPackageManager = packageManager; 57 } 58 59 @Override onCommand(String cmd)60 public int onCommand(String cmd) { 61 if (cmd == null) { 62 return handleDefaultCommands(cmd); 63 } 64 final PrintWriter pw = getOutPrintWriter(); 65 try { 66 switch (cmd) { 67 case "set": { 68 return runSetGameModeConfig(pw); 69 } 70 case "reset": { 71 return runResetGameModeConfig(pw); 72 } 73 case "mode": { 74 /** The "mode" command allows setting a package's current game mode outside of 75 * the game dashboard UI. This command requires that a mode already be supported 76 * by the package. Devs can forcibly support game modes via the manifest 77 * metadata flags: com.android.app.gamemode.performance.enabled, 78 * com.android.app.gamemode.battery.enabled 79 * OR by `adb shell device_config put game_overlay \ 80 * <PACKAGE_NAME> <CONFIG_STRING>` 81 * see: {@link GameManagerServiceTests#mockDeviceConfigAll()} 82 */ 83 return runSetGameMode(pw); 84 } 85 case "list-modes": { 86 return runListGameModes(pw); 87 } 88 case "list-configs": { 89 return runListGameModeConfigs(pw); 90 } 91 default: 92 return handleDefaultCommands(cmd); 93 } 94 } catch (Exception e) { 95 pw.println("Error: " + e); 96 } 97 return -1; 98 } 99 isPackageGame(String packageName, int userId, PrintWriter pw)100 private boolean isPackageGame(String packageName, int userId, PrintWriter pw) { 101 try { 102 final ApplicationInfo applicationInfo = mPackageManager 103 .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId); 104 boolean isGame = applicationInfo.category == ApplicationInfo.CATEGORY_GAME; 105 if (!isGame) { 106 pw.println("Package " + packageName + " is not of game type, to use the game " 107 + "mode commands, it must specify game category in the manifest as " 108 + "android:appCategory=\"game\""); 109 } 110 return isGame; 111 } catch (PackageManager.NameNotFoundException e) { 112 pw.println("Package " + packageName + " is not found for user " + userId); 113 return false; 114 } 115 } 116 runListGameModes(PrintWriter pw)117 private int runListGameModes(PrintWriter pw) throws ServiceNotFoundException, RemoteException { 118 final String packageName = getNextArgRequired(); 119 final int userId = ActivityManager.getCurrentUser(); 120 if (!isPackageGame(packageName, userId, pw)) { 121 return -1; 122 } 123 final GameManagerService gameManagerService = (GameManagerService) 124 ServiceManager.getService(Context.GAME_SERVICE); 125 final String currentMode = gameModeIntToString( 126 gameManagerService.getGameMode(packageName, userId)); 127 final StringJoiner sj = new StringJoiner(","); 128 for (int mode : gameManagerService.getAvailableGameModes(packageName, userId)) { 129 sj.add(gameModeIntToString(mode)); 130 } 131 pw.println(packageName + " current mode: " + currentMode + ", available game modes: [" + sj 132 + "]"); 133 return 0; 134 } 135 runListGameModeConfigs(PrintWriter pw)136 private int runListGameModeConfigs(PrintWriter pw) 137 throws ServiceNotFoundException, RemoteException { 138 final String packageName = getNextArgRequired(); 139 final int userId = ActivityManager.getCurrentUser(); 140 if (!isPackageGame(packageName, userId, pw)) { 141 return -1; 142 } 143 final GameManagerService gameManagerService = (GameManagerService) 144 ServiceManager.getService(Context.GAME_SERVICE); 145 146 final String listStr = gameManagerService.getInterventionList(packageName, 147 userId); 148 149 if (listStr == null) { 150 pw.println("No interventions found for " + packageName); 151 } else { 152 pw.println(packageName + " interventions: " + listStr); 153 } 154 return 0; 155 } 156 runSetGameMode(PrintWriter pw)157 private int runSetGameMode(PrintWriter pw) throws ServiceNotFoundException, RemoteException { 158 final String option = getNextOption(); 159 String userIdStr = null; 160 if (option != null && option.equals("--user")) { 161 userIdStr = getNextArgRequired(); 162 } 163 final String gameMode = getNextArgRequired(); 164 final String packageName = getNextArgRequired(); 165 int userId = userIdStr != null ? Integer.parseInt(userIdStr) 166 : ActivityManager.getCurrentUser(); 167 if (!isPackageGame(packageName, userId, pw)) { 168 return -1; 169 } 170 final IGameManagerService service = IGameManagerService.Stub.asInterface( 171 ServiceManager.getServiceOrThrow(Context.GAME_SERVICE)); 172 boolean batteryModeSupported = false; 173 boolean perfModeSupported = false; 174 int[] modes = service.getAvailableGameModes(packageName, userId); 175 for (int mode : modes) { 176 if (mode == GameManager.GAME_MODE_PERFORMANCE) { 177 perfModeSupported = true; 178 } else if (mode == GameManager.GAME_MODE_BATTERY) { 179 batteryModeSupported = true; 180 } 181 } 182 switch (gameMode.toLowerCase()) { 183 case STANDARD_MODE_NUM: 184 case STANDARD_MODE_STR: 185 // Standard mode can be used to specify loading ANGLE as the default OpenGL ES 186 // driver, so it should always be available. 187 service.setGameMode(packageName, GameManager.GAME_MODE_STANDARD, userId); 188 pw.println("Set game mode to `STANDARD` for user `" + userId + "` in game `" 189 + packageName + "`"); 190 break; 191 case PERFORMANCE_MODE_NUM: 192 case PERFORMANCE_MODE_STR: 193 if (perfModeSupported) { 194 service.setGameMode(packageName, GameManager.GAME_MODE_PERFORMANCE, 195 userId); 196 pw.println("Set game mode to `PERFORMANCE` for user `" + userId + "` in game `" 197 + packageName + "`"); 198 } else { 199 pw.println("Game mode: " + gameMode + " not supported by " 200 + packageName); 201 return -1; 202 } 203 break; 204 case BATTERY_MODE_NUM: 205 case BATTERY_MODE_STR: 206 if (batteryModeSupported) { 207 service.setGameMode(packageName, GameManager.GAME_MODE_BATTERY, 208 userId); 209 pw.println("Set game mode to `BATTERY` for user `" + userId + "` in game `" 210 + packageName + "`"); 211 } else { 212 pw.println("Game mode: " + gameMode + " not supported by " 213 + packageName); 214 return -1; 215 } 216 break; 217 case CUSTOM_MODE_NUM: 218 case CUSTOM_MODE_STR: 219 service.setGameMode(packageName, GameManager.GAME_MODE_CUSTOM, userId); 220 pw.println("Set game mode to `CUSTOM` for user `" + userId + "` in game `" 221 + packageName + "`"); 222 break; 223 default: 224 pw.println("Invalid game mode: " + gameMode); 225 return -1; 226 } 227 return 0; 228 } 229 runSetGameModeConfig(PrintWriter pw)230 private int runSetGameModeConfig(PrintWriter pw) 231 throws ServiceNotFoundException, RemoteException { 232 String option; 233 /** 234 * handling optional input 235 * "--user", "--downscale" and "--fps" can come in any order 236 */ 237 String userIdStr = null; 238 String fpsStr = null; 239 String downscaleRatio = null; 240 int gameMode = GameManager.GAME_MODE_CUSTOM; 241 while ((option = getNextOption()) != null) { 242 switch (option) { 243 case "--mode": 244 gameMode = Integer.parseInt(getNextArgRequired()); 245 break; 246 case "--user": 247 if (userIdStr == null) { 248 userIdStr = getNextArgRequired(); 249 } else { 250 pw.println("Duplicate option '" + option + "'"); 251 return -1; 252 } 253 break; 254 case "--downscale": 255 if (downscaleRatio == null) { 256 downscaleRatio = getNextArgRequired(); 257 if ("disable".equals(downscaleRatio)) { 258 downscaleRatio = "-1"; 259 } else { 260 try { 261 Float.parseFloat(downscaleRatio); 262 } catch (NumberFormatException e) { 263 pw.println("Invalid scaling ratio '" + downscaleRatio + "'"); 264 return -1; 265 } 266 } 267 } else { 268 pw.println("Duplicate option '" + option + "'"); 269 return -1; 270 } 271 break; 272 case "--fps": 273 if (fpsStr == null) { 274 fpsStr = getNextArgRequired(); 275 try { 276 Integer.parseInt(fpsStr); 277 } catch (NumberFormatException e) { 278 pw.println("Invalid frame rate: '" + fpsStr + "'"); 279 return -1; 280 } 281 } else { 282 pw.println("Duplicate option '" + option + "'"); 283 return -1; 284 } 285 break; 286 default: 287 pw.println("Invalid option '" + option + "'"); 288 return -1; 289 } 290 } 291 292 final String packageName = getNextArgRequired(); 293 294 int userId = userIdStr != null ? Integer.parseInt(userIdStr) 295 : ActivityManager.getCurrentUser(); 296 if (!isPackageGame(packageName, userId, pw)) { 297 return -1; 298 } 299 300 final GameManagerService gameManagerService = (GameManagerService) 301 ServiceManager.getService(Context.GAME_SERVICE); 302 if (gameManagerService == null) { 303 pw.println("Failed to find GameManagerService on device"); 304 return -1; 305 } 306 gameManagerService.setGameModeConfigOverride(packageName, userId, gameMode, 307 fpsStr, downscaleRatio); 308 pw.println("Set custom mode intervention config for user `" + userId + "` in game `" 309 + packageName + "` as: `" 310 + "downscaling-ratio: " + downscaleRatio + ";" 311 + "fps-override: " + fpsStr + "`"); 312 return 0; 313 } 314 runResetGameModeConfig(PrintWriter pw)315 private int runResetGameModeConfig(PrintWriter pw) 316 throws ServiceNotFoundException, RemoteException { 317 String option = null; 318 String gameMode = null; 319 String userIdStr = null; 320 while ((option = getNextOption()) != null) { 321 switch (option) { 322 case "--user": 323 if (userIdStr == null) { 324 userIdStr = getNextArgRequired(); 325 } else { 326 pw.println("Duplicate option '" + option + "'"); 327 return -1; 328 } 329 break; 330 case "--mode": 331 if (gameMode == null) { 332 gameMode = getNextArgRequired(); 333 } else { 334 pw.println("Duplicate option '" + option + "'"); 335 return -1; 336 } 337 break; 338 default: 339 pw.println("Invalid option '" + option + "'"); 340 return -1; 341 } 342 } 343 344 final String packageName = getNextArgRequired(); 345 int userId = userIdStr != null ? Integer.parseInt(userIdStr) 346 : ActivityManager.getCurrentUser(); 347 if (!isPackageGame(packageName, userId, pw)) { 348 return -1; 349 } 350 351 final GameManagerService gameManagerService = (GameManagerService) 352 ServiceManager.getService(Context.GAME_SERVICE); 353 if (gameMode == null) { 354 gameManagerService.resetGameModeConfigOverride(packageName, userId, -1); 355 return 0; 356 } 357 358 switch (gameMode.toLowerCase(Locale.getDefault())) { 359 case PERFORMANCE_MODE_NUM: 360 case PERFORMANCE_MODE_STR: 361 gameManagerService.resetGameModeConfigOverride(packageName, userId, 362 GameManager.GAME_MODE_PERFORMANCE); 363 break; 364 case BATTERY_MODE_NUM: 365 case BATTERY_MODE_STR: 366 gameManagerService.resetGameModeConfigOverride(packageName, userId, 367 GameManager.GAME_MODE_BATTERY); 368 break; 369 default: 370 pw.println("Invalid game mode: " + gameMode); 371 return -1; 372 } 373 return 0; 374 } 375 gameModeIntToString(@ameManager.GameMode int gameMode)376 private static String gameModeIntToString(@GameManager.GameMode int gameMode) { 377 switch (gameMode) { 378 case GameManager.GAME_MODE_BATTERY: 379 return BATTERY_MODE_STR; 380 case GameManager.GAME_MODE_PERFORMANCE: 381 return PERFORMANCE_MODE_STR; 382 case GameManager.GAME_MODE_CUSTOM: 383 return CUSTOM_MODE_STR; 384 case GameManager.GAME_MODE_STANDARD: 385 return STANDARD_MODE_STR; 386 case GameManager.GAME_MODE_UNSUPPORTED: 387 return UNSUPPORTED_MODE_STR; 388 } 389 return ""; 390 } 391 392 @Override onHelp()393 public void onHelp() { 394 PrintWriter pw = getOutPrintWriter(); 395 pw.println("Game manager (game) commands:"); 396 pw.println(" help"); 397 pw.println(" Print this help text."); 398 pw.println(" downscale"); 399 pw.println(" Deprecated. Please use `custom` command."); 400 pw.println(" list-configs <PACKAGE_NAME>"); 401 pw.println(" Lists the current intervention configs of an app."); 402 pw.println(" list-modes <PACKAGE_NAME>"); 403 pw.println(" Lists the current selected and available game modes of an app."); 404 pw.println(" mode [--user <USER_ID>] [1|2|3|4|standard|performance|battery|custom] " 405 + "<PACKAGE_NAME>"); 406 pw.println(" Set app to run in the specified game mode, if supported."); 407 pw.println(" --user <USER_ID>: apply for the given user,"); 408 pw.println(" the current user is used when unspecified."); 409 pw.println(" set [intervention configs] <PACKAGE_NAME>"); 410 pw.println(" Set app to run at custom mode using provided intervention configs"); 411 pw.println(" Intervention configs consists of:"); 412 pw.println(" --downscale [0.3|0.35|0.4|0.45|0.5|0.55|0.6|0.65"); 413 pw.println(" |0.7|0.75|0.8|0.85|0.9|disable]: Set app to run at the"); 414 pw.println(" specified scaling ratio."); 415 pw.println(" --fps: Integer value to set app to run at the specified fps,"); 416 pw.println(" if supported. 0 to disable."); 417 pw.println(" reset [--mode [2|3|performance|battery] --user <USER_ID>] <PACKAGE_NAME>"); 418 pw.println(" Resets the game mode of the app to device configuration."); 419 pw.println(" This should only be used to reset any override to non custom game mode"); 420 pw.println(" applied using the deprecated `set` command"); 421 pw.println(" --mode [2|3|performance|battery]: apply for the given mode,"); 422 pw.println(" resets all modes when unspecified."); 423 pw.println(" --user <USER_ID>: apply for the given user,"); 424 pw.println(" the current user is used when unspecified."); 425 } 426 } 427