Merge "Allow using display port for window settings"
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index db96847..a46fa13 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.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.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.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 @@
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 @@
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 @@
}
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;
}
- 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 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 @@
}
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 @@
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 @@
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 @@
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 @@
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 9a8a732..652ea7d 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 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.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 @@
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 @@
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 @@
mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
- assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ assertEquals(WINDOWING_MODE_FREEFORM,
mPrimaryDisplay.getWindowingMode());
}
@@ -185,7 +200,7 @@
mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
- assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ assertEquals(WINDOWING_MODE_FREEFORM,
mSecondaryDisplay.getWindowingMode());
}
@@ -196,7 +211,7 @@
mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
- assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ assertEquals(WINDOWING_MODE_FREEFORM,
mSecondaryDisplay.getWindowingMode());
}
@@ -474,6 +489,171 @@
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 @@
* 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 @@
}
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;
+ }
+ }
}