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