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 cursor.getInt(4)); 334 assertEquals(1024 * 1024 * 5, cursor.getInt(5)); 335 } 336 337 public void testQueryDocument_directory() 338 throws IOException, InterruptedException, TimeoutException { 339 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 340 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 341 setupDocuments( 342 0, 343 0, 344 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, 345 "1", 346 new MtpObjectInfo[] { 347 new MtpObjectInfo.Builder() 348 .setObjectHandle(2) 349 .setStorageId(1) 350 .setFormat(MtpConstants.FORMAT_ASSOCIATION) 351 .setName("directory") 352 .setDateModified(1422716400000L) 353 .build() 354 }); 355 356 final Cursor cursor = mProvider.queryDocument("3", null); 357 assertEquals(1, cursor.getCount()); 358 359 cursor.moveToNext(); 360 assertEquals("3", cursor.getString(0)); 361 assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1)); 362 assertEquals("directory", cursor.getString(2)); 363 assertEquals(1422716400000L, cursor.getLong(3)); 364 assertEquals( 365 DocumentsContract.Document.FLAG_SUPPORTS_DELETE | 366 DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE, 367 cursor.getInt(4)); 368 assertEquals(0, cursor.getInt(5)); 369 } 370 371 public void testQueryDocument_forStorage() 372 throws IOException, InterruptedException, TimeoutException { 373 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 374 setupRoots(0, new MtpRoot[] { 375 new MtpRoot( 376 0 /* deviceId */, 377 1 /* storageId */, 378 "Storage A" /* volume description */, 379 1024 /* free space */, 380 4096 /* total space */, 381 "" /* no volume identifier */) 382 }); 383 final Cursor cursor = mProvider.queryDocument("2", null); 384 assertEquals(1, cursor.getCount()); 385 386 cursor.moveToNext(); 387 assertEquals("2", cursor.getString(0)); 388 assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1)); 389 assertEquals("Storage A", cursor.getString(2)); 390 assertTrue(cursor.isNull(3)); 391 assertEquals(DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE, cursor.getInt(4)); 392 assertEquals(3072, cursor.getInt(5)); 393 } 394 395 public void testQueryDocument_forDeviceWithSingleStorage() 396 throws IOException, InterruptedException, TimeoutException { 397 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 398 setupRoots(0, new MtpRoot[] { 399 new MtpRoot( 400 0 /* deviceId */, 401 1 /* storageId */, 402 "Storage A" /* volume description */, 403 1024 /* free space */, 404 4096 /* total space */, 405 "" /* no volume identifier */) 406 }); 407 final Cursor cursor = mProvider.queryDocument("1", null); 408 assertEquals(1, cursor.getCount()); 409 410 cursor.moveToNext(); 411 assertEquals("1", cursor.getString(0)); 412 assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1)); 413 assertEquals("Device Storage A", cursor.getString(2)); 414 assertTrue(cursor.isNull(3)); 415 assertEquals(DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE, cursor.getInt(4)); 416 assertTrue(cursor.isNull(5)); 417 } 418 419 public void testQueryDocument_forDeviceWithTwoStorages() 420 throws IOException, InterruptedException, TimeoutException { 421 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 422 setupRoots(0, new MtpRoot[] { 423 new MtpRoot( 424 0 /* deviceId */, 425 1 /* storageId */, 426 "Storage A" /* volume description */, 427 1024 /* free space */, 428 4096 /* total space */, 429 "" /* no volume identifier */), 430 new MtpRoot( 431 0 /* deviceId */, 432 2 /* storageId */, 433 "Storage B" /* volume description */, 434 1024 /* free space */, 435 4096 /* total space */, 436 "" /* no volume identifier */) 437 }); 438 final Cursor cursor = mProvider.queryDocument("1", null); 439 assertEquals(1, cursor.getCount()); 440 441 cursor.moveToNext(); 442 assertEquals("1", cursor.getString(0)); 443 assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1)); 444 assertEquals("Device", cursor.getString(2)); 445 assertTrue(cursor.isNull(3)); 446 assertEquals(0, cursor.getInt(4)); 447 assertTrue(cursor.isNull(5)); 448 } 449 450 public void testQueryChildDocuments() throws Exception { 451 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 452 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 453 setupDocuments( 454 0, 455 0, 456 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, 457 "1", 458 new MtpObjectInfo[] { 459 new MtpObjectInfo.Builder() 460 .setObjectHandle(100) 461 .setFormat(MtpConstants.FORMAT_EXIF_JPEG) 462 .setName("image.jpg") 463 .setCompressedSize(1024 * 1024 * 5) 464 .setThumbCompressedSize(5 * 1024) 465 .setProtectionStatus(MtpConstants.PROTECTION_STATUS_READ_ONLY) 466 .build() 467 }); 468 469 final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null); 470 assertEquals(1, cursor.getCount()); 471 472 assertTrue(cursor.moveToNext()); 473 assertEquals("3", cursor.getString(0)); 474 assertEquals("image/jpeg", cursor.getString(1)); 475 assertEquals("image.jpg", cursor.getString(2)); 476 assertEquals(0, cursor.getLong(3)); 477 assertEquals(Document.FLAG_SUPPORTS_THUMBNAIL, cursor.getInt(4)); 478 assertEquals(1024 * 1024 * 5, cursor.getInt(5)); 479 480 cursor.close(); 481 } 482 483 public void testQueryChildDocuments_cursorError() throws Exception { 484 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 485 try { 486 mProvider.queryChildDocuments("1", null, (String) null); 487 fail(); 488 } catch (FileNotFoundException error) {} 489 } 490 491 public void testQueryChildDocuments_documentError() throws Exception { 492 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 493 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 494 mMtpManager.setObjectHandles(0, 0, -1, new int[] { 1 }); 495 try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { 496 assertEquals(0, cursor.getCount()); 497 assertFalse(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING)); 498 } 499 } 500 501 public void testDeleteDocument() throws IOException, InterruptedException, TimeoutException { 502 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 503 setupRoots(0, new MtpRoot[] { 504 new MtpRoot(0, 0, "Storage", 0, 0, "") 505 }); 506 setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] { 507 new MtpObjectInfo.Builder() 508 .setName("test.txt") 509 .setObjectHandle(1) 510 .setParent(-1) 511 .build() 512 }); 513 514 mProvider.deleteDocument("3"); 515 assertEquals(1, mResolver.getChangeCount( 516 DocumentsContract.buildChildDocumentsUri( 517 MtpDocumentsProvider.AUTHORITY, "1"))); 518 } 519 520 public void testDeleteDocument_error() 521 throws IOException, InterruptedException, TimeoutException { 522 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 523 setupRoots(0, new MtpRoot[] { 524 new MtpRoot(0, 0, "Storage", 0, 0, "") 525 }); 526 setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] { 527 new MtpObjectInfo.Builder() 528 .setName("test.txt") 529 .setObjectHandle(1) 530 .setParent(-1) 531 .build() 532 }); 533 try { 534 mProvider.deleteDocument("4"); 535 fail(); 536 } catch (Throwable e) { 537 assertTrue(e instanceof IOException); 538 } 539 assertEquals(0, mResolver.getChangeCount( 540 DocumentsContract.buildChildDocumentsUri( 541 MtpDocumentsProvider.AUTHORITY, "1"))); 542 } 543 544 public void testOpenDocument() throws Exception { 545 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 546 setupRoots(0, new MtpRoot[] { 547 new MtpRoot(0, 0, "Storage", 0, 0, "") 548 }); 549 final byte[] bytes = "Hello world".getBytes(); 550 setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] { 551 new MtpObjectInfo.Builder() 552 .setName("test.txt") 553 .setObjectHandle(1) 554 .setCompressedSize(bytes.length) 555 .setParent(-1) 556 .build() 557 }); 558 mMtpManager.setImportFileBytes(0, 1, bytes); 559 try (final ParcelFileDescriptor fd = mProvider.openDocument("3", "r", null)) { 560 final byte[] readBytes = new byte[5]; 561 assertEquals(6, Os.lseek(fd.getFileDescriptor(), 6, OsConstants.SEEK_SET)); 562 assertEquals(5, Os.read(fd.getFileDescriptor(), readBytes, 0, 5)); 563 assertTrue(Arrays.equals("world".getBytes(), readBytes)); 564 565 assertEquals(0, Os.lseek(fd.getFileDescriptor(), 0, OsConstants.SEEK_SET)); 566 assertEquals(5, Os.read(fd.getFileDescriptor(), readBytes, 0, 5)); 567 assertTrue(Arrays.equals("Hello".getBytes(), readBytes)); 568 } 569 } 570 571 public void testOpenDocument_shortBytes() throws Exception { 572 mMtpManager = new TestMtpManager(getContext()) { 573 @Override 574 MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException { 575 if (objectHandle == 1) { 576 return new MtpObjectInfo.Builder(super.getObjectInfo(deviceId, objectHandle)) 577 .setObjectHandle(1).setCompressedSize(1024 * 1024).build(); 578 } 579 580 return super.getObjectInfo(deviceId, objectHandle); 581 } 582 }; 583 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 584 setupRoots(0, new MtpRoot[] { 585 new MtpRoot(0, 0, "Storage", 0, 0, "") 586 }); 587 final byte[] bytes = "Hello world".getBytes(); 588 setupDocuments(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, "1", new MtpObjectInfo[] { 589 new MtpObjectInfo.Builder() 590 .setName("test.txt") 591 .setObjectHandle(1) 592 .setCompressedSize(bytes.length) 593 .setParent(-1) 594 .build() 595 }); 596 mMtpManager.setImportFileBytes(0, 1, bytes); 597 try (final ParcelFileDescriptor fd = mProvider.openDocument("3", "r", null)) { 598 final byte[] readBytes = new byte[1024 * 1024]; 599 assertEquals(11, Os.read(fd.getFileDescriptor(), readBytes, 0, readBytes.length)); 600 } 601 } 602 603 public void testOpenDocument_writing() throws Exception { 604 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 605 setupRoots(0, new MtpRoot[] { 606 new MtpRoot(0, 100, "Storage", 0, 0, "") 607 }); 608 final String documentId = mProvider.createDocument("2", "text/plain", "test.txt"); 609 { 610 final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "w", null); 611 try (ParcelFileDescriptor.AutoCloseOutputStream stream = 612 new ParcelFileDescriptor.AutoCloseOutputStream(fd)) { 613 stream.write("Hello".getBytes()); 614 fd.getFileDescriptor().sync(); 615 } 616 } 617 { 618 final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "r", null); 619 try (ParcelFileDescriptor.AutoCloseInputStream stream = 620 new ParcelFileDescriptor.AutoCloseInputStream(fd)) { 621 final byte[] bytes = new byte[5]; 622 stream.read(bytes); 623 assertTrue(Arrays.equals("Hello".getBytes(), bytes)); 624 } 625 } 626 } 627 628 public void testBusyDevice() throws Exception { 629 mMtpManager = new TestMtpManager(getContext()) { 630 @Override 631 synchronized MtpDeviceRecord openDevice(int deviceId) 632 throws IOException { 633 throw new BusyDeviceException(); 634 } 635 }; 636 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 637 mMtpManager.addValidDevice(new MtpDeviceRecord( 638 0, "Device A", null /* deviceKey */, false /* unopened */, new MtpRoot[0], 639 OPERATIONS_SUPPORTED, null)); 640 641 mProvider.resumeRootScanner(); 642 mResolver.waitForNotification(ROOTS_URI, 1); 643 644 try (final Cursor cursor = mProvider.queryRoots(null)) { 645 assertEquals(1, cursor.getCount()); 646 } 647 648 try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { 649 assertEquals(0, cursor.getCount()); 650 assertEquals( 651 "error_busy_device", 652 cursor.getExtras().getString(DocumentsContract.EXTRA_ERROR)); 653 } 654 } 655 656 public void testLockedDevice() throws Exception { 657 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 658 mMtpManager.addValidDevice(new MtpDeviceRecord( 659 0, "Device A", null, false /* unopened */, new MtpRoot[0], OPERATIONS_SUPPORTED, 660 null)); 661 662 mProvider.resumeRootScanner(); 663 mResolver.waitForNotification(ROOTS_URI, 1); 664 665 try (final Cursor cursor = mProvider.queryRoots(null)) { 666 assertEquals(1, cursor.getCount()); 667 } 668 669 try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { 670 assertEquals(0, cursor.getCount()); 671 assertEquals( 672 "error_locked_device", 673 cursor.getExtras().getString(DocumentsContract.EXTRA_ERROR)); 674 } 675 } 676 677 public void testMappingDisconnectedDocuments() throws Exception { 678 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 679 mMtpManager.addValidDevice(new MtpDeviceRecord( 680 0, 681 "Device A", 682 "device key", 683 true /* opened */, 684 new MtpRoot[] { 685 new MtpRoot( 686 0 /* deviceId */, 687 1 /* storageId */, 688 "Storage A" /* volume description */, 689 1024 /* free space */, 690 2048 /* total space */, 691 "" /* no volume identifier */) 692 }, 693 OPERATIONS_SUPPORTED, 694 null)); 695 696 final String[] names = strings("Directory A", "Directory B", "Directory C"); 697 final int objectHandleOffset = 100; 698 for (int i = 0; i < names.length; i++) { 699 final int parentHandle = i == 0 ? 700 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN : objectHandleOffset + i - 1; 701 final int objectHandle = i + objectHandleOffset; 702 mMtpManager.setObjectHandles(0, 1, parentHandle, new int[] { objectHandle }); 703 mMtpManager.setObjectInfo( 704 0, 705 new MtpObjectInfo.Builder() 706 .setName(names[i]) 707 .setObjectHandle(objectHandle) 708 .setFormat(MtpConstants.FORMAT_ASSOCIATION) 709 .setStorageId(1) 710 .build()); 711 } 712 713 mProvider.resumeRootScanner(); 714 mResolver.waitForNotification(ROOTS_URI, 1); 715 716 final int documentIdOffset = 2; 717 for (int i = 0; i < names.length; i++) { 718 try (final Cursor cursor = mProvider.queryChildDocuments( 719 String.valueOf(documentIdOffset + i), 720 strings(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME), 721 (String) null)) { 722 assertEquals(1, cursor.getCount()); 723 cursor.moveToNext(); 724 assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0)); 725 assertEquals(names[i], cursor.getString(1)); 726 } 727 } 728 729 mProvider.closeDevice(0); 730 mResolver.waitForNotification(ROOTS_URI, 2); 731 732 mProvider.openDevice(0); 733 mResolver.waitForNotification(ROOTS_URI, 3); 734 735 for (int i = 0; i < names.length; i++) { 736 mResolver.waitForNotification(DocumentsContract.buildChildDocumentsUri( 737 MtpDocumentsProvider.AUTHORITY, 738 String.valueOf(documentIdOffset + i)), 1); 739 try (final Cursor cursor = mProvider.queryChildDocuments( 740 String.valueOf(documentIdOffset + i), 741 strings(Document.COLUMN_DOCUMENT_ID), 742 (String) null)) { 743 assertEquals(1, cursor.getCount()); 744 cursor.moveToNext(); 745 assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0)); 746 } 747 } 748 } 749 750 public void testCreateDocument() throws Exception { 751 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 752 setupRoots(0, new MtpRoot[] { 753 new MtpRoot(0, 100, "Storage A", 100, 100, null) 754 }); 755 final String documentId = mProvider.createDocument("1", "text/plain", "note.txt"); 756 final Uri deviceUri = DocumentsContract.buildChildDocumentsUri( 757 MtpDocumentsProvider.AUTHORITY, "1"); 758 final Uri storageUri = DocumentsContract.buildChildDocumentsUri( 759 MtpDocumentsProvider.AUTHORITY, "2"); 760 mResolver.waitForNotification(storageUri, 1); 761 mResolver.waitForNotification(deviceUri, 1); 762 try (final Cursor cursor = mProvider.queryDocument(documentId, null)) { 763 assertTrue(cursor.moveToNext()); 764 assertEquals( 765 "note.txt", 766 cursor.getString(cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME))); 767 assertEquals( 768 "text/plain", 769 cursor.getString(cursor.getColumnIndex(Document.COLUMN_MIME_TYPE))); 770 } 771 } 772 773 public void testCreateDocument_noWritingSupport() throws Exception { 774 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 775 mMtpManager.addValidDevice(new MtpDeviceRecord( 776 0, "Device A", null /* deviceKey */, false /* unopened */, 777 new MtpRoot[] { 778 new MtpRoot( 779 0 /* deviceId */, 780 1 /* storageId */, 781 "Storage A" /* volume description */, 782 1024 /* free space */, 783 2048 /* total space */, 784 "" /* no volume identifier */) 785 }, 786 new int[0] /* no operations supported */, null)); 787 mProvider.resumeRootScanner(); 788 mResolver.waitForNotification(ROOTS_URI, 1); 789 try { 790 mProvider.createDocument("1", "text/palin", "note.txt"); 791 fail(); 792 } catch (UnsupportedOperationException exception) {} 793 } 794 795 public void testOpenDocument_noWritingSupport() throws Exception { 796 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 797 mMtpManager.addValidDevice(new MtpDeviceRecord( 798 0, "Device A", null /* deviceKey */, false /* unopened */, 799 new MtpRoot[] { 800 new MtpRoot( 801 0 /* deviceId */, 802 1 /* storageId */, 803 "Storage A" /* volume description */, 804 1024 /* free space */, 805 2048 /* total space */, 806 "" /* no volume identifier */) 807 }, 808 new int[0] /* no operations supported */, null)); 809 mMtpManager.setObjectHandles( 810 0, 1, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, new int[] { 100 }); 811 mMtpManager.setObjectInfo( 812 0, new MtpObjectInfo.Builder().setObjectHandle(100).setName("note.txt").build()); 813 mProvider.resumeRootScanner(); 814 mResolver.waitForNotification(ROOTS_URI, 1); 815 try (final Cursor cursor = mProvider.queryChildDocuments( 816 "1", strings(Document.COLUMN_DOCUMENT_ID), (String) null)) { 817 assertEquals(1, cursor.getCount()); 818 cursor.moveToNext(); 819 assertEquals("3", cursor.getString(0)); 820 } 821 try { 822 mProvider.openDocument("3", "w", null); 823 fail(); 824 } catch (UnsupportedOperationException exception) {} 825 } 826 827 public void testObjectSizeLong() throws Exception { 828 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 829 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 830 mMtpManager.setObjectSizeLong(0, 100, MtpConstants.FORMAT_EXIF_JPEG, 0x400000000L); 831 setupDocuments( 832 0, 833 0, 834 MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, 835 "1", 836 new MtpObjectInfo[] { 837 new MtpObjectInfo.Builder() 838 .setObjectHandle(100) 839 .setFormat(MtpConstants.FORMAT_EXIF_JPEG) 840 .setName("image.jpg") 841 .setCompressedSize(0xffffffffl) 842 .build() 843 }); 844 845 final Cursor cursor = mProvider.queryDocument("3", new String[] { 846 DocumentsContract.Document.COLUMN_SIZE 847 }); 848 assertEquals(1, cursor.getCount()); 849 850 cursor.moveToNext(); 851 assertEquals(0x400000000L, cursor.getLong(0)); 852 } 853 854 public void testFindDocumentPath_singleStorage_toRoot() throws Exception { 855 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 856 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 857 setupHierarchyDocuments("1"); 858 859 final Path path = mProvider.findDocumentPath(null, "15"); 860 assertEquals("1", path.getRootId()); 861 assertEquals(4, path.getPath().size()); 862 assertEquals("1", path.getPath().get(0)); 863 assertEquals("3", path.getPath().get(1)); 864 assertEquals("6", path.getPath().get(2)); 865 assertEquals("15", path.getPath().get(3)); 866 } 867 868 public void testFindDocumentPath_singleStorage_toDoc() throws Exception { 869 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 870 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 871 setupHierarchyDocuments("1"); 872 873 final Path path = mProvider.findDocumentPath("3", "18"); 874 assertNull(path.getRootId()); 875 assertEquals(3, path.getPath().size()); 876 assertEquals("3", path.getPath().get(0)); 877 assertEquals("7", path.getPath().get(1)); 878 assertEquals("18", path.getPath().get(2)); 879 } 880 881 public void testFindDocumentPath_multiStorage_toRoot() throws Exception { 882 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 883 setupRoots(0, new MtpRoot[] { 884 new MtpRoot(0, 0, "Storage A", 1000, 1000, ""), 885 new MtpRoot(0, 1, "Storage B", 1000, 1000, "") }); 886 setupHierarchyDocuments("2"); 887 888 final Path path = mProvider.findDocumentPath(null, "16"); 889 assertEquals("2", path.getRootId()); 890 assertEquals(4, path.getPath().size()); 891 assertEquals("2", path.getPath().get(0)); 892 assertEquals("4", path.getPath().get(1)); 893 assertEquals("7", path.getPath().get(2)); 894 assertEquals("16", path.getPath().get(3)); 895 } 896 897 public void testFindDocumentPath_multiStorage_toDoc() throws Exception { 898 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 899 setupRoots(0, new MtpRoot[] { 900 new MtpRoot(0, 0, "Storage A", 1000, 1000, ""), 901 new MtpRoot(0, 1, "Storage B", 1000, 1000, "") }); 902 setupHierarchyDocuments("2"); 903 904 final Path path = mProvider.findDocumentPath("4", "19"); 905 assertNull(path.getRootId()); 906 assertEquals(3, path.getPath().size()); 907 assertEquals("4", path.getPath().get(0)); 908 assertEquals("8", path.getPath().get(1)); 909 assertEquals("19", path.getPath().get(2)); 910 } 911 912 public void testIsChildDocument() throws Exception { 913 setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); 914 setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); 915 setupHierarchyDocuments("1"); 916 assertTrue(mProvider.isChildDocument("1", "1")); 917 assertTrue(mProvider.isChildDocument("1", "14")); 918 assertTrue(mProvider.isChildDocument("2", "14")); 919 assertTrue(mProvider.isChildDocument("5", "14")); 920 assertFalse(mProvider.isChildDocument("3", "14")); 921 assertFalse(mProvider.isChildDocument("6", "14")); 922 } 923 924 private void setupProvider(int flag) { 925 mDatabase = new MtpDatabase(getContext(), flag); 926 mProvider = new MtpDocumentsProvider(); 927 final StorageManager storageManager = getContext().getSystemService(StorageManager.class); 928 assertTrue(mProvider.onCreateForTesting( 929 getContext(), 930 mResources, 931 mMtpManager, 932 mResolver, 933 mDatabase, 934 storageManager, 935 new TestServiceIntentSender())); 936 } 937 938 private String[] getStrings(Cursor cursor) { 939 try { 940 final String[] results = new String[cursor.getCount()]; 941 for (int i = 0; cursor.moveToNext(); i++) { 942 results[i] = cursor.getString(0); 943 } 944 return results; 945 } finally { 946 cursor.close(); 947 } 948 } 949 950 private String[] setupRoots(int deviceId, MtpRoot[] roots) 951 throws InterruptedException, TimeoutException, IOException { 952 final int changeCount = mResolver.getChangeCount(ROOTS_URI); 953 mMtpManager.addValidDevice( 954 new MtpDeviceRecord(deviceId, "Device", null /* deviceKey */, false /* unopened */, 955 roots, OPERATIONS_SUPPORTED, null)); 956 mProvider.openDevice(deviceId); 957 mResolver.waitForNotification(ROOTS_URI, changeCount + 1); 958 return getStrings(mProvider.queryRoots(strings(DocumentsContract.Root.COLUMN_ROOT_ID))); 959 } 960 961 private String[] setupDocuments( 962 int deviceId, 963 int storageId, 964 int parentHandle, 965 String parentDocumentId, 966 MtpObjectInfo[] objects) throws FileNotFoundException { 967 final int[] handles = new int[objects.length]; 968 int i = 0; 969 for (final MtpObjectInfo info : objects) { 970 handles[i++] = info.getObjectHandle(); 971 mMtpManager.setObjectInfo(deviceId, info); 972 } 973 mMtpManager.setObjectHandles(deviceId, storageId, parentHandle, handles); 974 return getStrings(mProvider.queryChildDocuments( 975 parentDocumentId, 976 strings(DocumentsContract.Document.COLUMN_DOCUMENT_ID), 977 (String) null)); 978 } 979 980 static class HierarchyDocument { 981 int depth; 982 String documentId; 983 int objectHandle; 984 int parentHandle; 985 986 HierarchyDocument createChildDocument(int newHandle) { 987 final HierarchyDocument doc = new HierarchyDocument(); 988 doc.depth = depth - 1; 989 doc.objectHandle = newHandle; 990 doc.parentHandle = objectHandle; 991 return doc; 992 } 993 994 MtpObjectInfo toObjectInfo() { 995 return new MtpObjectInfo.Builder() 996 .setName("doc_" + documentId) 997 .setFormat(depth > 0 ? 998 MtpConstants.FORMAT_ASSOCIATION : MtpConstants.FORMAT_TEXT) 999 .setObjectHandle(objectHandle) 1000 .setParent(parentHandle) 1001 .build(); 1002 } 1003 } 1004 1005 private void setupHierarchyDocuments(String documentId) throws Exception { 1006 final Queue<HierarchyDocument> ids = new LinkedList<>(); 1007 final HierarchyDocument firstDocument = new HierarchyDocument(); 1008 firstDocument.depth = 3; 1009 firstDocument.documentId = documentId; 1010 firstDocument.objectHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN; 1011 ids.add(firstDocument); 1012 1013 int objectHandle = 100; 1014 while (!ids.isEmpty()) { 1015 final HierarchyDocument document = ids.remove(); 1016 final HierarchyDocument[] children = new HierarchyDocument[] { 1017 document.createChildDocument(objectHandle++), 1018 document.createChildDocument(objectHandle++), 1019 document.createChildDocument(objectHandle++), 1020 }; 1021 final String[] childDocIds = setupDocuments( 1022 0, 0, document.objectHandle, document.documentId, new MtpObjectInfo[] { 1023 children[0].toObjectInfo(), 1024 children[1].toObjectInfo(), 1025 children[2].toObjectInfo(), 1026 }); 1027 children[0].documentId = childDocIds[0]; 1028 children[1].documentId = childDocIds[1]; 1029 children[2].documentId = childDocIds[2]; 1030 1031 if (children[0].depth > 0) { 1032 ids.add(children[0]); 1033 ids.add(children[1]); 1034 ids.add(children[2]); 1035 } 1036 } 1037 } 1038 } 1039