Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 977cf487 authored by Tomasz Mikolajewski's avatar Tomasz Mikolajewski
Browse files

DO NOT MERGE: Do not create snapshot for seekable pipes.

Test: Covered by unit tests.
Bug: 31783726
Change-Id: Id8e4debf58e219dc0a15a254944db749f8e9701c
(cherry picked from commit 644be1c6)
parent c56fabbe
Loading
Loading
Loading
Loading
+29 −1
Original line number Original line Diff line number Diff line
@@ -34,6 +34,10 @@ import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.webkit.MimeTypeMap;


import android.system.Os;
import android.system.OsConstants;
import android.system.ErrnoException;

import libcore.io.IoUtils;
import libcore.io.IoUtils;


import com.android.internal.util.Preconditions;
import com.android.internal.util.Preconditions;
@@ -71,6 +75,10 @@ import java.util.zip.ZipInputStream;
public class Archive implements Closeable {
public class Archive implements Closeable {
    private static final String TAG = "Archive";
    private static final String TAG = "Archive";


    // Stores file representations of file descriptors. Used to open pipes
    // by path.
    private static final String PROC_FD_PATH = "/proc/self/fd/";

    public static final String[] DEFAULT_PROJECTION = new String[] {
    public static final String[] DEFAULT_PROJECTION = new String[] {
            Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_DISPLAY_NAME,
            Document.COLUMN_DISPLAY_NAME,
@@ -157,10 +165,25 @@ public class Archive implements Closeable {
        }
        }
    }
    }


    /**
     * Returns true if the file descriptor is seekable.
     * @param descriptor File descriptor to check.
     */
    public static boolean canSeek(ParcelFileDescriptor descriptor) {
        try {
            return Os.lseek(descriptor.getFileDescriptor(), 0,
                    OsConstants.SEEK_SET) == 0;
        } catch (ErrnoException e) {
            return false;
        }
    }

    /**
    /**
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * documents within the archive passed as a file descriptor.
     * documents within the archive passed as a file descriptor.
     *
     *
     * If the file descriptor is not seekable, then a snapshot will be created.
     *
     * @param context Context of the provider.
     * @param context Context of the provider.
     * @param descriptor File descriptor for the archive's contents.
     * @param descriptor File descriptor for the archive's contents.
     * @param archiveUri Uri of the archive document.
     * @param archiveUri Uri of the archive document.
@@ -170,8 +193,13 @@ public class Archive implements Closeable {
            Context context, ParcelFileDescriptor descriptor, Uri archiveUri,
            Context context, ParcelFileDescriptor descriptor, Uri archiveUri,
            @Nullable Uri notificationUri)
            @Nullable Uri notificationUri)
            throws IOException {
            throws IOException {
        if (canSeek(descriptor)) {
            return new Archive(context, new File(PROC_FD_PATH + descriptor.getFd()),
                    archiveUri, notificationUri);
        }

        // Fallback for non-seekable file descriptors.
        File snapshotFile = null;
        File snapshotFile = null;
        // TODO: Do not create a snapshot if a seekable file descriptor is passed.
        try {
        try {
            // Create a copy of the archive, as ZipFile doesn't operate on streams.
            // Create a copy of the archive, as ZipFile doesn't operate on streams.
            // Moreover, ZipInputStream would be inefficient for large files on
            // Moreover, ZipInputStream would be inefficient for large files on
+94 −20
Original line number Original line Diff line number Diff line
@@ -33,26 +33,50 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStream;
import java.util.Scanner;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


@MediumTest
@MediumTest
public class ArchiveTest extends AndroidTestCase {
public class ArchiveTest extends AndroidTestCase {
    private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
    private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
    private static final String NOTIFICATION_URI = "content://notification-uri";
    private static final String NOTIFICATION_URI = "content://notification-uri";
    private ExecutorService mExecutor = null;
    private Context mContext = null;
    private Archive mArchive = null;
    private Archive mArchive = null;
    private ArchiveId mArchiveId;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        mContext = InstrumentationRegistry.getTargetContext();
        mExecutor = Executors.newSingleThreadExecutor();
    }

    @Override
    public void tearDown() throws Exception {
        mExecutor.shutdown();
        assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
        if (mArchive != null) {
            mArchive.close();
        }
        super.tearDown();
    }


    public static ArchiveId createArchiveId(String path) {
    public static ArchiveId createArchiveId(String path) {
        return new ArchiveId(ARCHIVE_URI, path);
        return new ArchiveId(ARCHIVE_URI, path);
    }
    }


    public void loadArchive(int resource) {
    /**
     * Opens a resource and returns the contents via file descriptor to a local
     * snapshot file.
     */
    public ParcelFileDescriptor getSeekableDescriptor(int resource) {
        // Extract the file from resources.
        // Extract the file from resources.
        File file = null;
        File file = null;
        final Context context = InstrumentationRegistry.getTargetContext();
        final Context testContext = InstrumentationRegistry.getContext();
        final Context testContext = InstrumentationRegistry.getContext();
        try {
        try {
            file = File.createTempFile("com.android.documentsui.archives.tests{",
            file = File.createTempFile("com.android.documentsui.archives.tests{",
                    "}.zip", context.getCacheDir());
                    "}.zip", mContext.getCacheDir());
            try (
            try (
                final FileOutputStream outputStream =
                final FileOutputStream outputStream =
                        new ParcelFileDescriptor.AutoCloseOutputStream(
                        new ParcelFileDescriptor.AutoCloseOutputStream(
@@ -69,13 +93,10 @@ public class ArchiveTest extends AndroidTestCase {
                outputStream.flush();
                outputStream.flush();


            }
            }
            mArchive = Archive.createForParcelFileDescriptor(
            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
                  context,
                  ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY),
                  ARCHIVE_URI,
                  Uri.parse(NOTIFICATION_URI));
        } catch (IOException e) {
        } catch (IOException e) {
            fail(String.valueOf(e));
            fail(String.valueOf(e));
            return null;
        } finally {
        } finally {
            // On UNIX the file will be still available for processes which opened it, even
            // On UNIX the file will be still available for processes which opened it, even
            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
@@ -85,15 +106,53 @@ public class ArchiveTest extends AndroidTestCase {
        }
        }
    }
    }


    /**
     * Opens a resource and returns the contents via a pipe.
     */
    public ParcelFileDescriptor getNonSeekableDescriptor(int resource) {
        ParcelFileDescriptor[] pipe = null;
        final Context testContext = InstrumentationRegistry.getContext();
        try {
            pipe = ParcelFileDescriptor.createPipe();
            final ParcelFileDescriptor finalOutputPipe = pipe[1];
            mExecutor.execute(
                    new Runnable() {
                        @Override
                        @Override
    public void tearDown() {
                        public void run() {
        if (mArchive != null) {
                            try (
            mArchive.close();
                                final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
                                        new ParcelFileDescriptor.
                                                AutoCloseOutputStream(finalOutputPipe);
                                final InputStream inputStream =
                                        testContext.getResources().openRawResource(resource);
                            ) {
                                final byte[] buffer = new byte[32 * 1024];
                                int bytes;
                                while ((bytes = inputStream.read(buffer)) != -1) {
                                    outputStream.write(buffer, 0, bytes);
                                }
                            } catch (IOException e) {
                              fail(String.valueOf(e));
                            }
                        }
                    });
            return pipe[0];
        } catch (IOException e) {
            fail(String.valueOf(e));
            return null;
        }
        }
    }
    }


    public void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
        mArchive = Archive.createForParcelFileDescriptor(
                mContext,
                descriptor,
                ARCHIVE_URI,
                Uri.parse(NOTIFICATION_URI));
    }

    public void testQueryChildDocument() throws IOException {
    public void testQueryChildDocument() throws IOException {
        loadArchive(R.raw.archive);
        loadArchive(getNonSeekableDescriptor(R.raw.archive));
        final Cursor cursor = mArchive.queryChildDocuments(
        final Cursor cursor = mArchive.queryChildDocuments(
                new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
                new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);


@@ -151,7 +210,7 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testQueryChildDocument_NoDirs() throws IOException {
    public void testQueryChildDocument_NoDirs() throws IOException {
        loadArchive(R.raw.no_dirs);
        loadArchive(getNonSeekableDescriptor(R.raw.no_dirs));
        final Cursor cursor = mArchive.queryChildDocuments(
        final Cursor cursor = mArchive.queryChildDocuments(
            new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
            new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);


@@ -198,7 +257,7 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testQueryChildDocument_EmptyDirs() throws IOException {
    public void testQueryChildDocument_EmptyDirs() throws IOException {
        loadArchive(R.raw.empty_dirs);
        loadArchive(getNonSeekableDescriptor(R.raw.empty_dirs));
        final Cursor cursor = mArchive.queryChildDocuments(
        final Cursor cursor = mArchive.queryChildDocuments(
                new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
                new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);


@@ -258,7 +317,7 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testGetDocumentType() throws IOException {
    public void testGetDocumentType() throws IOException {
        loadArchive(R.raw.archive);
        loadArchive(getNonSeekableDescriptor(R.raw.archive));
        assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
        assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
                new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId()));
                new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId()));
        assertEquals("text/plain", mArchive.getDocumentType(
        assertEquals("text/plain", mArchive.getDocumentType(
@@ -266,7 +325,7 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testIsChildDocument() throws IOException {
    public void testIsChildDocument() throws IOException {
        loadArchive(R.raw.archive);
        loadArchive(getNonSeekableDescriptor(R.raw.archive));
        final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId();
        final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId();
        assertTrue(mArchive.isChildDocument(documentId,
        assertTrue(mArchive.isChildDocument(documentId,
                new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId()));
                new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId()));
@@ -280,7 +339,7 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testQueryDocument() throws IOException {
    public void testQueryDocument() throws IOException {
        loadArchive(R.raw.archive);
        loadArchive(getNonSeekableDescriptor(R.raw.archive));
        final Cursor cursor = mArchive.queryDocument(
        final Cursor cursor = mArchive.queryDocument(
                new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(),
                new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(),
                null);
                null);
@@ -298,7 +357,17 @@ public class ArchiveTest extends AndroidTestCase {
    }
    }


    public void testOpenDocument() throws IOException {
    public void testOpenDocument() throws IOException {
        loadArchive(R.raw.archive);
        loadArchive(getSeekableDescriptor(R.raw.archive));
        commonTestOpenDocument();
    }

    public void testOpenDocument_NonSeekable() throws IOException {
        loadArchive(getNonSeekableDescriptor(R.raw.archive));
        commonTestOpenDocument();
    }

    // Common part of testOpenDocument and testOpenDocument_NonSeekable.
    void commonTestOpenDocument() throws IOException {
        final ParcelFileDescriptor descriptor = mArchive.openDocument(
        final ParcelFileDescriptor descriptor = mArchive.openDocument(
                new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(),
                new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(),
                "r", null /* signal */);
                "r", null /* signal */);
@@ -307,4 +376,9 @@ public class ArchiveTest extends AndroidTestCase {
            assertEquals("I love strawberries!", new Scanner(inputStream).nextLine());
            assertEquals("I love strawberries!", new Scanner(inputStream).nextLine());
        }
        }
    }
    }

    public void testCanSeek() throws IOException {
        assertTrue(Archive.canSeek(getSeekableDescriptor(R.raw.archive)));
        assertFalse(Archive.canSeek(getNonSeekableDescriptor(R.raw.archive)));
    }
}
}