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