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