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.GameManager;
20 import android.os.FileUtils;
21 import android.util.ArrayMap;
22 import android.util.ArraySet;
23 import android.util.AtomicFile;
24 import android.util.Slog;
25 import android.util.Xml;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.util.XmlUtils;
29 import com.android.modules.utils.TypedXmlPullParser;
30 import com.android.modules.utils.TypedXmlSerializer;
31 import com.android.server.app.GameManagerService.GamePackageConfiguration;
32 import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 
42 /**
43  * Persists all GameService related settings.
44  *
45  * @hide
46  */
47 public class GameManagerSettings {
48     public static final String TAG = "GameManagerService_GameManagerSettings";
49     // The XML file follows the below format:
50     // <?xml>
51     // <packages>
52     //     <package name="" gameMode="">
53     //       <gameModeConfig gameMode="" fps="" scaling="" useAngle="" loadingBoost="">
54     //       </gameModeConfig>
55     //       ...
56     //     </package>
57     //     ...
58     // </packages>
59     private static final String GAME_SERVICE_FILE_NAME = "game-manager-service.xml";
60 
61     private static final String TAG_PACKAGE = "package";
62     private static final String TAG_PACKAGES = "packages";
63     private static final String TAG_GAME_MODE_CONFIG = "gameModeConfig";
64 
65     private static final String ATTR_NAME = "name";
66     private static final String ATTR_GAME_MODE = "gameMode";
67     private static final String ATTR_SCALING = "scaling";
68     private static final String ATTR_FPS = "fps";
69     private static final String ATTR_USE_ANGLE = "useAngle";
70     private static final String ATTR_LOADING_BOOST_DURATION = "loadingBoost";
71 
72     private final File mSystemDir;
73     @VisibleForTesting
74     final AtomicFile mSettingsFile;
75 
76     // PackageName -> GameMode
77     private final ArrayMap<String, Integer> mGameModes = new ArrayMap<>();
78     // PackageName -> GamePackageConfiguration
79     private final ArrayMap<String, GamePackageConfiguration> mConfigOverrides = new ArrayMap<>();
80 
GameManagerSettings(File dataDir)81     GameManagerSettings(File dataDir) {
82         mSystemDir = new File(dataDir, "system");
83         mSystemDir.mkdirs();
84         FileUtils.setPermissions(mSystemDir.toString(),
85                 FileUtils.S_IRWXU | FileUtils.S_IRWXG
86                         | FileUtils.S_IROTH | FileUtils.S_IXOTH,
87                 -1, -1);
88         mSettingsFile = new AtomicFile(new File(mSystemDir, GAME_SERVICE_FILE_NAME));
89     }
90 
91     /**
92      * Returns the game mode of a given package.
93      * This operation must be synced with an external lock.
94      */
getGameModeLocked(String packageName)95     int getGameModeLocked(String packageName) {
96         if (mGameModes.containsKey(packageName)) {
97             final int gameMode = mGameModes.get(packageName);
98             if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
99                 // force replace cached UNSUPPORTED mode with STANDARD starting in U
100                 return GameManager.GAME_MODE_STANDARD;
101             }
102             return gameMode;
103         }
104         return GameManager.GAME_MODE_STANDARD;
105     }
106 
107     /**
108      * Sets the game mode of a given package.
109      * This operation must be synced with an external lock.
110      */
setGameModeLocked(String packageName, int gameMode)111     void setGameModeLocked(String packageName, int gameMode) {
112         mGameModes.put(packageName, gameMode);
113     }
114 
115     /**
116      * Removes all game settings of a given package.
117      * This operation must be synced with an external lock.
118      */
removeGame(String packageName)119     void removeGame(String packageName) {
120         mGameModes.remove(packageName);
121         mConfigOverrides.remove(packageName);
122     }
123 
124     /**
125      * Returns the game config override of a given package or null if absent.
126      * This operation must be synced with an external lock.
127      */
getConfigOverride(String packageName)128     GamePackageConfiguration getConfigOverride(String packageName) {
129         return mConfigOverrides.get(packageName);
130     }
131 
132     /**
133      * Sets the game config override of a given package.
134      * This operation must be synced with an external lock.
135      */
setConfigOverride(String packageName, GamePackageConfiguration configOverride)136     void setConfigOverride(String packageName, GamePackageConfiguration configOverride) {
137         mConfigOverrides.put(packageName, configOverride);
138     }
139 
140     /**
141      * Removes the game mode config override of a given package.
142      * This operation must be synced with an external lock.
143      */
removeConfigOverride(String packageName)144     void removeConfigOverride(String packageName) {
145         mConfigOverrides.remove(packageName);
146     }
147 
148     /**
149      * Writes all current game service settings into disk.
150      * This operation must be synced with an external lock.
151      */
writePersistentDataLocked()152     void writePersistentDataLocked() {
153         FileOutputStream fstr = null;
154         try {
155             fstr = mSettingsFile.startWrite();
156 
157             final TypedXmlSerializer serializer = Xml.resolveSerializer(fstr);
158             serializer.startDocument(null, true);
159             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
160 
161             serializer.startTag(null, TAG_PACKAGES);
162             final ArraySet<String> packageNames = new ArraySet<>(mGameModes.keySet());
163             packageNames.addAll(mConfigOverrides.keySet());
164             for (String packageName : packageNames) {
165                 serializer.startTag(null, TAG_PACKAGE);
166                 serializer.attribute(null, ATTR_NAME, packageName);
167                 if (mGameModes.containsKey(packageName)) {
168                     serializer.attributeInt(null, ATTR_GAME_MODE, mGameModes.get(packageName));
169                 }
170                 writeGameModeConfigTags(serializer, mConfigOverrides.get(packageName));
171                 serializer.endTag(null, TAG_PACKAGE);
172             }
173             serializer.endTag(null, TAG_PACKAGES);
174 
175             serializer.endDocument();
176 
177             mSettingsFile.finishWrite(fstr);
178 
179             FileUtils.setPermissions(mSettingsFile.toString(),
180                     FileUtils.S_IRUSR | FileUtils.S_IWUSR
181                             | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
182                     -1, -1);
183             return;
184         } catch (java.io.IOException e) {
185             mSettingsFile.failWrite(fstr);
186             Slog.wtf(TAG, "Unable to write game manager service settings, "
187                     + "current changes will be lost at reboot", e);
188         }
189     }
190 
writeGameModeConfigTags(TypedXmlSerializer serializer, GamePackageConfiguration config)191     private void writeGameModeConfigTags(TypedXmlSerializer serializer,
192             GamePackageConfiguration config) throws IOException {
193         if (config == null) {
194             return;
195         }
196         final int[] gameModes = config.getAvailableGameModes();
197         for (final int mode : gameModes) {
198             final GameModeConfiguration modeConfig = config.getGameModeConfiguration(mode);
199             if (modeConfig != null) {
200                 serializer.startTag(null, TAG_GAME_MODE_CONFIG);
201                 serializer.attributeInt(null, ATTR_GAME_MODE, mode);
202                 serializer.attributeBoolean(null, ATTR_USE_ANGLE, modeConfig.getUseAngle());
203                 serializer.attribute(null, ATTR_FPS, modeConfig.getFpsStr());
204                 serializer.attributeFloat(null, ATTR_SCALING, modeConfig.getScaling());
205                 serializer.attributeInt(null, ATTR_LOADING_BOOST_DURATION,
206                         modeConfig.getLoadingBoostDuration());
207                 serializer.endTag(null, TAG_GAME_MODE_CONFIG);
208             }
209         }
210     }
211 
212     /**
213      * Reads game service settings from the disk.
214      * This operation must be synced with an external lock.
215      */
readPersistentDataLocked()216     boolean readPersistentDataLocked() {
217         mGameModes.clear();
218 
219         if (!mSettingsFile.exists()) {
220             Slog.v(TAG, "Settings file doesn't exist, skip reading");
221             return false;
222         }
223 
224         try {
225             final FileInputStream str = mSettingsFile.openRead();
226 
227             final TypedXmlPullParser parser = Xml.resolvePullParser(str);
228             int type;
229             while ((type = parser.next()) != XmlPullParser.START_TAG
230                     && type != XmlPullParser.END_DOCUMENT) {
231                 // Do nothing
232             }
233             if (type != XmlPullParser.START_TAG) {
234                 Slog.wtf(TAG, "No start tag found in game manager settings");
235                 return false;
236             }
237 
238             int outerDepth = parser.getDepth();
239             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
240                     && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
241                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
242                     continue;
243                 }
244 
245                 String tagName = parser.getName();
246                 if (type == XmlPullParser.START_TAG && TAG_PACKAGE.equals(tagName)) {
247                     readPackage(parser);
248                 } else {
249                     XmlUtils.skipCurrentTag(parser);
250                     Slog.w(TAG, "Unknown element under packages tag: " + tagName + " with type: "
251                             + type);
252                 }
253             }
254             str.close();
255         } catch (XmlPullParserException | java.io.IOException e) {
256             Slog.wtf(TAG, "Error reading game manager settings", e);
257             return false;
258         }
259         return true;
260     }
261 
262     // this must be called on tag of type START_TAG.
readPackage(TypedXmlPullParser parser)263     private void readPackage(TypedXmlPullParser parser) throws XmlPullParserException,
264             IOException {
265         final String name = parser.getAttributeValue(null, ATTR_NAME);
266         if (name == null) {
267             Slog.wtf(TAG, "No package name found in package tag");
268             XmlUtils.skipCurrentTag(parser);
269             return;
270         }
271         try {
272             final int gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
273             mGameModes.put(name, gameMode);
274         } catch (XmlPullParserException e) {
275             Slog.v(TAG, "No game mode selected by user for package" + name);
276         }
277         final int packageTagDepth = parser.getDepth();
278         int type;
279         final GamePackageConfiguration config = new GamePackageConfiguration(name);
280         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
281                 && (type != XmlPullParser.END_TAG
282                 || parser.getDepth() > packageTagDepth)) {
283             if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
284                 continue;
285             }
286             final String tagName = parser.getName();
287             if (type == XmlPullParser.START_TAG && TAG_GAME_MODE_CONFIG.equals(tagName)) {
288                 readGameModeConfig(parser, config);
289             } else {
290                 XmlUtils.skipCurrentTag(parser);
291                 Slog.w(TAG, "Unknown element under package tag: " + tagName + " with type: "
292                         + type);
293             }
294         }
295         if (config.hasActiveGameModeConfig()) {
296             mConfigOverrides.put(name, config);
297         }
298     }
299 
300     // this must be called on tag of type START_TAG.
readGameModeConfig(TypedXmlPullParser parser, GamePackageConfiguration config)301     private void readGameModeConfig(TypedXmlPullParser parser, GamePackageConfiguration config) {
302         final int gameMode;
303         try {
304             gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
305         } catch (XmlPullParserException e) {
306             Slog.wtf(TAG, "Invalid game mode value in config tag: " + parser.getAttributeValue(null,
307                     ATTR_GAME_MODE), e);
308             return;
309         }
310 
311         final GameModeConfiguration modeConfig = config.getOrAddDefaultGameModeConfiguration(
312                 gameMode);
313         try {
314             final float scaling = parser.getAttributeFloat(null, ATTR_SCALING);
315             modeConfig.setScaling(scaling);
316         } catch (XmlPullParserException e) {
317             final String rawScaling = parser.getAttributeValue(null, ATTR_SCALING);
318             if (rawScaling != null) {
319                 Slog.wtf(TAG, "Invalid scaling value in config tag: " + rawScaling, e);
320             }
321         }
322 
323         final String fps = parser.getAttributeValue(null, ATTR_FPS);
324         modeConfig.setFpsStr(fps != null ? fps : GameModeConfiguration.DEFAULT_FPS);
325 
326         try {
327             final boolean useAngle = parser.getAttributeBoolean(null, ATTR_USE_ANGLE);
328             modeConfig.setUseAngle(useAngle);
329         } catch (XmlPullParserException e) {
330             final String rawUseAngle = parser.getAttributeValue(null, ATTR_USE_ANGLE);
331             if (rawUseAngle != null) {
332                 Slog.wtf(TAG, "Invalid useAngle value in config tag: " + rawUseAngle, e);
333             }
334         }
335         try {
336             final int loadingBoostDuration = parser.getAttributeInt(null,
337                     ATTR_LOADING_BOOST_DURATION);
338             modeConfig.setLoadingBoostDuration(loadingBoostDuration);
339         } catch (XmlPullParserException e) {
340             final String rawLoadingBoost = parser.getAttributeValue(null,
341                     ATTR_LOADING_BOOST_DURATION);
342             if (rawLoadingBoost != null) {
343                 Slog.wtf(TAG, "Invalid loading boost in config tag: " + rawLoadingBoost, e);
344             }
345         }
346     }
347 }
348