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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 22 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; 23 24 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 25 26 import static org.junit.Assert.assertEquals; 27 import static org.junit.Assert.assertNotEquals; 28 import static org.junit.Assert.assertTrue; 29 30 import android.annotation.Nullable; 31 import android.platform.test.annotations.Presubmit; 32 import android.util.Xml; 33 import android.view.Display; 34 import android.view.DisplayAddress; 35 import android.view.DisplayInfo; 36 37 import androidx.test.filters.SmallTest; 38 39 import com.android.modules.utils.TypedXmlPullParser; 40 import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; 41 42 import org.junit.After; 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 import org.xmlpull.v1.XmlPullParser; 47 48 import java.io.ByteArrayInputStream; 49 import java.io.ByteArrayOutputStream; 50 import java.io.File; 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.io.OutputStream; 55 import java.nio.charset.StandardCharsets; 56 57 /** 58 * Tests for the {@link DisplayWindowSettingsProvider} class. 59 * 60 * Build/Install/Run: 61 * atest WmTests:DisplayWindowSettingsProviderTests 62 */ 63 @SmallTest 64 @Presubmit 65 @WindowTestsBase.UseTestDisplay 66 @RunWith(WindowTestRunner.class) 67 public class DisplayWindowSettingsProviderTests extends WindowTestsBase { 68 private static final int DISPLAY_PORT = 0xFF; 69 private static final long DISPLAY_MODEL = 0xEEEEEEEEL; 70 71 private static final File TEST_FOLDER = getInstrumentation().getTargetContext().getCacheDir(); 72 73 private TestStorage mDefaultVendorSettingsStorage; 74 private TestStorage mSecondaryVendorSettingsStorage; 75 private TestStorage mOverrideSettingsStorage; 76 77 private DisplayContent mPrimaryDisplay; 78 private DisplayContent mSecondaryDisplay; 79 80 @Before setUp()81 public void setUp() throws Exception { 82 deleteRecursively(TEST_FOLDER); 83 84 mDefaultVendorSettingsStorage = new TestStorage(); 85 mSecondaryVendorSettingsStorage = new TestStorage(); 86 mOverrideSettingsStorage = new TestStorage(); 87 88 mPrimaryDisplay = mWm.getDefaultDisplayContentLocked(); 89 mSecondaryDisplay = mDisplayContent; 90 assertNotEquals(Display.DEFAULT_DISPLAY, mSecondaryDisplay.getDisplayId()); 91 } 92 93 @After tearDown()94 public void tearDown() { 95 deleteRecursively(TEST_FOLDER); 96 } 97 98 @Test testReadingDisplaySettingsFromStorage()99 public void testReadingDisplaySettingsFromStorage() { 100 final String displayIdentifier = mSecondaryDisplay.getDisplayInfo().uniqueId; 101 prepareOverrideDisplaySettings(displayIdentifier); 102 103 SettingsEntry expectedSettings = new SettingsEntry(); 104 expectedSettings.mWindowingMode = WINDOWING_MODE_PINNED; 105 readAndAssertExpectedSettings(mSecondaryDisplay, expectedSettings); 106 } 107 108 @Test testReadingDisplaySettingsFromStorage_LegacyDisplayId()109 public void testReadingDisplaySettingsFromStorage_LegacyDisplayId() { 110 final String displayIdentifier = mPrimaryDisplay.getDisplayInfo().name; 111 prepareOverrideDisplaySettings(displayIdentifier); 112 113 SettingsEntry expectedSettings = new SettingsEntry(); 114 expectedSettings.mWindowingMode = WINDOWING_MODE_PINNED; 115 readAndAssertExpectedSettings(mPrimaryDisplay, expectedSettings); 116 } 117 118 @Test testReadingDisplaySettingsFromStorage_LegacyDisplayId_UpdateAfterAccess()119 public void testReadingDisplaySettingsFromStorage_LegacyDisplayId_UpdateAfterAccess() 120 throws Exception { 121 // Store display settings with legacy display identifier. 122 final DisplayInfo mPrimaryDisplayInfo = mPrimaryDisplay.getDisplayInfo(); 123 final String displayIdentifier = mPrimaryDisplayInfo.name; 124 prepareOverrideDisplaySettings(displayIdentifier); 125 126 // Update settings with new value, should trigger write to injector. 127 DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider( 128 mDefaultVendorSettingsStorage, mOverrideSettingsStorage); 129 SettingsEntry overrideSettings = provider.getOverrideSettings(mPrimaryDisplayInfo); 130 overrideSettings.mForcedDensity = 200; 131 provider.updateOverrideSettings(mPrimaryDisplayInfo, overrideSettings); 132 assertTrue(mOverrideSettingsStorage.wasWriteSuccessful()); 133 134 // Verify that display identifier was updated. 135 final String newDisplayIdentifier = getStoredDisplayAttributeValue( 136 mOverrideSettingsStorage, "name"); 137 assertEquals("Display identifier must be updated to use uniqueId", 138 mPrimaryDisplayInfo.uniqueId, newDisplayIdentifier); 139 } 140 141 @Test testReadingDisplaySettingsFromStorage_UsePortAsId()142 public void testReadingDisplaySettingsFromStorage_UsePortAsId() { 143 final DisplayAddress.Physical displayAddress = 144 DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL); 145 mPrimaryDisplay.getDisplayInfo().address = displayAddress; 146 147 final String displayIdentifier = "port:" + DISPLAY_PORT; 148 prepareOverrideDisplaySettings(displayIdentifier, true /* usePortAsId */); 149 150 SettingsEntry expectedSettings = new SettingsEntry(); 151 expectedSettings.mWindowingMode = WINDOWING_MODE_PINNED; 152 readAndAssertExpectedSettings(mPrimaryDisplay, expectedSettings); 153 } 154 155 @Test testReadingDisplaySettingsFromStorage_UsePortAsId_IncorrectAddress()156 public void testReadingDisplaySettingsFromStorage_UsePortAsId_IncorrectAddress() { 157 final String displayIdentifier = mPrimaryDisplay.getDisplayInfo().uniqueId; 158 prepareOverrideDisplaySettings(displayIdentifier, true /* usePortAsId */); 159 160 mPrimaryDisplay.getDisplayInfo().address = DisplayAddress.fromPhysicalDisplayId(123456); 161 162 // Verify that the entry is not matched and default settings are returned instead. 163 SettingsEntry expectedSettings = new SettingsEntry(); 164 readAndAssertExpectedSettings(mPrimaryDisplay, expectedSettings); 165 } 166 167 @Test testReadingDisplaySettingsFromStorage_secondayVendorDisplaySettingsLocation()168 public void testReadingDisplaySettingsFromStorage_secondayVendorDisplaySettingsLocation() { 169 final String displayIdentifier = mSecondaryDisplay.getDisplayInfo().uniqueId; 170 prepareSecondaryDisplaySettings(displayIdentifier); 171 172 final DisplayWindowSettingsProvider provider = 173 new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage, 174 mOverrideSettingsStorage); 175 176 // Expected settings should be empty because the default is to read from the primary vendor 177 // settings location. 178 SettingsEntry expectedSettings = new SettingsEntry(); 179 assertEquals(expectedSettings, provider.getSettings(mSecondaryDisplay.getDisplayInfo())); 180 181 // Now switch to secondary vendor settings and assert proper settings. 182 provider.setBaseSettingsStorage(mSecondaryVendorSettingsStorage); 183 expectedSettings.mWindowingMode = WINDOWING_MODE_FULLSCREEN; 184 assertEquals(expectedSettings, provider.getSettings(mSecondaryDisplay.getDisplayInfo())); 185 186 // Switch back to primary and assert settings are empty again. 187 provider.setBaseSettingsStorage(mDefaultVendorSettingsStorage); 188 expectedSettings.mWindowingMode = WINDOWING_MODE_UNDEFINED; 189 assertEquals(expectedSettings, provider.getSettings(mSecondaryDisplay.getDisplayInfo())); 190 } 191 192 @Test testReadingDisplaySettingsFromStorage_overrideSettingsTakePrecedenceOverVendor()193 public void testReadingDisplaySettingsFromStorage_overrideSettingsTakePrecedenceOverVendor() { 194 final String displayIdentifier = mSecondaryDisplay.getDisplayInfo().uniqueId; 195 prepareOverrideDisplaySettings(displayIdentifier); 196 prepareSecondaryDisplaySettings(displayIdentifier); 197 198 final DisplayWindowSettingsProvider provider = 199 new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage, 200 mOverrideSettingsStorage); 201 provider.setBaseSettingsStorage(mSecondaryVendorSettingsStorage); 202 203 // The windowing mode should be set to WINDOWING_MODE_PINNED because the override settings 204 // take precedence over the vendor provided settings. 205 SettingsEntry expectedSettings = new SettingsEntry(); 206 expectedSettings.mWindowingMode = WINDOWING_MODE_PINNED; 207 assertEquals(expectedSettings, provider.getSettings(mSecondaryDisplay.getDisplayInfo())); 208 } 209 210 @Test testWritingDisplaySettingsToStorage()211 public void testWritingDisplaySettingsToStorage() throws Exception { 212 final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo(); 213 214 // Write some settings to storage. 215 DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider( 216 mDefaultVendorSettingsStorage, mOverrideSettingsStorage); 217 SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo); 218 overrideSettings.mShouldShowSystemDecors = true; 219 overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL; 220 overrideSettings.mDontMoveToTop = true; 221 provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings); 222 assertTrue(mOverrideSettingsStorage.wasWriteSuccessful()); 223 224 // Verify that settings were stored correctly. 225 assertEquals("Attribute value must be stored", secondaryDisplayInfo.uniqueId, 226 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "name")); 227 assertEquals("Attribute value must be stored", "true", 228 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "shouldShowSystemDecors")); 229 assertEquals("Attribute value must be stored", "0", 230 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "imePolicy")); 231 assertEquals("Attribute value must be stored", "true", 232 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "dontMoveToTop")); 233 } 234 235 @Test testWritingDisplaySettingsToStorage_UsePortAsId()236 public void testWritingDisplaySettingsToStorage_UsePortAsId() throws Exception { 237 prepareOverrideDisplaySettings(null /* displayIdentifier */, true /* usePortAsId */); 238 239 // Store config to use port as identifier. 240 final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo(); 241 final DisplayAddress.Physical displayAddress = 242 DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL); 243 secondaryDisplayInfo.address = displayAddress; 244 245 // Write some settings to storage. 246 DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider( 247 mDefaultVendorSettingsStorage, mOverrideSettingsStorage); 248 SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo); 249 overrideSettings.mShouldShowSystemDecors = true; 250 overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL; 251 provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings); 252 assertTrue(mOverrideSettingsStorage.wasWriteSuccessful()); 253 254 // Verify that settings were stored correctly. 255 assertEquals("Attribute value must be stored", "port:" + DISPLAY_PORT, 256 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "name")); 257 assertEquals("Attribute value must be stored", "true", 258 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "shouldShowSystemDecors")); 259 assertEquals("Attribute value must be stored", "0", 260 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "imePolicy")); 261 } 262 263 /** 264 * Prepares display settings and stores in {@link #mOverrideSettingsStorage}. Uses provided 265 * display identifier and stores windowingMode=WINDOWING_MODE_PINNED. 266 */ prepareOverrideDisplaySettings(String displayIdentifier)267 private void prepareOverrideDisplaySettings(String displayIdentifier) { 268 prepareOverrideDisplaySettings(displayIdentifier, false /* usePortAsId */); 269 } 270 prepareOverrideDisplaySettings(String displayIdentifier, boolean usePortAsId)271 private void prepareOverrideDisplaySettings(String displayIdentifier, boolean usePortAsId) { 272 String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" 273 + "<display-settings>\n"; 274 if (usePortAsId) { 275 contents += " <config identifier=\"1\"/>\n"; 276 } 277 if (displayIdentifier != null) { 278 contents += " <display\n" 279 + " name=\"" + displayIdentifier + "\"\n" 280 + " windowingMode=\"" + WINDOWING_MODE_PINNED + "\"/>\n"; 281 } 282 contents += "</display-settings>\n"; 283 284 final InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)); 285 mOverrideSettingsStorage.setReadStream(is); 286 } 287 288 /** 289 * Prepares display settings and stores in {@link #mSecondaryVendorSettingsStorage}. Uses 290 * provided display identifier and stores windowingMode=WINDOWING_MODE_FULLSCREEN. 291 */ prepareSecondaryDisplaySettings(String displayIdentifier)292 private void prepareSecondaryDisplaySettings(String displayIdentifier) { 293 String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" 294 + "<display-settings>\n"; 295 if (displayIdentifier != null) { 296 contents += " <display\n" 297 + " name=\"" + displayIdentifier + "\"\n" 298 + " windowingMode=\"" + WINDOWING_MODE_FULLSCREEN + "\"/>\n"; 299 } 300 contents += "</display-settings>\n"; 301 302 final InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)); 303 mSecondaryVendorSettingsStorage.setReadStream(is); 304 } 305 readAndAssertExpectedSettings(DisplayContent displayContent, SettingsEntry expectedSettings)306 private void readAndAssertExpectedSettings(DisplayContent displayContent, 307 SettingsEntry expectedSettings) { 308 final DisplayWindowSettingsProvider provider = 309 new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage, 310 mOverrideSettingsStorage); 311 assertEquals(expectedSettings, provider.getSettings(displayContent.getDisplayInfo())); 312 } 313 314 @Nullable getStoredDisplayAttributeValue(TestStorage storage, String attr)315 private String getStoredDisplayAttributeValue(TestStorage storage, String attr) 316 throws Exception { 317 try (InputStream stream = storage.openRead()) { 318 TypedXmlPullParser parser = Xml.resolvePullParser(stream); 319 int type; 320 while ((type = parser.next()) != XmlPullParser.START_TAG 321 && type != XmlPullParser.END_DOCUMENT) { 322 // Do nothing. 323 } 324 325 if (type != XmlPullParser.START_TAG) { 326 throw new IllegalStateException("no start tag found"); 327 } 328 329 int outerDepth = parser.getDepth(); 330 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 331 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 332 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 333 continue; 334 } 335 336 String tagName = parser.getName(); 337 if (tagName.equals("display")) { 338 return parser.getAttributeValue(null, attr); 339 } 340 } 341 } finally { 342 storage.closeRead(); 343 } 344 return null; 345 } 346 deleteRecursively(File file)347 private static boolean deleteRecursively(File file) { 348 boolean fullyDeleted = true; 349 if (file.isFile()) { 350 return file.delete(); 351 } else if (file.isDirectory()) { 352 final File[] files = file.listFiles(); 353 for (File child : files) { 354 fullyDeleted &= deleteRecursively(child); 355 } 356 fullyDeleted &= file.delete(); 357 } 358 return fullyDeleted; 359 } 360 361 /** In-memory storage implementation. */ 362 public class TestStorage implements DisplayWindowSettingsProvider.WritableSettingsStorage { 363 private InputStream mReadStream; 364 private ByteArrayOutputStream mWriteStream; 365 366 private boolean mWasSuccessful; 367 368 /** 369 * Returns input stream for reading. By default tries forward the output stream if previous 370 * write was successful. 371 * @see #closeRead() 372 */ 373 @Override openRead()374 public InputStream openRead() throws FileNotFoundException { 375 if (mReadStream == null && mWasSuccessful) { 376 mReadStream = new ByteArrayInputStream(mWriteStream.toByteArray()); 377 } 378 if (mReadStream == null) { 379 throw new FileNotFoundException(); 380 } 381 if (mReadStream.markSupported()) { 382 mReadStream.mark(Integer.MAX_VALUE); 383 } 384 return mReadStream; 385 } 386 387 /** Must be called after each {@link #openRead} to reset the position in the stream. */ closeRead()388 void closeRead() throws IOException { 389 if (mReadStream == null) { 390 throw new FileNotFoundException(); 391 } 392 if (mReadStream.markSupported()) { 393 mReadStream.reset(); 394 } 395 mReadStream = null; 396 } 397 398 /** 399 * Creates new or resets existing output stream for write. Automatically closes previous 400 * read stream, since following reads should happen based on this new write. 401 */ 402 @Override startWrite()403 public OutputStream startWrite() throws IOException { 404 if (mWriteStream == null) { 405 mWriteStream = new ByteArrayOutputStream(); 406 } else { 407 mWriteStream.reset(); 408 } 409 if (mReadStream != null) { 410 closeRead(); 411 } 412 return mWriteStream; 413 } 414 415 @Override finishWrite(OutputStream os, boolean success)416 public void finishWrite(OutputStream os, boolean success) { 417 mWasSuccessful = success; 418 try { 419 os.close(); 420 } catch (IOException e) { 421 // This method can't throw IOException since the super implementation doesn't, so 422 // we just wrap it in a RuntimeException so we end up crashing the test all the 423 // same. 424 throw new RuntimeException(e); 425 } 426 } 427 428 /** Overrides the read stream of the injector. By default it uses current write stream. */ setReadStream(InputStream is)429 private void setReadStream(InputStream is) { 430 mReadStream = is; 431 } 432 wasWriteSuccessful()433 private boolean wasWriteSuccessful() { 434 return mWasSuccessful; 435 } 436 } 437 } 438