blob: 9b5982b96e31a2f43a451dba8799c0b46e534c37 [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;
Daichi Hirono9678f602015-10-22 12:35:05 +090037import android.provider.DocumentsContract.Document;
Daichi Hirono0378da42015-11-05 11:56:12 +090038import android.provider.DocumentsContract.Root;
Daichi Hirono9678f602015-10-22 12:35:05 +090039
40import com.android.internal.annotations.VisibleForTesting;
Daichi Hirono29657762016-02-10 16:55:37 -080041import com.android.internal.util.Preconditions;
Daichi Hirono9678f602015-10-22 12:35:05 +090042
Daichi Hirono49f920f2015-11-19 10:19:52 +090043import java.io.FileNotFoundException;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090044import java.util.HashSet;
Daichi Hirono259ce802015-11-20 17:51:53 +090045import java.util.Objects;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090046import java.util.Set;
Daichi Hirono9678f602015-10-22 12:35:05 +090047
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.
Daichi Hirono59686f62015-10-28 16:43:32 +090052 *
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 *
Daichi Hironocfcb0c02015-11-10 12:52:59 +090061 * To start putting documents into the database, the client needs to call
Daichi Hirono7a375c42015-12-14 17:14:29 +090062 * {@link Mapper#startAddingDocuments(String)} with the parent document ID. Also it
63 * needs to call {@link Mapper#stopAddingDocuments(String)} after putting all child
Daichi Hirono259ce802015-11-20 17:51:53 +090064 * documents to the database. (All explanations are same for root documents)
Daichi Hironocfcb0c02015-11-10 12:52:59 +090065 *
Daichi Hirono7a375c42015-12-14 17:14:29 +090066 * database.getMapper().startAddingDocuments();
Daichi Hirono259ce802015-11-20 17:51:53 +090067 * database.getMapper().putChildDocuments();
Daichi Hirono7a375c42015-12-14 17:14:29 +090068 * database.getMapper().stopAddingDocuments();
Daichi Hironocfcb0c02015-11-10 12:52:59 +090069 *
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 *
Daichi Hirono6de8a0e2015-10-27 16:18:17 +090082 * TODO: Improve performance by SQL optimization.
Daichi Hirono9678f602015-10-22 12:35:05 +090083 */
Daichi Hirono9678f602015-10-22 12:35:05 +090084class MtpDatabase {
Daichi Hirono259ce802015-11-20 17:51:53 +090085 private final SQLiteDatabase mDatabase;
86 private final Mapper mMapper;
Daichi Hironodc473442015-11-13 15:42:28 +090087
Daichi Hirono259ce802015-11-20 17:51:53 +090088 SQLiteDatabase getSQLiteDatabase() {
89 return mDatabase;
Daichi Hirono9678f602015-10-22 12:35:05 +090090 }
91
Daichi Hirono259ce802015-11-20 17:51:53 +090092 MtpDatabase(Context context, int flags) {
93 final OpenHelper helper = new OpenHelper(context, flags);
94 mDatabase = helper.getWritableDatabase();
95 mMapper = new Mapper(this);
96 }
97
Daichi Hironodc473442015-11-13 15:42:28 +090098 void close() {
99 mDatabase.close();
100 }
101
102 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900103 * Returns operations for mapping.
104 * @return Mapping operations.
Daichi Hironodc473442015-11-13 15:42:28 +0900105 */
Daichi Hirono259ce802015-11-20 17:51:53 +0900106 Mapper getMapper() {
107 return mMapper;
Daichi Hirono0378da42015-11-05 11:56:12 +0900108 }
109
Daichi Hironodc473442015-11-13 15:42:28 +0900110 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900111 * Queries roots information.
112 * @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
113 * @return Database cursor.
114 */
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900115 Cursor queryRoots(Resources resources, String[] columnNames) {
Daichi Hirono81d48532015-12-16 15:03:19 +0900116 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,
Daichi Hirono259ce802015-11-20 17:51:53 +0900124 null,
125 null,
126 null);
Daichi Hirono81d48532015-12-16 15:03:19 +0900127
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();
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900150 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 }
Daichi Hirono81d48532015-12-16 15:03:19 +0900164
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900165 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);
Daichi Hirono81d48532015-12-16 15:03:19 +0900177 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900178 if (availableIndex != -1) {
179 availableBytes += storageCursor.getLong(availableIndex);
180 }
Daichi Hirono81d48532015-12-16 15:03:19 +0900181 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900182 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();
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900190 // Add storage name to device name if we have only 1 storage.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900191 values.put(
192 Root.COLUMN_TITLE,
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900193 resources.getString(
194 R.string.root_name,
195 values.getAsString(Root.COLUMN_TITLE),
196 storageCursor.getString(
197 storageCursor.getColumnIndex(Root.COLUMN_TITLE))));
Daichi Hirono81d48532015-12-16 15:03:19 +0900198 }
199 } finally {
200 storageCursor.close();
201 }
202
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900203 putValuesToCursor(values, result);
Daichi Hirono81d48532015-12-16 15:03:19 +0900204 }
205
206 return result;
207 } finally {
208 deviceCursor.close();
209 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900210 }
211
212 /**
213 * Queries root documents information.
214 * @param columnNames Column names defined in
215 * {@link android.provider.DocumentsContract.Document}.
216 * @return Database cursor.
Daichi Hironodc473442015-11-13 15:42:28 +0900217 */
Daichi Hirono0378da42015-11-05 11:56:12 +0900218 @VisibleForTesting
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900219 Cursor queryRootDocuments(String[] columnNames) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900220 return mDatabase.query(
221 TABLE_DOCUMENTS,
222 columnNames,
Daichi Hirono8e873642016-02-07 15:17:16 +0900223 COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?",
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000224 strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE),
Daichi Hirono259ce802015-11-20 17:51:53 +0900225 null,
226 null,
227 null);
Daichi Hirono9678f602015-10-22 12:35:05 +0900228 }
229
Daichi Hironodc473442015-11-13 15:42:28 +0900230 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900231 * Queries documents information.
232 * @param columnNames Column names defined in
233 * {@link android.provider.DocumentsContract.Document}.
234 * @return Database cursor.
Daichi Hironodc473442015-11-13 15:42:28 +0900235 */
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900236 Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900237 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);
Daichi Hirono47eb1922015-11-16 13:01:31 +0900245 }
246
Daichi Hirono49f920f2015-11-19 10:19:52 +0900247 /**
Daichi Hirono29657762016-02-10 16:55:37 -0800248 * Returns document IDs of storages under the given device document.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900249 *
Daichi Hirono29657762016-02-10 16:55:37 -0800250 * @param documentId Document ID that points a device.
251 * @return Storage document IDs.
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900252 * @throws FileNotFoundException The given document ID is not registered in database.
253 */
Daichi Hirono29657762016-02-10 16:55:37 -0800254 String[] getStorageDocumentIds(String documentId)
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900255 throws FileNotFoundException {
Daichi Hirono29657762016-02-10 16:55:37 -0800256 Preconditions.checkArgument(createIdentifier(documentId).mDocumentType ==
257 DOCUMENT_TYPE_DEVICE);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900258 // 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)) {
Daichi Hirono29657762016-02-10 16:55:37 -0800272 final String[] ids = new String[cursor.getCount()];
273 for (int i = 0; cursor.moveToNext(); i++) {
274 ids[i] = cursor.getString(0);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900275 }
Daichi Hirono29657762016-02-10 16:55:37 -0800276 return ids;
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900277 }
278 }
279
280 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900281 * Queries a single document.
282 * @param documentId
283 * @param projection
284 * @return Database cursor.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900285 */
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900286 Cursor queryDocument(String documentId, String[] projection) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900287 return mDatabase.query(
288 TABLE_DOCUMENTS,
289 projection,
290 SELECTION_DOCUMENT_ID,
291 strings(documentId),
292 null,
293 null,
294 null,
295 "1");
Daichi Hirono49f920f2015-11-19 10:19:52 +0900296 }
297
Daichi Hirono20754c52015-12-15 18:52:26 +0900298 @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
Daichi Hironodc473442015-11-13 15:42:28 +0900319 /**
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900320 * Obtains parent identifier.
Daichi Hirono259ce802015-11-20 17:51:53 +0900321 * @param documentId
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900322 * @return parent identifier.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900323 * @throws FileNotFoundException
324 */
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900325 Identifier getParentIdentifier(String documentId) throws FileNotFoundException {
Daichi Hirono259ce802015-11-20 17:51:53 +0900326 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()) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900337 return createIdentifier(cursor.getString(0));
Daichi Hirono259ce802015-11-20 17:51:53 +0900338 } else {
Daichi Hirono8e873642016-02-07 15:17:16 +0900339 throw new FileNotFoundException("Cannot find a row having ID = " + documentId);
Daichi Hirono259ce802015-11-20 17:51:53 +0900340 }
341 } finally {
342 cursor.close();
343 }
Daichi Hirono49f920f2015-11-19 10:19:52 +0900344 }
345
Daichi Hirono1e374442016-02-11 10:08:21 -0800346 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
Daichi Hirono49f920f2015-11-19 10:19:52 +0900366 /**
Daichi Hirono259ce802015-11-20 17:51:53 +0900367 * 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
Daichi Hirono64111e02016-03-24 21:07:38 +0900372 * @param size Object size. info#getCompressedSize() will be ignored because it does not contain
373 * object size more than 4GB.
Daichi Hirono259ce802015-11-20 17:51:53 +0900374 * @return Document ID of added document.
Daichi Hirono49f920f2015-11-19 10:19:52 +0900375 */
Daichi Hirono61ba9232016-02-26 12:58:39 +0900376 String putNewDocument(
Daichi Hirono64111e02016-03-24 21:07:38 +0900377 int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info,
378 long size) {
Daichi Hirono49f920f2015-11-19 10:19:52 +0900379 final ContentValues values = new ContentValues();
Daichi Hirono64111e02016-03-24 21:07:38 +0900380 getObjectDocumentValues(
381 values, deviceId, parentDocumentId, operationsSupported, info, size);
Daichi Hirono0378da42015-11-05 11:56:12 +0900382 mDatabase.beginTransaction();
383 try {
Daichi Hirono259ce802015-11-20 17:51:53 +0900384 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,
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900410 strings(COLUMN_DEVICE_ID,
411 COLUMN_STORAGE_ID,
412 COLUMN_OBJECT_HANDLE,
413 COLUMN_DOCUMENT_TYPE),
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900414 SELECTION_DOCUMENT_ID + " AND " + COLUMN_ROW_STATE + " IN (?, ?)",
415 strings(documentId, ROW_STATE_VALID, ROW_STATE_INVALIDATED),
Daichi Hirono259ce802015-11-20 17:51:53 +0900416 null,
417 null,
418 null,
419 "1");
420 try {
421 if (cursor.getCount() == 0) {
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900422 throw new FileNotFoundException("ID \"" + documentId + "\" is not found.");
Daichi Hirono259ce802015-11-20 17:51:53 +0900423 } else {
424 cursor.moveToNext();
425 return new Identifier(
426 cursor.getInt(0),
427 cursor.getInt(1),
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900428 cursor.getInt(2),
429 documentId,
430 cursor.getInt(3));
Daichi Hirono49f920f2015-11-19 10:19:52 +0900431 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900432 } 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(
Daichi Hirono8e873642016-02-07 15:17:16 +0900458 COLUMN_PARENT_DOCUMENT_ID + " = ?",
Daichi Hirono259ce802015-11-20 17:51:53 +0900459 strings(cursor.getString(0)))) {
460 changed = true;
461 }
Daichi Hironodc473442015-11-13 15:42:28 +0900462 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900463 } finally {
464 cursor.close();
Daichi Hironodc473442015-11-13 15:42:28 +0900465 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900466 if (deleteDocumentsAndRoots(selection, args)) {
467 changed = true;
Daichi Hirono0378da42015-11-05 11:56:12 +0900468 }
469 mDatabase.setTransactionSuccessful();
Daichi Hironodc473442015-11-13 15:42:28 +0900470 return changed;
Daichi Hirono0378da42015-11-05 11:56:12 +0900471 } finally {
472 mDatabase.endTransaction();
Daichi Hirono9678f602015-10-22 12:35:05 +0900473 }
474 }
475
Daichi Hirono8e873642016-02-07 15:17:16 +0900476 /**
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) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900513 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();
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900534 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900535 }
536
Daichi Hirono8e873642016-02-07 15:17:16 +0900537 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
Daichi Hironoebd24052016-02-06 21:05:57 +0900570 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
Daichi Hirono61ba9232016-02-26 12:58:39 +0900589 void updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900590 MtpObjectInfo info, Long size) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900591 final ContentValues values = new ContentValues();
Daichi Hirono64111e02016-03-24 21:07:38 +0900592 getObjectDocumentValues(values, deviceId, parentId, operationsSupported, info, size);
Daichi Hironof578fa22016-02-19 18:05:42 +0900593
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
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900607 /**
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 */
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900612 @Nullable Identifier getUnmappedDocumentsParent(int deviceId) {
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900613 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 " +
Daichi Hirono071313e2016-03-18 17:34:29 +0900621 "parent." + COLUMN_DOCUMENT_TYPE + " != ? AND " +
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900622 "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,
Daichi Hirono071313e2016-03-18 17:34:29 +0900631 strings(deviceId, ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE,
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900632 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
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900650 /**
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
Daichi Hirono259ce802015-11-20 17:51:53 +0900709 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);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900715 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900716
717 @Override
718 public void onCreate(SQLiteDatabase db) {
719 db.execSQL(QUERY_CREATE_DOCUMENTS);
720 db.execSQL(QUERY_CREATE_ROOT_EXTRA);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900721 db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT);
Daichi Hirono259ce802015-11-20 17:51:53 +0900722 }
723
724 @Override
725 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900726 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);
Daichi Hironoef2feef2016-02-11 11:40:47 -0800729 onCreate(db);
Daichi Hirono259ce802015-11-20 17:51:53 +0900730 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900731 }
732
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900733 @VisibleForTesting
734 static void deleteDatabase(Context context) {
735 context.deleteDatabase(DATABASE_NAME);
736 }
737
Daichi Hirono81d48532015-12-16 15:03:19 +0900738 static void getDeviceDocumentValues(
739 ContentValues values,
740 ContentValues extraValues,
741 MtpDeviceRecord device) {
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000742 values.clear();
Daichi Hirono20754c52015-12-15 18:52:26 +0900743 values.put(COLUMN_DEVICE_ID, device.deviceId);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000744 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);
Daichi Hironoebd24052016-02-06 21:05:57 +0900749 values.put(COLUMN_MAPPING_KEY, device.deviceKey);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000750 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
Daichi Hirono20754c52015-12-15 18:52:26 +0900751 values.put(Document.COLUMN_DISPLAY_NAME, device.name);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000752 values.putNull(Document.COLUMN_SUMMARY);
753 values.putNull(Document.COLUMN_LAST_MODIFIED);
754 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
Daichi Hironob3383fd2016-03-18 15:25:00 +0900755 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
756 device.operationsSupported,
757 Document.MIME_TYPE_DIR,
758 0,
759 MtpConstants.PROTECTION_STATUS_NONE,
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900760 // Storages are placed under device so we cannot create a document just under
761 // device.
762 DOCUMENT_TYPE_DEVICE) & ~Document.FLAG_DIR_SUPPORTS_CREATE);
Daichi Hirono81d48532015-12-16 15:03:19 +0900763 values.putNull(Document.COLUMN_SIZE);
764
765 extraValues.clear();
Daichi Hirono0f325372016-02-21 15:50:30 +0900766 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(device.operationsSupported));
Daichi Hirono81d48532015-12-16 15:03:19 +0900767 extraValues.putNull(Root.COLUMN_AVAILABLE_BYTES);
768 extraValues.putNull(Root.COLUMN_CAPACITY_BYTES);
769 extraValues.put(Root.COLUMN_MIME_TYPES, "");
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000770 }
771
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900772 /**
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900773 * Gets {@link ContentValues} for the given root.
774 * @param values {@link ContentValues} that receives values.
Daichi Hirono0f325372016-02-21 15:50:30 +0900775 * @param extraValues {@link ContentValues} that receives extra values for roots.
776 * @param parentDocumentId Parent document ID.
Daichi Hironob3383fd2016-03-18 15:25:00 +0900777 * @param operationsSupported Array of Operation code supported by the device.
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900778 * @param root Root to be converted {@link ContentValues}.
779 */
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000780 static void getStorageDocumentValues(
Daichi Hirono81d48532015-12-16 15:03:19 +0900781 ContentValues values,
782 ContentValues extraValues,
Daichi Hirono81d48532015-12-16 15:03:19 +0900783 String parentDocumentId,
Daichi Hirono0f325372016-02-21 15:50:30 +0900784 int[] operationsSupported,
Daichi Hirono81d48532015-12-16 15:03:19 +0900785 MtpRoot root) {
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900786 values.clear();
787 values.put(COLUMN_DEVICE_ID, root.mDeviceId);
788 values.put(COLUMN_STORAGE_ID, root.mStorageId);
789 values.putNull(COLUMN_OBJECT_HANDLE);
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000790 values.put(COLUMN_PARENT_DOCUMENT_ID, parentDocumentId);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900791 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
Daichi Hirono4b54e032015-12-11 15:24:53 +0900792 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_STORAGE);
Daichi Hirono0378da42015-11-05 11:56:12 +0900793 values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900794 values.put(Document.COLUMN_DISPLAY_NAME, root.mDescription);
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900795 values.putNull(Document.COLUMN_SUMMARY);
796 values.putNull(Document.COLUMN_LAST_MODIFIED);
Daichi Hirono39795da2015-12-02 10:56:44 +0900797 values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
Daichi Hironob3383fd2016-03-18 15:25:00 +0900798 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
799 operationsSupported,
800 Document.MIME_TYPE_DIR,
801 0,
802 MtpConstants.PROTECTION_STATUS_NONE,
803 DOCUMENT_TYPE_STORAGE));
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900804 values.put(Document.COLUMN_SIZE, root.mMaxCapacity - root.mFreeSpace);
Daichi Hirono81d48532015-12-16 15:03:19 +0900805
Daichi Hirono0f325372016-02-21 15:50:30 +0900806 extraValues.put(Root.COLUMN_FLAGS, getRootFlags(operationsSupported));
Daichi Hirono81d48532015-12-16 15:03:19 +0900807 extraValues.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
808 extraValues.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
809 extraValues.put(Root.COLUMN_MIME_TYPES, "");
Daichi Hirono6de8a0e2015-10-27 16:18:17 +0900810 }
811
812 /**
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900813 * 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.
Daichi Hirono64111e02016-03-24 21:07:38 +0900817 * @param info MTP object info. getCompressedSize will be ignored.
818 * @param size 64-bit size of documents. Negative value is regarded as unknown size.
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900819 */
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000820 static void getObjectDocumentValues(
Daichi Hironob3383fd2016-03-18 15:25:00 +0900821 ContentValues values, int deviceId, String parentId,
Daichi Hirono64111e02016-03-24 21:07:38 +0900822 int[] operationsSupported, MtpObjectInfo info, long size) {
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900823 values.clear();
Daichi Hirono6213cef2016-02-05 17:21:13 +0900824 final String mimeType = getMimeType(info);
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900825 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);
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900829 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
Daichi Hirono4b54e032015-12-11 15:24:53 +0900830 values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_OBJECT);
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900831 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);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900838 values.put(Document.COLUMN_FLAGS, getDocumentFlags(
839 operationsSupported, mimeType, info.getThumbCompressedSizeLong(),
Daichi Hironob3383fd2016-03-18 15:25:00 +0900840 info.getProtectionStatus(), DOCUMENT_TYPE_OBJECT));
Daichi Hirono64111e02016-03-24 21:07:38 +0900841 if (size >= 0) {
842 values.put(Document.COLUMN_SIZE, size);
843 } else {
844 values.putNull(Document.COLUMN_SIZE);
845 }
Daichi Hironoa8a3722e2015-10-26 16:39:54 +0900846 }
847
Daichi Hirono6213cef2016-02-05 17:21:13 +0900848 private static String getMimeType(MtpObjectInfo info) {
849 if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) {
850 return DocumentsContract.Document.MIME_TYPE_DIR;
851 }
Daichi Hirono497b4732016-03-14 14:46:37 +0900852
Daichi Hirono6213cef2016-02-05 17:21:13 +0900853 final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat());
Daichi Hirono497b4732016-03-14 14:46:37 +0900854 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 }
Daichi Hirono6213cef2016-02-05 17:21:13 +0900864 if (formatCodeMimeType != null) {
865 return formatCodeMimeType;
866 }
Daichi Hironoef2feef2016-02-11 11:40:47 -0800867 if (mediaFileMimeType != null) {
868 return mediaFileMimeType;
869 }
870 // We don't know the file type.
871 return "application/octet-stream";
Daichi Hirono6213cef2016-02-05 17:21:13 +0900872 }
873
Daichi Hirono0f325372016-02-21 15:50:30 +0900874 private static int getRootFlags(int[] operationsSupported) {
Daichi Hirono83c679e72016-08-18 15:13:36 +0900875 int rootFlag = Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_LOCAL_ONLY;
Daichi Hirono0f325372016-02-21 15:50:30 +0900876 if (MtpDeviceRecord.isWritingSupported(operationsSupported)) {
877 rootFlag |= Root.FLAG_SUPPORTS_CREATE;
878 }
879 return rootFlag;
880 }
881
Daichi Hirono61ba9232016-02-26 12:58:39 +0900882 private static int getDocumentFlags(
Daichi Hironob3383fd2016-03-18 15:25:00 +0900883 @Nullable int[] operationsSupported, String mimeType, long thumbnailSize,
884 int protectionState, @DocumentType int documentType) {
Daichi Hirono61ba9232016-02-26 12:58:39 +0900885 int flag = 0;
Daichi Hironob3383fd2016-03-18 15:25:00 +0900886 if (!mimeType.equals(Document.MIME_TYPE_DIR) &&
887 MtpDeviceRecord.isWritingSupported(operationsSupported) &&
Daichi Hirono61ba9232016-02-26 12:58:39 +0900888 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 ||
Daichi Hironob3383fd2016-03-18 15:25:00 +0900894 protectionState == MtpConstants.PROTECTION_STATUS_NON_TRANSFERABLE_DATA) &&
895 documentType == DOCUMENT_TYPE_OBJECT) {
Daichi Hirono61ba9232016-02-26 12:58:39 +0900896 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
Daichi Hirono259ce802015-11-20 17:51:53 +0900909 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;
Daichi Hironocfcb0c02015-11-10 12:52:59 +0900915 }
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900916
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900917 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
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900924 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 }
Daichi Hirono9678f602015-10-22 12:35:05 +0900935}