blob: f243b51d6769b63bfda332c9d7548a413240de4c [file] [log] [blame]
Daichi Hirono6de8a0e2015-10-27 16:18:17 +09001/*
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
Daichi Hirono9678f602015-10-22 12:35:05 +090017package com.android.mtp;
18
Daichi Hirono18d70d52015-11-13 15:19:47 +090019import static com.android.mtp.MtpDatabaseConstants.*;
20
Daichi Hirono20754c52015-12-15 18:52:26 +090021import android.annotation.Nullable;
Daichi Hirono9678f602015-10-22 12:35:05 +090022import android.content.ContentValues;
23import android.content.Context;
Daichi Hirono85a808b2015-10-27 17:22:13 +090024import android.content.res.Resources;
Daichi Hirono9678f602015-10-22 12:35:05 +090025import android.database.Cursor;
Daichi Hirono81d48532015-12-16 15:03:19 +090026import android.database.DatabaseUtils;
27import android.database.MatrixCursor;
28import android.database.MatrixCursor.RowBuilder;
Daichi Hirono259ce802015-11-20 17:51:53 +090029import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.database.sqlite.SQLiteQueryBuilder;
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +090032import android.media.MediaFile;
33import android.mtp.MtpConstants;
Daichi Hirono9678f602015-10-22 12:35:05 +090034import android.mtp.MtpObjectInfo;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090035import android.net.Uri;
Daichi Hirono47eb1922015-11-16 13:01:31 +090036import android.provider.DocumentsContract;
Steve McKay5a10ff12017-08-01 15:02:50 -070037import android.provider.MetadataReader;
Daichi Hirono9678f602015-10-22 12:35:05 +090038import android.provider.DocumentsContract.Document;
Daichi Hirono0378da42015-11-05 11:56:12 +090039import android.provider.DocumentsContract.Root;
Daichi Hirono9678f602015-10-22 12:35:05 +090040
41import com.android.internal.annotations.VisibleForTesting;
Daichi Hirono29657762016-02-10 16:55:37 -080042import com.android.internal.util.Preconditions;
Daichi Hirono9678f602015-10-22 12:35:05 +090043
Daichi Hirono49f920f2015-11-19 10:19:52 +090044import java.io.FileNotFoundException;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090045import java.util.HashSet;
Daichi Hirono259ce802015-11-20 17:51:53 +090046import java.util.Objects;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090047import java.util.Set;
Daichi Hirono9678f602015-10-22 12:35:05 +090048
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.
Daichi Hirono59686f62015-10-28 16:43:32 +090053 *
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 *
Daichi Hironocfcb0c02015-11-10 12:52:59 +090062 * To start putting documents into the database, the client needs to call
Daichi Hirono7a375c42015-12-14 17:14:29 +090063 * {@link Mapper#startAddingDocuments(String)} with the parent document ID. Also it
64 * needs to call {@link Mapper#stopAddingDocuments(String)} after putting all child
Daichi Hirono259ce802015-11-20 17:51:53 +090065 * documents to the database. (All explanations are same for root documents)
Daichi Hironocfcb0c02015-11-10 12:52:59 +090066 *
Daichi Hirono7a375c42015-12-14 17:14:29 +090067 * database.getMapper().startAddingDocuments();
Daichi Hirono259ce802015-11-20 17:51:53 +090068 * database.getMapper().putChildDocuments();
Daichi Hirono7a375c42015-12-14 17:14:29 +090069 * database.getMapper().stopAddingDocuments();
Daichi Hironocfcb0c02015-11-10 12:52:59 +090070 *
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 *
Daichi Hirono6de8a0e2015-10-27 16:18:17 +090083 * TODO: Improve performance by SQL optimization.
Daichi Hirono9678f602015-10-22 12:35:05 +090084 */
Daichi Hirono9678f602015-10-22 12:35:05 +090085class MtpDatabase {
Daichi Hirono259ce802015-11-20 17:51:53 +090086 private final SQLiteDatabase mDatabase;
87 private final Mapper mMapper;
Daichi Hironodc473442015-11-13 15:42:28 +090088
Daichi Hirono259ce802015-11-20 17:51:53 +090089 SQLiteDatabase getSQLiteDatabase() {
90 return mDatabase;
Daichi Hirono9678f602015-10-22 12:35:05 +090091 }
92
Daichi Hirono259ce802015-11-20 17:51:53 +090093 MtpDatabase(Context context, int flags) {
94 final OpenHelper helper = new OpenHelper(context, flags);
95 mDatabase = helper.getWritableDatabase();
96 mMapper = new Mapper(this);
97 }
98
Daichi Hironodc473442015-11-13 15:42:28 +090099 void close() {
100 mDatabase.close();
101 }
102
103 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900104 * Returns operations for mapping.
105 * @return Mapping operations.
Daichi Hironodc473442015-11-13 15:42:28 +0900106 */
Daichi Hirono259ce802015-11-20 17:51:53 +0900107 Mapper getMapper() {
108 return mMapper;
Daichi Hirono0378da42015-11-05 11:56:12 +0900109 }
110
Daichi Hironodc473442015-11-13 15:42:28 +0900111 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900112 * Queries roots information.
113 * @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
114 * @return Database cursor.
115 */
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900116 Cursor queryRoots(Resources resources, String[] columnNames) {
Daichi Hirono81d48532015-12-16 15:03:19 +0900117 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,
Daichi Hirono259ce802015-11-20 17:51:53 +0900125 null,
126 null,
127 null);
Daichi Hirono81d48532015-12-16 15:03:19 +0900128
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();
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900151 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 }
Daichi Hirono81d48532015-12-16 15:03:19 +0900165
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900166 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);
Daichi Hirono81d48532015-12-16 15:03:19 +0900178 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900179 if (availableIndex != -1) {
180 availableBytes += storageCursor.getLong(availableIndex);
181 }
Daichi Hirono81d48532015-12-16 15:03:19 +0900182 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900183 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();
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900191 // Add storage name to device name if we have only 1 storage.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900192 values.put(
193 Root.COLUMN_TITLE,
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900194 resources.getString(
195 R.string.root_name,
196 values.getAsString(Root.COLUMN_TITLE),
197 storageCursor.getString(
198 storageCursor.getColumnIndex(Root.COLUMN_TITLE))));
Daichi Hirono81d48532015-12-16 15:03:19 +0900199 }
200 } finally {
201 storageCursor.close();
202 }
203
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900204 putValuesToCursor(values, result);
Daichi Hirono81d48532015-12-16 15:03:19 +0900205 }
206
207 return result;
208 } finally {
209 deviceCursor.close();
210 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900211 }
212
213 /**
214 * Queries root documents information.
215 * @param columnNames Column names defined in
216 * {@link android.provider.DocumentsContract.Document}.
217 * @return Database cursor.
Daichi Hironodc473442015-11-13 15:42:28 +0900218 */
Daichi Hirono0378da42015-11-05 11:56:12 +0900219 @VisibleForTesting
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900220 Cursor queryRootDocuments(String[] columnNames) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900221 return mDatabase.query(
222 TABLE_DOCUMENTS,
223 columnNames,
Daichi Hirono8e873642016-02-07 15:17:16 +0900224 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?",
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000225 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE),
Daichi Hirono259ce802015-11-20 17:51:53 +0900226 null,
227 null,
228 null);
Daichi Hirono9678f602015-10-22 12:35:05 +0900229 }
230
Daichi Hironodc473442015-11-13 15:42:28 +0900231 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900232 * Queries documents information.
233 * @param columnNames Column names defined in
234 * {@link android.provider.DocumentsContract.Document}.
235 * @return Database cursor.
Daichi Hironodc473442015-11-13 15:42:28 +0900236 */
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900237 Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900238 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);
Daichi Hirono47eb1922015-11-16 13:01:31 +0900246 }
247
Daichi Hirono49f920f2015-11-19 10:19:52 +0900248 /**
Daichi Hirono29657762016-02-10 16:55:37 -0800249 * Returns document IDs of storages under the given device document.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900250 *
Daichi Hirono29657762016-02-10 16:55:37 -0800251 * @param documentId Document ID that points a device.
252 * @return Storage document IDs.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900253 * @throws FileNotFoundException The given document ID is not registered in database.
254 */
Daichi Hirono29657762016-02-10 16:55:37 -0800255 String[] getStorageDocumentIds(String documentId)
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900256 throws FileNotFoundException {
Daichi Hirono29657762016-02-10 16:55:37 -0800257 Preconditions.checkArgument(createIdentifier(documentId).mDocumentType ==
258 DOCUMENT_TYPE_DEVICE);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900259 // 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)) {
Daichi Hirono29657762016-02-10 16:55:37 -0800273 final String[] ids = new String[cursor.getCount()];
274 for (int i = 0; cursor.moveToNext(); i++) {
275 ids[i] = cursor.getString(0);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900276 }
Daichi Hirono29657762016-02-10 16:55:37 -0800277 return ids;
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900278 }
279 }
280
281 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900282 * Queries a single document.
283 * @param documentId
284 * @param projection
285 * @return Database cursor.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900286 */
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900287 Cursor queryDocument(String documentId, String[] projection) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900288 return mDatabase.query(
289 TABLE_DOCUMENTS,
290 projection,
291 SELECTION_DOCUMENT_ID,
292 strings(documentId),
293 null,
294 null,
295 null,
296 "1");
Daichi Hirono49f920f2015-11-19 10:19:52 +0900297 }
298
Daichi Hirono20754c52015-12-15 18:52:26 +0900299 @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
Daichi Hironodc473442015-11-13 15:42:28 +0900320 /**
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900321 * Obtains parent identifier.
Daichi Hirono259ce802015-11-20 17:51:53 +0900322 * @param documentId
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900323 * @return parent identifier.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900324 * @throws FileNotFoundException
325 */
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900326 Identifier getParentIdentifier(String documentId) throws FileNotFoundException {
Daichi Hirono259ce802015-11-20 17:51:53 +0900327 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()) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900338 return createIdentifier(cursor.getString(0));
Daichi Hirono259ce802015-11-20 17:51:53 +0900339 } else {
Daichi Hirono8e873642016-02-07 15:17:16 +0900340 throw new FileNotFoundException("Cannot find a row having ID = " + documentId);
Daichi Hirono259ce802015-11-20 17:51:53 +0900341 }
342 } finally {
343 cursor.close();
344 }
Daichi Hirono49f920f2015-11-19 10:19:52 +0900345 }
346
Daichi Hirono1e374442016-02-11 10:08:21 -0800347 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
Daichi Hirono49f920f2015-11-19 10:19:52 +0900367 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900368 * 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
Daichi Hirono64111e02016-03-24 21:07:38 +0900373 * @param size Object size. info#getCompressedSize() will be ignored because it does not contain
374 * object size more than 4GB.
Daichi Hirono259ce802015-11-20 17:51:53 +0900375 * @return Document ID of added document.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900376 */
Daichi Hirono61ba9232016-02-26 12:58:39 +0900377 String putNewDocument(
Daichi Hirono64111e02016-03-24 21:07:38 +0900378 int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info,
379 long size) {
Daichi Hirono49f920f2015-11-19 10:19:52 +0900380 final ContentValues values = new ContentValues();
Daichi Hirono64111e02016-03-24 21:07:38 +0900381 getObjectDocumentValues(
382 values, deviceId, parentDocumentId, operationsSupported, info, size);
Daichi Hirono0378da42015-11-05 11:56:12 +0900383 mDatabase.beginTransaction();
384 try {
Daichi Hirono259ce802015-11-20 17:51:53 +0900385 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 */
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 */
407 Identifier createIdentifier(String documentId) throws FileNotFoundException {
408 // Currently documentId is old format.
409 final Cursor cursor = mDatabase.query(
410 TABLE_DOCUMENTS,
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900411 strings(COLUMN_DEVICE_ID,
412 COLUMN_STORAGE_ID,
413 COLUMN_OBJECT_HANDLE,
414 COLUMN_DOCUMENT_TYPE),
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900415 SELECTION_DOCUMENT_ID + " AND " + COLUMN_ROW_STATE + " IN (?, ?)",
416 strings(documentId, ROW_STATE_VALID, ROW_STATE_INVALIDATED),
Daichi Hirono259ce802015-11-20 17:51:53 +0900417 null,
418 null,
419 null,
420 "1");
421 try {
422 if (cursor.getCount() == 0) {
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900423 throw new FileNotFoundException("ID \"" + documentId + "\" is not found.");
Daichi Hirono259ce802015-11-20 17:51:53 +0900424 } else {
425 cursor.moveToNext();
426 return new Identifier(
427 cursor.getInt(0),
428 cursor.getInt(1),
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900429 cursor.getInt(2),
430 documentId,
431 cursor.getInt(3));
Daichi Hirono49f920f2015-11-19 10:19:52 +0900432 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900433 } 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 */
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(
Daichi Hirono8e873642016-02-07 15:17:16 +0900459 COLUMN_PARENT_DOCUMENT_ID + " = ?",
Daichi Hirono259ce802015-11-20 17:51:53 +0900460 strings(cursor.getString(0)))) {
461 changed = true;
462 }
Daichi Hironodc473442015-11-13 15:42:28 +0900463 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900464 } finally {
465 cursor.close();
Daichi Hironodc473442015-11-13 15:42:28 +0900466 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900467 if (deleteDocumentsAndRoots(selection, args)) {
468 changed = true;
Daichi Hirono0378da42015-11-05 11:56:12 +0900469 }
470 mDatabase.setTransactionSuccessful();
Daichi Hironodc473442015-11-13 15:42:28 +0900471 return changed;
Daichi Hirono0378da42015-11-05 11:56:12 +0900472 } finally {
473 mDatabase.endTransaction();
Daichi Hirono9678f602015-10-22 12:35:05 +0900474 }
475 }
476
Daichi Hirono8e873642016-02-07 15:17:16 +0900477 /**
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 */
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
513 boolean deleteDocumentsAndRoots(String selection, String[] args) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900514 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();
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900535 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900536 }
537
Daichi Hirono8e873642016-02-07 15:17:16 +0900538 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
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
Daichi Hironoebd24052016-02-06 21:05:57 +0900571 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
Daichi Hirono61ba9232016-02-26 12:58:39 +0900590 void updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900591 MtpObjectInfo info, Long size) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900592 final ContentValues values = new ContentValues();
Daichi Hirono64111e02016-03-24 21:07:38 +0900593 getObjectDocumentValues(values, deviceId, parentId, operationsSupported, info, size);
Daichi Hironof578fa22016-02-19 18:05:42 +0900594
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
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900608 /**
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 */
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900613 @Nullable Identifier getUnmappedDocumentsParent(int deviceId) {
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900614 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 " +
Daichi Hirono071313e2016-03-18 17:34:29 +0900622 "parent." + COLUMN_DOCUMENT_TYPE + " != ? AND " +
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900623 "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,
Daichi Hirono071313e2016-03-18 17:34:29 +0900632 strings(deviceId, ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE,
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900633 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
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900651 /**
652 * Removes metadata except for data used by outgoingPersistedUriPermissions.
653 */
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
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
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
Daichi Hirono259ce802015-11-20 17:51:53 +0900710 private static class OpenHelper extends SQLiteOpenHelper {
711 public OpenHelper(Context context, int flags) {
712 super(context,
713 flags == FLAG_DATABASE_IN_MEMORY ? null : DATABASE_NAME,
714 null,
715 DATABASE_VERSION);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900716 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900717
718 @Override
719 public void onCreate(SQLiteDatabase db) {
720 db.execSQL(QUERY_CREATE_DOCUMENTS);
721 db.execSQL(QUERY_CREATE_ROOT_EXTRA);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900722 db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT);
Daichi Hirono259ce802015-11-20 17:51:53 +0900723 }
724
725 @Override
726 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900727 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);
Daichi Hironoef2feef2016-02-11 11:40:47 -0800730 onCreate(db);
Daichi Hirono259ce802015-11-20 17:51:53 +0900731 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900732 }
733
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900734 @VisibleForTesting
735 static void deleteDatabase(Context context) {
736 context.deleteDatabase(DATABASE_NAME);
737 }
738
Daichi Hirono81d48532015-12-16 15:03:19 +0900739 static void getDeviceDocumentValues(
740 ContentValues values,
741 ContentValues extraValues,
742 MtpDeviceRecord device) {
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000743 values.clear();
Daichi Hirono20754c52015-12-15 18:52:26 +0900744 values.put(COLUMN_DEVICE_ID, device.deviceId);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000745 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);
Daichi Hironoebd24052016-02-06 21:05:57 +0900750 values.put(COLUMN_MAPPING_KEY, device.deviceKey);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000751 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
Daichi Hirono20754c52015-12-15 18:52:26 +0900752 values.put(Document.COLUMN_DISPLAY_NAME, device.name);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000753 values.putNull(Document.COLUMN_SUMMARY);
754 values.putNull(Document.COLUMN_LAST_MODIFIED);
755 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
Daichi Hironob3383fd2016-03-18 15:25:00 +0900756 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
757 device.operationsSupported,
758 Document.MIME_TYPE_DIR,
759 0,
760 MtpConstants.PROTECTION_STATUS_NONE,
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900761 // Storages are placed under device so we cannot create a document just under
762 // device.
763 DOCUMENT_TYPE_DEVICE) & ~Document.FLAG_DIR_SUPPORTS_CREATE);
Daichi Hirono81d48532015-12-16 15:03:19 +0900764 values.putNull(Document.COLUMN_SIZE);
765
766 extraValues.clear();
Daichi Hirono0f325372016-02-21 15:50:30 +0900767 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(device.operationsSupported));
Daichi Hirono81d48532015-12-16 15:03:19 +0900768 extraValues.putNull(Root.COLUMN_AVAILABLE_BYTES);
769 extraValues.putNull(Root.COLUMN_CAPACITY_BYTES);
770 extraValues.put(Root.COLUMN_MIME_TYPES, "");
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000771 }
772
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900773 /**
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900774 * Gets {@link ContentValues} for the given root.
775 * @param values {@link ContentValues} that receives values.
Daichi Hirono0f325372016-02-21 15:50:30 +0900776 * @param extraValues {@link ContentValues} that receives extra values for roots.
777 * @param parentDocumentId Parent document ID.
Daichi Hironob3383fd2016-03-18 15:25:00 +0900778 * @param operationsSupported Array of Operation code supported by the device.
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900779 * @param root Root to be converted {@link ContentValues}.
780 */
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000781 static void getStorageDocumentValues(
Daichi Hirono81d48532015-12-16 15:03:19 +0900782 ContentValues values,
783 ContentValues extraValues,
Daichi Hirono81d48532015-12-16 15:03:19 +0900784 String parentDocumentId,
Daichi Hirono0f325372016-02-21 15:50:30 +0900785 int[] operationsSupported,
Daichi Hirono81d48532015-12-16 15:03:19 +0900786 MtpRoot root) {
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900787 values.clear();
788 values.put(COLUMN_DEVICE_ID, root.mDeviceId);
789 values.put(COLUMN_STORAGE_ID, root.mStorageId);
790 values.putNull(COLUMN_OBJECT_HANDLE);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000791 values.put(COLUMN_PARENT_DOCUMENT_ID, parentDocumentId);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900792 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
Daichi Hirono4b54e032015-12-11 15:24:53 +0900793 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_STORAGE);
Daichi Hirono0378da42015-11-05 11:56:12 +0900794 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900795 values.put(Document.COLUMN_DISPLAY_NAME, root.mDescription);
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900796 values.putNull(Document.COLUMN_SUMMARY);
797 values.putNull(Document.COLUMN_LAST_MODIFIED);
Daichi Hirono39795da2015-12-02 10:56:44 +0900798 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
Daichi Hironob3383fd2016-03-18 15:25:00 +0900799 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
800 operationsSupported,
801 Document.MIME_TYPE_DIR,
802 0,
803 MtpConstants.PROTECTION_STATUS_NONE,
804 DOCUMENT_TYPE_STORAGE));
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900805 values.put(Document.COLUMN_SIZE, root.mMaxCapacity - root.mFreeSpace);
Daichi Hirono81d48532015-12-16 15:03:19 +0900806
Daichi Hirono0f325372016-02-21 15:50:30 +0900807 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(operationsSupported));
Daichi Hirono81d48532015-12-16 15:03:19 +0900808 extraValues.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
809 extraValues.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
810 extraValues.put(Root.COLUMN_MIME_TYPES, "");
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900811 }
812
813 /**
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900814 * 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.
Daichi Hirono64111e02016-03-24 21:07:38 +0900818 * @param info MTP object info. getCompressedSize will be ignored.
819 * @param size 64-bit size of documents. Negative value is regarded as unknown size.
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900820 */
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000821 static void getObjectDocumentValues(
Daichi Hironob3383fd2016-03-18 15:25:00 +0900822 ContentValues values, int deviceId, String parentId,
Daichi Hirono64111e02016-03-24 21:07:38 +0900823 int[] operationsSupported, MtpObjectInfo info, long size) {
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900824 values.clear();
Daichi Hirono6213cef2016-02-05 17:21:13 +0900825 final String mimeType = getMimeType(info);
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900826 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);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900830 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
Daichi Hirono4b54e032015-12-11 15:24:53 +0900831 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_OBJECT);
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900832 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);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900839 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
840 operationsSupported, mimeType, info.getThumbCompressedSizeLong(),
Daichi Hironob3383fd2016-03-18 15:25:00 +0900841 info.getProtectionStatus(), DOCUMENT_TYPE_OBJECT));
Daichi Hirono64111e02016-03-24 21:07:38 +0900842 if (size >= 0) {
843 values.put(Document.COLUMN_SIZE, size);
844 } else {
845 values.putNull(Document.COLUMN_SIZE);
846 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900847 }
848
Daichi Hirono6213cef2016-02-05 17:21:13 +0900849 private static String getMimeType(MtpObjectInfo info) {
850 if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) {
851 return DocumentsContract.Document.MIME_TYPE_DIR;
852 }
Daichi Hirono497b4732016-03-14 14:46:37 +0900853
Daichi Hirono6213cef2016-02-05 17:21:13 +0900854 final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat());
Daichi Hirono497b4732016-03-14 14:46:37 +0900855 final String mediaFileMimeType = MediaFile.getMimeTypeForFile(info.getName());
856
857 // Format code can be mapped with multiple mime types, e.g. FORMAT_MPEG is mapped with
858 // audio/mp4 and video/mp4.
859 // As file extension contains more information than format code, returns mime type obtained
860 // from file extension if it is consistent with format code.
861 if (mediaFileMimeType != null &&
862 MediaFile.getFormatCode("", mediaFileMimeType) == info.getFormat()) {
863 return mediaFileMimeType;
864 }
Daichi Hirono6213cef2016-02-05 17:21:13 +0900865 if (formatCodeMimeType != null) {
866 return formatCodeMimeType;
867 }
Daichi Hironoef2feef2016-02-11 11:40:47 -0800868 if (mediaFileMimeType != null) {
869 return mediaFileMimeType;
870 }
871 // We don't know the file type.
872 return "application/octet-stream";
Daichi Hirono6213cef2016-02-05 17:21:13 +0900873 }
874
Daichi Hirono0f325372016-02-21 15:50:30 +0900875 private static int getRootFlags(int[] operationsSupported) {
Daichi Hirono83c679e72016-08-18 15:13:36 +0900876 int rootFlag = Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_LOCAL_ONLY;
Daichi Hirono0f325372016-02-21 15:50:30 +0900877 if (MtpDeviceRecord.isWritingSupported(operationsSupported)) {
878 rootFlag |= Root.FLAG_SUPPORTS_CREATE;
879 }
880 return rootFlag;
881 }
882
Daichi Hirono61ba9232016-02-26 12:58:39 +0900883 private static int getDocumentFlags(
Daichi Hironob3383fd2016-03-18 15:25:00 +0900884 @Nullable int[] operationsSupported, String mimeType, long thumbnailSize,
885 int protectionState, @DocumentType int documentType) {
Daichi Hirono61ba9232016-02-26 12:58:39 +0900886 int flag = 0;
Daichi Hironob3383fd2016-03-18 15:25:00 +0900887 if (!mimeType.equals(Document.MIME_TYPE_DIR) &&
888 MtpDeviceRecord.isWritingSupported(operationsSupported) &&
Daichi Hirono61ba9232016-02-26 12:58:39 +0900889 protectionState == MtpConstants.PROTECTION_STATUS_NONE) {
890 flag |= Document.FLAG_SUPPORTS_WRITE;
891 }
892 if (MtpDeviceRecord.isSupported(
893 operationsSupported, MtpConstants.OPERATION_DELETE_OBJECT) &&
894 (protectionState == MtpConstants.PROTECTION_STATUS_NONE ||
Daichi Hironob3383fd2016-03-18 15:25:00 +0900895 protectionState == MtpConstants.PROTECTION_STATUS_NON_TRANSFERABLE_DATA) &&
896 documentType == DOCUMENT_TYPE_OBJECT) {
Daichi Hirono61ba9232016-02-26 12:58:39 +0900897 flag |= Document.FLAG_SUPPORTS_DELETE;
898 }
899 if (mimeType.equals(Document.MIME_TYPE_DIR) &&
900 MtpDeviceRecord.isWritingSupported(operationsSupported) &&
901 protectionState == MtpConstants.PROTECTION_STATUS_NONE) {
902 flag |= Document.FLAG_DIR_SUPPORTS_CREATE;
903 }
Steve McKay5a10ff12017-08-01 15:02:50 -0700904 if (MetadataReader.isSupportedMimeType(mimeType)) {
905 flag |= Document.FLAG_SUPPORTS_METADATA;
906 }
Daichi Hirono61ba9232016-02-26 12:58:39 +0900907 if (thumbnailSize > 0) {
908 flag |= Document.FLAG_SUPPORTS_THUMBNAIL;
909 }
910 return flag;
911 }
912
Daichi Hirono259ce802015-11-20 17:51:53 +0900913 static String[] strings(Object... args) {
914 final String[] results = new String[args.length];
915 for (int i = 0; i < args.length; i++) {
916 results[i] = Objects.toString(args[i]);
917 }
918 return results;
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900919 }
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900920
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900921 static void putValuesToCursor(ContentValues values, MatrixCursor cursor) {
922 final RowBuilder row = cursor.newRow();
923 for (final String name : cursor.getColumnNames()) {
924 row.add(values.get(name));
925 }
926 }
927
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900928 private static String getIdList(Set<String> ids) {
929 String result = "(";
930 for (final String id : ids) {
931 if (result.length() > 1) {
932 result += ",";
933 }
934 result += id;
935 }
936 result += ")";
937 return result;
938 }
Daichi Hirono9678f602015-10-22 12:35:05 +0900939}