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.TypedXmlPullParser; 34 import android.util.TypedXmlSerializer; 35 import android.util.Xml; 36 import android.view.DisplayAddress; 37 import android.view.DisplayInfo; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.XmlUtils; 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 final File vendorFile = new File(Environment.getVendorDirectory(), 268 VENDOR_DISPLAY_SETTINGS_FILE_PATH); 269 return new AtomicFile(vendorFile, WM_DISPLAY_COMMIT_TAG); 270 } 271 272 @NonNull getOverrideSettingsFile()273 private static AtomicFile getOverrideSettingsFile() { 274 final File overrideSettingsFile = new File(Environment.getDataDirectory(), 275 DATA_DISPLAY_SETTINGS_FILE_PATH); 276 return new AtomicFile(overrideSettingsFile, WM_DISPLAY_COMMIT_TAG); 277 } 278 279 @Nullable readSettings(ReadableSettingsStorage storage)280 private static FileData readSettings(ReadableSettingsStorage storage) { 281 InputStream stream; 282 try { 283 stream = storage.openRead(); 284 } catch (IOException e) { 285 Slog.i(TAG, "No existing display settings, starting empty"); 286 return null; 287 } 288 FileData fileData = new FileData(); 289 boolean success = false; 290 try { 291 TypedXmlPullParser parser = Xml.resolvePullParser(stream); 292 int type; 293 while ((type = parser.next()) != XmlPullParser.START_TAG 294 && type != XmlPullParser.END_DOCUMENT) { 295 // Do nothing. 296 } 297 298 if (type != XmlPullParser.START_TAG) { 299 throw new IllegalStateException("no start tag found"); 300 } 301 302 int outerDepth = parser.getDepth(); 303 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 304 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 305 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 306 continue; 307 } 308 309 String tagName = parser.getName(); 310 if (tagName.equals("display")) { 311 readDisplay(parser, fileData); 312 } else if (tagName.equals("config")) { 313 readConfig(parser, fileData); 314 } else { 315 Slog.w(TAG, "Unknown element under <display-settings>: " 316 + parser.getName()); 317 XmlUtils.skipCurrentTag(parser); 318 } 319 } 320 success = true; 321 } catch (IllegalStateException e) { 322 Slog.w(TAG, "Failed parsing " + e); 323 } catch (NullPointerException e) { 324 Slog.w(TAG, "Failed parsing " + e); 325 } catch (NumberFormatException e) { 326 Slog.w(TAG, "Failed parsing " + e); 327 } catch (XmlPullParserException e) { 328 Slog.w(TAG, "Failed parsing " + e); 329 } catch (IOException e) { 330 Slog.w(TAG, "Failed parsing " + e); 331 } catch (IndexOutOfBoundsException e) { 332 Slog.w(TAG, "Failed parsing " + e); 333 } finally { 334 try { 335 stream.close(); 336 } catch (IOException ignored) { 337 } 338 } 339 if (!success) { 340 fileData.mSettings.clear(); 341 } 342 return fileData; 343 } 344 getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue)345 private static int getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue) { 346 return parser.getAttributeInt(null, name, defaultValue); 347 } 348 349 @Nullable getIntegerAttribute(TypedXmlPullParser parser, String name, @Nullable Integer defaultValue)350 private static Integer getIntegerAttribute(TypedXmlPullParser parser, String name, 351 @Nullable Integer defaultValue) { 352 try { 353 return parser.getAttributeInt(null, name); 354 } catch (Exception ignored) { 355 return defaultValue; 356 } 357 } 358 359 @Nullable getBooleanAttribute(TypedXmlPullParser parser, String name, @Nullable Boolean defaultValue)360 private static Boolean getBooleanAttribute(TypedXmlPullParser parser, String name, 361 @Nullable Boolean defaultValue) { 362 try { 363 return parser.getAttributeBoolean(null, name); 364 } catch (Exception ignored) { 365 return defaultValue; 366 } 367 } 368 readDisplay(TypedXmlPullParser parser, FileData fileData)369 private static void readDisplay(TypedXmlPullParser parser, FileData fileData) 370 throws NumberFormatException, XmlPullParserException, IOException { 371 String name = parser.getAttributeValue(null, "name"); 372 if (name != null) { 373 SettingsEntry settingsEntry = new SettingsEntry(); 374 settingsEntry.mWindowingMode = getIntAttribute(parser, "windowingMode", 375 WindowConfiguration.WINDOWING_MODE_UNDEFINED /* defaultValue */); 376 settingsEntry.mUserRotationMode = getIntegerAttribute(parser, "userRotationMode", 377 null /* defaultValue */); 378 settingsEntry.mUserRotation = getIntegerAttribute(parser, "userRotation", 379 null /* defaultValue */); 380 settingsEntry.mForcedWidth = getIntAttribute(parser, "forcedWidth", 381 0 /* defaultValue */); 382 settingsEntry.mForcedHeight = getIntAttribute(parser, "forcedHeight", 383 0 /* defaultValue */); 384 settingsEntry.mForcedDensity = getIntAttribute(parser, "forcedDensity", 385 0 /* defaultValue */); 386 settingsEntry.mForcedScalingMode = getIntegerAttribute(parser, "forcedScalingMode", 387 null /* defaultValue */); 388 settingsEntry.mRemoveContentMode = getIntAttribute(parser, "removeContentMode", 389 REMOVE_CONTENT_MODE_UNDEFINED /* defaultValue */); 390 settingsEntry.mShouldShowWithInsecureKeyguard = getBooleanAttribute(parser, 391 "shouldShowWithInsecureKeyguard", null /* defaultValue */); 392 settingsEntry.mShouldShowSystemDecors = getBooleanAttribute(parser, 393 "shouldShowSystemDecors", null /* defaultValue */); 394 final Boolean shouldShowIme = getBooleanAttribute(parser, "shouldShowIme", 395 null /* defaultValue */); 396 if (shouldShowIme != null) { 397 settingsEntry.mImePolicy = shouldShowIme ? DISPLAY_IME_POLICY_LOCAL 398 : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; 399 } else { 400 settingsEntry.mImePolicy = getIntegerAttribute(parser, "imePolicy", 401 null /* defaultValue */); 402 } 403 settingsEntry.mFixedToUserRotation = getIntegerAttribute(parser, "fixedToUserRotation", 404 null /* defaultValue */); 405 settingsEntry.mIgnoreOrientationRequest = getBooleanAttribute(parser, 406 "ignoreOrientationRequest", null /* defaultValue */); 407 settingsEntry.mIgnoreDisplayCutout = getBooleanAttribute(parser, 408 "ignoreDisplayCutout", null /* defaultValue */); 409 settingsEntry.mDontMoveToTop = getBooleanAttribute(parser, 410 "dontMoveToTop", null /* defaultValue */); 411 412 fileData.mSettings.put(name, settingsEntry); 413 } 414 XmlUtils.skipCurrentTag(parser); 415 } 416 readConfig(TypedXmlPullParser parser, FileData fileData)417 private static void readConfig(TypedXmlPullParser parser, FileData fileData) 418 throws NumberFormatException, 419 XmlPullParserException, IOException { 420 fileData.mIdentifierType = getIntAttribute(parser, "identifier", 421 IDENTIFIER_UNIQUE_ID); 422 XmlUtils.skipCurrentTag(parser); 423 } 424 writeSettings(WritableSettingsStorage storage, FileData data)425 private static void writeSettings(WritableSettingsStorage storage, FileData data) { 426 OutputStream stream; 427 try { 428 stream = storage.startWrite(); 429 } catch (IOException e) { 430 Slog.w(TAG, "Failed to write display settings: " + e); 431 return; 432 } 433 434 boolean success = false; 435 try { 436 TypedXmlSerializer out = Xml.resolveSerializer(stream); 437 out.startDocument(null, true); 438 439 out.startTag(null, "display-settings"); 440 441 out.startTag(null, "config"); 442 out.attributeInt(null, "identifier", data.mIdentifierType); 443 out.endTag(null, "config"); 444 445 for (Map.Entry<String, SettingsEntry> entry 446 : data.mSettings.entrySet()) { 447 String displayIdentifier = entry.getKey(); 448 SettingsEntry settingsEntry = entry.getValue(); 449 if (settingsEntry.isEmpty()) { 450 continue; 451 } 452 453 out.startTag(null, "display"); 454 out.attribute(null, "name", displayIdentifier); 455 if (settingsEntry.mWindowingMode != WindowConfiguration.WINDOWING_MODE_UNDEFINED) { 456 out.attributeInt(null, "windowingMode", settingsEntry.mWindowingMode); 457 } 458 if (settingsEntry.mUserRotationMode != null) { 459 out.attributeInt(null, "userRotationMode", 460 settingsEntry.mUserRotationMode); 461 } 462 if (settingsEntry.mUserRotation != null) { 463 out.attributeInt(null, "userRotation", 464 settingsEntry.mUserRotation); 465 } 466 if (settingsEntry.mForcedWidth != 0 && settingsEntry.mForcedHeight != 0) { 467 out.attributeInt(null, "forcedWidth", settingsEntry.mForcedWidth); 468 out.attributeInt(null, "forcedHeight", settingsEntry.mForcedHeight); 469 } 470 if (settingsEntry.mForcedDensity != 0) { 471 out.attributeInt(null, "forcedDensity", settingsEntry.mForcedDensity); 472 } 473 if (settingsEntry.mForcedScalingMode != null) { 474 out.attributeInt(null, "forcedScalingMode", 475 settingsEntry.mForcedScalingMode); 476 } 477 if (settingsEntry.mRemoveContentMode != REMOVE_CONTENT_MODE_UNDEFINED) { 478 out.attributeInt(null, "removeContentMode", settingsEntry.mRemoveContentMode); 479 } 480 if (settingsEntry.mShouldShowWithInsecureKeyguard != null) { 481 out.attributeBoolean(null, "shouldShowWithInsecureKeyguard", 482 settingsEntry.mShouldShowWithInsecureKeyguard); 483 } 484 if (settingsEntry.mShouldShowSystemDecors != null) { 485 out.attributeBoolean(null, "shouldShowSystemDecors", 486 settingsEntry.mShouldShowSystemDecors); 487 } 488 if (settingsEntry.mImePolicy != null) { 489 out.attributeInt(null, "imePolicy", settingsEntry.mImePolicy); 490 } 491 if (settingsEntry.mFixedToUserRotation != null) { 492 out.attributeInt(null, "fixedToUserRotation", 493 settingsEntry.mFixedToUserRotation); 494 } 495 if (settingsEntry.mIgnoreOrientationRequest != null) { 496 out.attributeBoolean(null, "ignoreOrientationRequest", 497 settingsEntry.mIgnoreOrientationRequest); 498 } 499 if (settingsEntry.mIgnoreDisplayCutout != null) { 500 out.attributeBoolean(null, "ignoreDisplayCutout", 501 settingsEntry.mIgnoreDisplayCutout); 502 } 503 if (settingsEntry.mDontMoveToTop != null) { 504 out.attributeBoolean(null, "dontMoveToTop", 505 settingsEntry.mDontMoveToTop); 506 } 507 out.endTag(null, "display"); 508 } 509 510 out.endTag(null, "display-settings"); 511 out.endDocument(); 512 success = true; 513 } catch (IOException e) { 514 Slog.w(TAG, "Failed to write display window settings.", e); 515 } finally { 516 storage.finishWrite(stream, success); 517 } 518 } 519 520 private static final class FileData { 521 int mIdentifierType; 522 final Map<String, SettingsEntry> mSettings = new HashMap<>(); 523 524 @Override toString()525 public String toString() { 526 return "FileData{" 527 + "mIdentifierType=" + mIdentifierType 528 + ", mSettings=" + mSettings 529 + '}'; 530 } 531 } 532 533 private static final class AtomicFileStorage implements WritableSettingsStorage { 534 private final AtomicFile mAtomicFile; 535 AtomicFileStorage(@onNull AtomicFile atomicFile)536 AtomicFileStorage(@NonNull AtomicFile atomicFile) { 537 mAtomicFile = atomicFile; 538 } 539 540 @Override openRead()541 public InputStream openRead() throws FileNotFoundException { 542 return mAtomicFile.openRead(); 543 } 544 545 @Override startWrite()546 public OutputStream startWrite() throws IOException { 547 return mAtomicFile.startWrite(); 548 } 549 550 @Override finishWrite(OutputStream os, boolean success)551 public void finishWrite(OutputStream os, boolean success) { 552 if (!(os instanceof FileOutputStream)) { 553 throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os); 554 } 555 FileOutputStream fos = (FileOutputStream) os; 556 if (success) { 557 mAtomicFile.finishWrite(fos); 558 } else { 559 mAtomicFile.failWrite(fos); 560 } 561 } 562 } 563 } 564