1 /*
2  * Copyright (C) 2015 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.mtp;
18 
19 import android.database.Cursor;
20 import android.mtp.MtpConstants;
21 import android.mtp.MtpObjectInfo;
22 import android.net.Uri;
23 import android.os.ParcelFileDescriptor;
24 import android.os.storage.StorageManager;
25 import android.provider.DocumentsContract.Document;
26 import android.provider.DocumentsContract.Path;
27 import android.provider.DocumentsContract.Root;
28 import android.system.Os;
29 import android.system.OsConstants;
30 import android.provider.DocumentsContract;
31 import android.test.AndroidTestCase;
32 import android.test.suitebuilder.annotation.MediumTest;
33 
34 import java.io.FileNotFoundException;
35 import java.io.IOException;
36 import java.util.Arrays;
37 import java.util.LinkedList;
38 import java.util.Queue;
39 import java.util.concurrent.TimeoutException;
40 
41 import static com.android.mtp.MtpDatabase.strings;
42 import static com.android.mtp.TestUtil.OPERATIONS_SUPPORTED;
43 
44 @MediumTest
45 public class MtpDocumentsProviderTest extends AndroidTestCase {
46     private final static Uri ROOTS_URI =
47             DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY);
48     private TestContentResolver mResolver;
49     private MtpDocumentsProvider mProvider;
50     private TestMtpManager mMtpManager;
51     private final TestResources mResources = new TestResources();
52     private MtpDatabase mDatabase;
53 
54     @Override
setUp()55     public void setUp() throws IOException {
56         mResolver = new TestContentResolver();
57         mMtpManager = new TestMtpManager(getContext());
58     }
59 
60     @Override
tearDown()61     public void tearDown() {
62         mProvider.shutdown();
63         MtpDatabase.deleteDatabase(getContext());
64     }
65 
testOpenAndCloseDevice()66     public void testOpenAndCloseDevice() throws Exception {
67         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
68         mMtpManager.addValidDevice(new MtpDeviceRecord(
69                 0,
70                 "Device A",
71                 null /* deviceKey */,
72                 false /* unopened */,
73                 new MtpRoot[] {
74                     new MtpRoot(
75                             0 /* deviceId */,
76                             1 /* storageId */,
77                             "Storage A" /* volume description */,
78                             1024 /* free space */,
79                             2048 /* total space */,
80                             "" /* no volume identifier */)
81                 },
82                 OPERATIONS_SUPPORTED,
83                 null));
84 
85         mProvider.resumeRootScanner();
86         mResolver.waitForNotification(ROOTS_URI, 1);
87 
88         mProvider.openDevice(0);
89         mResolver.waitForNotification(ROOTS_URI, 2);
90 
91         mProvider.closeDevice(0);
92         mResolver.waitForNotification(ROOTS_URI, 3);
93     }
94 
testOpenAndCloseErrorDevice()95     public void testOpenAndCloseErrorDevice() throws Exception {
96         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
97         try {
98             mProvider.openDevice(1);
99             fail();
100         } catch (Throwable error) {
101             assertTrue(error instanceof IOException);
102         }
103         assertEquals(0, mProvider.getOpenedDeviceRecordsCache().length);
104 
105         // Check if the following notification is the first one or not.
106         mMtpManager.addValidDevice(new MtpDeviceRecord(
107                 0,
108                 "Device A",
109                 null /* deviceKey */,
110                 false /* unopened */,
111                 new MtpRoot[] {
112                     new MtpRoot(
113                             0 /* deviceId */,
114                             1 /* storageId */,
115                             "Storage A" /* volume description */,
116                             1024 /* free space */,
117                             2048 /* total space */,
118                             "" /* no volume identifier */)
119                 },
120                 OPERATIONS_SUPPORTED,
121                 null));
122         mProvider.resumeRootScanner();
123         mResolver.waitForNotification(ROOTS_URI, 1);
124         mProvider.openDevice(0);
125         mResolver.waitForNotification(ROOTS_URI, 2);
126     }
127 
testOpenDeviceOnDemand()128     public void testOpenDeviceOnDemand() throws Exception {
129         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
130         mMtpManager.addValidDevice(new MtpDeviceRecord(
131                 0,
132                 "Device A",
133                 null /* deviceKey */,
134                 false /* unopened */,
135                 new MtpRoot[] {
136                     new MtpRoot(
137                             0 /* deviceId */,
138                             1 /* storageId */,
139                             "Storage A" /* volume description */,
140                             1024 /* free space */,
141                             2048 /* total space */,
142                             "" /* no volume identifier */)
143                 },
144                 OPERATIONS_SUPPORTED,
145                 null));
146         mMtpManager.setObjectHandles(0, 1, -1, new int[0]);
147         mProvider.resumeRootScanner();
148         mResolver.waitForNotification(ROOTS_URI, 1);
149         final String[] columns = new String[] {
150                 DocumentsContract.Root.COLUMN_TITLE,
151                 DocumentsContract.Root.COLUMN_DOCUMENT_ID
152         };
153         try (final Cursor cursor = mProvider.queryRoots(columns)) {
154             assertEquals(1, cursor.getCount());
155             assertTrue(cursor.moveToNext());
156             assertEquals("Device A", cursor.getString(0));
157             assertEquals(1, cursor.getLong(1));
158         }
159         {
160             final MtpDeviceRecord[] openedDevice = mProvider.getOpenedDeviceRecordsCache();
161             assertEquals(0, openedDevice.length);
162         }
163         // Device is opened automatically when querying its children.
164         try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) {}
165 
166         {
167             final MtpDeviceRecord[] openedDevice = mProvider.getOpenedDeviceRecordsCache();
168             assertEquals(1, openedDevice.length);
169             assertEquals(0, openedDevice[0].deviceId);
170         }
171     }
172 
testQueryRoots()173     public void testQueryRoots() throws Exception {
174         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
175         mMtpManager.addValidDevice(new MtpDeviceRecord(
176                 0,
177                 "Device A",
178                 "Device key A",
179                 false /* unopened */,
180                 new MtpRoot[] {
181                         new MtpRoot(
182                                 0 /* deviceId */,
183                                 1 /* storageId */,
184                                 "Storage A" /* volume description */,
185                                 1024 /* free space */,
186                                 2048 /* total space */,
187                                 "" /* no volume identifier */)
188                 },
189                 OPERATIONS_SUPPORTED,
190                 null));
191         mMtpManager.addValidDevice(new MtpDeviceRecord(
192                 1,
193                 "Device B",
194                 "Device key B",
195                 false /* unopened */,
196                 new MtpRoot[] {
197                     new MtpRoot(
198                             1 /* deviceId */,
199                             1 /* storageId */,
200                             "Storage B" /* volume description */,
201                             2048 /* free space */,
202                             4096 /* total space */,
203                             "Identifier B" /* no volume identifier */)
204                 },
205                 new int[0] /* No operations supported */,
206                 null));
207 
208         {
209             mProvider.openDevice(0);
210             mResolver.waitForNotification(ROOTS_URI, 1);
211             final Cursor cursor = mProvider.queryRoots(null);
212             assertEquals(2, cursor.getCount());
213             cursor.moveToNext();
214             assertEquals("1", cursor.getString(0));
215             assertEquals(
216                     Root.FLAG_SUPPORTS_IS_CHILD |
217                     Root.FLAG_SUPPORTS_CREATE |
218                     Root.FLAG_LOCAL_ONLY,
219                     cursor.getInt(1));
220             assertEquals(R.drawable.ic_root_mtp, cursor.getInt(2));
221             assertEquals("Device A Storage A", cursor.getString(3));
222             assertEquals("1", cursor.getString(4));
223             assertEquals(1024, cursor.getInt(5));
224         }
225 
226         {
227             mProvider.openDevice(1);
228             mResolver.waitForNotification(ROOTS_URI, 2);
229             final Cursor cursor = mProvider.queryRoots(null);
230             assertEquals(2, cursor.getCount());
231             cursor.moveToNext();
232             cursor.moveToNext();
233             assertEquals("2", cursor.getString(0));
234             assertEquals(
235                     Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_LOCAL_ONLY, cursor.getInt(1));
236             assertEquals(R.drawable.ic_root_mtp, cursor.getInt(2));
237             assertEquals("Device B Storage B", cursor.getString(3));
238             assertEquals("2", cursor.getString(4));
239             assertEquals(2048, cursor.getInt(5));
240         }
241     }
242 
testQueryRoots_error()243     public void testQueryRoots_error() throws Exception {
244         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
245         mMtpManager.addValidDevice(new MtpDeviceRecord(
246                 0,
247                 "Device A",
248                 "Device key A",
249                 false /* unopened */,
250                 new MtpRoot[0],
251                 OPERATIONS_SUPPORTED,
252                 null));
253         mMtpManager.addValidDevice(new MtpDeviceRecord(
254                 1,
255                 "Device B",
256                 "Device key B",
257                 false /* unopened */,
258                 new MtpRoot[] {
259                     new MtpRoot(
260                             1 /* deviceId */,
261                             1 /* storageId */,
262                             "Storage B" /* volume description */,
263                             2048 /* free space */,
264                             4096 /* total space */,
265                             "Identifier B" /* no volume identifier */)
266                 },
267                 OPERATIONS_SUPPORTED,
268                 null));
269         {
270             mProvider.openDevice(0);
271             mResolver.waitForNotification(ROOTS_URI, 1);
272 
273             mProvider.openDevice(1);
274             mResolver.waitForNotification(ROOTS_URI, 2);
275 
276             final Cursor cursor = mProvider.queryRoots(null);
277             assertEquals(2, cursor.getCount());
278 
279             cursor.moveToNext();
280             assertEquals("1", cursor.getString(0));
281             assertEquals(
282                     Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY,
283                     cursor.getInt(1));
284             assertEquals(R.drawable.ic_root_mtp, cursor.getInt(2));
285             assertEquals("Device A", cursor.getString(3));
286             assertEquals("1", cursor.getString(4));
287             assertEquals(0, cursor.getInt(5));
288 
289             cursor.moveToNext();
290             assertEquals("2", cursor.getString(0));
291             assertEquals(
292                     Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY,
293                     cursor.getInt(1));
294             assertEquals(R.drawable.ic_root_mtp, cursor.getInt(2));
295             assertEquals("Device B Storage B", cursor.getString(3));
296             assertEquals("2", cursor.getString(4));
297             assertEquals(2048, cursor.getInt(5));
298         }
299     }
300 
testQueryDocument()301     public void testQueryDocument() throws IOException, InterruptedException, TimeoutException {
302         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
303         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
304         setupDocuments(
305                 0,
306                 0,
307                 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN,
308                 "1",
309                 new MtpObjectInfo[] {
310                         new MtpObjectInfo.Builder()
311                                 .setObjectHandle(100)
312                                 .setFormat(MtpConstants.FORMAT_EXIF_JPEG)
313                                 .setName("image.jpg")
314                                 .setDateModified(1422716400000L)
315                                 .setCompressedSize(1024 * 1024 * 5)
316                                 .setThumbCompressedSize(50 * 1024)
317                                 .build()
318                 });
319 
320         final Cursor cursor = mProvider.queryDocument("3", null);
321         assertEquals(1, cursor.getCount());
322 
323         cursor.moveToNext();
324 
325         assertEquals("3", cursor.getString(0));
326         assertEquals("image/jpeg", cursor.getString(1));
327         assertEquals("image.jpg", cursor.getString(2));
328         assertEquals(1422716400000L, cursor.getLong(3));
329         assertEquals(
330                 DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
331                 DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
332                 DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL |
333                 DocumentsContract.Document.FLAG_SUPPORTS_METADATA,
334                 cursor.getInt(4));
335         assertEquals(1024 * 1024 * 5, cursor.getInt(5));
336     }
337 
testQueryDocument_directory()338     public void testQueryDocument_directory()
339             throws IOException, InterruptedException, TimeoutException {
340         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
341         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
342         setupDocuments(
343                 0,
344                 0,
345                 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN,
346                 "1",
347                 new MtpObjectInfo[] {
348                         new MtpObjectInfo.Builder()
349                                 .setObjectHandle(2)
350                                 .setStorageId(1)
351                                 .setFormat(MtpConstants.FORMAT_ASSOCIATION)
352                                 .setName("directory")
353                                 .setDateModified(1422716400000L)
354                                 .build()
355                 });
356 
357         final Cursor cursor = mProvider.queryDocument("3", null);
358         assertEquals(1, cursor.getCount());
359 
360         cursor.moveToNext();
361         assertEquals("3", cursor.getString(0));
362         assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1));
363         assertEquals("directory", cursor.getString(2));
364         assertEquals(1422716400000L, cursor.getLong(3));
365         assertEquals(
366                 DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
367                 DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE,
368                 cursor.getInt(4));
369         assertEquals(0, cursor.getInt(5));
370     }
371 
testQueryDocument_forStorage()372     public void testQueryDocument_forStorage()
373             throws IOException, InterruptedException, TimeoutException {
374         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
375         setupRoots(0, new MtpRoot[] {
376                 new MtpRoot(
377                         0 /* deviceId */,
378                         1 /* storageId */,
379                         "Storage A" /* volume description */,
380                         1024 /* free space */,
381                         4096 /* total space */,
382                         "" /* no volume identifier */)
383         });
384         final Cursor cursor = mProvider.queryDocument("2", null);
385         assertEquals(1, cursor.getCount());
386 
387         cursor.moveToNext();
388         assertEquals("2", cursor.getString(0));
389         assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1));
390         assertEquals("Storage A", cursor.getString(2));
391         assertTrue(cursor.isNull(3));
392         assertEquals(DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE, cursor.getInt(4));
393         assertEquals(3072, cursor.getInt(5));
394     }
395 
testQueryDocument_forDeviceWithSingleStorage()396     public void testQueryDocument_forDeviceWithSingleStorage()
397             throws IOException, InterruptedException, TimeoutException {
398         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
399         setupRoots(0, new MtpRoot[] {
400                 new MtpRoot(
401                         0 /* deviceId */,
402                         1 /* storageId */,
403                         "Storage A" /* volume description */,
404                         1024 /* free space */,
405                         4096 /* total space */,
406                         "" /* no volume identifier */)
407         });
408         final Cursor cursor = mProvider.queryDocument("1", null);
409         assertEquals(1, cursor.getCount());
410 
411         cursor.moveToNext();
412         assertEquals("1", cursor.getString(0));
413         assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1));
414         assertEquals("Device Storage A", cursor.getString(2));
415         assertTrue(cursor.isNull(3));
416         assertEquals(DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE, cursor.getInt(4));
417         assertTrue(cursor.isNull(5));
418     }
419 
testQueryDocument_forDeviceWithTwoStorages()420     public void testQueryDocument_forDeviceWithTwoStorages()
421             throws IOException, InterruptedException, TimeoutException {
422         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
423         setupRoots(0, new MtpRoot[] {
424                 new MtpRoot(
425                         0 /* deviceId */,
426                         1 /* storageId */,
427                         "Storage A" /* volume description */,
428                         1024 /* free space */,
429                         4096 /* total space */,
430                         "" /* no volume identifier */),
431                 new MtpRoot(
432                         0 /* deviceId */,
433                         2 /* storageId */,
434                         "Storage B" /* volume description */,
435                         1024 /* free space */,
436                         4096 /* total space */,
437                         "" /* no volume identifier */)
438         });
439         final Cursor cursor = mProvider.queryDocument("1", null);
440         assertEquals(1, cursor.getCount());
441 
442         cursor.moveToNext();
443         assertEquals("1", cursor.getString(0));
444         assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1));
445         assertEquals("Device", cursor.getString(2));
446         assertTrue(cursor.isNull(3));
447         assertEquals(0, cursor.getInt(4));
448         assertTrue(cursor.isNull(5));
449     }
450 
testQueryChildDocuments()451     public void testQueryChildDocuments() throws Exception {
452         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
453         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
454         setupDocuments(
455                 0,
456                 0,
457                 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN,
458                 "1",
459                 new MtpObjectInfo[] {
460                         new MtpObjectInfo.Builder()
461                                 .setObjectHandle(100)
462                                 .setFormat(MtpConstants.FORMAT_EXIF_JPEG)
463                                 .setName("image.jpg")
464                                 .setCompressedSize(1024 * 1024 * 5)
465                                 .setThumbCompressedSize(5 * 1024)
466                                 .setProtectionStatus(MtpConstants.PROTECTION_STATUS_READ_ONLY)
467                                 .build()
468                 });
469 
470         final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null);
471         assertEquals(1, cursor.getCount());
472 
473         assertTrue(cursor.moveToNext());
474         assertEquals("3", cursor.getString(0));
475         assertEquals("image/jpeg", cursor.getString(1));
476         assertEquals("image.jpg", cursor.getString(2));
477         assertEquals(0, cursor.getLong(3));
478         assertEquals(Document.FLAG_SUPPORTS_THUMBNAIL
479                 | Document.FLAG_SUPPORTS_METADATA, cursor.getInt(4));
480         assertEquals(1024 * 1024 * 5, cursor.getInt(5));
481 
482         cursor.close();
483     }
484 
testQueryChildDocuments_cursorError()485     public void testQueryChildDocuments_cursorError() throws Exception {
486         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
487         try {
488             mProvider.queryChildDocuments("1", null, (String) null);
489             fail();
490         } catch (FileNotFoundException error) {}
491     }
492 
testQueryChildDocuments_documentError()493     public void testQueryChildDocuments_documentError() throws Exception {
494         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
495         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
496         mMtpManager.setObjectHandles(0, 0, -1, new int[] { 1 });
497         try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) {
498             assertEquals(0, cursor.getCount());
499             assertFalse(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING));
500         }
501     }
502 
testDeleteDocument()503     public void testDeleteDocument() throws IOException, InterruptedException, TimeoutException {
504         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
505         setupRoots(0, new MtpRoot[] {
506                 new MtpRoot(0, 0, "Storage", 0, 0, "")
507         });
508         setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] {
509                 new MtpObjectInfo.Builder()
510                     .setName("test.txt")
511                     .setObjectHandle(1)
512                     .setParent(-1)
513                     .build()
514         });
515 
516         mProvider.deleteDocument("3");
517         assertEquals(1, mResolver.getChangeCount(
518                 DocumentsContract.buildChildDocumentsUri(
519                         MtpDocumentsProvider.AUTHORITY, "1")));
520     }
521 
testDeleteDocument_error()522     public void testDeleteDocument_error()
523             throws IOException, InterruptedException, TimeoutException {
524         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
525         setupRoots(0, new MtpRoot[] {
526                 new MtpRoot(0, 0, "Storage", 0, 0, "")
527         });
528         setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] {
529                 new MtpObjectInfo.Builder()
530                     .setName("test.txt")
531                     .setObjectHandle(1)
532                     .setParent(-1)
533                     .build()
534         });
535         try {
536             mProvider.deleteDocument("4");
537             fail();
538         } catch (Throwable e) {
539             assertTrue(e instanceof IOException);
540         }
541         assertEquals(0, mResolver.getChangeCount(
542                 DocumentsContract.buildChildDocumentsUri(
543                         MtpDocumentsProvider.AUTHORITY, "1")));
544     }
545 
testOpenDocument()546     public void testOpenDocument() throws Exception {
547         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
548         setupRoots(0, new MtpRoot[] {
549                 new MtpRoot(0, 0, "Storage", 0, 0, "")
550         });
551         final byte[] bytes = "Hello world".getBytes();
552         setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] {
553                 new MtpObjectInfo.Builder()
554                         .setName("test.txt")
555                         .setObjectHandle(1)
556                         .setCompressedSize(bytes.length)
557                         .setParent(-1)
558                         .build()
559         });
560         mMtpManager.setImportFileBytes(0, 1, bytes);
561         try (final ParcelFileDescriptor fd = mProvider.openDocument("3", "r", null)) {
562             final byte[] readBytes = new byte[5];
563             assertEquals(6, Os.lseek(fd.getFileDescriptor(), 6, OsConstants.SEEK_SET));
564             assertEquals(5, Os.read(fd.getFileDescriptor(), readBytes, 0, 5));
565             assertTrue(Arrays.equals("world".getBytes(), readBytes));
566 
567             assertEquals(0, Os.lseek(fd.getFileDescriptor(), 0, OsConstants.SEEK_SET));
568             assertEquals(5, Os.read(fd.getFileDescriptor(), readBytes, 0, 5));
569             assertTrue(Arrays.equals("Hello".getBytes(), readBytes));
570         }
571     }
572 
testOpenDocument_shortBytes()573     public void testOpenDocument_shortBytes() throws Exception {
574         mMtpManager = new TestMtpManager(getContext()) {
575             @Override
576             MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
577                 if (objectHandle == 1) {
578                     return new MtpObjectInfo.Builder(super.getObjectInfo(deviceId, objectHandle))
579                             .setObjectHandle(1).setCompressedSize(1024 * 1024).build();
580                 }
581 
582                 return super.getObjectInfo(deviceId, objectHandle);
583             }
584         };
585         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
586         setupRoots(0, new MtpRoot[] {
587                 new MtpRoot(0, 0, "Storage", 0, 0, "")
588         });
589         final byte[] bytes = "Hello world".getBytes();
590         setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] {
591                 new MtpObjectInfo.Builder()
592                         .setName("test.txt")
593                         .setObjectHandle(1)
594                         .setCompressedSize(bytes.length)
595                         .setParent(-1)
596                         .build()
597         });
598         mMtpManager.setImportFileBytes(0, 1, bytes);
599         try (final ParcelFileDescriptor fd = mProvider.openDocument("3", "r", null)) {
600             final byte[] readBytes = new byte[1024 * 1024];
601             assertEquals(11, Os.read(fd.getFileDescriptor(), readBytes, 0, readBytes.length));
602         }
603     }
604 
testOpenDocument_writing()605     public void testOpenDocument_writing() throws Exception {
606         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
607         setupRoots(0, new MtpRoot[] {
608                 new MtpRoot(0, 100, "Storage", 0, 0, "")
609         });
610         final String documentId = mProvider.createDocument("2", "text/plain", "test.txt");
611         {
612             final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "w", null);
613             try (ParcelFileDescriptor.AutoCloseOutputStream stream =
614                     new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
615                 stream.write("Hello".getBytes());
616                 fd.getFileDescriptor().sync();
617             }
618         }
619         {
620             final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "r", null);
621             try (ParcelFileDescriptor.AutoCloseInputStream stream =
622                     new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
623                 final byte[] bytes = new byte[5];
624                 stream.read(bytes);
625                 assertTrue(Arrays.equals("Hello".getBytes(), bytes));
626             }
627         }
628     }
629 
testBusyDevice()630     public void testBusyDevice() throws Exception {
631         mMtpManager = new TestMtpManager(getContext()) {
632             @Override
633             synchronized MtpDeviceRecord openDevice(int deviceId)
634                     throws IOException {
635                 throw new BusyDeviceException();
636             }
637         };
638         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
639         mMtpManager.addValidDevice(new MtpDeviceRecord(
640                 0, "Device A", null /* deviceKey */, false /* unopened */, new MtpRoot[0],
641                 OPERATIONS_SUPPORTED, null));
642 
643         mProvider.resumeRootScanner();
644         mResolver.waitForNotification(ROOTS_URI, 1);
645 
646         try (final Cursor cursor = mProvider.queryRoots(null)) {
647             assertEquals(1, cursor.getCount());
648         }
649 
650         try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) {
651             assertEquals(0, cursor.getCount());
652             assertEquals(
653                     "error_busy_device",
654                     cursor.getExtras().getString(DocumentsContract.EXTRA_ERROR));
655         }
656     }
657 
testLockedDevice()658     public void testLockedDevice() throws Exception {
659         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
660         mMtpManager.addValidDevice(new MtpDeviceRecord(
661                 0, "Device A", null, false /* unopened */, new MtpRoot[0], OPERATIONS_SUPPORTED,
662                 null));
663 
664         mProvider.resumeRootScanner();
665         mResolver.waitForNotification(ROOTS_URI, 1);
666 
667         try (final Cursor cursor = mProvider.queryRoots(null)) {
668             assertEquals(1, cursor.getCount());
669         }
670 
671         try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) {
672             assertEquals(0, cursor.getCount());
673             assertEquals(
674                     "error_locked_device",
675                     cursor.getExtras().getString(DocumentsContract.EXTRA_ERROR));
676         }
677     }
678 
testMappingDisconnectedDocuments()679     public void testMappingDisconnectedDocuments() throws Exception {
680         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
681         mMtpManager.addValidDevice(new MtpDeviceRecord(
682                 0,
683                 "Device A",
684                 "device key",
685                 true /* opened */,
686                 new MtpRoot[] {
687                     new MtpRoot(
688                             0 /* deviceId */,
689                             1 /* storageId */,
690                             "Storage A" /* volume description */,
691                             1024 /* free space */,
692                             2048 /* total space */,
693                             "" /* no volume identifier */)
694                 },
695                 OPERATIONS_SUPPORTED,
696                 null));
697 
698         final String[] names = strings("Directory A", "Directory B", "Directory C");
699         final int objectHandleOffset = 100;
700         for (int i = 0; i < names.length; i++) {
701             final int parentHandle = i == 0 ?
702                     MtpManager.OBJECT_HANDLE_ROOT_CHILDREN : objectHandleOffset + i - 1;
703             final int objectHandle = i + objectHandleOffset;
704             mMtpManager.setObjectHandles(0, 1, parentHandle, new int[] { objectHandle });
705             mMtpManager.setObjectInfo(
706                     0,
707                     new MtpObjectInfo.Builder()
708                             .setName(names[i])
709                             .setObjectHandle(objectHandle)
710                             .setFormat(MtpConstants.FORMAT_ASSOCIATION)
711                             .setStorageId(1)
712                             .build());
713         }
714 
715         mProvider.resumeRootScanner();
716         mResolver.waitForNotification(ROOTS_URI, 1);
717 
718         final int documentIdOffset = 2;
719         for (int i = 0; i < names.length; i++) {
720             try (final Cursor cursor = mProvider.queryChildDocuments(
721                     String.valueOf(documentIdOffset + i),
722                     strings(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME),
723                     (String) null)) {
724                 assertEquals(1, cursor.getCount());
725                 cursor.moveToNext();
726                 assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0));
727                 assertEquals(names[i], cursor.getString(1));
728             }
729         }
730 
731         mProvider.closeDevice(0);
732         mResolver.waitForNotification(ROOTS_URI, 2);
733 
734         mProvider.openDevice(0);
735         mResolver.waitForNotification(ROOTS_URI, 3);
736 
737         for (int i = 0; i < names.length; i++) {
738             mResolver.waitForNotification(DocumentsContract.buildChildDocumentsUri(
739                     MtpDocumentsProvider.AUTHORITY,
740                     String.valueOf(documentIdOffset + i)), 1);
741             try (final Cursor cursor = mProvider.queryChildDocuments(
742                     String.valueOf(documentIdOffset + i),
743                     strings(Document.COLUMN_DOCUMENT_ID),
744                     (String) null)) {
745                 assertEquals(1, cursor.getCount());
746                 cursor.moveToNext();
747                 assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0));
748             }
749         }
750     }
751 
testCreateDocument()752     public void testCreateDocument() throws Exception {
753         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
754         setupRoots(0, new MtpRoot[] {
755                 new MtpRoot(0, 100, "Storage A", 100, 100, null)
756         });
757         final String documentId = mProvider.createDocument("1", "text/plain", "note.txt");
758         final Uri deviceUri = DocumentsContract.buildChildDocumentsUri(
759                 MtpDocumentsProvider.AUTHORITY, "1");
760         final Uri storageUri = DocumentsContract.buildChildDocumentsUri(
761                 MtpDocumentsProvider.AUTHORITY, "2");
762         mResolver.waitForNotification(storageUri, 1);
763         mResolver.waitForNotification(deviceUri, 1);
764         try (final Cursor cursor = mProvider.queryDocument(documentId, null)) {
765             assertTrue(cursor.moveToNext());
766             assertEquals(
767                     "note.txt",
768                     cursor.getString(cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME)));
769             assertEquals(
770                     "text/plain",
771                     cursor.getString(cursor.getColumnIndex(Document.COLUMN_MIME_TYPE)));
772         }
773     }
774 
testCreateDocument_noWritingSupport()775     public void testCreateDocument_noWritingSupport() throws Exception {
776         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
777         mMtpManager.addValidDevice(new MtpDeviceRecord(
778                 0, "Device A", null /* deviceKey */, false /* unopened */,
779                 new MtpRoot[] {
780                         new MtpRoot(
781                                 0 /* deviceId */,
782                                 1 /* storageId */,
783                                 "Storage A" /* volume description */,
784                                 1024 /* free space */,
785                                 2048 /* total space */,
786                                 "" /* no volume identifier */)
787                 },
788                 new int[0] /* no operations supported */, null));
789         mProvider.resumeRootScanner();
790         mResolver.waitForNotification(ROOTS_URI, 1);
791         try {
792             mProvider.createDocument("1", "text/palin", "note.txt");
793             fail();
794         } catch (UnsupportedOperationException exception) {}
795     }
796 
testOpenDocument_noWritingSupport()797     public void testOpenDocument_noWritingSupport() throws Exception {
798         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
799         mMtpManager.addValidDevice(new MtpDeviceRecord(
800                 0, "Device A", null /* deviceKey */, false /* unopened */,
801                 new MtpRoot[] {
802                         new MtpRoot(
803                                 0 /* deviceId */,
804                                 1 /* storageId */,
805                                 "Storage A" /* volume description */,
806                                 1024 /* free space */,
807                                 2048 /* total space */,
808                                 "" /* no volume identifier */)
809                 },
810                 new int[0] /* no operations supported */, null));
811         mMtpManager.setObjectHandles(
812                 0, 1, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, new int[] { 100 });
813         mMtpManager.setObjectInfo(
814                 0, new MtpObjectInfo.Builder().setObjectHandle(100).setName("note.txt").build());
815         mProvider.resumeRootScanner();
816         mResolver.waitForNotification(ROOTS_URI, 1);
817         try (final Cursor cursor = mProvider.queryChildDocuments(
818                 "1", strings(Document.COLUMN_DOCUMENT_ID), (String) null)) {
819             assertEquals(1, cursor.getCount());
820             cursor.moveToNext();
821             assertEquals("3", cursor.getString(0));
822         }
823         try {
824             mProvider.openDocument("3", "w", null);
825             fail();
826         } catch (UnsupportedOperationException exception) {}
827     }
828 
testObjectSizeLong()829     public void testObjectSizeLong() throws Exception {
830         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
831         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
832         mMtpManager.setObjectSizeLong(0, 100, MtpConstants.FORMAT_EXIF_JPEG, 0x400000000L);
833         setupDocuments(
834                 0,
835                 0,
836                 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN,
837                 "1",
838                 new MtpObjectInfo[] {
839                         new MtpObjectInfo.Builder()
840                                 .setObjectHandle(100)
841                                 .setFormat(MtpConstants.FORMAT_EXIF_JPEG)
842                                 .setName("image.jpg")
843                                 .setCompressedSize(0xffffffffl)
844                                 .build()
845                 });
846 
847         final Cursor cursor = mProvider.queryDocument("3", new String[] {
848                 DocumentsContract.Document.COLUMN_SIZE
849         });
850         assertEquals(1, cursor.getCount());
851 
852         cursor.moveToNext();
853         assertEquals(0x400000000L, cursor.getLong(0));
854     }
855 
testFindDocumentPath_singleStorage_toRoot()856     public void testFindDocumentPath_singleStorage_toRoot() throws Exception {
857         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
858         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
859         setupHierarchyDocuments("1");
860 
861         final Path path = mProvider.findDocumentPath(null, "15");
862         assertEquals("1", path.getRootId());
863         assertEquals(4, path.getPath().size());
864         assertEquals("1", path.getPath().get(0));
865         assertEquals("3", path.getPath().get(1));
866         assertEquals("6", path.getPath().get(2));
867         assertEquals("15", path.getPath().get(3));
868     }
869 
testFindDocumentPath_singleStorage_toDoc()870     public void testFindDocumentPath_singleStorage_toDoc() throws Exception {
871         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
872         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
873         setupHierarchyDocuments("1");
874 
875         final Path path = mProvider.findDocumentPath("3", "18");
876         assertNull(path.getRootId());
877         assertEquals(3, path.getPath().size());
878         assertEquals("3", path.getPath().get(0));
879         assertEquals("7", path.getPath().get(1));
880         assertEquals("18", path.getPath().get(2));
881     }
882 
testFindDocumentPath_multiStorage_toRoot()883     public void testFindDocumentPath_multiStorage_toRoot() throws Exception {
884         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
885         setupRoots(0, new MtpRoot[] {
886                 new MtpRoot(0, 0, "Storage A", 1000, 1000, ""),
887                 new MtpRoot(0, 1, "Storage B", 1000, 1000, "") });
888         setupHierarchyDocuments("2");
889 
890         final Path path = mProvider.findDocumentPath(null, "16");
891         assertEquals("2", path.getRootId());
892         assertEquals(4, path.getPath().size());
893         assertEquals("2", path.getPath().get(0));
894         assertEquals("4", path.getPath().get(1));
895         assertEquals("7", path.getPath().get(2));
896         assertEquals("16", path.getPath().get(3));
897     }
898 
testFindDocumentPath_multiStorage_toDoc()899     public void testFindDocumentPath_multiStorage_toDoc() throws Exception {
900         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
901         setupRoots(0, new MtpRoot[] {
902                 new MtpRoot(0, 0, "Storage A", 1000, 1000, ""),
903                 new MtpRoot(0, 1, "Storage B", 1000, 1000, "") });
904         setupHierarchyDocuments("2");
905 
906         final Path path = mProvider.findDocumentPath("4", "19");
907         assertNull(path.getRootId());
908         assertEquals(3, path.getPath().size());
909         assertEquals("4", path.getPath().get(0));
910         assertEquals("8", path.getPath().get(1));
911         assertEquals("19", path.getPath().get(2));
912     }
913 
testIsChildDocument()914     public void testIsChildDocument() throws Exception {
915         setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
916         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") });
917         setupHierarchyDocuments("1");
918         assertTrue(mProvider.isChildDocument("1", "1"));
919         assertTrue(mProvider.isChildDocument("1", "14"));
920         assertTrue(mProvider.isChildDocument("2", "14"));
921         assertTrue(mProvider.isChildDocument("5", "14"));
922         assertFalse(mProvider.isChildDocument("3", "14"));
923         assertFalse(mProvider.isChildDocument("6", "14"));
924     }
925 
setupProvider(int flag)926     private void setupProvider(int flag) {
927         mDatabase = new MtpDatabase(getContext(), flag);
928         mProvider = new MtpDocumentsProvider();
929         final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
930         assertTrue(mProvider.onCreateForTesting(
931                 getContext(),
932                 mResources,
933                 mMtpManager,
934                 mResolver,
935                 mDatabase,
936                 storageManager,
937                 new TestServiceIntentSender()));
938     }
939 
getStrings(Cursor cursor)940     private String[] getStrings(Cursor cursor) {
941         try {
942             final String[] results = new String[cursor.getCount()];
943             for (int i = 0; cursor.moveToNext(); i++) {
944                 results[i] = cursor.getString(0);
945             }
946             return results;
947         } finally {
948             cursor.close();
949         }
950     }
951 
setupRoots(int deviceId, MtpRoot[] roots)952     private String[] setupRoots(int deviceId, MtpRoot[] roots)
953             throws InterruptedException, TimeoutException, IOException {
954         final int changeCount = mResolver.getChangeCount(ROOTS_URI);
955         mMtpManager.addValidDevice(
956                 new MtpDeviceRecord(deviceId, "Device", null /* deviceKey */, false /* unopened */,
957                 roots, OPERATIONS_SUPPORTED, null));
958         mProvider.openDevice(deviceId);
959         mResolver.waitForNotification(ROOTS_URI, changeCount + 1);
960         return getStrings(mProvider.queryRoots(strings(DocumentsContract.Root.COLUMN_ROOT_ID)));
961     }
962 
setupDocuments( int deviceId, int storageId, int parentHandle, String parentDocumentId, MtpObjectInfo[] objects)963     private String[] setupDocuments(
964             int deviceId,
965             int storageId,
966             int parentHandle,
967             String parentDocumentId,
968             MtpObjectInfo[] objects) throws FileNotFoundException {
969         final int[] handles = new int[objects.length];
970         int i = 0;
971         for (final MtpObjectInfo info : objects) {
972             handles[i++] = info.getObjectHandle();
973             mMtpManager.setObjectInfo(deviceId, info);
974         }
975         mMtpManager.setObjectHandles(deviceId, storageId, parentHandle, handles);
976         return getStrings(mProvider.queryChildDocuments(
977                 parentDocumentId,
978                 strings(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
979                 (String) null));
980     }
981 
982     static class HierarchyDocument {
983         int depth;
984         String documentId;
985         int objectHandle;
986         int parentHandle;
987 
createChildDocument(int newHandle)988         HierarchyDocument createChildDocument(int newHandle) {
989             final HierarchyDocument doc = new HierarchyDocument();
990             doc.depth = depth - 1;
991             doc.objectHandle = newHandle;
992             doc.parentHandle = objectHandle;
993             return doc;
994         }
995 
toObjectInfo()996         MtpObjectInfo toObjectInfo() {
997             return new MtpObjectInfo.Builder()
998                     .setName("doc_" + documentId)
999                     .setFormat(depth > 0 ?
1000                             MtpConstants.FORMAT_ASSOCIATION : MtpConstants.FORMAT_TEXT)
1001                     .setObjectHandle(objectHandle)
1002                     .setParent(parentHandle)
1003                     .build();
1004         }
1005     }
1006 
setupHierarchyDocuments(String documentId)1007     private void setupHierarchyDocuments(String documentId) throws Exception {
1008         final Queue<HierarchyDocument> ids = new LinkedList<>();
1009         final HierarchyDocument firstDocument = new HierarchyDocument();
1010         firstDocument.depth = 3;
1011         firstDocument.documentId = documentId;
1012         firstDocument.objectHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
1013         ids.add(firstDocument);
1014 
1015         int objectHandle = 100;
1016         while (!ids.isEmpty()) {
1017             final HierarchyDocument document = ids.remove();
1018             final HierarchyDocument[] children = new HierarchyDocument[] {
1019                     document.createChildDocument(objectHandle++),
1020                     document.createChildDocument(objectHandle++),
1021                     document.createChildDocument(objectHandle++),
1022             };
1023             final String[] childDocIds = setupDocuments(
1024                     0, 0, document.objectHandle, document.documentId, new MtpObjectInfo[] {
1025                             children[0].toObjectInfo(),
1026                             children[1].toObjectInfo(),
1027                             children[2].toObjectInfo(),
1028                     });
1029             children[0].documentId = childDocIds[0];
1030             children[1].documentId = childDocIds[1];
1031             children[2].documentId = childDocIds[2];
1032 
1033             if (children[0].depth > 0) {
1034                 ids.add(children[0]);
1035                 ids.add(children[1]);
1036                 ids.add(children[2]);
1037             }
1038         }
1039     }
1040 }
1041