1 /*
2  * Copyright (C) 2017 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.documentsui.archives;
18 
19 import android.database.Cursor;
20 import android.net.Uri;
21 import android.os.ParcelFileDescriptor;
22 import android.provider.DocumentsContract.Document;
23 import android.test.AndroidTestCase;
24 
25 import androidx.test.InstrumentationRegistry;
26 import androidx.test.filters.MediumTest;
27 
28 import java.io.File;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.util.Enumeration;
32 import java.util.Scanner;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.TimeUnit;
36 import java.util.zip.ZipEntry;
37 import java.util.zip.ZipFile;
38 
39 @MediumTest
40 public class WriteableArchiveTest extends AndroidTestCase {
41 
42     private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
43     private static final String NOTIFICATION_URI =
44             "content://com.android.documentsui.archives/notification-uri";
45     private ExecutorService mExecutor = null;
46     private Archive mArchive = null;
47     private TestUtils mTestUtils = null;
48     private File mFile = null;
49 
50     @Override
setUp()51     public void setUp() throws Exception {
52         super.setUp();
53         mExecutor = Executors.newSingleThreadExecutor();
54         mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
55                 InstrumentationRegistry.getContext(), mExecutor);
56         mFile = mTestUtils.createTemporaryFile();
57 
58         mArchive = WriteableArchive.createForParcelFileDescriptor(
59                 InstrumentationRegistry.getTargetContext(),
60                 ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_WRITE_ONLY),
61                 ARCHIVE_URI,
62                 ParcelFileDescriptor.MODE_WRITE_ONLY,
63                 Uri.parse(NOTIFICATION_URI));
64     }
65 
66     @Override
tearDown()67     public void tearDown() throws Exception {
68         mExecutor.shutdown();
69         assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
70         if (mFile != null) {
71             mFile.delete();
72         }
73         if (mArchive != null) {
74             mArchive.close();
75         }
76         super.tearDown();
77     }
78 
createArchiveId(String path)79     public static ArchiveId createArchiveId(String path) {
80         return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_WRITE_ONLY, path);
81     }
82 
testCreateDocument()83     public void testCreateDocument() throws IOException {
84         final String dirDocumentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
85                 Document.MIME_TYPE_DIR, "dir");
86         assertEquals(createArchiveId("/dir/").toDocumentId(), dirDocumentId);
87 
88         final String documentId = mArchive.createDocument(dirDocumentId, "image/jpeg", "test.jpeg");
89         assertEquals(createArchiveId("/dir/test.jpeg").toDocumentId(), documentId);
90 
91         try {
92             mArchive.createDocument(dirDocumentId,
93                     "image/jpeg", "test.jpeg");
94             fail("Creating should fail, as the document already exists.");
95         } catch (IllegalStateException e) {
96             // Expected.
97         }
98 
99         try {
100             mArchive.createDocument(createArchiveId("/").toDocumentId(),
101                     "image/jpeg", "test.jpeg/");
102             fail("Creating should fail, as the document name is invalid.");
103         } catch (IllegalStateException e) {
104             // Expected.
105         }
106 
107         try {
108             mArchive.createDocument(createArchiveId("/").toDocumentId(),
109                     Document.MIME_TYPE_DIR, "test/");
110             fail("Creating should fail, as the document name is invalid.");
111         } catch (IllegalStateException e) {
112             // Expected.
113         }
114 
115         try {
116             mArchive.createDocument(createArchiveId("/").toDocumentId(),
117                     Document.MIME_TYPE_DIR, "..");
118             fail("Creating should fail, as the document name is invalid.");
119         } catch (IllegalStateException e) {
120             // Expected.
121         }
122 
123         try {
124             mArchive.createDocument(createArchiveId("/").toDocumentId(),
125                     Document.MIME_TYPE_DIR, ".");
126             fail("Creating should fail, as the document name is invalid.");
127         } catch (IllegalStateException e) {
128             // Expected.
129         }
130 
131         try {
132             mArchive.createDocument(createArchiveId("/").toDocumentId(),
133                     Document.MIME_TYPE_DIR, "");
134             fail("Creating should fail, as the document name is invalid.");
135         } catch (IllegalStateException e) {
136             // Expected.
137         }
138 
139         try {
140             mArchive.createDocument(createArchiveId("/").toDocumentId(),
141                     "image/jpeg", "a/b.jpeg");
142             fail("Creating should fail, as the document name is invalid.");
143         } catch (IllegalStateException e) {
144             // Expected.
145         }
146     }
147 
testAddDirectory()148     public void testAddDirectory() throws IOException {
149         final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
150                 Document.MIME_TYPE_DIR, "dir");
151 
152         {
153             final Cursor cursor = mArchive.queryDocument(documentId, null);
154             assertTrue(cursor.moveToFirst());
155             assertEquals(documentId,
156                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
157             assertEquals("dir",
158                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
159             assertEquals(Document.MIME_TYPE_DIR,
160                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
161             assertEquals(0,
162                     cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
163         }
164 
165         {
166             final Cursor cursor = mArchive.queryChildDocuments(
167                     createArchiveId("/").toDocumentId(), null, null);
168 
169             assertTrue(cursor.moveToFirst());
170             assertEquals(documentId,
171                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
172             assertEquals("dir",
173                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
174             assertEquals(Document.MIME_TYPE_DIR,
175                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
176             assertEquals(0,
177                     cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
178         }
179 
180         mArchive.close();
181 
182         // Verify archive.
183         ZipFile zip = null;
184         try {
185             zip = new ZipFile(mFile);
186             final Enumeration<? extends ZipEntry> entries = zip.entries();
187             assertTrue(entries.hasMoreElements());
188             final ZipEntry entry = entries.nextElement();
189             assertEquals("dir/", entry.getName());
190             assertFalse(entries.hasMoreElements());
191         } finally {
192             if (zip != null) {
193                 zip.close();
194             }
195         }
196     }
197 
testAddFile()198     public void testAddFile() throws IOException, InterruptedException {
199         final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
200                 "text/plain", "hoge.txt");
201 
202         {
203             final Cursor cursor = mArchive.queryDocument(documentId, null);
204             assertTrue(cursor.moveToFirst());
205             assertEquals(documentId,
206                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
207             assertEquals("hoge.txt",
208                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
209             assertEquals("text/plain",
210                     cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
211             assertEquals(0,
212                     cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
213         }
214 
215         try {
216             mArchive.openDocument(documentId, "r", null);
217             fail("Should fail when opened for reading!");
218         } catch (IllegalArgumentException e) {
219             // Expected.
220         }
221 
222         final ParcelFileDescriptor fd = mArchive.openDocument(documentId, "w", null);
223         try (ParcelFileDescriptor.AutoCloseOutputStream outputStream =
224                 new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
225             outputStream.write("Hello world!".getBytes());
226         }
227 
228         try {
229             mArchive.openDocument(documentId, "w", null);
230             fail("Should fail when opened for the second time!");
231         } catch (IllegalStateException e) {
232             // Expected.
233         }
234 
235         // Wait until the pipe thread fully writes all the data from the pipe.
236         // TODO: Maybe add some method in WriteableArchive to wait until the executor
237         // completes the job?
238         Thread.sleep(500);
239 
240         {
241             final Cursor cursor = mArchive.queryDocument(documentId, null);
242             assertTrue(cursor.moveToFirst());
243             assertEquals(12,
244                     cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
245         }
246 
247         mArchive.close();
248 
249         // Verify archive.
250         ZipFile zip = null;
251         try {
252             try {
253                 zip = new ZipFile(mFile);
254             } catch (Exception e) {
255                 throw new IOException(mFile.getAbsolutePath());
256             }
257             final Enumeration<? extends ZipEntry> entries = zip.entries();
258             assertTrue(entries.hasMoreElements());
259             final ZipEntry entry = entries.nextElement();
260             assertEquals("hoge.txt", entry.getName());
261             assertFalse(entries.hasMoreElements());
262             final InputStream inputStream = zip.getInputStream(entry);
263             final Scanner scanner = new Scanner(inputStream);
264             assertEquals("Hello world!", scanner.nextLine());
265             assertFalse(scanner.hasNext());
266         } finally {
267             if (zip != null) {
268                 zip.close();
269             }
270         }
271     }
272 
testAddFile_empty()273     public void testAddFile_empty() throws IOException, Exception {
274         final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
275                 "text/plain", "hoge.txt");
276         mArchive.close();
277 
278         // Verify archive.
279         ZipFile zip = null;
280         try {
281             try {
282                 zip = new ZipFile(mFile);
283             } catch (Exception e) {
284                 throw new IOException(mFile.getAbsolutePath());
285             }
286             final Enumeration<? extends ZipEntry> entries = zip.entries();
287             assertTrue(entries.hasMoreElements());
288             final ZipEntry entry = entries.nextElement();
289             assertEquals("hoge.txt", entry.getName());
290             assertFalse(entries.hasMoreElements());
291             final InputStream inputStream = zip.getInputStream(entry);
292             final Scanner scanner = new Scanner(inputStream);
293             assertFalse(scanner.hasNext());
294         } finally {
295             if (zip != null) {
296                 zip.close();
297             }
298         }
299     }
300 }
301