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