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