Home | History | Annotate | Download | only in mtp
      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
     55     public void setUp() throws IOException {
     56         mResolver = new TestContentResolver();
     57         mMtpManager = new TestMtpManager(getContext());
     58     }
     59 
     60     @Override
     61     public void tearDown() {
     62         mProvider.shutdown();
     63         MtpDatabase.deleteDatabase(getContext());
     64     }
     65 
     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 
     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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
   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