blob: 8c8116bd342ae0f120a53b1bc9a6c1108cf58bbe [file] [log] [blame]
/*
* 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);
}
}
}
}