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 static com.android.mtp.MtpDatabaseConstants.*; 20 21 import android.annotation.Nullable; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.database.DatabaseUtils; 27 import android.database.MatrixCursor; 28 import android.database.MatrixCursor.RowBuilder; 29 import android.database.sqlite.SQLiteDatabase; 30 import android.database.sqlite.SQLiteOpenHelper; 31 import android.database.sqlite.SQLiteQueryBuilder; 32 import android.media.MediaFile; 33 import android.mtp.MtpConstants; 34 import android.mtp.MtpObjectInfo; 35 import android.net.Uri; 36 import android.provider.DocumentsContract; 37 import android.provider.DocumentsContract.Document; 38 import android.provider.DocumentsContract.Root; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.util.Preconditions; 42 43 import java.io.FileNotFoundException; 44 import java.util.HashSet; 45 import java.util.Objects; 46 import java.util.Set; 47 48 /** 49 * Database for MTP objects. 50 * The object handle which is identifier for object in MTP protocol is not stable over sessions. 51 * When we resume the process, we need to remap our document ID with MTP's object handle. 52 * 53 * If the remote MTP device is backed by typical file system, the file name 54 * is unique among files in a directory. However, MTP protocol itself does 55 * not guarantee the uniqueness of name so we cannot use fullpath as ID. 56 * 57 * Instead of fullpath, we use artificial ID generated by MtpDatabase itself. The database object 58 * remembers the map of document ID and object handle, and remaps new object handle with document ID 59 * by comparing the directory structure and object name. 60 * 61 * To start putting documents into the database, the client needs to call 62 * {@link Mapper#startAddingDocuments(String)} with the parent document ID. Also it 63 * needs to call {@link Mapper#stopAddingDocuments(String)} after putting all child 64 * documents to the database. (All explanations are same for root documents) 65 * 66 * database.getMapper().startAddingDocuments(); 67 * database.getMapper().putChildDocuments(); 68 * database.getMapper().stopAddingDocuments(); 69 * 70 * To update the existing documents, the client code can repeat to call the three methods again. 71 * The newly added rows update corresponding existing rows that have same MTP identifier like 72 * objectHandle. 73 * 74 * The client can call putChildDocuments multiple times to add documents by chunk, but it needs to 75 * put all documents under the parent before calling stopAddingChildDocuments. Otherwise missing 76 * documents are regarded as deleted, and will be removed from the database. 77 * 78 * If the client calls clearMtpIdentifier(), it clears MTP identifier in the database. In this case, 79 * the database tries to find corresponding rows by using document's name instead of MTP identifier 80 * at the next update cycle. 81 * 82 * TODO: Improve performance by SQL optimization. 83 */ 84 class MtpDatabase { 85 private final SQLiteDatabase mDatabase; 86 private final Mapper mMapper; 87 getSQLiteDatabase()88 SQLiteDatabase getSQLiteDatabase() { 89 return mDatabase; 90 } 91 MtpDatabase(Context context, int flags)92 MtpDatabase(Context context, int flags) { 93 final OpenHelper helper = new OpenHelper(context, flags); 94 mDatabase = helper.getWritableDatabase(); 95 mMapper = new Mapper(this); 96 } 97 close()98 void close() { 99 mDatabase.close(); 100 } 101 102 /** 103 * Returns operations for mapping. 104 * @return Mapping operations. 105 */ getMapper()106 Mapper getMapper() { 107 return mMapper; 108 } 109 110 /** 111 * Queries roots information. 112 * @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}. 113 * @return Database cursor. 114 */ queryRoots(Resources resources, String[] columnNames)115 Cursor queryRoots(Resources resources, String[] columnNames) { 116 final String selection = 117 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?"; 118 final Cursor deviceCursor = mDatabase.query( 119 TABLE_DOCUMENTS, 120 strings(COLUMN_DEVICE_ID), 121 selection, 122 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE), 123 COLUMN_DEVICE_ID, 124 null, 125 null, 126 null); 127 128 try { 129 final SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); 130 builder.setTables(JOIN_ROOTS); 131 builder.setProjectionMap(COLUMN_MAP_ROOTS); 132 final MatrixCursor result = new MatrixCursor(columnNames); 133 final ContentValues values = new ContentValues(); 134 135 while (deviceCursor.moveToNext()) { 136 final int deviceId = deviceCursor.getInt(0); 137 final Cursor storageCursor = builder.query( 138 mDatabase, 139 columnNames, 140 selection + " AND " + COLUMN_DEVICE_ID + " = ?", 141 strings(ROW_STATE_VALID, 142 ROW_STATE_INVALIDATED, 143 DOCUMENT_TYPE_STORAGE, 144 deviceId), 145 null, 146 null, 147 null); 148 try { 149 values.clear(); 150 try (final Cursor deviceRoot = builder.query( 151 mDatabase, 152 columnNames, 153 selection + " AND " + COLUMN_DEVICE_ID + " = ?", 154 strings(ROW_STATE_VALID, 155 ROW_STATE_INVALIDATED, 156 DOCUMENT_TYPE_DEVICE, 157 deviceId), 158 null, 159 null, 160 null)) { 161 deviceRoot.moveToNext(); 162 DatabaseUtils.cursorRowToContentValues(deviceRoot, values); 163 } 164 165 if (storageCursor.getCount() != 0) { 166 long capacityBytes = 0; 167 long availableBytes = 0; 168 final int capacityIndex = 169 storageCursor.getColumnIndex(Root.COLUMN_CAPACITY_BYTES); 170 final int availableIndex = 171 storageCursor.getColumnIndex(Root.COLUMN_AVAILABLE_BYTES); 172 while (storageCursor.moveToNext()) { 173 // If requested columnNames does not include COLUMN_XXX_BYTES, we 174 // don't calculate corresponding values. 175 if (capacityIndex != -1) { 176 capacityBytes += storageCursor.getLong(capacityIndex); 177 } 178 if (availableIndex != -1) { 179 availableBytes += storageCursor.getLong(availableIndex); 180 } 181 } 182 values.put(Root.COLUMN_CAPACITY_BYTES, capacityBytes); 183 values.put(Root.COLUMN_AVAILABLE_BYTES, availableBytes); 184 } else { 185 values.putNull(Root.COLUMN_CAPACITY_BYTES); 186 values.putNull(Root.COLUMN_AVAILABLE_BYTES); 187 } 188 if (storageCursor.getCount() == 1 && values.containsKey(Root.COLUMN_TITLE)) { 189 storageCursor.moveToFirst(); 190 // Add storage name to device name if we have only 1 storage. 191 values.put( 192 Root.COLUMN_TITLE, 193 resources.getString( 194 R.string.root_name, 195 values.getAsString(Root.COLUMN_TITLE), 196 storageCursor.getString( 197 storageCursor.getColumnIndex(Root.COLUMN_TITLE)))); 198 } 199 } finally { 200 storageCursor.close(); 201 } 202 203 putValuesToCursor(values, result); 204 } 205 206 return result; 207 } finally { 208 deviceCursor.close(); 209 } 210 } 211 212 /** 213 * Queries root documents information. 214 * @param columnNames Column names defined in 215 * {@link android.provider.DocumentsContract.Document}. 216 * @return Database cursor. 217 */ 218 @VisibleForTesting queryRootDocuments(String[] columnNames)219 Cursor queryRootDocuments(String[] columnNames) { 220 return mDatabase.query( 221 TABLE_DOCUMENTS, 222 columnNames, 223 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?", 224 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE), 225 null, 226 null, 227 null); 228 } 229 230 /** 231 * Queries documents information. 232 * @param columnNames Column names defined in 233 * {@link android.provider.DocumentsContract.Document}. 234 * @return Database cursor. 235 */ queryChildDocuments(String[] columnNames, String parentDocumentId)236 Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) { 237 return mDatabase.query( 238 TABLE_DOCUMENTS, 239 columnNames, 240 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?", 241 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, parentDocumentId), 242 null, 243 null, 244 null); 245 } 246 247 /** 248 * Returns document IDs of storages under the given device document. 249 * 250 * @param documentId Document ID that points a device. 251 * @return Storage document IDs. 252 * @throws FileNotFoundException The given document ID is not registered in database. 253 */ getStorageDocumentIds(String documentId)254 String[] getStorageDocumentIds(String documentId) 255 throws FileNotFoundException { 256 Preconditions.checkArgument(createIdentifier(documentId).mDocumentType == 257 DOCUMENT_TYPE_DEVICE); 258 // Check if the parent document is device that has single storage. 259 try (final Cursor cursor = mDatabase.query( 260 TABLE_DOCUMENTS, 261 strings(Document.COLUMN_DOCUMENT_ID), 262 COLUMN_ROW_STATE + " IN (?, ?) AND " + 263 COLUMN_PARENT_DOCUMENT_ID + " = ? AND " + 264 COLUMN_DOCUMENT_TYPE + " = ?", 265 strings(ROW_STATE_VALID, 266 ROW_STATE_INVALIDATED, 267 documentId, 268 DOCUMENT_TYPE_STORAGE), 269 null, 270 null, 271 null)) { 272 final String[] ids = new String[cursor.getCount()]; 273 for (int i = 0; cursor.moveToNext(); i++) { 274 ids[i] = cursor.getString(0); 275 } 276 return ids; 277 } 278 } 279 280 /** 281 * Queries a single document. 282 * @param documentId 283 * @param projection 284 * @return Database cursor. 285 */ queryDocument(String documentId, String[] projection)286 Cursor queryDocument(String documentId, String[] projection) { 287 return mDatabase.query( 288 TABLE_DOCUMENTS, 289 projection, 290 SELECTION_DOCUMENT_ID, 291 strings(documentId), 292 null, 293 null, 294 null, 295 "1"); 296 } 297 getDocumentIdForDevice(int deviceId)298 @Nullable String getDocumentIdForDevice(int deviceId) { 299 final Cursor cursor = mDatabase.query( 300 TABLE_DOCUMENTS, 301 strings(Document.COLUMN_DOCUMENT_ID), 302 COLUMN_DOCUMENT_TYPE + " = ? AND " + COLUMN_DEVICE_ID + " = ?", 303 strings(DOCUMENT_TYPE_DEVICE, deviceId), 304 null, 305 null, 306 null, 307 "1"); 308 try { 309 if (cursor.moveToNext()) { 310 return cursor.getString(0); 311 } else { 312 return null; 313 } 314 } finally { 315 cursor.close(); 316 } 317 } 318 319 /** 320 * Obtains parent identifier. 321 * @param documentId 322 * @return parent identifier. 323 * @throws FileNotFoundException 324 */ getParentIdentifier(String documentId)325 Identifier getParentIdentifier(String documentId) throws FileNotFoundException { 326 final Cursor cursor = mDatabase.query( 327 TABLE_DOCUMENTS, 328 strings(COLUMN_PARENT_DOCUMENT_ID), 329 SELECTION_DOCUMENT_ID, 330 strings(documentId), 331 null, 332 null, 333 null, 334 "1"); 335 try { 336 if (cursor.moveToNext()) { 337 return createIdentifier(cursor.getString(0)); 338 } else { 339 throw new FileNotFoundException("Cannot find a row having ID = " + documentId); 340 } 341 } finally { 342 cursor.close(); 343 } 344 } 345 getDeviceDocumentId(int deviceId)346 String getDeviceDocumentId(int deviceId) throws FileNotFoundException { 347 try (final Cursor cursor = mDatabase.query( 348 TABLE_DOCUMENTS, 349 strings(Document.COLUMN_DOCUMENT_ID), 350 COLUMN_DEVICE_ID + " = ? AND " + COLUMN_DOCUMENT_TYPE + " = ? AND " + 351 COLUMN_ROW_STATE + " != ?", 352 strings(deviceId, DOCUMENT_TYPE_DEVICE, ROW_STATE_DISCONNECTED), 353 null, 354 null, 355 null, 356 "1")) { 357 if (cursor.getCount() > 0) { 358 cursor.moveToNext(); 359 return cursor.getString(0); 360 } else { 361 throw new FileNotFoundException("The device ID not found: " + deviceId); 362 } 363 } 364 } 365 366 /** 367 * Adds new document under the parent. 368 * The method does not affect invalidated and pending documents because we know the document is 369 * newly added and never mapped with existing ones. 370 * @param parentDocumentId 371 * @param info 372 * @param size Object size. info#getCompressedSize() will be ignored because it does not contain 373 * object size more than 4GB. 374 * @return Document ID of added document. 375 */ putNewDocument( int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info, long size)376 String putNewDocument( 377 int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info, 378 long size) { 379 final ContentValues values = new ContentValues(); 380 getObjectDocumentValues( 381 values, deviceId, parentDocumentId, operationsSupported, info, size); 382 mDatabase.beginTransaction(); 383 try { 384 final long id = mDatabase.insert(TABLE_DOCUMENTS, null, values); 385 mDatabase.setTransactionSuccessful(); 386 return Long.toString(id); 387 } finally { 388 mDatabase.endTransaction(); 389 } 390 } 391 392 /** 393 * Deletes document and its children. 394 * @param documentId 395 */ deleteDocument(String documentId)396 void deleteDocument(String documentId) { 397 deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId)); 398 } 399 400 /** 401 * Gets identifier from document ID. 402 * @param documentId Document ID. 403 * @return Identifier. 404 * @throws FileNotFoundException 405 */ createIdentifier(String documentId)406 Identifier createIdentifier(String documentId) throws FileNotFoundException { 407 // Currently documentId is old format. 408 final Cursor cursor = mDatabase.query( 409 TABLE_DOCUMENTS, 410 strings(COLUMN_DEVICE_ID, 411 COLUMN_STORAGE_ID, 412 COLUMN_OBJECT_HANDLE, 413 COLUMN_DOCUMENT_TYPE), 414 SELECTION_DOCUMENT_ID + " AND " + COLUMN_ROW_STATE + " IN (?, ?)", 415 strings(documentId, ROW_STATE_VALID, ROW_STATE_INVALIDATED), 416 null, 417 null, 418 null, 419 "1"); 420 try { 421 if (cursor.getCount() == 0) { 422 throw new FileNotFoundException("ID \"" + documentId + "\" is not found."); 423 } else { 424 cursor.moveToNext(); 425 return new Identifier( 426 cursor.getInt(0), 427 cursor.getInt(1), 428 cursor.getInt(2), 429 documentId, 430 cursor.getInt(3)); 431 } 432 } finally { 433 cursor.close(); 434 } 435 } 436 437 /** 438 * Deletes a document, and its root information if the document is a root document. 439 * @param selection Query to select documents. 440 * @param args Arguments for selection. 441 * @return Whether the method deletes rows. 442 */ deleteDocumentsAndRootsRecursively(String selection, String[] args)443 boolean deleteDocumentsAndRootsRecursively(String selection, String[] args) { 444 mDatabase.beginTransaction(); 445 try { 446 boolean changed = false; 447 final Cursor cursor = mDatabase.query( 448 TABLE_DOCUMENTS, 449 strings(Document.COLUMN_DOCUMENT_ID), 450 selection, 451 args, 452 null, 453 null, 454 null); 455 try { 456 while (cursor.moveToNext()) { 457 if (deleteDocumentsAndRootsRecursively( 458 COLUMN_PARENT_DOCUMENT_ID + " = ?", 459 strings(cursor.getString(0)))) { 460 changed = true; 461 } 462 } 463 } finally { 464 cursor.close(); 465 } 466 if (deleteDocumentsAndRoots(selection, args)) { 467 changed = true; 468 } 469 mDatabase.setTransactionSuccessful(); 470 return changed; 471 } finally { 472 mDatabase.endTransaction(); 473 } 474 } 475 476 /** 477 * Marks the documents and their child as disconnected documents. 478 * @param selection 479 * @param args 480 * @return True if at least one row is updated. 481 */ disconnectDocumentsRecursively(String selection, String[] args)482 boolean disconnectDocumentsRecursively(String selection, String[] args) { 483 mDatabase.beginTransaction(); 484 try { 485 boolean changed = false; 486 try (final Cursor cursor = mDatabase.query( 487 TABLE_DOCUMENTS, 488 strings(Document.COLUMN_DOCUMENT_ID), 489 selection, 490 args, 491 null, 492 null, 493 null)) { 494 while (cursor.moveToNext()) { 495 if (disconnectDocumentsRecursively( 496 COLUMN_PARENT_DOCUMENT_ID + " = ?", 497 strings(cursor.getString(0)))) { 498 changed = true; 499 } 500 } 501 } 502 if (disconnectDocuments(selection, args)) { 503 changed = true; 504 } 505 mDatabase.setTransactionSuccessful(); 506 return changed; 507 } finally { 508 mDatabase.endTransaction(); 509 } 510 } 511 deleteDocumentsAndRoots(String selection, String[] args)512 boolean deleteDocumentsAndRoots(String selection, String[] args) { 513 mDatabase.beginTransaction(); 514 try { 515 int deleted = 0; 516 deleted += mDatabase.delete( 517 TABLE_ROOT_EXTRA, 518 Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString( 519 false, 520 TABLE_DOCUMENTS, 521 new String[] { Document.COLUMN_DOCUMENT_ID }, 522 selection, 523 null, 524 null, 525 null, 526 null) + ")", 527 args); 528 deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args); 529 mDatabase.setTransactionSuccessful(); 530 // TODO Remove mappingState. 531 return deleted != 0; 532 } finally { 533 mDatabase.endTransaction(); 534 } 535 } 536 disconnectDocuments(String selection, String[] args)537 boolean disconnectDocuments(String selection, String[] args) { 538 mDatabase.beginTransaction(); 539 try { 540 final ContentValues values = new ContentValues(); 541 values.put(COLUMN_ROW_STATE, ROW_STATE_DISCONNECTED); 542 values.putNull(COLUMN_DEVICE_ID); 543 values.putNull(COLUMN_STORAGE_ID); 544 values.putNull(COLUMN_OBJECT_HANDLE); 545 final boolean updated = mDatabase.update(TABLE_DOCUMENTS, values, selection, args) != 0; 546 mDatabase.setTransactionSuccessful(); 547 return updated; 548 } finally { 549 mDatabase.endTransaction(); 550 } 551 } 552 getRowState(String documentId)553 int getRowState(String documentId) throws FileNotFoundException { 554 try (final Cursor cursor = mDatabase.query( 555 TABLE_DOCUMENTS, 556 strings(COLUMN_ROW_STATE), 557 SELECTION_DOCUMENT_ID, 558 strings(documentId), 559 null, 560 null, 561 null)) { 562 if (cursor.getCount() == 0) { 563 throw new FileNotFoundException(); 564 } 565 cursor.moveToNext(); 566 return cursor.getInt(0); 567 } 568 } 569 writeRowSnapshot(String documentId, ContentValues values)570 void writeRowSnapshot(String documentId, ContentValues values) throws FileNotFoundException { 571 try (final Cursor cursor = mDatabase.query( 572 JOIN_ROOTS, 573 strings("*"), 574 SELECTION_DOCUMENT_ID, 575 strings(documentId), 576 null, 577 null, 578 null, 579 "1")) { 580 if (cursor.getCount() == 0) { 581 throw new FileNotFoundException(); 582 } 583 cursor.moveToNext(); 584 values.clear(); 585 DatabaseUtils.cursorRowToContentValues(cursor, values); 586 } 587 } 588 updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported, MtpObjectInfo info, Long size)589 void updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported, 590 MtpObjectInfo info, Long size) { 591 final ContentValues values = new ContentValues(); 592 getObjectDocumentValues(values, deviceId, parentId, operationsSupported, info, size); 593 594 mDatabase.beginTransaction(); 595 try { 596 mDatabase.update( 597 TABLE_DOCUMENTS, 598 values, 599 Document.COLUMN_DOCUMENT_ID + " = ?", 600 strings(documentId)); 601 mDatabase.setTransactionSuccessful(); 602 } finally { 603 mDatabase.endTransaction(); 604 } 605 } 606 607 /** 608 * Obtains a document that has already mapped but has unmapped children. 609 * @param deviceId Device to find documents. 610 * @return Identifier of found document or null. 611 */ getUnmappedDocumentsParent(int deviceId)612 @Nullable Identifier getUnmappedDocumentsParent(int deviceId) { 613 final String fromClosure = 614 TABLE_DOCUMENTS + " AS child INNER JOIN " + 615 TABLE_DOCUMENTS + " AS parent ON " + 616 "child." + COLUMN_PARENT_DOCUMENT_ID + " = " + 617 "parent." + Document.COLUMN_DOCUMENT_ID; 618 final String whereClosure = 619 "parent." + COLUMN_DEVICE_ID + " = ? AND " + 620 "parent." + COLUMN_ROW_STATE + " IN (?, ?) AND " + 621 "parent." + COLUMN_DOCUMENT_TYPE + " != ? AND " + 622 "child." + COLUMN_ROW_STATE + " = ?"; 623 try (final Cursor cursor = mDatabase.query( 624 fromClosure, 625 strings("parent." + COLUMN_DEVICE_ID, 626 "parent." + COLUMN_STORAGE_ID, 627 "parent." + COLUMN_OBJECT_HANDLE, 628 "parent." + Document.COLUMN_DOCUMENT_ID, 629 "parent." + COLUMN_DOCUMENT_TYPE), 630 whereClosure, 631 strings(deviceId, ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE, 632 ROW_STATE_DISCONNECTED), 633 null, 634 null, 635 null, 636 "1")) { 637 if (cursor.getCount() == 0) { 638 return null; 639 } 640 cursor.moveToNext(); 641 return new Identifier( 642 cursor.getInt(0), 643 cursor.getInt(1), 644 cursor.getInt(2), 645 cursor.getString(3), 646 cursor.getInt(4)); 647 } 648 } 649 650 /** 651 * Removes metadata except for data used by outgoingPersistedUriPermissions. 652 */ cleanDatabase(Uri[] outgoingPersistedUris)653 void cleanDatabase(Uri[] outgoingPersistedUris) { 654 mDatabase.beginTransaction(); 655 try { 656 final Set<String> ids = new HashSet<>(); 657 for (final Uri uri : outgoingPersistedUris) { 658 String documentId = DocumentsContract.getDocumentId(uri); 659 while (documentId != null) { 660 if (ids.contains(documentId)) { 661 break; 662 } 663 ids.add(documentId); 664 try (final Cursor cursor = mDatabase.query( 665 TABLE_DOCUMENTS, 666 strings(COLUMN_PARENT_DOCUMENT_ID), 667 SELECTION_DOCUMENT_ID, 668 strings(documentId), 669 null, 670 null, 671 null)) { 672 documentId = cursor.moveToNext() ? cursor.getString(0) : null; 673 } 674 } 675 } 676 deleteDocumentsAndRoots( 677 Document.COLUMN_DOCUMENT_ID + " NOT IN " + getIdList(ids), null); 678 mDatabase.setTransactionSuccessful(); 679 } finally { 680 mDatabase.endTransaction(); 681 } 682 } 683 getLastBootCount()684 int getLastBootCount() { 685 try (final Cursor cursor = mDatabase.query( 686 TABLE_LAST_BOOT_COUNT, strings(COLUMN_VALUE), null, null, null, null, null)) { 687 if (cursor.moveToNext()) { 688 return cursor.getInt(0); 689 } else { 690 return 0; 691 } 692 } 693 } 694 setLastBootCount(int value)695 void setLastBootCount(int value) { 696 Preconditions.checkArgumentNonnegative(value, "Boot count must not be negative."); 697 mDatabase.beginTransaction(); 698 try { 699 final ContentValues values = new ContentValues(); 700 values.put(COLUMN_VALUE, value); 701 mDatabase.delete(TABLE_LAST_BOOT_COUNT, null, null); 702 mDatabase.insert(TABLE_LAST_BOOT_COUNT, null, values); 703 mDatabase.setTransactionSuccessful(); 704 } finally { 705 mDatabase.endTransaction(); 706 } 707 } 708 709 private static class OpenHelper extends SQLiteOpenHelper { OpenHelper(Context context, int flags)710 public OpenHelper(Context context, int flags) { 711 super(context, 712 flags == FLAG_DATABASE_IN_MEMORY ? null : DATABASE_NAME, 713 null, 714 DATABASE_VERSION); 715 } 716 717 @Override onCreate(SQLiteDatabase db)718 public void onCreate(SQLiteDatabase db) { 719 db.execSQL(QUERY_CREATE_DOCUMENTS); 720 db.execSQL(QUERY_CREATE_ROOT_EXTRA); 721 db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT); 722 } 723 724 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)725 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 726 db.execSQL("DROP TABLE IF EXISTS " + TABLE_DOCUMENTS); 727 db.execSQL("DROP TABLE IF EXISTS " + TABLE_ROOT_EXTRA); 728 db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_BOOT_COUNT); 729 onCreate(db); 730 } 731 } 732 733 @VisibleForTesting deleteDatabase(Context context)734 static void deleteDatabase(Context context) { 735 context.deleteDatabase(DATABASE_NAME); 736 } 737 getDeviceDocumentValues( ContentValues values, ContentValues extraValues, MtpDeviceRecord device)738 static void getDeviceDocumentValues( 739 ContentValues values, 740 ContentValues extraValues, 741 MtpDeviceRecord device) { 742 values.clear(); 743 values.put(COLUMN_DEVICE_ID, device.deviceId); 744 values.putNull(COLUMN_STORAGE_ID); 745 values.putNull(COLUMN_OBJECT_HANDLE); 746 values.putNull(COLUMN_PARENT_DOCUMENT_ID); 747 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 748 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_DEVICE); 749 values.put(COLUMN_MAPPING_KEY, device.deviceKey); 750 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 751 values.put(Document.COLUMN_DISPLAY_NAME, device.name); 752 values.putNull(Document.COLUMN_SUMMARY); 753 values.putNull(Document.COLUMN_LAST_MODIFIED); 754 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp); 755 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 756 device.operationsSupported, 757 Document.MIME_TYPE_DIR, 758 0, 759 MtpConstants.PROTECTION_STATUS_NONE, 760 // Storages are placed under device so we cannot create a document just under 761 // device. 762 DOCUMENT_TYPE_DEVICE) & ~Document.FLAG_DIR_SUPPORTS_CREATE); 763 values.putNull(Document.COLUMN_SIZE); 764 765 extraValues.clear(); 766 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(device.operationsSupported)); 767 extraValues.putNull(Root.COLUMN_AVAILABLE_BYTES); 768 extraValues.putNull(Root.COLUMN_CAPACITY_BYTES); 769 extraValues.put(Root.COLUMN_MIME_TYPES, ""); 770 } 771 772 /** 773 * Gets {@link ContentValues} for the given root. 774 * @param values {@link ContentValues} that receives values. 775 * @param extraValues {@link ContentValues} that receives extra values for roots. 776 * @param parentDocumentId Parent document ID. 777 * @param operationsSupported Array of Operation code supported by the device. 778 * @param root Root to be converted {@link ContentValues}. 779 */ getStorageDocumentValues( ContentValues values, ContentValues extraValues, String parentDocumentId, int[] operationsSupported, MtpRoot root)780 static void getStorageDocumentValues( 781 ContentValues values, 782 ContentValues extraValues, 783 String parentDocumentId, 784 int[] operationsSupported, 785 MtpRoot root) { 786 values.clear(); 787 values.put(COLUMN_DEVICE_ID, root.mDeviceId); 788 values.put(COLUMN_STORAGE_ID, root.mStorageId); 789 values.putNull(COLUMN_OBJECT_HANDLE); 790 values.put(COLUMN_PARENT_DOCUMENT_ID, parentDocumentId); 791 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 792 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_STORAGE); 793 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 794 values.put(Document.COLUMN_DISPLAY_NAME, root.mDescription); 795 values.putNull(Document.COLUMN_SUMMARY); 796 values.putNull(Document.COLUMN_LAST_MODIFIED); 797 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp); 798 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 799 operationsSupported, 800 Document.MIME_TYPE_DIR, 801 0, 802 MtpConstants.PROTECTION_STATUS_NONE, 803 DOCUMENT_TYPE_STORAGE)); 804 values.put(Document.COLUMN_SIZE, root.mMaxCapacity - root.mFreeSpace); 805 806 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(operationsSupported)); 807 extraValues.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace); 808 extraValues.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity); 809 extraValues.put(Root.COLUMN_MIME_TYPES, ""); 810 } 811 812 /** 813 * Gets {@link ContentValues} for the given MTP object. 814 * @param values {@link ContentValues} that receives values. 815 * @param deviceId Device ID of the object. 816 * @param parentId Parent document ID of the object. 817 * @param info MTP object info. getCompressedSize will be ignored. 818 * @param size 64-bit size of documents. Negative value is regarded as unknown size. 819 */ getObjectDocumentValues( ContentValues values, int deviceId, String parentId, int[] operationsSupported, MtpObjectInfo info, long size)820 static void getObjectDocumentValues( 821 ContentValues values, int deviceId, String parentId, 822 int[] operationsSupported, MtpObjectInfo info, long size) { 823 values.clear(); 824 final String mimeType = getMimeType(info); 825 values.put(COLUMN_DEVICE_ID, deviceId); 826 values.put(COLUMN_STORAGE_ID, info.getStorageId()); 827 values.put(COLUMN_OBJECT_HANDLE, info.getObjectHandle()); 828 values.put(COLUMN_PARENT_DOCUMENT_ID, parentId); 829 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 830 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_OBJECT); 831 values.put(Document.COLUMN_MIME_TYPE, mimeType); 832 values.put(Document.COLUMN_DISPLAY_NAME, info.getName()); 833 values.putNull(Document.COLUMN_SUMMARY); 834 values.put( 835 Document.COLUMN_LAST_MODIFIED, 836 info.getDateModified() != 0 ? info.getDateModified() : null); 837 values.putNull(Document.COLUMN_ICON); 838 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 839 operationsSupported, mimeType, info.getThumbCompressedSizeLong(), 840 info.getProtectionStatus(), DOCUMENT_TYPE_OBJECT)); 841 if (size >= 0) { 842 values.put(Document.COLUMN_SIZE, size); 843 } else { 844 values.putNull(Document.COLUMN_SIZE); 845 } 846 } 847 getMimeType(MtpObjectInfo info)848 private static String getMimeType(MtpObjectInfo info) { 849 if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) { 850 return DocumentsContract.Document.MIME_TYPE_DIR; 851 } 852 853 final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat()); 854 final String mediaFileMimeType = MediaFile.getMimeTypeForFile(info.getName()); 855 856 // Format code can be mapped with multiple mime types, e.g. FORMAT_MPEG is mapped with 857 // audio/mp4 and video/mp4. 858 // As file extension contains more information than format code, returns mime type obtained 859 // from file extension if it is consistent with format code. 860 if (mediaFileMimeType != null && 861 MediaFile.getFormatCode("", mediaFileMimeType) == info.getFormat()) { 862 return mediaFileMimeType; 863 } 864 if (formatCodeMimeType != null) { 865 return formatCodeMimeType; 866 } 867 if (mediaFileMimeType != null) { 868 return mediaFileMimeType; 869 } 870 // We don't know the file type. 871 return "application/octet-stream"; 872 } 873 getRootFlags(int[] operationsSupported)874 private static int getRootFlags(int[] operationsSupported) { 875 int rootFlag = Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_LOCAL_ONLY; 876 if (MtpDeviceRecord.isWritingSupported(operationsSupported)) { 877 rootFlag |= Root.FLAG_SUPPORTS_CREATE; 878 } 879 return rootFlag; 880 } 881 getDocumentFlags( @ullable int[] operationsSupported, String mimeType, long thumbnailSize, int protectionState, @DocumentType int documentType)882 private static int getDocumentFlags( 883 @Nullable int[] operationsSupported, String mimeType, long thumbnailSize, 884 int protectionState, @DocumentType int documentType) { 885 int flag = 0; 886 if (!mimeType.equals(Document.MIME_TYPE_DIR) && 887 MtpDeviceRecord.isWritingSupported(operationsSupported) && 888 protectionState == MtpConstants.PROTECTION_STATUS_NONE) { 889 flag |= Document.FLAG_SUPPORTS_WRITE; 890 } 891 if (MtpDeviceRecord.isSupported( 892 operationsSupported, MtpConstants.OPERATION_DELETE_OBJECT) && 893 (protectionState == MtpConstants.PROTECTION_STATUS_NONE || 894 protectionState == MtpConstants.PROTECTION_STATUS_NON_TRANSFERABLE_DATA) && 895 documentType == DOCUMENT_TYPE_OBJECT) { 896 flag |= Document.FLAG_SUPPORTS_DELETE; 897 } 898 if (mimeType.equals(Document.MIME_TYPE_DIR) && 899 MtpDeviceRecord.isWritingSupported(operationsSupported) && 900 protectionState == MtpConstants.PROTECTION_STATUS_NONE) { 901 flag |= Document.FLAG_DIR_SUPPORTS_CREATE; 902 } 903 if (thumbnailSize > 0) { 904 flag |= Document.FLAG_SUPPORTS_THUMBNAIL; 905 } 906 return flag; 907 } 908 strings(Object... args)909 static String[] strings(Object... args) { 910 final String[] results = new String[args.length]; 911 for (int i = 0; i < args.length; i++) { 912 results[i] = Objects.toString(args[i]); 913 } 914 return results; 915 } 916 putValuesToCursor(ContentValues values, MatrixCursor cursor)917 static void putValuesToCursor(ContentValues values, MatrixCursor cursor) { 918 final RowBuilder row = cursor.newRow(); 919 for (final String name : cursor.getColumnNames()) { 920 row.add(values.get(name)); 921 } 922 } 923 getIdList(Set<String> ids)924 private static String getIdList(Set<String> ids) { 925 String result = "("; 926 for (final String id : ids) { 927 if (result.length() > 1) { 928 result += ","; 929 } 930 result += id; 931 } 932 result += ")"; 933 return result; 934 } 935 } 936