summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSudheer Shanka <sudheersai@google.com>2019-05-16 16:56:54 -0700
committerSudheer Shanka <sudheersai@google.com>2019-05-21 12:03:11 -0700
commitbf94a4e6c2cdb4bad60324661cff12a8fbd8ae30 (patch)
tree34cd9b2305cd181312e1a92e6859d30435691aa2 /src
parentd79866aadd52570901ad49a2d73097e7878fef31 (diff)
downloadandroid_packages_providers_DownloadProvider-bf94a4e6c2cdb4bad60324661cff12a8fbd8ae30.tar.gz
android_packages_providers_DownloadProvider-bf94a4e6c2cdb4bad60324661cff12a8fbd8ae30.tar.bz2
android_packages_providers_DownloadProvider-bf94a4e6c2cdb4bad60324661cff12a8fbd8ae30.zip
Don't use linked mediastore uris in DownloadProvider operations.
When MediaProvider db gets recreated, all the media content ids get renumbered. It's possible that when DownloadProvider is trying to delete an entry, it is holding onto a invalid mediastore uri. So, don't use linked mediastore uris in DownloadProvider operations. Also, revoke any prior uri grants of media content from DownloadStorageProvider. Bug: 132087334 Test: manual Test: atest DownloadProviderTests Test: atest cts/tests/app/src/android/app/cts/DownloadManagerTest.java Test: atest cts/tests/app/DownloadManagerLegacyTest/src/android/app/cts/DownloadManagerLegacyTest.java Test: atest cts/tests/app/DownloadManagerApi28Test/src/android/app/cts/DownloadManagerApi28Test.java Test: atest cts/hostsidetests/appsecurity/src/android/appsecurity/cts/AppSecurityTests.java Change-Id: If6fb479da7e937ecdfa23136811f3456f7bcd75c
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java167
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java99
2 files changed, 78 insertions, 188 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index a2f8532e..550c8fba 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -79,6 +79,7 @@ import android.util.SparseArray;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
@@ -636,8 +637,7 @@ public final class DownloadProvider extends ContentProvider {
public Bundle call(String method, String arg, Bundle extras) {
switch (method) {
case Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED: {
- getContext().enforceCallingOrSelfPermission(
- android.Manifest.permission.WRITE_MEDIA_STORAGE,
+ Preconditions.checkArgument(Binder.getCallingUid() == Process.myUid(),
"Not allowed to call " + Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED);
final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS);
final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES);
@@ -662,6 +662,12 @@ public final class DownloadProvider extends ContentProvider {
}
return null;
}
+ case Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS : {
+ Preconditions.checkArgument(Binder.getCallingUid() == Process.myUid(),
+ "Not allowed to call " + Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS);
+ DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext());
+ return null;
+ }
default:
throw new UnsupportedOperationException("Unsupported call: " + method);
}
@@ -1324,82 +1330,7 @@ public final class DownloadProvider extends ContentProvider {
final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
- // map of volumeName -> { mediastore ids that need to be queried }
- final ArrayMap<String, LongArray> mediaStoreIdsForVolumes = new ArrayMap<>();
- try (Cursor cursor = qb.query(db, new String[] { Downloads.Impl.COLUMN_MEDIASTORE_URI },
- selection, selectionArgs, null, null, sort)) {
- while (cursor.moveToNext()) {
- final String uriString = cursor.getString(0);
- if (uriString == null) {
- continue;
- }
- final Uri mediaStoreUri = Uri.parse(uriString);
- final String volumeName = MediaStore.getVolumeName(mediaStoreUri);
- LongArray ids = mediaStoreIdsForVolumes.get(volumeName);
- if (ids == null) {
- ids = new LongArray();
- mediaStoreIdsForVolumes.put(volumeName, ids);
- }
- ids.add(ContentUris.parseId(mediaStoreUri));
- }
- }
- // map of volumeName -> { map of {mediastore id -> mediastore data} }
- final ArrayMap<String, LongSparseArray<String>> mediaStoreDataForVolumes
- = new ArrayMap<>();
- final CallingIdentity token = clearCallingIdentity();
- try (ContentProviderClient client = getContext().getContentResolver()
- .acquireContentProviderClient(MediaStore.AUTHORITY)) {
- final String[] projectionIn = new String[] {
- MediaStore.Downloads._ID,
- MediaStore.Downloads.DATA,
- };
- for (int i = 0; i < mediaStoreIdsForVolumes.size(); ++i) {
- final String volumeName = mediaStoreIdsForVolumes.keyAt(i);
- final LongArray ids = mediaStoreIdsForVolumes.valueAt(i);
- final LongSparseArray<String> mediaStoreDataForIds
- = new LongSparseArray<>();
- mediaStoreDataForVolumes.put(volumeName, mediaStoreDataForIds);
- try (Cursor mediaCursor = getMediaProviderRowsForIds(
- client, projectionIn, volumeName, ids)) {
- while (mediaCursor.moveToNext()) {
- final long id = mediaCursor.getLong(0);
- final String filePath = mediaCursor.getString(1);
- mediaStoreDataForIds.put(id, filePath);
- }
- }
- }
- } catch (RemoteException e) {
- // Should not happen
- } finally {
- restoreCallingIdentity(token);
- }
-
- final TranslatingCursor.Config config = getTranslatingCursorConfig(match);
- final TranslatingCursor.Translator translator
- = (data, auxiliaryColIndex, matchingColumn, cursor) -> {
- final String uriString = cursor.getString(auxiliaryColIndex);
- if (uriString != null) {
- final Uri mediaStoreUri = Uri.parse(uriString);
- final String volumeName = MediaStore.getVolumeName(mediaStoreUri);
- final LongSparseArray<String> mediaStoreDataForIds
- = mediaStoreDataForVolumes.get(volumeName);
- if (mediaStoreDataForIds != null) {
- switch (matchingColumn) {
- case Downloads.Impl._DATA:
- case Downloads.Impl.COLUMN_FILE_NAME_HINT:
- case DownloadManager.COLUMN_LOCAL_FILENAME:
- final long id = ContentUris.parseId(mediaStoreUri);
- return mediaStoreDataForIds.get(id, data);
- default:
- return data;
- }
- }
- }
-
- return data;
- };
- final Cursor ret = TranslatingCursor.query(config, translator,
- qb, db, projection, selection, selectionArgs, null, null, sort, null, null);
+ final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort);
if (ret != null) {
ret.setNotificationUri(getContext().getContentResolver(), uri);
@@ -1416,39 +1347,6 @@ public final class DownloadProvider extends ContentProvider {
return ret;
}
- private Cursor getMediaProviderRowsForIds(ContentProviderClient mediaProvider,
- String[] projection, String volumeName, LongArray ids) throws RemoteException {
- final StringBuilder queryString = new StringBuilder();
- queryString.append(MediaStore.Downloads._ID + " in (");
- final int size = ids.size();
- for (int i = 0; i < size; ++i) {
- queryString.append(ids.get(i));
- queryString.append((i == size - 1) ? ")" : ",");
- }
- return mediaProvider.query(MediaStore.Downloads.getContentUri(volumeName),
- projection, queryString.toString(), null, null);
- }
-
- private TranslatingCursor.Config getTranslatingCursorConfig(int match) {
- final Uri baseUri;
- switch (match) {
- case MY_DOWNLOADS:
- case MY_DOWNLOADS_ID:
- baseUri = Downloads.Impl.CONTENT_URI;
- break;
- case ALL_DOWNLOADS:
- case ALL_DOWNLOADS_ID:
- baseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
- break;
- default:
- baseUri = null;
- }
- return new TranslatingCursor.Config(baseUri, Downloads.Impl.COLUMN_MEDIASTORE_URI,
- Downloads.Impl._DATA,
- Downloads.Impl.COLUMN_FILE_NAME_HINT,
- DownloadManager.COLUMN_LOCAL_FILENAME);
- }
-
private void logVerboseQueryInfo(String[] projection, final String selection,
final String[] selectionArgs, final String sort, SQLiteDatabase db) {
java.lang.StringBuilder sb = new java.lang.StringBuilder();
@@ -1622,7 +1520,8 @@ public final class DownloadProvider extends ContentProvider {
|| (info.mMediaScanned != MEDIA_NOT_SCANNABLE);
if (info.mFileName == null) {
if (info.mMediaStoreUri != null) {
- client.delete(Uri.parse(info.mMediaStoreUri), null, null);
+ // If there was a mediastore entry, it would be deleted in it's
+ // next idle pass.
updateValues.clear();
updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
qb.update(db, updateValues, Downloads.Impl._ID + "=?",
@@ -1660,8 +1559,6 @@ public final class DownloadProvider extends ContentProvider {
info.sendIntentIfRequested();
}
}
- } catch (RemoteException e) {
- // Should not happen
} finally {
restoreCallingIdentity(token);
}
@@ -1788,32 +1685,18 @@ public final class DownloadProvider extends ContentProvider {
final String path = info.mFileName;
if (!TextUtils.isEmpty(path)) {
- boolean fileDeleted = false;
try {
final File file = new File(path).getCanonicalFile();
if (Helpers.isFilenameValid(getContext(), file)) {
Log.v(Constants.TAG,
"Deleting " + file + " via provider delete");
file.delete();
- fileDeleted = true;
+ deleteMediaStoreEntry(file);
+ } else {
+ Log.d(Constants.TAG, "Ignoring invalid file: " + file);
}
- } catch (IOException ignore) {
- }
- if (!fileDeleted) {
- Log.d(Constants.TAG, "Ignoring invalid path: " + path);
- }
- }
-
- final String mediaUri = info.mMediaStoreUri;
- if (!TextUtils.isEmpty(mediaUri)) {
- final long token = Binder.clearCallingIdentity();
- try {
- getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
- null);
- } catch (Exception e) {
- Log.w(Constants.TAG, "Failed to delete media entry: " + e);
- } finally {
- Binder.restoreCallingIdentity(token);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Couldn't delete file: " + path, e);
}
}
@@ -1848,6 +1731,24 @@ public final class DownloadProvider extends ContentProvider {
return count;
}
+ private void deleteMediaStoreEntry(File file) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final String path = file.getAbsolutePath();
+ final Uri.Builder builder = MediaStore.setIncludePending(
+ MediaStore.Files.getContentUriForPath(path).buildUpon());
+ builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
+
+ final Uri filesUri = builder.build();
+ getContext().getContentResolver().delete(filesUri,
+ MediaStore.Files.FileColumns.DATA + "=?", new String[] { path });
+ } catch (Exception e) {
+ Log.d(Constants.TAG, "Failed to delete mediastore entry for file:" + file, e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
/**
* Remotely opens a file
*/
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
index 171bdd3d..549619e4 100644
--- a/src/com/android/providers/downloads/DownloadStorageProvider.java
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -30,12 +30,11 @@ import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
+import android.content.UriPermission;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
-import android.database.sqlite.SQLiteQueryBuilder;
import android.media.MediaFile;
-import android.mtp.MtpConstants;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -53,7 +52,6 @@ import android.provider.MediaStore;
import android.provider.MediaStore.DownloadColumns;
import android.text.TextUtils;
import android.util.Log;
-import android.util.LongArray;
import android.util.Pair;
import com.android.internal.annotations.GuardedBy;
@@ -66,6 +64,7 @@ import java.io.FileNotFoundException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -97,15 +96,6 @@ public class DownloadStorageProvider extends FileSystemProvider {
private DownloadManager mDm;
- private static final String[] DOWNLOADS_PROJECTION
- = new String[DownloadManager.UNDERLYING_COLUMNS.length + 1];
- static {
- System.arraycopy(DownloadManager.UNDERLYING_COLUMNS, 0,
- DOWNLOADS_PROJECTION, 0, DownloadManager.UNDERLYING_COLUMNS.length);
- DOWNLOADS_PROJECTION[DOWNLOADS_PROJECTION.length - 1]
- = Downloads.Impl.COLUMN_MEDIASTORE_URI;
- }
-
private static final int NO_LIMIT = -1;
@Override
@@ -148,6 +138,22 @@ public class DownloadStorageProvider extends FileSystemProvider {
}
}
+ static void revokeAllMediaStoreUriPermissions(Context context) {
+ final List<UriPermission> uriPermissions =
+ context.getContentResolver().getOutgoingUriPermissions();
+ final int size = uriPermissions.size();
+ final StringBuilder sb = new StringBuilder("Revoking permissions for uris: ");
+ for (int i = 0; i < size; ++i) {
+ final Uri uri = uriPermissions.get(i).getUri();
+ if (AUTHORITY.equals(uri.getAuthority())
+ && isMediaStoreDownload(DocumentsContract.getDocumentId(uri))) {
+ context.revokeUriPermission(uri, ~0);
+ sb.append(uri + ",");
+ }
+ }
+ Log.d(TAG, sb.toString());
+ }
+
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
// It's possible that the folder does not exist on disk, so we will create the folder if
@@ -288,14 +294,13 @@ public class DownloadStorageProvider extends FileSystemProvider {
includeDownloadFromMediaStore(result, cursor, null /* filePaths */);
}
} else {
- cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)),
- DOWNLOADS_PROJECTION);
+ cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
// We don't know if this queryDocument() call is from Downloads (manage)
// or Files. Safely assume it's Files.
includeDownloadFromCursor(result, cursor, null /* filePaths */,
- null /* mediaStoreIds */, null /* queryArgs */);
+ null /* queryArgs */);
}
}
result.start();
@@ -335,29 +340,25 @@ public class DownloadStorageProvider extends FileSystemProvider {
final ArrayList<Uri> notificationUris = new ArrayList<>();
if (isMediaStoreDownloadDir(parentDocId)) {
includeDownloadsFromMediaStore(result, null /* queryArgs */,
- null /* idsToExclude */, null /* filePaths */, notificationUris,
+ null /* filePaths */, notificationUris,
getMediaStoreIdString(parentDocId), NO_LIMIT, manage);
} else {
assert (DOC_ID_ROOT.equals(parentDocId));
if (manage) {
cursor = mDm.query(
- new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true),
- DOWNLOADS_PROJECTION);
+ new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
} else {
cursor = mDm.query(
new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
- .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL),
- DOWNLOADS_PROJECTION);
+ .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
}
final Set<String> filePaths = new HashSet<>();
- final LongArray mediaStoreIds = new LongArray();
while (cursor.moveToNext()) {
- includeDownloadFromCursor(result, cursor, filePaths, mediaStoreIds,
- null /* queryArgs */);
+ includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */);
}
notificationUris.add(cursor.getNotificationUri());
includeDownloadsFromMediaStore(result, null /* queryArgs */,
- mediaStoreIds, filePaths, notificationUris,
+ filePaths, notificationUris,
null /* parentId */, NO_LIMIT, manage);
includeFilesFromSharedStorage(result, filePaths, null);
}
@@ -401,9 +402,8 @@ public class DownloadStorageProvider extends FileSystemProvider {
final ArrayList<Uri> notificationUris = new ArrayList<>();
try {
cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
- .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL),
- DOWNLOADS_PROJECTION);
- final LongArray mediaStoreIds = new LongArray();
+ .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
+ final Set<String> filePaths = new HashSet<>();
while (cursor.moveToNext() && result.getCount() < limit) {
final String mimeType = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
@@ -417,8 +417,8 @@ public class DownloadStorageProvider extends FileSystemProvider {
|| MediaFile.isVideoMimeType(mimeType)) && !TextUtils.isEmpty(uri)) {
continue;
}
- includeDownloadFromCursor(result, cursor, null /* filePaths */,
- mediaStoreIds, null /* queryArgs */);
+ includeDownloadFromCursor(result, cursor, filePaths,
+ null /* queryArgs */);
}
notificationUris.add(cursor.getNotificationUri());
@@ -427,7 +427,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
final Bundle args = new Bundle();
args.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true);
- includeDownloadsFromMediaStore(result, args, mediaStoreIds, null /* filePaths */,
+ includeDownloadsFromMediaStore(result, args, filePaths,
notificationUris, null /* parentId */, (limit - result.getCount()),
false /* includePending */);
} finally {
@@ -453,15 +453,13 @@ public class DownloadStorageProvider extends FileSystemProvider {
Cursor cursor = null;
try {
cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
- .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)),
- DOWNLOADS_PROJECTION);
- final LongArray mediaStoreIds = new LongArray();
+ .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)));
final Set<String> filePaths = new HashSet<>();
while (cursor.moveToNext()) {
- includeDownloadFromCursor(result, cursor, filePaths, mediaStoreIds, queryArgs);
+ includeDownloadFromCursor(result, cursor, filePaths, queryArgs);
}
notificationUris.add(cursor.getNotificationUri());
- includeDownloadsFromMediaStore(result, queryArgs, mediaStoreIds, filePaths,
+ includeDownloadsFromMediaStore(result, queryArgs, filePaths,
notificationUris, null /* parentId */, NO_LIMIT, true /* includePending */);
includeSearchFilesFromSharedStorage(result, projection, filePaths, queryArgs);
@@ -572,8 +570,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
Cursor cursor = null;
String localFilePath = null;
try {
- cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)),
- DOWNLOADS_PROJECTION);
+ cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
if (cursor.moveToFirst()) {
localFilePath = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
@@ -620,7 +617,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
* if the file exists in the file system.
*/
private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor,
- Set<String> filePaths, LongArray mediaStoreIds, Bundle queryArgs) {
+ Set<String> filePaths, Bundle queryArgs) {
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
final String docId = String.valueOf(id);
@@ -703,14 +700,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
includeDownload(result, docId, displayName, summary, size, mimeType,
lastModified, extraFlags, status == DownloadManager.STATUS_RUNNING);
- if (mediaStoreIds != null) {
- final String mediaStoreUri = cursor.getString(
- cursor.getColumnIndex(Downloads.Impl.COLUMN_MEDIASTORE_URI));
- if (mediaStoreUri != null) {
- mediaStoreIds.add(ContentUris.parseId(Uri.parse(mediaStoreUri)));
- }
- }
- if (filePaths != null) {
+ if (filePaths != null && localFilePath != null) {
filePaths.add(localFilePath);
}
}
@@ -881,7 +871,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
}
private void includeDownloadsFromMediaStore(@NonNull MatrixCursor result,
- @Nullable Bundle queryArgs, @Nullable LongArray idsToExclude,
+ @Nullable Bundle queryArgs,
@Nullable Set<String> filePaths, @NonNull ArrayList<Uri> notificationUris,
@Nullable String parentId, int limit, boolean includePending) {
if (limit == 0) {
@@ -890,7 +880,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
final long token = Binder.clearCallingIdentity();
final Pair<String, String[]> selectionPair
- = buildSearchSelection(queryArgs, idsToExclude, parentId);
+ = buildSearchSelection(queryArgs, filePaths, parentId);
final Uri.Builder queryUriBuilder = MediaStore.Downloads.EXTERNAL_CONTENT_URI.buildUpon();
if (limit != NO_LIMIT) {
queryUriBuilder.appendQueryParameter(MediaStore.PARAM_LIMIT, String.valueOf(limit));
@@ -953,19 +943,18 @@ public class DownloadStorageProvider extends FileSystemProvider {
// Copied from MediaDocumentsProvider with some tweaks
private Pair<String, String[]> buildSearchSelection(@Nullable Bundle queryArgs,
- @Nullable LongArray idsToExclude, @Nullable String parentId) {
+ @Nullable Set<String> filePaths, @Nullable String parentId) {
final StringBuilder selection = new StringBuilder();
final ArrayList<String> selectionArgs = new ArrayList<>();
- if (parentId == null && idsToExclude != null && idsToExclude.size() > 0) {
+ if (parentId == null && filePaths != null && filePaths.size() > 0) {
if (selection.length() > 0) {
selection.append(" AND ");
}
- selection.append(DownloadColumns._ID + " NOT IN (");
- final int size = idsToExclude.size();
- for (int i = 0; i < size; ++i) {
- selection.append(idsToExclude.get(i) + ((i == size - 1) ? ")" : ","));
- }
+ selection.append(DownloadColumns.DATA + " NOT IN (");
+ selection.append(TextUtils.join(",", Collections.nCopies(filePaths.size(), "?")));
+ selection.append(")");
+ selectionArgs.addAll(filePaths);
}
if (parentId != null) {