1 /*
2  * Copyright (C) 2020 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.wm;
18 
19 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
20 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
21 import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
22 
23 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
24 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
25 
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.app.WindowConfiguration;
30 import android.os.Environment;
31 import android.util.AtomicFile;
32 import android.util.Slog;
33 import android.util.Xml;
34 import android.view.DisplayAddress;
35 import android.view.DisplayInfo;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.internal.util.XmlUtils;
39 import com.android.modules.utils.TypedXmlPullParser;
40 import com.android.modules.utils.TypedXmlSerializer;
41 import com.android.server.wm.DisplayWindowSettings.SettingsProvider;
42 
43 import org.xmlpull.v1.XmlPullParser;
44 import org.xmlpull.v1.XmlPullParserException;
45 
46 import java.io.File;
47 import java.io.FileNotFoundException;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.OutputStream;
52 import java.util.HashMap;
53 import java.util.Map;
54 
55 /**
56  * Implementation of {@link SettingsProvider} that reads the base settings provided in a display
57  * settings file stored in /vendor/etc and then overlays those values with the settings provided in
58  * /data/system.
59  *
60  * @see DisplayWindowSettings
61  */
62 class DisplayWindowSettingsProvider implements SettingsProvider {
63     private static final String TAG = TAG_WITH_CLASS_NAME
64             ? "DisplayWindowSettingsProvider" : TAG_WM;
65 
66     private static final String DATA_DISPLAY_SETTINGS_FILE_PATH = "system/display_settings.xml";
67     private static final String VENDOR_DISPLAY_SETTINGS_FILE_PATH = "etc/display_settings.xml";
68     private static final String WM_DISPLAY_COMMIT_TAG = "wm-displays";
69 
70     private static final int IDENTIFIER_UNIQUE_ID = 0;
71     private static final int IDENTIFIER_PORT = 1;
72     @IntDef(prefix = { "IDENTIFIER_" }, value = {
73             IDENTIFIER_UNIQUE_ID,
74             IDENTIFIER_PORT,
75     })
76     @interface DisplayIdentifierType {}
77 
78     /** Interface that allows reading the display window settings. */
79     interface ReadableSettingsStorage {
openRead()80         InputStream openRead() throws IOException;
81     }
82 
83     /** Interface that allows reading and writing the display window settings. */
84     interface WritableSettingsStorage extends ReadableSettingsStorage {
startWrite()85         OutputStream startWrite() throws IOException;
finishWrite(OutputStream os, boolean success)86         void finishWrite(OutputStream os, boolean success);
87     }
88 
89     private ReadableSettings mBaseSettings;
90     private final WritableSettings mOverrideSettings;
91 
DisplayWindowSettingsProvider()92     DisplayWindowSettingsProvider() {
93         this(new AtomicFileStorage(getVendorSettingsFile()),
94                 new AtomicFileStorage(getOverrideSettingsFile()));
95     }
96 
97     @VisibleForTesting
DisplayWindowSettingsProvider(@onNull ReadableSettingsStorage baseSettingsStorage, @NonNull WritableSettingsStorage overrideSettingsStorage)98     DisplayWindowSettingsProvider(@NonNull ReadableSettingsStorage baseSettingsStorage,
99             @NonNull WritableSettingsStorage overrideSettingsStorage) {
100         mBaseSettings = new ReadableSettings(baseSettingsStorage);
101         mOverrideSettings = new WritableSettings(overrideSettingsStorage);
102     }
103 
104     /**
105      * Overrides the path for the file that should be used to read base settings. If {@code null} is
106      * passed the default base settings file path will be used.
107      *
108      * @see #VENDOR_DISPLAY_SETTINGS_FILE_PATH
109      */
setBaseSettingsFilePath(@ullable String path)110     void setBaseSettingsFilePath(@Nullable String path) {
111         AtomicFile settingsFile;
112         File file = path != null ? new File(path) : null;
113         if (file != null && file.exists()) {
114             settingsFile = new AtomicFile(file, WM_DISPLAY_COMMIT_TAG);
115         } else {
116             Slog.w(TAG, "display settings " + path + " does not exist, using vendor defaults");
117             settingsFile = getVendorSettingsFile();
118         }
119         setBaseSettingsStorage(new AtomicFileStorage(settingsFile));
120     }
121 
122     /**
123      * Overrides the storage that should be used to read base settings.
124      *
125      * @see #setBaseSettingsFilePath(String)
126      */
127     @VisibleForTesting
setBaseSettingsStorage(@onNull ReadableSettingsStorage baseSettingsStorage)128     void setBaseSettingsStorage(@NonNull ReadableSettingsStorage baseSettingsStorage) {
129         mBaseSettings = new ReadableSettings(baseSettingsStorage);
130     }
131 
132     @Override
133     @NonNull
getSettings(@onNull DisplayInfo info)134     public SettingsEntry getSettings(@NonNull DisplayInfo info) {
135         SettingsEntry baseSettings = mBaseSettings.getSettingsEntry(info);
136         SettingsEntry overrideSettings = mOverrideSettings.getOrCreateSettingsEntry(info);
137         if (baseSettings == null) {
138             return new SettingsEntry(overrideSettings);
139         } else {
140             SettingsEntry mergedSettings = new SettingsEntry(baseSettings);
141             mergedSettings.updateFrom(overrideSettings);
142             return mergedSettings;
143         }
144     }
145 
146     @Override
147     @NonNull
getOverrideSettings(@onNull DisplayInfo info)148     public SettingsEntry getOverrideSettings(@NonNull DisplayInfo info) {
149         return new SettingsEntry(mOverrideSettings.getOrCreateSettingsEntry(info));
150     }
151 
152     @Override
updateOverrideSettings(@onNull DisplayInfo info, @NonNull SettingsEntry overrides)153     public void updateOverrideSettings(@NonNull DisplayInfo info,
154             @NonNull SettingsEntry overrides) {
155         mOverrideSettings.updateSettingsEntry(info, overrides);
156     }
157 
158     /**
159      * Class that allows reading {@link SettingsEntry entries} from a
160      * {@link ReadableSettingsStorage}.
161      */
162     private static class ReadableSettings {
163         /**
164          * The preferred type of a display identifier to use when storing and retrieving entries
165          * from the settings entries.
166          *
167          * @see #getIdentifier(DisplayInfo)
168          */
169         @DisplayIdentifierType
170         protected int mIdentifierType;
171         protected final Map<String, SettingsEntry> mSettings = new HashMap<>();
172 
ReadableSettings(ReadableSettingsStorage settingsStorage)173         ReadableSettings(ReadableSettingsStorage settingsStorage) {
174             loadSettings(settingsStorage);
175         }
176 
177         @Nullable
getSettingsEntry(DisplayInfo info)178         final SettingsEntry getSettingsEntry(DisplayInfo info) {
179             final String identifier = getIdentifier(info);
180             SettingsEntry settings;
181             // Try to get corresponding settings using preferred identifier for the current config.
182             if ((settings = mSettings.get(identifier)) != null) {
183                 return settings;
184             }
185             // Else, fall back to the display name.
186             if ((settings = mSettings.get(info.name)) != null) {
187                 // Found an entry stored with old identifier.
188                 mSettings.remove(info.name);
189                 mSettings.put(identifier, settings);
190                 return settings;
191             }
192             return null;
193         }
194 
195         /** Gets the identifier of choice for the current config. */
getIdentifier(DisplayInfo displayInfo)196         protected final String getIdentifier(DisplayInfo displayInfo) {
197             if (mIdentifierType == IDENTIFIER_PORT && displayInfo.address != null) {
198                 // Config suggests using port as identifier for physical displays.
199                 if (displayInfo.address instanceof DisplayAddress.Physical) {
200                     return "port:" + ((DisplayAddress.Physical) displayInfo.address).getPort();
201                 }
202             }
203             return displayInfo.uniqueId;
204         }
205 
loadSettings(ReadableSettingsStorage settingsStorage)206         private void loadSettings(ReadableSettingsStorage settingsStorage) {
207             FileData fileData = readSettings(settingsStorage);
208             if (fileData != null) {
209                 mIdentifierType = fileData.mIdentifierType;
210                 mSettings.putAll(fileData.mSettings);
211             }
212         }
213     }
214 
215     /**
216      * Class that allows reading {@link SettingsEntry entries} from, and writing entries to, a
217      * {@link WritableSettingsStorage}.
218      */
219     private static final class WritableSettings extends ReadableSettings {
220         private final WritableSettingsStorage mSettingsStorage;
221 
WritableSettings(WritableSettingsStorage settingsStorage)222         WritableSettings(WritableSettingsStorage settingsStorage) {
223             super(settingsStorage);
224             mSettingsStorage = settingsStorage;
225         }
226 
227         @NonNull
getOrCreateSettingsEntry(DisplayInfo info)228         SettingsEntry getOrCreateSettingsEntry(DisplayInfo info) {
229             final String identifier = getIdentifier(info);
230             SettingsEntry settings;
231             // Try to get corresponding settings using preferred identifier for the current config.
232             if ((settings = mSettings.get(identifier)) != null) {
233                 return settings;
234             }
235             // Else, fall back to the display name.
236             if ((settings = mSettings.get(info.name)) != null) {
237                 // Found an entry stored with old identifier.
238                 mSettings.remove(info.name);
239                 mSettings.put(identifier, settings);
240                 writeSettings();
241                 return settings;
242             }
243 
244             settings = new SettingsEntry();
245             mSettings.put(identifier, settings);
246             return settings;
247         }
248 
updateSettingsEntry(DisplayInfo info, SettingsEntry settings)249         void updateSettingsEntry(DisplayInfo info, SettingsEntry settings) {
250             final SettingsEntry overrideSettings = getOrCreateSettingsEntry(info);
251             final boolean changed = overrideSettings.setTo(settings);
252             if (changed) {
253                 writeSettings();
254             }
255         }
256 
writeSettings()257         private void writeSettings() {
258             FileData fileData = new FileData();
259             fileData.mIdentifierType = mIdentifierType;
260             fileData.mSettings.putAll(mSettings);
261             DisplayWindowSettingsProvider.writeSettings(mSettingsStorage, fileData);
262         }
263     }
264 
265     @NonNull
getVendorSettingsFile()266     private static AtomicFile getVendorSettingsFile() {
267         // First look under product path for treblized builds.
268         File vendorFile = new File(Environment.getProductDirectory(),
269                 VENDOR_DISPLAY_SETTINGS_FILE_PATH);
270         if (!vendorFile.exists()) {
271             // Try and look in vendor path.
272             vendorFile = new File(Environment.getVendorDirectory(),
273                 VENDOR_DISPLAY_SETTINGS_FILE_PATH);
274         }
275         return new AtomicFile(vendorFile, WM_DISPLAY_COMMIT_TAG);
276     }
277 
278     @NonNull
getOverrideSettingsFile()279     private static AtomicFile getOverrideSettingsFile() {
280         final File overrideSettingsFile = new File(Environment.getDataDirectory(),
281                 DATA_DISPLAY_SETTINGS_FILE_PATH);
282         return new AtomicFile(overrideSettingsFile, WM_DISPLAY_COMMIT_TAG);
283     }
284 
285     @Nullable
readSettings(ReadableSettingsStorage storage)286     private static FileData readSettings(ReadableSettingsStorage storage) {
287         InputStream stream;
288         try {
289             stream = storage.openRead();
290         } catch (IOException e) {
291             Slog.i(TAG, "No existing display settings, starting empty");
292             return null;
293         }
294         FileData fileData = new FileData();
295         boolean success = false;
296         try {
297             TypedXmlPullParser parser = Xml.resolvePullParser(stream);
298             int type;
299             while ((type = parser.next()) != XmlPullParser.START_TAG
300                     && type != XmlPullParser.END_DOCUMENT) {
301                 // Do nothing.
302             }
303 
304             if (type != XmlPullParser.START_TAG) {
305                 throw new IllegalStateException("no start tag found");
306             }
307 
308             int outerDepth = parser.getDepth();
309             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
310                     && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
311                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
312                     continue;
313                 }
314 
315                 String tagName = parser.getName();
316                 if (tagName.equals("display")) {
317                     readDisplay(parser, fileData);
318                 } else if (tagName.equals("config")) {
319                     readConfig(parser, fileData);
320                 } else {
321                     Slog.w(TAG, "Unknown element under <display-settings>: "
322                             + parser.getName());
323                     XmlUtils.skipCurrentTag(parser);
324                 }
325             }
326             success = true;
327         } catch (IllegalStateException e) {
328             Slog.w(TAG, "Failed parsing " + e);
329         } catch (NullPointerException e) {
330             Slog.w(TAG, "Failed parsing " + e);
331         } catch (NumberFormatException e) {
332             Slog.w(TAG, "Failed parsing " + e);
333         } catch (XmlPullParserException e) {
334             Slog.w(TAG, "Failed parsing " + e);
335         } catch (IOException e) {
336             Slog.w(TAG, "Failed parsing " + e);
337         } catch (IndexOutOfBoundsException e) {
338             Slog.w(TAG, "Failed parsing " + e);
339         } finally {
340             try {
341                 stream.close();
342             } catch (IOException ignored) {
343             }
344         }
345         if (!success) {
346             fileData.mSettings.clear();
347         }
348         return fileData;
349     }
350 
getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue)351     private static int getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue) {
352         return parser.getAttributeInt(null, name, defaultValue);
353     }
354 
355     @Nullable
getIntegerAttribute(TypedXmlPullParser parser, String name, @Nullable Integer defaultValue)356     private static Integer getIntegerAttribute(TypedXmlPullParser parser, String name,
357             @Nullable Integer defaultValue) {
358         try {
359             return parser.getAttributeInt(null, name);
360         } catch (Exception ignored) {
361             return defaultValue;
362         }
363     }
364 
365     @Nullable
getBooleanAttribute(TypedXmlPullParser parser, String name, @Nullable Boolean defaultValue)366     private static Boolean getBooleanAttribute(TypedXmlPullParser parser, String name,
367             @Nullable Boolean defaultValue) {
368         try {
369             return parser.getAttributeBoolean(null, name);
370         } catch (Exception ignored) {
371             return defaultValue;
372         }
373     }
374 
readDisplay(TypedXmlPullParser parser, FileData fileData)375     private static void readDisplay(TypedXmlPullParser parser, FileData fileData)
376             throws NumberFormatException, XmlPullParserException, IOException {
377         String name = parser.getAttributeValue(null, "name");
378         if (name != null) {
379             SettingsEntry settingsEntry = new SettingsEntry();
380             settingsEntry.mWindowingMode = getIntAttribute(parser, "windowingMode",
381                     WindowConfiguration.WINDOWING_MODE_UNDEFINED /* defaultValue */);
382             settingsEntry.mUserRotationMode = getIntegerAttribute(parser, "userRotationMode",
383                     null /* defaultValue */);
384             settingsEntry.mUserRotation = getIntegerAttribute(parser, "userRotation",
385                     null /* defaultValue */);
386             settingsEntry.mForcedWidth = getIntAttribute(parser, "forcedWidth",
387                     0 /* defaultValue */);
388             settingsEntry.mForcedHeight = getIntAttribute(parser, "forcedHeight",
389                     0 /* defaultValue */);
390             settingsEntry.mForcedDensity = getIntAttribute(parser, "forcedDensity",
391                     0 /* defaultValue */);
392             settingsEntry.mForcedScalingMode = getIntegerAttribute(parser, "forcedScalingMode",
393                     null /* defaultValue */);
394             settingsEntry.mRemoveContentMode = getIntAttribute(parser, "removeContentMode",
395                     REMOVE_CONTENT_MODE_UNDEFINED /* defaultValue */);
396             settingsEntry.mShouldShowWithInsecureKeyguard = getBooleanAttribute(parser,
397                     "shouldShowWithInsecureKeyguard", null /* defaultValue */);
398             settingsEntry.mShouldShowSystemDecors = getBooleanAttribute(parser,
399                     "shouldShowSystemDecors", null /* defaultValue */);
400             final Boolean shouldShowIme = getBooleanAttribute(parser, "shouldShowIme",
401                     null /* defaultValue */);
402             if (shouldShowIme != null) {
403                 settingsEntry.mImePolicy = shouldShowIme ? DISPLAY_IME_POLICY_LOCAL
404                         : DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
405             } else {
406                 settingsEntry.mImePolicy = getIntegerAttribute(parser, "imePolicy",
407                         null /* defaultValue */);
408             }
409             settingsEntry.mFixedToUserRotation = getIntegerAttribute(parser, "fixedToUserRotation",
410                     null /* defaultValue */);
411             settingsEntry.mIgnoreOrientationRequest = getBooleanAttribute(parser,
412                     "ignoreOrientationRequest", null /* defaultValue */);
413             settingsEntry.mIgnoreDisplayCutout = getBooleanAttribute(parser,
414                     "ignoreDisplayCutout", null /* defaultValue */);
415             settingsEntry.mDontMoveToTop = getBooleanAttribute(parser,
416                     "dontMoveToTop", null /* defaultValue */);
417 
418             fileData.mSettings.put(name, settingsEntry);
419         }
420         XmlUtils.skipCurrentTag(parser);
421     }
422 
readConfig(TypedXmlPullParser parser, FileData fileData)423     private static void readConfig(TypedXmlPullParser parser, FileData fileData)
424             throws NumberFormatException,
425             XmlPullParserException, IOException {
426         fileData.mIdentifierType = getIntAttribute(parser, "identifier",
427                 IDENTIFIER_UNIQUE_ID);
428         XmlUtils.skipCurrentTag(parser);
429     }
430 
writeSettings(WritableSettingsStorage storage, FileData data)431     private static void writeSettings(WritableSettingsStorage storage, FileData data) {
432         OutputStream stream;
433         try {
434             stream = storage.startWrite();
435         } catch (IOException e) {
436             Slog.w(TAG, "Failed to write display settings: " + e);
437             return;
438         }
439 
440         boolean success = false;
441         try {
442             TypedXmlSerializer out = Xml.resolveSerializer(stream);
443             out.startDocument(null, true);
444 
445             out.startTag(null, "display-settings");
446 
447             out.startTag(null, "config");
448             out.attributeInt(null, "identifier", data.mIdentifierType);
449             out.endTag(null, "config");
450 
451             for (Map.Entry<String, SettingsEntry> entry
452                     : data.mSettings.entrySet()) {
453                 String displayIdentifier = entry.getKey();
454                 SettingsEntry settingsEntry = entry.getValue();
455                 if (settingsEntry.isEmpty()) {
456                     continue;
457                 }
458 
459                 out.startTag(null, "display");
460                 out.attribute(null, "name", displayIdentifier);
461                 if (settingsEntry.mWindowingMode != WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
462                     out.attributeInt(null, "windowingMode", settingsEntry.mWindowingMode);
463                 }
464                 if (settingsEntry.mUserRotationMode != null) {
465                     out.attributeInt(null, "userRotationMode",
466                             settingsEntry.mUserRotationMode);
467                 }
468                 if (settingsEntry.mUserRotation != null) {
469                     out.attributeInt(null, "userRotation",
470                             settingsEntry.mUserRotation);
471                 }
472                 if (settingsEntry.mForcedWidth != 0 && settingsEntry.mForcedHeight != 0) {
473                     out.attributeInt(null, "forcedWidth", settingsEntry.mForcedWidth);
474                     out.attributeInt(null, "forcedHeight", settingsEntry.mForcedHeight);
475                 }
476                 if (settingsEntry.mForcedDensity != 0) {
477                     out.attributeInt(null, "forcedDensity", settingsEntry.mForcedDensity);
478                 }
479                 if (settingsEntry.mForcedScalingMode != null) {
480                     out.attributeInt(null, "forcedScalingMode",
481                             settingsEntry.mForcedScalingMode);
482                 }
483                 if (settingsEntry.mRemoveContentMode != REMOVE_CONTENT_MODE_UNDEFINED) {
484                     out.attributeInt(null, "removeContentMode", settingsEntry.mRemoveContentMode);
485                 }
486                 if (settingsEntry.mShouldShowWithInsecureKeyguard != null) {
487                     out.attributeBoolean(null, "shouldShowWithInsecureKeyguard",
488                             settingsEntry.mShouldShowWithInsecureKeyguard);
489                 }
490                 if (settingsEntry.mShouldShowSystemDecors != null) {
491                     out.attributeBoolean(null, "shouldShowSystemDecors",
492                             settingsEntry.mShouldShowSystemDecors);
493                 }
494                 if (settingsEntry.mImePolicy != null) {
495                     out.attributeInt(null, "imePolicy", settingsEntry.mImePolicy);
496                 }
497                 if (settingsEntry.mFixedToUserRotation != null) {
498                     out.attributeInt(null, "fixedToUserRotation",
499                             settingsEntry.mFixedToUserRotation);
500                 }
501                 if (settingsEntry.mIgnoreOrientationRequest != null) {
502                     out.attributeBoolean(null, "ignoreOrientationRequest",
503                             settingsEntry.mIgnoreOrientationRequest);
504                 }
505                 if (settingsEntry.mIgnoreDisplayCutout != null) {
506                     out.attributeBoolean(null, "ignoreDisplayCutout",
507                             settingsEntry.mIgnoreDisplayCutout);
508                 }
509                 if (settingsEntry.mDontMoveToTop != null) {
510                     out.attributeBoolean(null, "dontMoveToTop",
511                             settingsEntry.mDontMoveToTop);
512                 }
513                 out.endTag(null, "display");
514             }
515 
516             out.endTag(null, "display-settings");
517             out.endDocument();
518             success = true;
519         } catch (IOException e) {
520             Slog.w(TAG, "Failed to write display window settings.", e);
521         } finally {
522             storage.finishWrite(stream, success);
523         }
524     }
525 
526     private static final class FileData {
527         int mIdentifierType;
528         final Map<String, SettingsEntry> mSettings = new HashMap<>();
529 
530         @Override
toString()531         public String toString() {
532             return "FileData{"
533                     + "mIdentifierType=" + mIdentifierType
534                     + ", mSettings=" + mSettings
535                     + '}';
536         }
537     }
538 
539     private static final class AtomicFileStorage implements WritableSettingsStorage {
540         private final AtomicFile mAtomicFile;
541 
AtomicFileStorage(@onNull AtomicFile atomicFile)542         AtomicFileStorage(@NonNull AtomicFile atomicFile) {
543             mAtomicFile = atomicFile;
544         }
545 
546         @Override
openRead()547         public InputStream openRead() throws FileNotFoundException {
548             return mAtomicFile.openRead();
549         }
550 
551         @Override
startWrite()552         public OutputStream startWrite() throws IOException {
553             return mAtomicFile.startWrite();
554         }
555 
556         @Override
finishWrite(OutputStream os, boolean success)557         public void finishWrite(OutputStream os, boolean success) {
558             if (!(os instanceof FileOutputStream)) {
559                 throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os);
560             }
561             FileOutputStream fos = (FileOutputStream) os;
562             if (success) {
563                 mAtomicFile.finishWrite(fos);
564             } else {
565                 mAtomicFile.failWrite(fos);
566             }
567         }
568     }
569 }
570