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 final RowBuilder row = result.newRow(); 204 for (final String key : values.keySet()) { 205 row.add(key, values.get(key)); 206 } 207 } 208 209 return result; 210 } finally { 211 deviceCursor.close(); 212 } 213 } 214 215 /** 216 * Queries root documents information. 217 * @param columnNames Column names defined in 218 * {@link android.provider.DocumentsContract.Document}. 219 * @return Database cursor. 220 */ 221 @VisibleForTesting queryRootDocuments(String[] columnNames)222 Cursor queryRootDocuments(String[] columnNames) { 223 return mDatabase.query( 224 TABLE_DOCUMENTS, 225 columnNames, 226 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?", 227 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE), 228 null, 229 null, 230 null); 231 } 232 233 /** 234 * Queries documents information. 235 * @param columnNames Column names defined in 236 * {@link android.provider.DocumentsContract.Document}. 237 * @return Database cursor. 238 */ queryChildDocuments(String[] columnNames, String parentDocumentId)239 Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) { 240 return mDatabase.query( 241 TABLE_DOCUMENTS, 242 columnNames, 243 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?", 244 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, parentDocumentId), 245 null, 246 null, 247 null); 248 } 249 250 /** 251 * Returns document IDs of storages under the given device document. 252 * 253 * @param documentId Document ID that points a device. 254 * @return Storage document IDs. 255 * @throws FileNotFoundException The given document ID is not registered in database. 256 */ getStorageDocumentIds(String documentId)257 String[] getStorageDocumentIds(String documentId) 258 throws FileNotFoundException { 259 Preconditions.checkArgument(createIdentifier(documentId).mDocumentType == 260 DOCUMENT_TYPE_DEVICE); 261 // Check if the parent document is device that has single storage. 262 try (final Cursor cursor = mDatabase.query( 263 TABLE_DOCUMENTS, 264 strings(Document.COLUMN_DOCUMENT_ID), 265 COLUMN_ROW_STATE + " IN (?, ?) AND " + 266 COLUMN_PARENT_DOCUMENT_ID + " = ? AND " + 267 COLUMN_DOCUMENT_TYPE + " = ?", 268 strings(ROW_STATE_VALID, 269 ROW_STATE_INVALIDATED, 270 documentId, 271 DOCUMENT_TYPE_STORAGE), 272 null, 273 null, 274 null)) { 275 final String[] ids = new String[cursor.getCount()]; 276 for (int i = 0; cursor.moveToNext(); i++) { 277 ids[i] = cursor.getString(0); 278 } 279 return ids; 280 } 281 } 282 283 /** 284 * Queries a single document. 285 * @param documentId 286 * @param projection 287 * @return Database cursor. 288 */ queryDocument(String documentId, String[] projection)289 Cursor queryDocument(String documentId, String[] projection) { 290 return mDatabase.query( 291 TABLE_DOCUMENTS, 292 projection, 293 SELECTION_DOCUMENT_ID, 294 strings(documentId), 295 null, 296 null, 297 null, 298 "1"); 299 } 300 getDocumentIdForDevice(int deviceId)301 @Nullable String getDocumentIdForDevice(int deviceId) { 302 final Cursor cursor = mDatabase.query( 303 TABLE_DOCUMENTS, 304 strings(Document.COLUMN_DOCUMENT_ID), 305 COLUMN_DOCUMENT_TYPE + " = ? AND " + COLUMN_DEVICE_ID + " = ?", 306 strings(DOCUMENT_TYPE_DEVICE, deviceId), 307 null, 308 null, 309 null, 310 "1"); 311 try { 312 if (cursor.moveToNext()) { 313 return cursor.getString(0); 314 } else { 315 return null; 316 } 317 } finally { 318 cursor.close(); 319 } 320 } 321 322 /** 323 * Obtains parent identifier. 324 * @param documentId 325 * @return parent identifier. 326 * @throws FileNotFoundException 327 */ getParentIdentifier(String documentId)328 Identifier getParentIdentifier(String documentId) throws FileNotFoundException { 329 final Cursor cursor = mDatabase.query( 330 TABLE_DOCUMENTS, 331 strings(COLUMN_PARENT_DOCUMENT_ID), 332 SELECTION_DOCUMENT_ID, 333 strings(documentId), 334 null, 335 null, 336 null, 337 "1"); 338 try { 339 if (cursor.moveToNext()) { 340 return createIdentifier(cursor.getString(0)); 341 } else { 342 throw new FileNotFoundException("Cannot find a row having ID = " + documentId); 343 } 344 } finally { 345 cursor.close(); 346 } 347 } 348 getDeviceDocumentId(int deviceId)349 String getDeviceDocumentId(int deviceId) throws FileNotFoundException { 350 try (final Cursor cursor = mDatabase.query( 351 TABLE_DOCUMENTS, 352 strings(Document.COLUMN_DOCUMENT_ID), 353 COLUMN_DEVICE_ID + " = ? AND " + COLUMN_DOCUMENT_TYPE + " = ? AND " + 354 COLUMN_ROW_STATE + " != ?", 355 strings(deviceId, DOCUMENT_TYPE_DEVICE, ROW_STATE_DISCONNECTED), 356 null, 357 null, 358 null, 359 "1")) { 360 if (cursor.getCount() > 0) { 361 cursor.moveToNext(); 362 return cursor.getString(0); 363 } else { 364 throw new FileNotFoundException("The device ID not found: " + deviceId); 365 } 366 } 367 } 368 369 /** 370 * Adds new document under the parent. 371 * The method does not affect invalidated and pending documents because we know the document is 372 * newly added and never mapped with existing ones. 373 * @param parentDocumentId 374 * @param info 375 * @param size Object size. info#getCompressedSize() will be ignored because it does not contain 376 * object size more than 4GB. 377 * @return Document ID of added document. 378 */ putNewDocument( int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info, long size)379 String putNewDocument( 380 int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info, 381 long size) { 382 final ContentValues values = new ContentValues(); 383 getObjectDocumentValues( 384 values, deviceId, parentDocumentId, operationsSupported, info, size); 385 mDatabase.beginTransaction(); 386 try { 387 final long id = mDatabase.insert(TABLE_DOCUMENTS, null, values); 388 mDatabase.setTransactionSuccessful(); 389 return Long.toString(id); 390 } finally { 391 mDatabase.endTransaction(); 392 } 393 } 394 395 /** 396 * Deletes document and its children. 397 * @param documentId 398 */ deleteDocument(String documentId)399 void deleteDocument(String documentId) { 400 deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId)); 401 } 402 403 /** 404 * Gets identifier from document ID. 405 * @param documentId Document ID. 406 * @return Identifier. 407 * @throws FileNotFoundException 408 */ createIdentifier(String documentId)409 Identifier createIdentifier(String documentId) throws FileNotFoundException { 410 // Currently documentId is old format. 411 final Cursor cursor = mDatabase.query( 412 TABLE_DOCUMENTS, 413 strings(COLUMN_DEVICE_ID, 414 COLUMN_STORAGE_ID, 415 COLUMN_OBJECT_HANDLE, 416 COLUMN_DOCUMENT_TYPE), 417 SELECTION_DOCUMENT_ID + " AND " + COLUMN_ROW_STATE + " IN (?, ?)", 418 strings(documentId, ROW_STATE_VALID, ROW_STATE_INVALIDATED), 419 null, 420 null, 421 null, 422 "1"); 423 try { 424 if (cursor.getCount() == 0) { 425 throw new FileNotFoundException("ID \"" + documentId + "\" is not found."); 426 } else { 427 cursor.moveToNext(); 428 return new Identifier( 429 cursor.getInt(0), 430 cursor.getInt(1), 431 cursor.getInt(2), 432 documentId, 433 cursor.getInt(3)); 434 } 435 } finally { 436 cursor.close(); 437 } 438 } 439 440 /** 441 * Deletes a document, and its root information if the document is a root document. 442 * @param selection Query to select documents. 443 * @param args Arguments for selection. 444 * @return Whether the method deletes rows. 445 */ deleteDocumentsAndRootsRecursively(String selection, String[] args)446 boolean deleteDocumentsAndRootsRecursively(String selection, String[] args) { 447 mDatabase.beginTransaction(); 448 try { 449 boolean changed = false; 450 final Cursor cursor = mDatabase.query( 451 TABLE_DOCUMENTS, 452 strings(Document.COLUMN_DOCUMENT_ID), 453 selection, 454 args, 455 null, 456 null, 457 null); 458 try { 459 while (cursor.moveToNext()) { 460 if (deleteDocumentsAndRootsRecursively( 461 COLUMN_PARENT_DOCUMENT_ID + " = ?", 462 strings(cursor.getString(0)))) { 463 changed = true; 464 } 465 } 466 } finally { 467 cursor.close(); 468 } 469 if (deleteDocumentsAndRoots(selection, args)) { 470 changed = true; 471 } 472 mDatabase.setTransactionSuccessful(); 473 return changed; 474 } finally { 475 mDatabase.endTransaction(); 476 } 477 } 478 479 /** 480 * Marks the documents and their child as disconnected documents. 481 * @param selection 482 * @param args 483 * @return True if at least one row is updated. 484 */ disconnectDocumentsRecursively(String selection, String[] args)485 boolean disconnectDocumentsRecursively(String selection, String[] args) { 486 mDatabase.beginTransaction(); 487 try { 488 boolean changed = false; 489 try (final Cursor cursor = mDatabase.query( 490 TABLE_DOCUMENTS, 491 strings(Document.COLUMN_DOCUMENT_ID), 492 selection, 493 args, 494 null, 495 null, 496 null)) { 497 while (cursor.moveToNext()) { 498 if (disconnectDocumentsRecursively( 499 COLUMN_PARENT_DOCUMENT_ID + " = ?", 500 strings(cursor.getString(0)))) { 501 changed = true; 502 } 503 } 504 } 505 if (disconnectDocuments(selection, args)) { 506 changed = true; 507 } 508 mDatabase.setTransactionSuccessful(); 509 return changed; 510 } finally { 511 mDatabase.endTransaction(); 512 } 513 } 514 deleteDocumentsAndRoots(String selection, String[] args)515 boolean deleteDocumentsAndRoots(String selection, String[] args) { 516 mDatabase.beginTransaction(); 517 try { 518 int deleted = 0; 519 deleted += mDatabase.delete( 520 TABLE_ROOT_EXTRA, 521 Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString( 522 false, 523 TABLE_DOCUMENTS, 524 new String[] { Document.COLUMN_DOCUMENT_ID }, 525 selection, 526 null, 527 null, 528 null, 529 null) + ")", 530 args); 531 deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args); 532 mDatabase.setTransactionSuccessful(); 533 // TODO Remove mappingState. 534 return deleted != 0; 535 } finally { 536 mDatabase.endTransaction(); 537 } 538 } 539 disconnectDocuments(String selection, String[] args)540 boolean disconnectDocuments(String selection, String[] args) { 541 mDatabase.beginTransaction(); 542 try { 543 final ContentValues values = new ContentValues(); 544 values.put(COLUMN_ROW_STATE, ROW_STATE_DISCONNECTED); 545 values.putNull(COLUMN_DEVICE_ID); 546 values.putNull(COLUMN_STORAGE_ID); 547 values.putNull(COLUMN_OBJECT_HANDLE); 548 final boolean updated = mDatabase.update(TABLE_DOCUMENTS, values, selection, args) != 0; 549 mDatabase.setTransactionSuccessful(); 550 return updated; 551 } finally { 552 mDatabase.endTransaction(); 553 } 554 } 555 getRowState(String documentId)556 int getRowState(String documentId) throws FileNotFoundException { 557 try (final Cursor cursor = mDatabase.query( 558 TABLE_DOCUMENTS, 559 strings(COLUMN_ROW_STATE), 560 SELECTION_DOCUMENT_ID, 561 strings(documentId), 562 null, 563 null, 564 null)) { 565 if (cursor.getCount() == 0) { 566 throw new FileNotFoundException(); 567 } 568 cursor.moveToNext(); 569 return cursor.getInt(0); 570 } 571 } 572 writeRowSnapshot(String documentId, ContentValues values)573 void writeRowSnapshot(String documentId, ContentValues values) throws FileNotFoundException { 574 try (final Cursor cursor = mDatabase.query( 575 JOIN_ROOTS, 576 strings("*"), 577 SELECTION_DOCUMENT_ID, 578 strings(documentId), 579 null, 580 null, 581 null, 582 "1")) { 583 if (cursor.getCount() == 0) { 584 throw new FileNotFoundException(); 585 } 586 cursor.moveToNext(); 587 values.clear(); 588 DatabaseUtils.cursorRowToContentValues(cursor, values); 589 } 590 } 591 updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported, MtpObjectInfo info, Long size)592 void updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported, 593 MtpObjectInfo info, Long size) { 594 final ContentValues values = new ContentValues(); 595 getObjectDocumentValues(values, deviceId, parentId, operationsSupported, info, size); 596 597 mDatabase.beginTransaction(); 598 try { 599 mDatabase.update( 600 TABLE_DOCUMENTS, 601 values, 602 Document.COLUMN_DOCUMENT_ID + " = ?", 603 strings(documentId)); 604 mDatabase.setTransactionSuccessful(); 605 } finally { 606 mDatabase.endTransaction(); 607 } 608 } 609 610 /** 611 * Obtains a document that has already mapped but has unmapped children. 612 * @param deviceId Device to find documents. 613 * @return Identifier of found document or null. 614 */ getUnmappedDocumentsParent(int deviceId)615 @Nullable Identifier getUnmappedDocumentsParent(int deviceId) { 616 final String fromClosure = 617 TABLE_DOCUMENTS + " AS child INNER JOIN " + 618 TABLE_DOCUMENTS + " AS parent ON " + 619 "child." + COLUMN_PARENT_DOCUMENT_ID + " = " + 620 "parent." + Document.COLUMN_DOCUMENT_ID; 621 final String whereClosure = 622 "parent." + COLUMN_DEVICE_ID + " = ? AND " + 623 "parent." + COLUMN_ROW_STATE + " IN (?, ?) AND " + 624 "parent." + COLUMN_DOCUMENT_TYPE + " != ? AND " + 625 "child." + COLUMN_ROW_STATE + " = ?"; 626 try (final Cursor cursor = mDatabase.query( 627 fromClosure, 628 strings("parent." + COLUMN_DEVICE_ID, 629 "parent." + COLUMN_STORAGE_ID, 630 "parent." + COLUMN_OBJECT_HANDLE, 631 "parent." + Document.COLUMN_DOCUMENT_ID, 632 "parent." + COLUMN_DOCUMENT_TYPE), 633 whereClosure, 634 strings(deviceId, ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE, 635 ROW_STATE_DISCONNECTED), 636 null, 637 null, 638 null, 639 "1")) { 640 if (cursor.getCount() == 0) { 641 return null; 642 } 643 cursor.moveToNext(); 644 return new Identifier( 645 cursor.getInt(0), 646 cursor.getInt(1), 647 cursor.getInt(2), 648 cursor.getString(3), 649 cursor.getInt(4)); 650 } 651 } 652 653 /** 654 * Removes metadata except for data used by outgoingPersistedUriPermissions. 655 */ cleanDatabase(Uri[] outgoingPersistedUris)656 void cleanDatabase(Uri[] outgoingPersistedUris) { 657 mDatabase.beginTransaction(); 658 try { 659 final Set<String> ids = new HashSet<>(); 660 for (final Uri uri : outgoingPersistedUris) { 661 String documentId = DocumentsContract.getDocumentId(uri); 662 while (documentId != null) { 663 if (ids.contains(documentId)) { 664 break; 665 } 666 ids.add(documentId); 667 try (final Cursor cursor = mDatabase.query( 668 TABLE_DOCUMENTS, 669 strings(COLUMN_PARENT_DOCUMENT_ID), 670 SELECTION_DOCUMENT_ID, 671 strings(documentId), 672 null, 673 null, 674 null)) { 675 documentId = cursor.moveToNext() ? cursor.getString(0) : null; 676 } 677 } 678 } 679 deleteDocumentsAndRoots( 680 Document.COLUMN_DOCUMENT_ID + " NOT IN " + getIdList(ids), null); 681 mDatabase.setTransactionSuccessful(); 682 } finally { 683 mDatabase.endTransaction(); 684 } 685 } 686 getLastBootCount()687 int getLastBootCount() { 688 try (final Cursor cursor = mDatabase.query( 689 TABLE_LAST_BOOT_COUNT, strings(COLUMN_VALUE), null, null, null, null, null)) { 690 if (cursor.moveToNext()) { 691 return cursor.getInt(0); 692 } else { 693 return 0; 694 } 695 } 696 } 697 setLastBootCount(int value)698 void setLastBootCount(int value) { 699 Preconditions.checkArgumentNonnegative(value, "Boot count must not be negative."); 700 mDatabase.beginTransaction(); 701 try { 702 final ContentValues values = new ContentValues(); 703 values.put(COLUMN_VALUE, value); 704 mDatabase.delete(TABLE_LAST_BOOT_COUNT, null, null); 705 mDatabase.insert(TABLE_LAST_BOOT_COUNT, null, values); 706 mDatabase.setTransactionSuccessful(); 707 } finally { 708 mDatabase.endTransaction(); 709 } 710 } 711 712 private static class OpenHelper extends SQLiteOpenHelper { OpenHelper(Context context, int flags)713 public OpenHelper(Context context, int flags) { 714 super(context, 715 flags == FLAG_DATABASE_IN_MEMORY ? null : DATABASE_NAME, 716 null, 717 DATABASE_VERSION); 718 } 719 720 @Override onCreate(SQLiteDatabase db)721 public void onCreate(SQLiteDatabase db) { 722 db.execSQL(QUERY_CREATE_DOCUMENTS); 723 db.execSQL(QUERY_CREATE_ROOT_EXTRA); 724 db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT); 725 } 726 727 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)728 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 729 db.execSQL("DROP TABLE IF EXISTS " + TABLE_DOCUMENTS); 730 db.execSQL("DROP TABLE IF EXISTS " + TABLE_ROOT_EXTRA); 731 db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_BOOT_COUNT); 732 onCreate(db); 733 } 734 } 735 736 @VisibleForTesting deleteDatabase(Context context)737 static void deleteDatabase(Context context) { 738 context.deleteDatabase(DATABASE_NAME); 739 } 740 getDeviceDocumentValues( ContentValues values, ContentValues extraValues, MtpDeviceRecord device)741 static void getDeviceDocumentValues( 742 ContentValues values, 743 ContentValues extraValues, 744 MtpDeviceRecord device) { 745 values.clear(); 746 values.put(COLUMN_DEVICE_ID, device.deviceId); 747 values.putNull(COLUMN_STORAGE_ID); 748 values.putNull(COLUMN_OBJECT_HANDLE); 749 values.putNull(COLUMN_PARENT_DOCUMENT_ID); 750 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 751 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_DEVICE); 752 values.put(COLUMN_MAPPING_KEY, device.deviceKey); 753 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 754 values.put(Document.COLUMN_DISPLAY_NAME, device.name); 755 values.putNull(Document.COLUMN_SUMMARY); 756 values.putNull(Document.COLUMN_LAST_MODIFIED); 757 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp); 758 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 759 device.operationsSupported, 760 Document.MIME_TYPE_DIR, 761 0, 762 MtpConstants.PROTECTION_STATUS_NONE, 763 DOCUMENT_TYPE_DEVICE)); 764 values.putNull(Document.COLUMN_SIZE); 765 766 extraValues.clear(); 767 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(device.operationsSupported)); 768 extraValues.putNull(Root.COLUMN_AVAILABLE_BYTES); 769 extraValues.putNull(Root.COLUMN_CAPACITY_BYTES); 770 extraValues.put(Root.COLUMN_MIME_TYPES, ""); 771 } 772 773 /** 774 * Gets {@link ContentValues} for the given root. 775 * @param values {@link ContentValues} that receives values. 776 * @param extraValues {@link ContentValues} that receives extra values for roots. 777 * @param parentDocumentId Parent document ID. 778 * @param operationsSupported Array of Operation code supported by the device. 779 * @param root Root to be converted {@link ContentValues}. 780 */ getStorageDocumentValues( ContentValues values, ContentValues extraValues, String parentDocumentId, int[] operationsSupported, MtpRoot root)781 static void getStorageDocumentValues( 782 ContentValues values, 783 ContentValues extraValues, 784 String parentDocumentId, 785 int[] operationsSupported, 786 MtpRoot root) { 787 values.clear(); 788 values.put(COLUMN_DEVICE_ID, root.mDeviceId); 789 values.put(COLUMN_STORAGE_ID, root.mStorageId); 790 values.putNull(COLUMN_OBJECT_HANDLE); 791 values.put(COLUMN_PARENT_DOCUMENT_ID, parentDocumentId); 792 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 793 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_STORAGE); 794 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 795 values.put(Document.COLUMN_DISPLAY_NAME, root.mDescription); 796 values.putNull(Document.COLUMN_SUMMARY); 797 values.putNull(Document.COLUMN_LAST_MODIFIED); 798 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp); 799 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 800 operationsSupported, 801 Document.MIME_TYPE_DIR, 802 0, 803 MtpConstants.PROTECTION_STATUS_NONE, 804 DOCUMENT_TYPE_STORAGE)); 805 values.put(Document.COLUMN_SIZE, root.mMaxCapacity - root.mFreeSpace); 806 807 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(operationsSupported)); 808 extraValues.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace); 809 extraValues.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity); 810 extraValues.put(Root.COLUMN_MIME_TYPES, ""); 811 } 812 813 /** 814 * Gets {@link ContentValues} for the given MTP object. 815 * @param values {@link ContentValues} that receives values. 816 * @param deviceId Device ID of the object. 817 * @param parentId Parent document ID of the object. 818 * @param info MTP object info. getCompressedSize will be ignored. 819 * @param size 64-bit size of documents. Negative value is regarded as unknown size. 820 */ getObjectDocumentValues( ContentValues values, int deviceId, String parentId, int[] operationsSupported, MtpObjectInfo info, long size)821 static void getObjectDocumentValues( 822 ContentValues values, int deviceId, String parentId, 823 int[] operationsSupported, MtpObjectInfo info, long size) { 824 values.clear(); 825 final String mimeType = getMimeType(info); 826 values.put(COLUMN_DEVICE_ID, deviceId); 827 values.put(COLUMN_STORAGE_ID, info.getStorageId()); 828 values.put(COLUMN_OBJECT_HANDLE, info.getObjectHandle()); 829 values.put(COLUMN_PARENT_DOCUMENT_ID, parentId); 830 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID); 831 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_OBJECT); 832 values.put(Document.COLUMN_MIME_TYPE, mimeType); 833 values.put(Document.COLUMN_DISPLAY_NAME, info.getName()); 834 values.putNull(Document.COLUMN_SUMMARY); 835 values.put( 836 Document.COLUMN_LAST_MODIFIED, 837 info.getDateModified() != 0 ? info.getDateModified() : null); 838 values.putNull(Document.COLUMN_ICON); 839 values.put(Document.COLUMN_FLAGS, getDocumentFlags( 840 operationsSupported, mimeType, info.getThumbCompressedSizeLong(), 841 info.getProtectionStatus(), DOCUMENT_TYPE_OBJECT)); 842 if (size >= 0) { 843 values.put(Document.COLUMN_SIZE, size); 844 } else { 845 values.putNull(Document.COLUMN_SIZE); 846 } 847 } 848 getMimeType(MtpObjectInfo info)849 private static String getMimeType(MtpObjectInfo info) { 850 if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) { 851 return DocumentsContract.Document.MIME_TYPE_DIR; 852 } 853 854 final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat()); 855 final String mediaFileMimeType = MediaFile.getMimeTypeForFile(info.getName()); 856 857 // Format code can be mapped with multiple mime types, e.g. FORMAT_MPEG is mapped with 858 // audio/mp4 and video/mp4. 859 // As file extension contains more information than format code, returns mime type obtained 860 // from file extension if it is consistent with format code. 861 if (mediaFileMimeType != null && 862 MediaFile.getFormatCode("", mediaFileMimeType) == info.getFormat()) { 863 return mediaFileMimeType; 864 } 865 if (formatCodeMimeType != null) { 866 return formatCodeMimeType; 867 } 868 if (mediaFileMimeType != null) { 869 return mediaFileMimeType; 870 } 871 // We don't know the file type. 872 return "application/octet-stream"; 873 } 874 getRootFlags(int[] operationsSupported)875 private static int getRootFlags(int[] operationsSupported) { 876 int rootFlag = Root.FLAG_SUPPORTS_IS_CHILD; 877 if (MtpDeviceRecord.isWritingSupported(operationsSupported)) { 878 rootFlag |= Root.FLAG_SUPPORTS_CREATE; 879 } 880 return rootFlag; 881 } 882 getDocumentFlags( @ullable int[] operationsSupported, String mimeType, long thumbnailSize, int protectionState, @DocumentType int documentType)883 private static int getDocumentFlags( 884 @Nullable int[] operationsSupported, String mimeType, long thumbnailSize, 885 int protectionState, @DocumentType int documentType) { 886 int flag = 0; 887 if (!mimeType.equals(Document.MIME_TYPE_DIR) && 888 MtpDeviceRecord.isWritingSupported(operationsSupported) && 889 protectionState == MtpConstants.PROTECTION_STATUS_NONE) { 890 flag |= Document.FLAG_SUPPORTS_WRITE; 891 } 892 if (MtpDeviceRecord.isSupported( 893 operationsSupported, MtpConstants.OPERATION_DELETE_OBJECT) && 894 (protectionState == MtpConstants.PROTECTION_STATUS_NONE || 895 protectionState == MtpConstants.PROTECTION_STATUS_NON_TRANSFERABLE_DATA) && 896 documentType == DOCUMENT_TYPE_OBJECT) { 897 flag |= Document.FLAG_SUPPORTS_DELETE; 898 } 899 if (mimeType.equals(Document.MIME_TYPE_DIR) && 900 MtpDeviceRecord.isWritingSupported(operationsSupported) && 901 protectionState == MtpConstants.PROTECTION_STATUS_NONE) { 902 flag |= Document.FLAG_DIR_SUPPORTS_CREATE; 903 } 904 if (thumbnailSize > 0) { 905 flag |= Document.FLAG_SUPPORTS_THUMBNAIL; 906 } 907 return flag; 908 } 909 strings(Object... args)910 static String[] strings(Object... args) { 911 final String[] results = new String[args.length]; 912 for (int i = 0; i < args.length; i++) { 913 results[i] = Objects.toString(args[i]); 914 } 915 return results; 916 } 917 getIdList(Set<String> ids)918 private static String getIdList(Set<String> ids) { 919 String result = "("; 920 for (final String id : ids) { 921 if (result.length() > 1) { 922 result += ","; 923 } 924 result += id; 925 } 926 result += ")"; 927 return result; 928 } 929 } 930