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