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