Remove convenience method from DocumentsProvider, clients to use MetadataReader directly. Concentrate mimetype checking in MetadataReader.isSupportedType. Update FileSystemProvider to use MetadataReader directly. Test: Updated mtp tests. Other functioanlity manually verified. Change-Id: Ie1e3d3092b53107f6c980c18b1451290dd2a9653
857 lines
34 KiB
Java
857 lines
34 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.mtp;
|
|
|
|
import android.annotation.Nullable;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.UriPermission;
|
|
import android.content.res.AssetFileDescriptor;
|
|
import android.content.res.Resources;
|
|
import android.database.Cursor;
|
|
import android.database.DatabaseUtils;
|
|
import android.database.MatrixCursor;
|
|
import android.database.sqlite.SQLiteDiskIOException;
|
|
import android.graphics.Point;
|
|
import android.media.MediaFile;
|
|
import android.mtp.MtpConstants;
|
|
import android.mtp.MtpObjectInfo;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.CancellationSignal;
|
|
import android.os.FileUtils;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.ProxyFileDescriptorCallback;
|
|
import android.os.storage.StorageManager;
|
|
import android.provider.DocumentsContract;
|
|
import android.provider.DocumentsContract.Document;
|
|
import android.provider.DocumentsContract.Path;
|
|
import android.provider.DocumentsContract.Root;
|
|
import android.provider.DocumentsProvider;
|
|
import android.provider.MetadataReader;
|
|
import android.provider.Settings;
|
|
import android.system.ErrnoException;
|
|
import android.system.OsConstants;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
|
|
import libcore.io.IoUtils;
|
|
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.TimeoutException;
|
|
|
|
/**
|
|
* DocumentsProvider for MTP devices.
|
|
*/
|
|
public class MtpDocumentsProvider extends DocumentsProvider {
|
|
static final String AUTHORITY = "com.android.mtp.documents";
|
|
static final String TAG = "MtpDocumentsProvider";
|
|
static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
|
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
|
|
Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
|
|
Root.COLUMN_AVAILABLE_BYTES,
|
|
};
|
|
static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
|
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
|
|
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
|
|
Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
|
|
};
|
|
|
|
static final boolean DEBUG = false;
|
|
|
|
private final Object mDeviceListLock = new Object();
|
|
|
|
private static MtpDocumentsProvider sSingleton;
|
|
|
|
private MtpManager mMtpManager;
|
|
private ContentResolver mResolver;
|
|
@GuardedBy("mDeviceListLock")
|
|
private Map<Integer, DeviceToolkit> mDeviceToolkits;
|
|
private RootScanner mRootScanner;
|
|
private Resources mResources;
|
|
private MtpDatabase mDatabase;
|
|
private ServiceIntentSender mIntentSender;
|
|
private Context mContext;
|
|
private StorageManager mStorageManager;
|
|
|
|
/**
|
|
* Provides singleton instance to MtpDocumentsService.
|
|
*/
|
|
static MtpDocumentsProvider getInstance() {
|
|
return sSingleton;
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreate() {
|
|
sSingleton = this;
|
|
mContext = getContext();
|
|
mResources = getContext().getResources();
|
|
mMtpManager = new MtpManager(getContext());
|
|
mResolver = getContext().getContentResolver();
|
|
mDeviceToolkits = new HashMap<>();
|
|
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
|
|
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
|
mIntentSender = new ServiceIntentSender(getContext());
|
|
mStorageManager = getContext().getSystemService(StorageManager.class);
|
|
|
|
// Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
|
|
// after booting.
|
|
try {
|
|
final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
|
|
final int lastBootCount = mDatabase.getLastBootCount();
|
|
if (bootCount != -1 && bootCount != lastBootCount) {
|
|
mDatabase.setLastBootCount(bootCount);
|
|
final List<UriPermission> permissions =
|
|
mResolver.getOutgoingPersistedUriPermissions();
|
|
final Uri[] uris = new Uri[permissions.size()];
|
|
for (int i = 0; i < permissions.size(); i++) {
|
|
uris[i] = permissions.get(i).getUri();
|
|
}
|
|
mDatabase.cleanDatabase(uris);
|
|
}
|
|
} catch (SQLiteDiskIOException error) {
|
|
// It can happen due to disk shortage.
|
|
Log.e(TAG, "Failed to clean database.", error);
|
|
return false;
|
|
}
|
|
|
|
resume();
|
|
return true;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
boolean onCreateForTesting(
|
|
Context context,
|
|
Resources resources,
|
|
MtpManager mtpManager,
|
|
ContentResolver resolver,
|
|
MtpDatabase database,
|
|
StorageManager storageManager,
|
|
ServiceIntentSender intentSender) {
|
|
mContext = context;
|
|
mResources = resources;
|
|
mMtpManager = mtpManager;
|
|
mResolver = resolver;
|
|
mDeviceToolkits = new HashMap<>();
|
|
mDatabase = database;
|
|
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
|
mIntentSender = intentSender;
|
|
mStorageManager = storageManager;
|
|
|
|
resume();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
|
if (projection == null) {
|
|
projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
|
|
}
|
|
final Cursor cursor = mDatabase.queryRoots(mResources, projection);
|
|
cursor.setNotificationUri(
|
|
mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
|
|
return cursor;
|
|
}
|
|
|
|
@Override
|
|
public Cursor queryDocument(String documentId, String[] projection)
|
|
throws FileNotFoundException {
|
|
if (projection == null) {
|
|
projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
|
|
}
|
|
final Cursor cursor = mDatabase.queryDocument(documentId, projection);
|
|
final int cursorCount = cursor.getCount();
|
|
if (cursorCount == 0) {
|
|
cursor.close();
|
|
throw new FileNotFoundException();
|
|
} else if (cursorCount != 1) {
|
|
cursor.close();
|
|
Log.wtf(TAG, "Unexpected cursor size: " + cursorCount);
|
|
return null;
|
|
}
|
|
|
|
final Identifier identifier = mDatabase.createIdentifier(documentId);
|
|
if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
|
|
return cursor;
|
|
}
|
|
final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId);
|
|
if (storageDocIds.length != 1) {
|
|
return mDatabase.queryDocument(documentId, projection);
|
|
}
|
|
|
|
// If the documentId specifies a device having exact one storage, we repalce some device
|
|
// attributes with the storage attributes.
|
|
try {
|
|
final String storageName;
|
|
final int storageFlags;
|
|
try (final Cursor storageCursor = mDatabase.queryDocument(
|
|
storageDocIds[0],
|
|
MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) {
|
|
if (!storageCursor.moveToNext()) {
|
|
throw new FileNotFoundException();
|
|
}
|
|
storageName = storageCursor.getString(0);
|
|
storageFlags = storageCursor.getInt(1);
|
|
}
|
|
|
|
cursor.moveToNext();
|
|
final ContentValues values = new ContentValues();
|
|
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
|
if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) {
|
|
values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString(
|
|
R.string.root_name,
|
|
values.getAsString(Document.COLUMN_DISPLAY_NAME),
|
|
storageName));
|
|
}
|
|
values.put(Document.COLUMN_FLAGS, storageFlags);
|
|
final MatrixCursor output = new MatrixCursor(projection, 1);
|
|
MtpDatabase.putValuesToCursor(values, output);
|
|
return output;
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Cursor queryChildDocuments(String parentDocumentId,
|
|
String[] projection, String sortOrder) throws FileNotFoundException {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
|
|
}
|
|
if (projection == null) {
|
|
projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
|
|
}
|
|
Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
|
|
try {
|
|
openDevice(parentIdentifier.mDeviceId);
|
|
if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
|
|
final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
|
|
if (storageDocIds.length == 0) {
|
|
// Remote device does not provide storages. Maybe it is locked.
|
|
return createErrorCursor(projection, R.string.error_locked_device);
|
|
} else if (storageDocIds.length > 1) {
|
|
// Returns storage list from database.
|
|
return mDatabase.queryChildDocuments(projection, parentDocumentId);
|
|
}
|
|
|
|
// Exact one storage is found. Skip storage and returns object in the single
|
|
// storage.
|
|
parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
|
|
}
|
|
|
|
// Returns object list from document loader.
|
|
return getDocumentLoader(parentIdentifier).queryChildDocuments(
|
|
projection, parentIdentifier);
|
|
} catch (BusyDeviceException exception) {
|
|
return createErrorCursor(projection, R.string.error_busy_device);
|
|
} catch (IOException exception) {
|
|
Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
|
|
throw new FileNotFoundException(exception.getMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ParcelFileDescriptor openDocument(
|
|
String documentId, String mode, CancellationSignal signal)
|
|
throws FileNotFoundException {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "openDocument: " + documentId);
|
|
}
|
|
final Identifier identifier = mDatabase.createIdentifier(documentId);
|
|
try {
|
|
openDevice(identifier.mDeviceId);
|
|
final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
|
|
// Turn off MODE_CREATE because openDocument does not allow to create new files.
|
|
final int modeFlag =
|
|
ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
|
|
if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
|
|
long fileSize;
|
|
try {
|
|
fileSize = getFileSize(documentId);
|
|
} catch (UnsupportedOperationException exception) {
|
|
fileSize = -1;
|
|
}
|
|
if (MtpDeviceRecord.isPartialReadSupported(
|
|
device.operationsSupported, fileSize)) {
|
|
|
|
return mStorageManager.openProxyFileDescriptor(
|
|
modeFlag,
|
|
new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
|
|
} else {
|
|
// If getPartialObject{|64} are not supported for the device, returns
|
|
// non-seekable pipe FD instead.
|
|
return getPipeManager(identifier).readDocument(mMtpManager, identifier);
|
|
}
|
|
} else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
|
|
// TODO: Clear the parent document loader task (if exists) and call notify
|
|
// when writing is completed.
|
|
if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
|
|
return mStorageManager.openProxyFileDescriptor(
|
|
modeFlag,
|
|
new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
|
|
} else {
|
|
throw new UnsupportedOperationException(
|
|
"The device does not support writing operation.");
|
|
}
|
|
} else {
|
|
// TODO: Add support for "rw" mode.
|
|
throw new UnsupportedOperationException("The provider does not support 'rw' mode.");
|
|
}
|
|
} catch (FileNotFoundException | RuntimeException error) {
|
|
Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
|
|
throw error;
|
|
} catch (IOException error) {
|
|
Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
|
|
throw new IllegalStateException(error);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public AssetFileDescriptor openDocumentThumbnail(
|
|
String documentId,
|
|
Point sizeHint,
|
|
CancellationSignal signal) throws FileNotFoundException {
|
|
final Identifier identifier = mDatabase.createIdentifier(documentId);
|
|
try {
|
|
openDevice(identifier.mDeviceId);
|
|
return new AssetFileDescriptor(
|
|
getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
|
|
0, // Start offset.
|
|
AssetFileDescriptor.UNKNOWN_LENGTH);
|
|
} catch (IOException error) {
|
|
Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
|
|
throw new FileNotFoundException(error.getMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void deleteDocument(String documentId) throws FileNotFoundException {
|
|
try {
|
|
final Identifier identifier = mDatabase.createIdentifier(documentId);
|
|
openDevice(identifier.mDeviceId);
|
|
final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
|
|
mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
|
|
mDatabase.deleteDocument(documentId);
|
|
getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier);
|
|
notifyChildDocumentsChange(parentIdentifier.mDocumentId);
|
|
if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
|
|
// If the parent is storage, the object might be appeared as child of device because
|
|
// we skip storage when the device has only one storage.
|
|
final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
|
|
parentIdentifier.mDocumentId);
|
|
notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
|
|
}
|
|
} catch (IOException error) {
|
|
Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
|
|
throw new FileNotFoundException(error.getMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTrimMemory(int level) {
|
|
synchronized (mDeviceListLock) {
|
|
for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
|
|
toolkit.mDocumentLoader.clearCompletedTasks();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
|
throws FileNotFoundException {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "createDocument: " + displayName);
|
|
}
|
|
final Identifier parentId;
|
|
final MtpDeviceRecord record;
|
|
final ParcelFileDescriptor[] pipe;
|
|
try {
|
|
parentId = mDatabase.createIdentifier(parentDocumentId);
|
|
openDevice(parentId.mDeviceId);
|
|
record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
|
|
if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
|
|
throw new UnsupportedOperationException(
|
|
"Writing operation is not supported by the device.");
|
|
}
|
|
|
|
final int parentObjectHandle;
|
|
final int storageId;
|
|
switch (parentId.mDocumentType) {
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
|
|
final String[] storageDocumentIds =
|
|
mDatabase.getStorageDocumentIds(parentId.mDocumentId);
|
|
if (storageDocumentIds.length == 1) {
|
|
final String newDocumentId =
|
|
createDocument(storageDocumentIds[0], mimeType, displayName);
|
|
notifyChildDocumentsChange(parentDocumentId);
|
|
return newDocumentId;
|
|
} else {
|
|
throw new UnsupportedOperationException(
|
|
"Cannot create a file under the device.");
|
|
}
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE:
|
|
storageId = parentId.mStorageId;
|
|
parentObjectHandle = -1;
|
|
break;
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
|
|
storageId = parentId.mStorageId;
|
|
parentObjectHandle = parentId.mObjectHandle;
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unexpected document type.");
|
|
}
|
|
|
|
pipe = ParcelFileDescriptor.createReliablePipe();
|
|
int objectHandle = -1;
|
|
MtpObjectInfo info = null;
|
|
try {
|
|
pipe[0].close(); // 0 bytes for a new document.
|
|
|
|
final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
|
|
MtpConstants.FORMAT_ASSOCIATION :
|
|
MediaFile.getFormatCode(displayName, mimeType);
|
|
info = new MtpObjectInfo.Builder()
|
|
.setStorageId(storageId)
|
|
.setParent(parentObjectHandle)
|
|
.setFormat(formatCode)
|
|
.setName(displayName)
|
|
.build();
|
|
|
|
final String[] parts = FileUtils.splitFileName(mimeType, displayName);
|
|
final String baseName = parts[0];
|
|
final String extension = parts[1];
|
|
for (int i = 0; i <= 32; i++) {
|
|
final MtpObjectInfo infoUniqueName;
|
|
if (i == 0) {
|
|
infoUniqueName = info;
|
|
} else {
|
|
String suffixedName = baseName + " (" + i + " )";
|
|
if (!extension.isEmpty()) {
|
|
suffixedName += "." + extension;
|
|
}
|
|
infoUniqueName =
|
|
new MtpObjectInfo.Builder(info).setName(suffixedName).build();
|
|
}
|
|
try {
|
|
objectHandle = mMtpManager.createDocument(
|
|
parentId.mDeviceId, infoUniqueName, pipe[1]);
|
|
break;
|
|
} catch (SendObjectInfoFailure exp) {
|
|
// This can be caused when we have an existing file with the same name.
|
|
continue;
|
|
}
|
|
}
|
|
} finally {
|
|
pipe[1].close();
|
|
}
|
|
if (objectHandle == -1) {
|
|
throw new IllegalArgumentException(
|
|
"The file name \"" + displayName + "\" is conflicted with existing files " +
|
|
"and the provider failed to find unique name.");
|
|
}
|
|
final MtpObjectInfo infoWithHandle =
|
|
new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
|
|
final String documentId = mDatabase.putNewDocument(
|
|
parentId.mDeviceId, parentDocumentId, record.operationsSupported,
|
|
infoWithHandle, 0l);
|
|
getDocumentLoader(parentId).cancelTask(parentId);
|
|
notifyChildDocumentsChange(parentDocumentId);
|
|
return documentId;
|
|
} catch (FileNotFoundException | RuntimeException error) {
|
|
Log.e(TAG, "createDocument", error);
|
|
throw error;
|
|
} catch (IOException error) {
|
|
Log.e(TAG, "createDocument", error);
|
|
throw new IllegalStateException(error);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Path findDocumentPath(String parentDocumentId, String childDocumentId)
|
|
throws FileNotFoundException {
|
|
final LinkedList<String> ids = new LinkedList<>();
|
|
final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId);
|
|
|
|
Identifier i = childIdentifier;
|
|
outer: while (true) {
|
|
if (i.mDocumentId.equals(parentDocumentId)) {
|
|
ids.addFirst(i.mDocumentId);
|
|
break;
|
|
}
|
|
switch (i.mDocumentType) {
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
|
|
ids.addFirst(i.mDocumentId);
|
|
i = mDatabase.getParentIdentifier(i.mDocumentId);
|
|
break;
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: {
|
|
// Check if there is the multiple storage.
|
|
final Identifier deviceIdentifier =
|
|
mDatabase.getParentIdentifier(i.mDocumentId);
|
|
final String[] storageIds =
|
|
mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId);
|
|
// Add storage's document ID to the path only when the device has multiple
|
|
// storages.
|
|
if (storageIds.length > 1) {
|
|
ids.addFirst(i.mDocumentId);
|
|
break outer;
|
|
}
|
|
i = deviceIdentifier;
|
|
break;
|
|
}
|
|
case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
|
|
ids.addFirst(i.mDocumentId);
|
|
break outer;
|
|
}
|
|
}
|
|
|
|
if (parentDocumentId != null) {
|
|
return new Path(null, ids);
|
|
} else {
|
|
return new Path(/* Should be same with root ID */ i.mDocumentId, ids);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
|
try {
|
|
Identifier identifier = mDatabase.createIdentifier(documentId);
|
|
while (true) {
|
|
if (parentDocumentId.equals(identifier.mDocumentId)) {
|
|
return true;
|
|
}
|
|
if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
|
|
return false;
|
|
}
|
|
identifier = mDatabase.getParentIdentifier(identifier.mDocumentId);
|
|
}
|
|
} catch (FileNotFoundException error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
|
|
String mimeType = getDocumentType(docId);
|
|
|
|
if (!MetadataReader.isSupportedMimeType(mimeType)) {
|
|
return null;
|
|
}
|
|
|
|
InputStream stream = null;
|
|
try {
|
|
stream = new ParcelFileDescriptor.AutoCloseInputStream(
|
|
openDocument(docId, "r", null));
|
|
Bundle metadata = new Bundle();
|
|
MetadataReader.getMetadata(metadata, stream, mimeType, null);
|
|
return metadata;
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "An error occurred retrieving the metadata", e);
|
|
return null;
|
|
} finally {
|
|
IoUtils.closeQuietly(stream);
|
|
}
|
|
}
|
|
|
|
void openDevice(int deviceId) throws IOException {
|
|
synchronized (mDeviceListLock) {
|
|
if (mDeviceToolkits.containsKey(deviceId)) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Open device " + deviceId);
|
|
}
|
|
final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
|
|
final DeviceToolkit toolkit =
|
|
new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
|
|
mDeviceToolkits.put(deviceId, toolkit);
|
|
mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
|
|
try {
|
|
mRootScanner.resume().await();
|
|
} catch (InterruptedException error) {
|
|
Log.e(TAG, "openDevice", error);
|
|
}
|
|
// Resume document loader to remap disconnected document ID. Must be invoked after the
|
|
// root scanner resumes.
|
|
toolkit.mDocumentLoader.resume();
|
|
}
|
|
}
|
|
|
|
void closeDevice(int deviceId) throws IOException, InterruptedException {
|
|
synchronized (mDeviceListLock) {
|
|
closeDeviceInternal(deviceId);
|
|
mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
|
|
}
|
|
mRootScanner.resume();
|
|
}
|
|
|
|
MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
|
|
synchronized (mDeviceListLock) {
|
|
final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
|
|
int i = 0;
|
|
for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
|
|
records[i] = toolkit.mDeviceRecord;
|
|
i++;
|
|
}
|
|
return records;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtains document ID for the given device ID.
|
|
* @param deviceId
|
|
* @return document ID
|
|
* @throws FileNotFoundException device ID has not been build.
|
|
*/
|
|
public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
|
|
return mDatabase.getDeviceDocumentId(deviceId);
|
|
}
|
|
|
|
/**
|
|
* Resumes root scanner to handle the update of device list.
|
|
*/
|
|
void resumeRootScanner() {
|
|
if (DEBUG) {
|
|
Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
|
|
}
|
|
mRootScanner.resume();
|
|
}
|
|
|
|
/**
|
|
* Finalize the content provider for unit tests.
|
|
*/
|
|
@Override
|
|
public void shutdown() {
|
|
synchronized (mDeviceListLock) {
|
|
try {
|
|
// Copy the opened key set because it will be modified when closing devices.
|
|
final Integer[] keySet =
|
|
mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
|
|
for (final int id : keySet) {
|
|
closeDeviceInternal(id);
|
|
}
|
|
mRootScanner.pause();
|
|
} catch (InterruptedException | IOException | TimeoutException e) {
|
|
// It should fail unit tests by throwing runtime exception.
|
|
throw new RuntimeException(e);
|
|
} finally {
|
|
mDatabase.close();
|
|
super.shutdown();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void notifyChildDocumentsChange(String parentDocumentId) {
|
|
mResolver.notifyChange(
|
|
DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
|
|
null,
|
|
false);
|
|
}
|
|
|
|
/**
|
|
* Clears MTP identifier in the database.
|
|
*/
|
|
private void resume() {
|
|
synchronized (mDeviceListLock) {
|
|
mDatabase.getMapper().clearMapping();
|
|
}
|
|
}
|
|
|
|
private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
|
|
// TODO: Flush the device before closing (if not closed externally).
|
|
if (!mDeviceToolkits.containsKey(deviceId)) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Close device " + deviceId);
|
|
}
|
|
getDeviceToolkit(deviceId).close();
|
|
mDeviceToolkits.remove(deviceId);
|
|
mMtpManager.closeDevice(deviceId);
|
|
}
|
|
|
|
private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
|
|
synchronized (mDeviceListLock) {
|
|
final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
|
|
if (toolkit == null) {
|
|
throw new FileNotFoundException();
|
|
}
|
|
return toolkit;
|
|
}
|
|
}
|
|
|
|
private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
|
|
return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
|
|
}
|
|
|
|
private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
|
|
return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
|
|
}
|
|
|
|
private long getFileSize(String documentId) throws FileNotFoundException {
|
|
final Cursor cursor = mDatabase.queryDocument(
|
|
documentId,
|
|
MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
|
|
try {
|
|
if (cursor.moveToNext()) {
|
|
if (cursor.isNull(0)) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
return cursor.getLong(0);
|
|
} else {
|
|
throw new FileNotFoundException();
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates empty cursor with specific error message.
|
|
*
|
|
* @param projection Column names.
|
|
* @param stringResId String resource ID of error message.
|
|
* @return Empty cursor with error message.
|
|
*/
|
|
private Cursor createErrorCursor(String[] projection, int stringResId) {
|
|
final Bundle bundle = new Bundle();
|
|
bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
|
|
final Cursor cursor = new MatrixCursor(projection);
|
|
cursor.setExtras(bundle);
|
|
return cursor;
|
|
}
|
|
|
|
private static class DeviceToolkit implements AutoCloseable {
|
|
public final PipeManager mPipeManager;
|
|
public final DocumentLoader mDocumentLoader;
|
|
public final MtpDeviceRecord mDeviceRecord;
|
|
|
|
public DeviceToolkit(MtpManager manager,
|
|
ContentResolver resolver,
|
|
MtpDatabase database,
|
|
MtpDeviceRecord record) {
|
|
mPipeManager = new PipeManager(database);
|
|
mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
|
|
mDeviceRecord = record;
|
|
}
|
|
|
|
@Override
|
|
public void close() throws InterruptedException {
|
|
mPipeManager.close();
|
|
mDocumentLoader.close();
|
|
}
|
|
}
|
|
|
|
private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
|
|
private final int mInode;
|
|
private MtpFileWriter mWriter;
|
|
|
|
MtpProxyFileDescriptorCallback(int inode) {
|
|
mInode = inode;
|
|
}
|
|
|
|
@Override
|
|
public long onGetSize() throws ErrnoException {
|
|
try {
|
|
return getFileSize(String.valueOf(mInode));
|
|
} catch (FileNotFoundException e) {
|
|
Log.e(TAG, e.getMessage(), e);
|
|
throw new ErrnoException("onGetSize", OsConstants.ENOENT);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int onRead(long offset, int size, byte[] data) throws ErrnoException {
|
|
try {
|
|
final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode));
|
|
final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
|
|
if (MtpDeviceRecord.isSupported(
|
|
record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
|
|
|
|
return (int) mMtpManager.getPartialObject64(
|
|
identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
|
|
|
|
}
|
|
if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
|
|
record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
|
|
return (int) mMtpManager.getPartialObject(
|
|
identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
|
|
}
|
|
throw new ErrnoException("onRead", OsConstants.ENOTSUP);
|
|
} catch (IOException e) {
|
|
Log.e(TAG, e.getMessage(), e);
|
|
throw new ErrnoException("onRead", OsConstants.EIO);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
|
|
try {
|
|
if (mWriter == null) {
|
|
mWriter = new MtpFileWriter(mContext, String.valueOf(mInode));
|
|
}
|
|
return mWriter.write(offset, size, data);
|
|
} catch (IOException e) {
|
|
Log.e(TAG, e.getMessage(), e);
|
|
throw new ErrnoException("onWrite", OsConstants.EIO);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFsync() throws ErrnoException {
|
|
tryFsync();
|
|
}
|
|
|
|
@Override
|
|
public void onRelease() {
|
|
try {
|
|
tryFsync();
|
|
} catch (ErrnoException error) {
|
|
// Cannot recover from the error at onRelease. Client app should use fsync to
|
|
// ensure the provider writes data correctly.
|
|
Log.e(TAG, "Cannot recover from the error at onRelease.", error);
|
|
} finally {
|
|
if (mWriter != null) {
|
|
IoUtils.closeQuietly(mWriter);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void tryFsync() throws ErrnoException {
|
|
try {
|
|
if (mWriter != null) {
|
|
final MtpDeviceRecord device =
|
|
getDeviceToolkit(mDatabase.createIdentifier(
|
|
mWriter.getDocumentId()).mDeviceId).mDeviceRecord;
|
|
mWriter.flush(mMtpManager, mDatabase, device.operationsSupported);
|
|
}
|
|
} catch (IOException e) {
|
|
Log.e(TAG, e.getMessage(), e);
|
|
throw new ErrnoException("onWrite", OsConstants.EIO);
|
|
}
|
|
}
|
|
}
|
|
}
|