diff options
| -rw-r--r-- | services/core/java/com/android/server/wm/DisplayWindowSettings.java | 197 | ||||
| -rw-r--r-- | services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java | 275 |
2 files changed, 432 insertions, 40 deletions
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index db96847e802b..a46fa13adf4e 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -26,6 +26,8 @@ import static com.android.server.wm.DisplayRotation.FIXED_TO_USER_ROTATION_DEFAU import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.app.WindowConfiguration; import android.os.Environment; import android.provider.Settings; @@ -33,6 +35,7 @@ import android.util.AtomicFile; import android.util.Slog; import android.util.Xml; import android.view.Display; +import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.Surface; @@ -47,10 +50,11 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -60,9 +64,33 @@ import java.util.HashMap; class DisplayWindowSettings { private static final String TAG = TAG_WITH_CLASS_NAME ? "DisplayWindowSettings" : TAG_WM; + private static final int IDENTIFIER_UNIQUE_ID = 0; + private static final int IDENTIFIER_PORT = 1; + @IntDef(prefix = { "IDENTIFIER_" }, value = { + IDENTIFIER_UNIQUE_ID, + IDENTIFIER_PORT, + }) + @interface DisplayIdentifierType {} + private final WindowManagerService mService; - private final AtomicFile mFile; - private final HashMap<String, Entry> mEntries = new HashMap<String, Entry>(); + private final HashMap<String, Entry> mEntries = new HashMap<>(); + private final SettingPersister mStorage; + + /** + * The preferred type of a display identifier to use when storing and retrieving entries. + * {@link #getIdentifier(DisplayInfo)} must be used to get current preferred identifier for each + * display. It will fall back to using {@link #IDENTIFIER_UNIQUE_ID} if the currently selected + * one is not applicable to a particular display. + */ + @DisplayIdentifierType + private int mIdentifier = IDENTIFIER_UNIQUE_ID; + + /** Interface for persisting the display window settings. */ + interface SettingPersister { + InputStream openRead() throws IOException; + OutputStream startWrite() throws IOException; + void finishWrite(OutputStream os, boolean success); + } private static class Entry { private final String mName; @@ -88,6 +116,26 @@ class DisplayWindowSettings { mName = name; } + private Entry(String name, Entry copyFrom) { + this(name); + mOverscanLeft = copyFrom.mOverscanLeft; + mOverscanTop = copyFrom.mOverscanTop; + mOverscanRight = copyFrom.mOverscanRight; + mOverscanBottom = copyFrom.mOverscanBottom; + mWindowingMode = copyFrom.mWindowingMode; + mUserRotationMode = copyFrom.mUserRotationMode; + mUserRotation = copyFrom.mUserRotation; + mForcedWidth = copyFrom.mForcedWidth; + mForcedHeight = copyFrom.mForcedHeight; + mForcedDensity = copyFrom.mForcedDensity; + mForcedScalingMode = copyFrom.mForcedScalingMode; + mRemoveContentMode = copyFrom.mRemoveContentMode; + mShouldShowWithInsecureKeyguard = copyFrom.mShouldShowWithInsecureKeyguard; + mShouldShowSystemDecors = copyFrom.mShouldShowSystemDecors; + mShouldShowIme = copyFrom.mShouldShowIme; + mFixedToUserRotation = copyFrom.mFixedToUserRotation; + } + /** @return {@code true} if all values are default. */ private boolean isEmpty() { return mOverscanLeft == 0 && mOverscanTop == 0 && mOverscanRight == 0 @@ -106,29 +154,46 @@ class DisplayWindowSettings { } DisplayWindowSettings(WindowManagerService service) { - this(service, new File(Environment.getDataDirectory(), "system")); + this(service, new AtomicFileStorage()); } @VisibleForTesting - DisplayWindowSettings(WindowManagerService service, File folder) { + DisplayWindowSettings(WindowManagerService service, SettingPersister storageImpl) { mService = service; - mFile = new AtomicFile(new File(folder, "display_settings.xml"), "wm-displays"); + mStorage = storageImpl; readSettings(); } - private Entry getEntry(DisplayInfo displayInfo) { - // Try to get the entry with the unique if possible. - // Else, fall back on the display name. + private @Nullable Entry getEntry(DisplayInfo displayInfo) { + final String identifier = getIdentifier(displayInfo); Entry entry; - if (displayInfo.uniqueId == null || (entry = mEntries.get(displayInfo.uniqueId)) == null) { - entry = mEntries.get(displayInfo.name); + // Try to get corresponding entry using preferred identifier for the current config. + if ((entry = mEntries.get(identifier)) != null) { + return entry; + } + // Else, fall back to the display name. + if ((entry = mEntries.get(displayInfo.name)) != null) { + // Found an entry stored with old identifier - upgrade to the new type now. + return updateIdentifierForEntry(entry, displayInfo); } - return entry; + return null; } private Entry getOrCreateEntry(DisplayInfo displayInfo) { final Entry entry = getEntry(displayInfo); - return entry != null ? entry : new Entry(displayInfo.uniqueId); + return entry != null ? entry : new Entry(getIdentifier(displayInfo)); + } + + /** + * Upgrades the identifier of a legacy entry. Does it by copying the data from the old record + * and clearing the old key in memory. The entry will be written to storage next time when a + * setting changes. + */ + private Entry updateIdentifierForEntry(Entry entry, DisplayInfo displayInfo) { + final Entry newEntry = new Entry(getIdentifier(displayInfo), entry); + removeEntry(displayInfo); + mEntries.put(newEntry.mName, newEntry); + return newEntry; } void setOverscanLocked(DisplayInfo displayInfo, int left, int top, int right, int bottom) { @@ -371,12 +436,11 @@ class DisplayWindowSettings { } private void readSettings() { - FileInputStream stream; + InputStream stream; try { - stream = mFile.openRead(); - } catch (FileNotFoundException e) { - Slog.i(TAG, "No existing display settings " + mFile.getBaseFile() - + "; starting empty"); + stream = mStorage.openRead(); + } catch (IOException e) { + Slog.i(TAG, "No existing display settings, starting empty"); return; } boolean success = false; @@ -403,6 +467,8 @@ class DisplayWindowSettings { String tagName = parser.getName(); if (tagName.equals("display")) { readDisplay(parser); + } else if (tagName.equals("config")) { + readConfig(parser); } else { Slog.w(TAG, "Unknown element under <display-settings>: " + parser.getName()); @@ -491,22 +557,26 @@ class DisplayWindowSettings { XmlUtils.skipCurrentTag(parser); } + private void readConfig(XmlPullParser parser) throws NumberFormatException, + XmlPullParserException, IOException { + mIdentifier = getIntAttribute(parser, "identifier"); + XmlUtils.skipCurrentTag(parser); + } + private void writeSettingsIfNeeded(Entry changedEntry, DisplayInfo displayInfo) { - if (changedEntry.isEmpty()) { - boolean removed = mEntries.remove(displayInfo.uniqueId) != null; - // Legacy name might have been in used, so we need to clear it. - removed |= mEntries.remove(displayInfo.name) != null; - if (!removed) { - // The entry didn't exist so nothing is changed and no need to update the file. - return; - } - } else { - mEntries.put(displayInfo.uniqueId, changedEntry); + if (changedEntry.isEmpty() && !removeEntry(displayInfo)) { + // The entry didn't exist so nothing is changed and no need to update the file. + return; } - FileOutputStream stream; + mEntries.put(getIdentifier(displayInfo), changedEntry); + writeSettings(); + } + + private void writeSettings() { + OutputStream stream; try { - stream = mFile.startWrite(); + stream = mStorage.startWrite(); } catch (IOException e) { Slog.w(TAG, "Failed to write display settings: " + e); return; @@ -516,8 +586,13 @@ class DisplayWindowSettings { XmlSerializer out = new FastXmlSerializer(); out.setOutput(stream, StandardCharsets.UTF_8.name()); out.startDocument(null, true); + out.startTag(null, "display-settings"); + out.startTag(null, "config"); + out.attribute(null, "identifier", Integer.toString(mIdentifier)); + out.endTag(null, "config"); + for (Entry entry : mEntries.values()) { out.startTag(null, "display"); out.attribute(null, "name", entry.mName); @@ -578,10 +653,66 @@ class DisplayWindowSettings { out.endTag(null, "display-settings"); out.endDocument(); - mFile.finishWrite(stream); + mStorage.finishWrite(stream, true /* success */); } catch (IOException e) { - Slog.w(TAG, "Failed to write display settings, restoring backup.", e); - mFile.failWrite(stream); + Slog.w(TAG, "Failed to write display window settings.", e); + mStorage.finishWrite(stream, false /* success */); + } + } + + /** + * Removes an entry from {@link #mEntries} cache. Looks up by new and previously used + * identifiers. + */ + private boolean removeEntry(DisplayInfo displayInfo) { + // Remove entry based on primary identifier. + boolean removed = mEntries.remove(getIdentifier(displayInfo)) != null; + // Ensure that legacy entries are cleared as well. + removed |= mEntries.remove(displayInfo.uniqueId) != null; + removed |= mEntries.remove(displayInfo.name) != null; + return removed; + } + + /** Gets the identifier of choice for the current config. */ + private String getIdentifier(DisplayInfo displayInfo) { + if (mIdentifier == IDENTIFIER_PORT && displayInfo.address != null) { + // Config suggests using port as identifier for physical displays. + if (displayInfo.address instanceof DisplayAddress.Physical) { + return "port:" + ((DisplayAddress.Physical) displayInfo.address).getPort(); + } + } + return displayInfo.uniqueId; + } + + private static class AtomicFileStorage implements SettingPersister { + private final AtomicFile mAtomicFile; + + AtomicFileStorage() { + final File folder = new File(Environment.getDataDirectory(), "system"); + mAtomicFile = new AtomicFile(new File(folder, "display_settings.xml"), "wm-displays"); + } + + @Override + public InputStream openRead() throws FileNotFoundException { + return mAtomicFile.openRead(); + } + + @Override + public OutputStream startWrite() throws IOException { + return mAtomicFile.startWrite(); + } + + @Override + public void finishWrite(OutputStream os, boolean success) { + if (!(os instanceof FileOutputStream)) { + throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os); + } + FileOutputStream fos = (FileOutputStream) os; + if (success) { + mAtomicFile.finishWrite(fos); + } else { + mAtomicFile.failWrite(fos); + } } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index 9a8a732a9da8..652ea7d32953 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowManager.REMOVE_CONTENT_MODE_DESTROY; import static android.view.WindowManager.REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY; @@ -40,7 +42,9 @@ import static org.mockito.Matchers.eq; import android.app.WindowConfiguration; import android.platform.test.annotations.Presubmit; +import android.util.Xml; import android.view.Display; +import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.Surface; @@ -53,14 +57,22 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.MockitoSession; +import org.xmlpull.v1.XmlPullParser; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; /** * Tests for the {@link DisplayWindowSettings} class. * * Build/Install/Run: - * atest FrameworksServicesTests:DisplayWindowSettingsTests + * atest WmTests:DisplayWindowSettingsTests */ @SmallTest @Presubmit @@ -69,12 +81,14 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { private static final File TEST_FOLDER = getInstrumentation().getTargetContext().getCacheDir(); private DisplayWindowSettings mTarget; - DisplayInfo mPrivateDisplayInfo; + private DisplayInfo mPrivateDisplayInfo; private DisplayContent mPrimaryDisplay; private DisplayContent mSecondaryDisplay; private DisplayContent mPrivateDisplay; + private TestStorage mStorage; + @Before public void setUp() throws Exception { deleteRecursively(TEST_FOLDER); @@ -83,7 +97,8 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mWm.setIsPc(false); mWm.setForceDesktopModeOnExternalDisplays(false); - mTarget = new DisplayWindowSettings(mWm, TEST_FOLDER); + mStorage = new TestStorage(); + mTarget = new DisplayWindowSettings(mWm, mStorage); mPrimaryDisplay = mWm.getDefaultDisplayContentLocked(); mSecondaryDisplay = mDisplayContent; @@ -143,7 +158,7 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mTarget.applySettingsToDisplayLocked(mPrimaryDisplay); - assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM, + assertEquals(WINDOWING_MODE_FREEFORM, mPrimaryDisplay.getWindowingMode()); } @@ -185,7 +200,7 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mTarget.applySettingsToDisplayLocked(mSecondaryDisplay); - assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM, + assertEquals(WINDOWING_MODE_FREEFORM, mSecondaryDisplay.getWindowingMode()); } @@ -196,7 +211,7 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mTarget.applySettingsToDisplayLocked(mSecondaryDisplay); - assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM, + assertEquals(WINDOWING_MODE_FREEFORM, mSecondaryDisplay.getWindowingMode()); } @@ -474,6 +489,171 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mockitoSession.finishMocking(); } + @Test + public void testReadingDisplaySettingsFromStorage() { + final String displayIdentifier = mSecondaryDisplay.getDisplayInfo().uniqueId; + prepareDisplaySettings(displayIdentifier); + + readAndAssertDisplaySettings(mPrimaryDisplay); + } + + @Test + public void testReadingDisplaySettingsFromStorage_LegacyDisplayId() { + final String displayIdentifier = mPrimaryDisplay.getDisplayInfo().name; + prepareDisplaySettings(displayIdentifier); + + readAndAssertDisplaySettings(mPrimaryDisplay); + } + + @Test + public void testReadingDisplaySettingsFromStorage_LegacyDisplayId_UpdateAfterAccess() + throws Exception { + // Store display settings with legacy display identifier. + final String displayIdentifier = mPrimaryDisplay.getDisplayInfo().name; + prepareDisplaySettings(displayIdentifier); + + // Update settings with new value, should trigger write to injector. + final DisplayWindowSettings settings = new DisplayWindowSettings(mWm, mStorage); + settings.setRemoveContentModeLocked(mPrimaryDisplay, REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY); + assertEquals("Settings value must be updated", REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY, + settings.getRemoveContentModeLocked(mPrimaryDisplay)); + assertTrue(mStorage.wasWriteSuccessful()); + + // Verify that display identifier was updated. + final String newDisplayIdentifier = getStoredDisplayAttributeValue("name"); + assertEquals("Display identifier must be updated to use uniqueId", + mPrimaryDisplay.getDisplayInfo().uniqueId, newDisplayIdentifier); + } + + @Test + public void testReadingDisplaySettingsFromStorage_UsePortAsId() { + final DisplayAddress.Physical displayAddress = DisplayAddress.fromPhysicalDisplayId(123456); + mPrimaryDisplay.getDisplayInfo().address = displayAddress; + + final String displayIdentifier = "port:" + displayAddress.getPort(); + prepareDisplaySettings(displayIdentifier, true /* usePortAsId */); + + readAndAssertDisplaySettings(mPrimaryDisplay); + } + + @Test + public void testReadingDisplaySettingsFromStorage_UsePortAsId_IncorrectAddress() { + final String displayIdentifier = mPrimaryDisplay.getDisplayInfo().uniqueId; + prepareDisplaySettings(displayIdentifier, true /* usePortAsId */); + + mPrimaryDisplay.getDisplayInfo().address = DisplayAddress.fromPhysicalDisplayId(123456); + + // Verify that the entry is not matched and default settings are returned instead. + final DisplayWindowSettings settings = new DisplayWindowSettings(mWm); + assertNotEquals("Default setting must be returned for new entry", + WINDOWING_MODE_PINNED, settings.getWindowingModeLocked(mPrimaryDisplay)); + } + + @Test + public void testWritingDisplaySettingsToStorage() throws Exception { + // Write some settings to storage. + final DisplayWindowSettings settings = new DisplayWindowSettings(mWm, mStorage); + settings.setShouldShowSystemDecorsLocked(mSecondaryDisplay, true); + settings.setShouldShowImeLocked(mSecondaryDisplay, true); + assertTrue(mStorage.wasWriteSuccessful()); + + // Verify that settings were stored correctly. + assertEquals("Attribute value must be stored", mSecondaryDisplay.getDisplayInfo().uniqueId, + getStoredDisplayAttributeValue("name")); + assertEquals("Attribute value must be stored", "true", + getStoredDisplayAttributeValue("shouldShowSystemDecors")); + assertEquals("Attribute value must be stored", "true", + getStoredDisplayAttributeValue("shouldShowIme")); + } + + @Test + public void testWritingDisplaySettingsToStorage_UsePortAsId() throws Exception { + // Store config to use port as identifier. + final DisplayAddress.Physical displayAddress = DisplayAddress.fromPhysicalDisplayId(123456); + mSecondaryDisplay.getDisplayInfo().address = displayAddress; + prepareDisplaySettings(null /* displayIdentifier */, true /* usePortAsId */); + + // Write some settings. + final DisplayWindowSettings settings = new DisplayWindowSettings(mWm, mStorage); + settings.setShouldShowSystemDecorsLocked(mSecondaryDisplay, true); + settings.setShouldShowImeLocked(mSecondaryDisplay, true); + assertTrue(mStorage.wasWriteSuccessful()); + + // Verify that settings were stored correctly. + assertEquals("Attribute value must be stored", "port:" + displayAddress.getPort(), + getStoredDisplayAttributeValue("name")); + assertEquals("Attribute value must be stored", "true", + getStoredDisplayAttributeValue("shouldShowSystemDecors")); + assertEquals("Attribute value must be stored", "true", + getStoredDisplayAttributeValue("shouldShowIme")); + } + + /** + * Prepares display settings and stores in {@link #mStorage}. Uses provided display identifier + * and stores windowingMode=WINDOWING_MODE_PINNED. + */ + private void prepareDisplaySettings(String displayIdentifier) { + prepareDisplaySettings(displayIdentifier, false /* usePortAsId */); + } + + private void prepareDisplaySettings(String displayIdentifier, boolean usePortAsId) { + String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<display-settings>\n"; + if (usePortAsId) { + contents += " <config identifier=\"1\"/>\n"; + } + if (displayIdentifier != null) { + contents += " <display\n" + + " name=\"" + displayIdentifier + "\"\n" + + " windowingMode=\"" + WINDOWING_MODE_PINNED + "\"/>\n"; + } + contents += "</display-settings>\n"; + + final InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)); + mStorage.setReadStream(is); + } + + private void readAndAssertDisplaySettings(DisplayContent displayContent) { + final DisplayWindowSettings settings = new DisplayWindowSettings(mWm, mStorage); + assertEquals("Stored setting must be read", + WINDOWING_MODE_PINNED, settings.getWindowingModeLocked(displayContent)); + assertEquals("Not stored setting must be set to default value", + REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY, + settings.getRemoveContentModeLocked(displayContent)); + } + + private String getStoredDisplayAttributeValue(String attr) throws Exception { + try (InputStream stream = mStorage.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Do nothing. + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("display")) { + return parser.getAttributeValue(null, attr); + } + } + } finally { + mStorage.closeRead(); + } + return null; + } + private static void assertOverscan(DisplayContent display, int left, int top, int right, int bottom) { final DisplayInfo info = display.getDisplayInfo(); @@ -490,7 +670,11 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { * path that also means the previous state must be written correctly. */ private void applySettingsToDisplayByNewInstance(DisplayContent display) { - new DisplayWindowSettings(mWm, TEST_FOLDER).applySettingsToDisplayLocked(display); + // Assert that prior write completed successfully. + assertTrue(mStorage.wasWriteSuccessful()); + + // Read and apply settings. + new DisplayWindowSettings(mWm, mStorage).applySettingsToDisplayLocked(display); } private static boolean deleteRecursively(File file) { @@ -506,4 +690,81 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { } return fullyDeleted; } + + /** In-memory storage implementation. */ + public class TestStorage implements DisplayWindowSettings.SettingPersister { + private InputStream mReadStream; + private ByteArrayOutputStream mWriteStream; + + private boolean mWasSuccessful; + + /** + * Returns input stream for reading. By default tries forward the output stream if previous + * write was successful. + * @see #closeRead() + */ + @Override + public InputStream openRead() throws FileNotFoundException { + if (mReadStream == null && mWasSuccessful) { + mReadStream = new ByteArrayInputStream(mWriteStream.toByteArray()); + } + if (mReadStream == null) { + throw new FileNotFoundException(); + } + if (mReadStream.markSupported()) { + mReadStream.mark(Integer.MAX_VALUE); + } + return mReadStream; + } + + /** Must be called after each {@link #openRead} to reset the position in the stream. */ + void closeRead() throws IOException { + if (mReadStream == null) { + throw new FileNotFoundException(); + } + if (mReadStream.markSupported()) { + mReadStream.reset(); + } + mReadStream = null; + } + + /** + * Creates new or resets existing output stream for write. Automatically closes previous + * read stream, since following reads should happen based on this new write. + */ + @Override + public OutputStream startWrite() throws IOException { + if (mWriteStream == null) { + mWriteStream = new ByteArrayOutputStream(); + } else { + mWriteStream.reset(); + } + if (mReadStream != null) { + closeRead(); + } + return mWriteStream; + } + + @Override + public void finishWrite(OutputStream os, boolean success) { + mWasSuccessful = success; + try { + os.close(); + } catch (IOException e) { + // This method can't throw IOException since the super implementation doesn't, so + // we just wrap it in a RuntimeException so we end up crashing the test all the + // same. + throw new RuntimeException(e); + } + } + + /** Override the read stream of the injector. By default it uses current write stream. */ + private void setReadStream(InputStream is) { + mReadStream = is; + } + + private boolean wasWriteSuccessful() { + return mWasSuccessful; + } + } } |