1 /*
2  * Copyright (C) 2016 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;
18 
19 import static android.content.ContentResolver.wrap;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static junit.framework.Assert.assertEquals;
24 import static junit.framework.Assert.assertFalse;
25 import static junit.framework.Assert.assertNotNull;
26 import static junit.framework.Assert.assertNull;
27 import static junit.framework.Assert.assertTrue;
28 import static junit.framework.Assert.fail;
29 
30 import android.content.ContentProviderClient;
31 import android.content.ContentResolver;
32 import android.content.Context;
33 import android.database.ContentObserver;
34 import android.database.Cursor;
35 import android.media.ExifInterface;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.ParcelFileDescriptor;
39 import android.os.RemoteException;
40 import android.provider.DocumentsContract;
41 import android.text.TextUtils;
42 
43 import androidx.test.InstrumentationRegistry;
44 import androidx.test.filters.MediumTest;
45 import androidx.test.runner.AndroidJUnit4;
46 
47 import com.android.documentsui.archives.ArchivesProvider;
48 import com.android.documentsui.archives.ResourcesProvider;
49 
50 import org.junit.After;
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.util.concurrent.CountDownLatch;
56 import java.util.concurrent.ExecutorService;
57 import java.util.concurrent.Executors;
58 import java.util.concurrent.TimeUnit;
59 
60 @RunWith(AndroidJUnit4.class)
61 @MediumTest
62 public class ArchivesProviderTest {
63 
64     private Context mContext;
65     private ExecutorService mExecutor = null;
66 
67     @Before
setUp()68     public void setUp() throws Exception {
69         mContext = InstrumentationRegistry.getContext();
70         mExecutor = Executors.newSingleThreadExecutor();
71     }
72 
73     @After
tearDown()74     public void tearDown() throws Exception {
75         mExecutor.shutdown();
76         assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
77     }
78 
79     @Test
testQueryRoots()80     public void testQueryRoots() throws InterruptedException, RemoteException {
81         final ContentResolver resolver = mContext.getContentResolver();
82         final Uri rootsUri = DocumentsContract.buildRootsUri(ArchivesProvider.AUTHORITY);
83         try (ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
84                 rootsUri)) {
85             Cursor cursor = client.query(rootsUri, null, null, null, null, null);
86             assertNotNull("Cursor must not be null.", cursor);
87             assertEquals(0, cursor.getCount());
88         }
89     }
90 
91     @Test
testOpen_Success()92     public void testOpen_Success() throws InterruptedException {
93         final Uri sourceUri = DocumentsContract.buildDocumentUri(
94                 ResourcesProvider.AUTHORITY, "archive.zip");
95         final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
96                 ParcelFileDescriptor.MODE_READ_ONLY);
97 
98         final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
99                 ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
100 
101         final ContentResolver resolver = mContext.getContentResolver();
102         final CountDownLatch latch = new CountDownLatch(1);
103 
104         final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
105                 archiveUri);
106         ArchivesProvider.acquireArchive(client, archiveUri);
107 
108         {
109             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
110             assertNotNull("Cursor must not be null. File not found?", cursor);
111 
112             assertEquals(0, cursor.getCount());
113             final Bundle extras = cursor.getExtras();
114             assertEquals(true, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
115             assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
116 
117             final Uri notificationUri = cursor.getNotificationUri();
118             assertNotNull(notificationUri);
119 
120             resolver.registerContentObserver(notificationUri, false, new ContentObserver(null) {
121                 @Override
122                 public void onChange(boolean selfChange, Uri uri) {
123                     latch.countDown();
124                 }
125             });
126         }
127 
128         latch.await(3, TimeUnit.SECONDS);
129         {
130             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
131             assertNotNull("Cursor must not be null. File not found?", cursor);
132 
133             assertEquals(3, cursor.getCount());
134             final Bundle extras = cursor.getExtras();
135             assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
136             assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
137         }
138 
139         ArchivesProvider.releaseArchive(client, archiveUri);
140         client.release();
141     }
142 
143     @Test
testOpen_Failure()144     public void testOpen_Failure() throws InterruptedException {
145         final Uri sourceUri = DocumentsContract.buildDocumentUri(
146                 ResourcesProvider.AUTHORITY, "broken.zip");
147         final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
148                 ParcelFileDescriptor.MODE_READ_ONLY);
149 
150         final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
151                 ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
152 
153         final ContentResolver resolver = mContext.getContentResolver();
154         final CountDownLatch latch = new CountDownLatch(1);
155 
156         final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
157                 archiveUri);
158         ArchivesProvider.acquireArchive(client, archiveUri);
159 
160         {
161             // TODO: Close this and any other cursor in this file.
162             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
163             assertNotNull("Cursor must not be null. File not found?", cursor);
164 
165             assertEquals(0, cursor.getCount());
166             final Bundle extras = cursor.getExtras();
167             assertEquals(true, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
168             assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
169 
170             final Uri notificationUri = cursor.getNotificationUri();
171             assertNotNull(notificationUri);
172 
173             resolver.registerContentObserver(notificationUri, false, new ContentObserver(null) {
174                 @Override
175                 public void onChange(boolean selfChange, Uri uri) {
176                     latch.countDown();
177                 }
178             });
179         }
180 
181         latch.await(3, TimeUnit.SECONDS);
182         {
183             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
184             assertNotNull("Cursor must not be null. File not found?", cursor);
185 
186             assertEquals(0, cursor.getCount());
187             final Bundle extras = cursor.getExtras();
188             assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
189             assertFalse(TextUtils.isEmpty(extras.getString(DocumentsContract.EXTRA_ERROR)));
190         }
191 
192         ArchivesProvider.releaseArchive(client, archiveUri);
193         client.release();
194     }
195 
196     @Test
testOpen_ClosesOnRelease()197     public void testOpen_ClosesOnRelease() throws InterruptedException {
198         final Uri sourceUri = DocumentsContract.buildDocumentUri(
199                 ResourcesProvider.AUTHORITY, "archive.zip");
200         final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
201                 ParcelFileDescriptor.MODE_READ_ONLY);
202 
203         final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
204                 ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
205 
206         final ContentResolver resolver = mContext.getContentResolver();
207 
208         final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
209                 archiveUri);
210 
211         // Acquire twice to ensure that the refcount works correctly.
212         ArchivesProvider.acquireArchive(client, archiveUri);
213         ArchivesProvider.acquireArchive(client, archiveUri);
214 
215         {
216             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
217             assertNotNull("Cursor must not be null. File not found?", cursor);
218         }
219 
220         ArchivesProvider.releaseArchive(client, archiveUri);
221 
222         {
223             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
224             assertNotNull("Cursor must not be null. File not found?", cursor);
225         }
226 
227         ArchivesProvider.releaseArchive(client, archiveUri);
228 
229         try {
230             resolver.query(childrenUri, null, null, null, null, null);
231             fail("The archive was expected to be invalid on the last release call.");
232         } catch (IllegalStateException e) {
233             // Expected.
234         }
235 
236         client.release();
237     }
238 
239     @Test
testNoNotificationAfterAllReleased()240     public void testNoNotificationAfterAllReleased() throws InterruptedException, RemoteException {
241         final Uri sourceUri = DocumentsContract.buildDocumentUri(
242                 ResourcesProvider.AUTHORITY, "archive.zip");
243         final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
244                 ParcelFileDescriptor.MODE_READ_ONLY);
245 
246         final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
247                 ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
248 
249         final ContentResolver resolver = mContext.getContentResolver();
250 
251         final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
252                 archiveUri);
253 
254         ArchivesProvider.acquireArchive(client, archiveUri);
255         final Cursor cursor = client.query(childrenUri, null, null, null, null, null);
256         final Bundle extra = cursor.getExtras();
257         assertTrue(extra.getBoolean(DocumentsContract.EXTRA_LOADING, false));
258         final Uri notificationUri = cursor.getNotificationUri();
259 
260         ArchivesProvider.releaseArchive(client, archiveUri);
261         final CountDownLatch latch = new CountDownLatch(1);
262         resolver.registerContentObserver(notificationUri, false, new ContentObserver(null) {
263             @Override
264             public void onChange(boolean selfChange, Uri uri) {
265                 latch.countDown();
266             }
267         });
268 
269         // Assert that there is no notification if no one has acquired this archive and this wait
270         // times out.
271         assertFalse(latch.await(1, TimeUnit.SECONDS));
272 
273         client.release();
274     }
275 
getDocumentMetadata_byDocumentId_shouldMatchSize(String documentId)276     private void getDocumentMetadata_byDocumentId_shouldMatchSize(String documentId)
277             throws Exception {
278         final Uri sourceUri = DocumentsContract.buildDocumentUri(
279                 ResourcesProvider.AUTHORITY, documentId);
280         final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
281                 ParcelFileDescriptor.MODE_READ_ONLY);
282 
283         final ContentResolver resolver = mContext.getContentResolver();
284         final ContentProviderClient client =
285                 resolver.acquireUnstableContentProviderClient(archiveUri);
286 
287         ArchivesProvider.acquireArchive(client, archiveUri);
288 
289         Uri archivedImageUri = Uri.parse(
290                 "content://com.android.documentsui.archives/document/content%3A%2F%2F"
291                         + "com.android.documentsui.archives.resourcesprovider%2F"
292                         + "document%2F" + documentId + "%23268435456%23%2Ffreddy.jpg");
293 
294         Bundle metadata = DocumentsContract.getDocumentMetadata(wrap(client), archivedImageUri);
295         assertNotNull(metadata);
296         Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF);
297         assertNotNull(exif);
298 
299         assertThat(exif.getInt(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo(3036);
300         assertThat(exif.getInt(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo(4048);
301         assertThat(exif.getString(ExifInterface.TAG_MODEL)).isEqualTo("Pixel");
302 
303         ArchivesProvider.releaseArchive(client, archiveUri);
304         client.close();
305     }
306 
307     @Test
testGetDocumentMetadata()308     public void testGetDocumentMetadata() throws Exception {
309         getDocumentMetadata_byDocumentId_shouldMatchSize("images.zip");
310     }
311 
312     @Test
getDocumentMetadata_sevenZFile_shouldMatchSize()313     public void getDocumentMetadata_sevenZFile_shouldMatchSize() throws Exception {
314         getDocumentMetadata_byDocumentId_shouldMatchSize("images.7z");
315     }
316 
317     @Test
getDocumentMetadata_tgz_shouldMatchSize()318     public void getDocumentMetadata_tgz_shouldMatchSize() throws Exception {
319         getDocumentMetadata_byDocumentId_shouldMatchSize("images.tgz");
320     }
321 
322     @Test
getDocumentMetadata_tar_shouldMatchSize()323     public void getDocumentMetadata_tar_shouldMatchSize() throws Exception {
324         getDocumentMetadata_byDocumentId_shouldMatchSize("images.tar");
325     }
326 }
327