Merge "Use AppFuse to write document." into nyc-dev
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index 9ed2abf..f10af43 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -45,6 +45,8 @@
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
+import android.system.ErrnoException;
+import android.system.Os;
import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -451,7 +453,7 @@
ParcelFileDescriptor srcFile = null;
ParcelFileDescriptor dstFile = null;
InputStream in = null;
- OutputStream out = null;
+ ParcelFileDescriptor.AutoCloseOutputStream out = null;
boolean success = false;
try {
@@ -502,6 +504,8 @@
makeCopyProgress(len);
}
+ // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
+ IoUtils.close(dstFile.getFileDescriptor());
srcFile.checkError();
} catch (IOException e) {
throw new ResourceException(
diff --git a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
index 1d4ed1d..eb96015 100644
--- a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
+++ b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
@@ -52,6 +52,8 @@
static jmethodID app_fuse_get_file_size;
static jmethodID app_fuse_read_object_bytes;
static jmethodID app_fuse_write_object_bytes;
+static jmethodID app_fuse_flush_file_handle;
+static jmethodID app_fuse_close_file_handle;
static jfieldID app_fuse_buffer;
// NOTE:
@@ -307,7 +309,8 @@
const uint32_t size = in->size;
const void* const buffer = reinterpret_cast<const uint8_t*>(in) + sizeof(fuse_write_in);
uint32_t written_size;
- const int result = write_object_bytes(it->second, offset, size, buffer, &written_size);
+ const int result = write_object_bytes(
+ in->fh, it->second, offset, size, buffer, &written_size);
if (result < 0) {
return result;
}
@@ -320,13 +323,13 @@
const fuse_release_in* in,
FuseResponse<void>* /* out */) {
handles_.erase(in->fh);
- return 0;
+ return env_->CallIntMethod(self_, app_fuse_close_file_handle, file_handle_to_jlong(in->fh));
}
int handle_fuse_flush(const fuse_in_header& /* header */,
- const void* /* in */,
+ const fuse_flush_in* in,
FuseResponse<void>* /* out */) {
- return 0;
+ return env_->CallIntMethod(self_, app_fuse_flush_file_handle, file_handle_to_jlong(in->fh));
}
template <typename T, typename S>
@@ -382,8 +385,10 @@
return read_size;
}
- int write_object_bytes(int inode, uint64_t offset, uint32_t size, const void* buffer,
- uint32_t* written_size) {
+ int write_object_bytes(uint64_t handle, int inode, uint64_t offset, uint32_t size,
+ const void* buffer, uint32_t* written_size) {
+ static_assert(sizeof(uint64_t) <= sizeof(jlong),
+ "jlong must be able to express any uint64_t values");
ScopedLocalRef<jbyteArray> array(
env_,
static_cast<jbyteArray>(env_->GetObjectField(self_, app_fuse_buffer)));
@@ -394,15 +399,28 @@
}
memcpy(bytes.get(), buffer, size);
}
- *written_size = env_->CallIntMethod(
- self_, app_fuse_write_object_bytes, inode, offset, size, array.get());
- if (env_->ExceptionCheck()) {
- env_->ExceptionClear();
- return -EIO;
+ const int result = env_->CallIntMethod(
+ self_,
+ app_fuse_write_object_bytes,
+ file_handle_to_jlong(handle),
+ inode,
+ offset,
+ size,
+ array.get());
+ if (result < 0) {
+ return result;
}
+ *written_size = result;
return 0;
}
+ static jlong file_handle_to_jlong(uint64_t handle) {
+ static_assert(
+ sizeof(uint64_t) <= sizeof(jlong),
+ "jlong must be able to express any uint64_t values");
+ return static_cast<jlong>(handle);
+ }
+
static void fuse_reply(int fd, int unique, int reply_code, void* reply_data,
size_t reply_size) {
// Don't send any data for error case.
@@ -511,15 +529,21 @@
return -1;
}
- app_fuse_buffer = env->GetFieldID(app_fuse_class, "mBuffer", "[B");
- if (app_fuse_buffer == nullptr) {
- ALOGE("Can't find mBuffer");
+ app_fuse_write_object_bytes = env->GetMethodID(app_fuse_class, "writeObjectBytes", "(JIJI[B)I");
+ if (app_fuse_write_object_bytes == nullptr) {
+ ALOGE("Can't find writeObjectBytes");
return -1;
}
- app_fuse_write_object_bytes = env->GetMethodID(app_fuse_class, "writeObjectBytes", "(IJI[B)I");
- if (app_fuse_write_object_bytes == nullptr) {
- ALOGE("Can't find getWriteObjectBytes");
+ app_fuse_flush_file_handle = env->GetMethodID(app_fuse_class, "flushFileHandle", "(J)I");
+ if (app_fuse_flush_file_handle == nullptr) {
+ ALOGE("Can't find flushFileHandle");
+ return -1;
+ }
+
+ app_fuse_close_file_handle = env->GetMethodID(app_fuse_class, "closeFileHandle", "(J)I");
+ if (app_fuse_close_file_handle == nullptr) {
+ ALOGE("Can't find closeFileHandle");
return -1;
}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
index 777dc60..88858a8 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
@@ -20,6 +20,7 @@
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.storage.StorageManager;
+import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -34,6 +35,8 @@
System.loadLibrary("appfuse_jni");
}
+ private static final boolean DEBUG = false;
+
/**
* Max read amount specified at the FUSE kernel implementation.
* The value is copied from sdcard.c.
@@ -94,7 +97,8 @@
public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
Preconditions.checkArgument(
mode == ParcelFileDescriptor.MODE_READ_ONLY ||
- mode == ParcelFileDescriptor.MODE_WRITE_ONLY);
+ mode == (ParcelFileDescriptor.MODE_WRITE_ONLY |
+ ParcelFileDescriptor.MODE_TRUNCATE));
return ParcelFileDescriptor.open(new File(
getMountPoint(),
Integer.toString(i)),
@@ -127,6 +131,7 @@
/**
* Handles writing bytes for the give inode.
+ * @param fileHandle
* @param inode
* @param offset Offset for file bytes.
* @param size Size for file bytes.
@@ -134,7 +139,23 @@
* @return Number of read bytes. Must not be negative.
* @throws IOException
*/
- int writeObjectBytes(int inode, long offset, int size, byte[] bytes) throws IOException;
+ int writeObjectBytes(long fileHandle, int inode, long offset, int size, byte[] bytes)
+ throws IOException, ErrnoException;
+
+ /**
+ * Flushes bytes for file handle.
+ * @param fileHandle
+ * @throws IOException
+ * @throws ErrnoException
+ */
+ void flushFileHandle(long fileHandle) throws IOException, ErrnoException;
+
+ /**
+ * Closes file handle.
+ * @param fileHandle
+ * @throws IOException
+ */
+ void closeFileHandle(long fileHandle) throws IOException, ErrnoException;
}
@UsedByNative("com_android_mtp_AppFuse.cpp")
@@ -142,10 +163,8 @@
private long getFileSize(int inode) {
try {
return mCallback.getFileSize(inode);
- } catch (FileNotFoundException e) {
- return -OsConstants.ENOENT;
- } catch (UnsupportedOperationException e) {
- return -OsConstants.ENOTSUP;
+ } catch (Exception error) {
+ return -getErrnoFromException(error);
}
}
@@ -159,20 +178,62 @@
// It's OK to share the same mBuffer among requests because the requests are processed
// by AppFuseMessageThread sequentially.
return mCallback.readObjectBytes(inode, offset, size, mBuffer);
- } catch (IOException e) {
- return -OsConstants.EIO;
- } catch (UnsupportedOperationException e) {
- return -OsConstants.ENOTSUP;
+ } catch (Exception error) {
+ return -getErrnoFromException(error);
}
}
@UsedByNative("com_android_mtp_AppFuse.cpp")
@WorkerThread
- private /* unsgined */ int writeObjectBytes(int inode,
+ private /* unsgined */ int writeObjectBytes(long fileHandler,
+ int inode,
/* unsigned */ long offset,
/* unsigned */ int size,
- byte[] bytes) throws IOException {
- return mCallback.writeObjectBytes(inode, offset, size, bytes);
+ byte[] bytes) {
+ try {
+ return mCallback.writeObjectBytes(fileHandler, inode, offset, size, bytes);
+ } catch (Exception error) {
+ return -getErrnoFromException(error);
+ }
+ }
+
+ @UsedByNative("com_android_mtp_AppFuse.cpp")
+ @WorkerThread
+ private int flushFileHandle(long fileHandle) {
+ try {
+ mCallback.flushFileHandle(fileHandle);
+ return 0;
+ } catch (Exception error) {
+ return -getErrnoFromException(error);
+ }
+ }
+
+ @UsedByNative("com_android_mtp_AppFuse.cpp")
+ @WorkerThread
+ private int closeFileHandle(long fileHandle) {
+ try {
+ mCallback.closeFileHandle(fileHandle);
+ return 0;
+ } catch (Exception error) {
+ return -getErrnoFromException(error);
+ }
+ }
+
+ private static int getErrnoFromException(Exception error) {
+ if (DEBUG) {
+ Log.e(MtpDocumentsProvider.TAG, "AppFuse callbacks", error);
+ }
+ if (error instanceof FileNotFoundException) {
+ return OsConstants.ENOENT;
+ } else if (error instanceof IOException) {
+ return OsConstants.EIO;
+ } else if (error instanceof UnsupportedOperationException) {
+ return OsConstants.ENOTSUP;
+ } else if (error instanceof IllegalArgumentException) {
+ return OsConstants.EINVAL;
+ } else {
+ return OsConstants.EIO;
+ }
}
private native boolean native_start_app_fuse_loop(int fd);
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 9f64046ce..50781bf 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -17,6 +17,7 @@
package com.android.mtp;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.UriPermission;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
@@ -38,11 +39,16 @@
import android.provider.DocumentsContract;
import android.provider.DocumentsProvider;
import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
@@ -82,6 +88,7 @@
private MtpDatabase mDatabase;
private AppFuse mAppFuse;
private ServiceIntentSender mIntentSender;
+ private Context mContext;
/**
* Provides singleton instance to MtpDocumentsService.
@@ -93,6 +100,7 @@
@Override
public boolean onCreate() {
sSingleton = this;
+ mContext = getContext();
mResources = getContext().getResources();
mMtpManager = new MtpManager(getContext());
mResolver = getContext().getContentResolver();
@@ -137,12 +145,14 @@
@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;
@@ -232,43 +242,43 @@
try {
openDevice(identifier.mDeviceId);
final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
- switch (mode) {
- case "r":
- long fileSize;
- try {
- fileSize = getFileSize(documentId);
- } catch (UnsupportedOperationException exception) {
- fileSize = -1;
- }
- // MTP getPartialObject operation does not support files that are larger than
- // 4GB. Fallback to non-seekable file descriptor.
- if (MtpDeviceRecord.isPartialReadSupported(
- device.operationsSupported, fileSize)) {
- return mAppFuse.openFile(
- Integer.parseInt(documentId), ParcelFileDescriptor.MODE_READ_ONLY);
- } else {
- return getPipeManager(identifier).readDocument(mMtpManager, identifier);
- }
- case "w":
- // TODO: Clear the parent document loader task (if exists) and call notify
- // when writing is completed.
- if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
- return getPipeManager(identifier).writeDocument(
- getContext(), mMtpManager, identifier, device.operationsSupported);
- } else {
- throw new UnsupportedOperationException(
- "The device does not support writing operation.");
- }
- case "rw":
- // TODO: Add support for "rw" mode.
+ // 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 mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
+ } 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 mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
+ } else {
throw new UnsupportedOperationException(
- "The provider does not support 'rw' mode.");
- default:
- throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
+ "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 FileNotFoundException(error.getMessage());
+ throw new IllegalStateException(error);
}
}
@@ -595,6 +605,13 @@
}
private class AppFuseCallback implements AppFuse.Callback {
+ private final Map<Long, MtpFileWriter> mWriters = new HashMap<>();
+
+ @Override
+ public long getFileSize(int inode) throws FileNotFoundException {
+ return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
+ }
+
@Override
public long readObjectBytes(
int inode, long offset, long size, byte[] buffer) throws IOException {
@@ -617,15 +634,43 @@
}
@Override
- public long getFileSize(int inode) throws FileNotFoundException {
- return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
+ public int writeObjectBytes(
+ long fileHandle, int inode, long offset, int size, byte[] bytes)
+ throws IOException, ErrnoException {
+ final MtpFileWriter writer;
+ if (mWriters.containsKey(fileHandle)) {
+ writer = mWriters.get(fileHandle);
+ } else {
+ writer = new MtpFileWriter(mContext, String.valueOf(inode));
+ mWriters.put(fileHandle, writer);
+ }
+ return writer.write(offset, size, bytes);
}
@Override
- public int writeObjectBytes(int inode, long offset, int size, byte[] bytes)
- throws IOException {
- // TODO: Implement it.
- throw new IOException();
+ public void flushFileHandle(long fileHandle) throws IOException, ErrnoException {
+ final MtpFileWriter writer = mWriters.get(fileHandle);
+ if (writer == null) {
+ // File handle for reading.
+ return;
+ }
+ final MtpDeviceRecord device = getDeviceToolkit(
+ mDatabase.createIdentifier(writer.getDocumentId()).mDeviceId).mDeviceRecord;
+ writer.flush(mMtpManager, mDatabase, device.operationsSupported);
+ }
+
+ @Override
+ public void closeFileHandle(long fileHandle) throws IOException, ErrnoException {
+ final MtpFileWriter writer = mWriters.get(fileHandle);
+ if (writer == null) {
+ // File handle for reading.
+ return;
+ }
+ try {
+ writer.close();
+ } finally {
+ mWriters.remove(fileHandle);
+ }
}
}
}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpFileWriter.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpFileWriter.java
new file mode 100644
index 0000000..3e1bedc
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpFileWriter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.File;
+import java.io.IOException;
+
+class MtpFileWriter implements AutoCloseable {
+ final ParcelFileDescriptor mCacheFd;
+ final String mDocumentId;
+ boolean mDirty;
+
+ MtpFileWriter(Context context, String documentId) throws IOException {
+ mDocumentId = documentId;
+ mDirty = false;
+ final File tempFile = File.createTempFile("mtp", "tmp", context.getCacheDir());
+ mCacheFd = ParcelFileDescriptor.open(
+ tempFile,
+ ParcelFileDescriptor.MODE_READ_WRITE |
+ ParcelFileDescriptor.MODE_TRUNCATE |
+ ParcelFileDescriptor.MODE_CREATE);
+ tempFile.delete();
+ }
+
+ String getDocumentId() {
+ return mDocumentId;
+ }
+
+ int write(long offset, int size, byte[] bytes) throws IOException, ErrnoException {
+ Preconditions.checkArgumentNonnegative(offset, "offset");
+ Preconditions.checkArgumentNonnegative(size, "size");
+ Preconditions.checkArgument(size <= bytes.length);
+ if (size == 0) {
+ return 0;
+ }
+ mDirty = true;
+ Os.lseek(mCacheFd.getFileDescriptor(), offset, OsConstants.SEEK_SET);
+ return Os.write(mCacheFd.getFileDescriptor(), bytes, 0, size);
+ }
+
+ void flush(MtpManager manager, MtpDatabase database, int[] operationsSupported)
+ throws IOException, ErrnoException {
+ // Skip unnecessary flush.
+ if (!mDirty) {
+ return;
+ }
+
+ // Get the placeholder object info.
+ final Identifier identifier = database.createIdentifier(mDocumentId);
+ final MtpObjectInfo placeholderObjectInfo =
+ manager.getObjectInfo(identifier.mDeviceId, identifier.mObjectHandle);
+
+ // Delete the target object info if it already exists (as a placeholder).
+ manager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
+
+ // Create the target object info with a correct file size and upload the file.
+ final long size = Os.lseek(mCacheFd.getFileDescriptor(), 0, OsConstants.SEEK_END);
+ final MtpObjectInfo targetObjectInfo = new MtpObjectInfo.Builder(placeholderObjectInfo)
+ .setCompressedSize(size)
+ .build();
+
+ Os.lseek(mCacheFd.getFileDescriptor(), 0, OsConstants.SEEK_SET);
+ final int newObjectHandle = manager.createDocument(
+ identifier.mDeviceId, targetObjectInfo, mCacheFd);
+
+ final MtpObjectInfo newObjectInfo = manager.getObjectInfo(
+ identifier.mDeviceId, newObjectHandle);
+ final Identifier parentIdentifier =
+ database.getParentIdentifier(identifier.mDocumentId);
+ database.updateObject(
+ identifier.mDocumentId,
+ identifier.mDeviceId,
+ parentIdentifier.mDocumentId,
+ operationsSupported,
+ newObjectInfo,
+ size);
+
+ mDirty = false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mCacheFd.close();
+ }
+}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
index 1520f3b..795bbc1 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
@@ -16,13 +16,9 @@
package com.android.mtp;
-import android.content.Context;
-import android.mtp.MtpObjectInfo;
import android.os.ParcelFileDescriptor;
import android.util.Log;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -52,15 +48,6 @@
return task.getReadingFileDescriptor();
}
- ParcelFileDescriptor writeDocument(Context context, MtpManager model, Identifier identifier,
- int[] operationsSupported)
- throws IOException {
- final Task task = new WriteDocumentTask(
- context, model, identifier, operationsSupported, mDatabase);
- mExecutor.execute(task);
- return task.getWritingFileDescriptor();
- }
-
ParcelFileDescriptor readThumbnail(MtpManager model, Identifier identifier) throws IOException {
final Task task = new GetThumbnailTask(model, identifier);
mExecutor.execute(task);
@@ -81,10 +68,6 @@
ParcelFileDescriptor getReadingFileDescriptor() {
return mDescriptors[0];
}
-
- ParcelFileDescriptor getWritingFileDescriptor() {
- return mDescriptors[1];
- }
}
private static class ImportFileTask extends Task {
@@ -108,85 +91,6 @@
}
}
- private static class WriteDocumentTask extends Task {
- private final Context mContext;
- private final MtpDatabase mDatabase;
- private final int[] mOperationsSupported;
-
- WriteDocumentTask(Context context,
- MtpManager model,
- Identifier identifier,
- int[] supportedOperations,
- MtpDatabase database)
- throws IOException {
- super(model, identifier);
- mContext = context;
- mDatabase = database;
- mOperationsSupported = supportedOperations;
- }
-
- @Override
- public void run() {
- File tempFile = null;
- try {
- // Obtain a temporary file and copy the data to it.
- tempFile = File.createTempFile("mtp", "tmp", mContext.getCacheDir());
- try (
- final FileOutputStream tempOutputStream =
- new ParcelFileDescriptor.AutoCloseOutputStream(
- ParcelFileDescriptor.open(
- tempFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
- final ParcelFileDescriptor.AutoCloseInputStream inputStream =
- new ParcelFileDescriptor.AutoCloseInputStream(mDescriptors[0])
- ) {
- final byte[] buffer = new byte[32 * 1024];
- int bytes;
- while ((bytes = inputStream.read(buffer)) != -1) {
- mDescriptors[0].checkError();
- tempOutputStream.write(buffer, 0, bytes);
- }
- tempOutputStream.flush();
- }
-
- // Get the placeholder object info.
- final MtpObjectInfo placeholderObjectInfo =
- mManager.getObjectInfo(mIdentifier.mDeviceId, mIdentifier.mObjectHandle);
-
- // Delete the target object info if it already exists (as a placeholder).
- mManager.deleteDocument(mIdentifier.mDeviceId, mIdentifier.mObjectHandle);
-
- // Create the target object info with a correct file size and upload the file.
- final MtpObjectInfo targetObjectInfo =
- new MtpObjectInfo.Builder(placeholderObjectInfo)
- .setCompressedSize(tempFile.length())
- .build();
- final ParcelFileDescriptor tempInputDescriptor = ParcelFileDescriptor.open(
- tempFile, ParcelFileDescriptor.MODE_READ_ONLY);
- final int newObjectHandle = mManager.createDocument(
- mIdentifier.mDeviceId, targetObjectInfo, tempInputDescriptor);
-
- final MtpObjectInfo newObjectInfo = mManager.getObjectInfo(
- mIdentifier.mDeviceId, newObjectHandle);
- final Identifier parentIdentifier =
- mDatabase.getParentIdentifier(mIdentifier.mDocumentId);
- mDatabase.updateObject(
- mIdentifier.mDocumentId,
- mIdentifier.mDeviceId,
- parentIdentifier.mDocumentId,
- mOperationsSupported,
- newObjectInfo,
- tempFile.length());
- } catch (IOException error) {
- Log.w(MtpDocumentsProvider.TAG,
- "Failed to send a file because of: " + error.getMessage());
- } finally {
- if (tempFile != null) {
- tempFile.delete();
- }
- }
- }
- }
-
private static class GetThumbnailTask extends Task {
GetThumbnailTask(MtpManager model, Identifier identifier) throws IOException {
super(model, identifier);
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
index 3b92506..e421de7 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
@@ -23,6 +23,8 @@
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
+import libcore.io.IoUtils;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -143,7 +145,8 @@
}
@Override
- public int writeObjectBytes(int inode, long offset, int size, byte[] bytes) {
+ public int writeObjectBytes(
+ long fileHandle, int inode, long offset, int size, byte[] bytes) {
for (int i = 0; i < size; i++) {
resultBytes[(int)(offset + i)] = bytes[i];
}
@@ -152,7 +155,7 @@
});
appFuse.mount(storageManager);
final ParcelFileDescriptor fd = appFuse.openFile(
- INODE, ParcelFileDescriptor.MODE_WRITE_ONLY);
+ INODE, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
stream.write('a');
@@ -182,7 +185,7 @@
});
appFuse.mount(storageManager);
final ParcelFileDescriptor fd = appFuse.openFile(
- INODE, ParcelFileDescriptor.MODE_WRITE_ONLY);
+ INODE, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
stream.write('a');
@@ -192,6 +195,46 @@
appFuse.close();
}
+ public void testWriteFile_flushError() throws IOException {
+ final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
+ final int INODE = 10;
+ final AppFuse appFuse = new AppFuse(
+ "test",
+ new TestCallback() {
+ @Override
+ public long getFileSize(int inode) throws FileNotFoundException {
+ if (inode != INODE) {
+ throw new FileNotFoundException();
+ }
+ return 5;
+ }
+
+ @Override
+ public int writeObjectBytes(
+ long fileHandle, int inode, long offset, int size, byte[] bytes) {
+ return size;
+ }
+
+ @Override
+ public void flushFileHandle(long fileHandle) throws IOException {
+ throw new IOException();
+ }
+ });
+ appFuse.mount(storageManager);
+ final ParcelFileDescriptor fd = appFuse.openFile(
+ INODE, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
+ try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
+ new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
+ stream.write('a');
+ try {
+ IoUtils.close(fd.getFileDescriptor());
+ fail();
+ } catch (IOException e) {
+ }
+ }
+ appFuse.close();
+ }
+
private static class TestCallback implements AppFuse.Callback {
@Override
public long getFileSize(int inode) throws FileNotFoundException {
@@ -205,9 +248,15 @@
}
@Override
- public int writeObjectBytes(int inode, long offset, int size, byte[] bytes)
+ public int writeObjectBytes(long fileHandle, int inode, long offset, int size, byte[] bytes)
throws IOException {
throw new IOException();
}
+
+ @Override
+ public void flushFileHandle(long fileHandle) throws IOException {}
+
+ @Override
+ public void closeFileHandle(long fileHandle) {}
}
}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 0de761c..9ed15c8 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -21,6 +21,7 @@
import android.mtp.MtpObjectInfo;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
@@ -533,6 +534,30 @@
}
}
+ public void testOpenDocument_writing() throws Exception {
+ setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
+ setupRoots(0, new MtpRoot[] {
+ new MtpRoot(0, 0, "Storage", 0, 0, "")
+ });
+ final String documentId = mProvider.createDocument("2", "text/plain", "test.txt");
+ {
+ final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "w", null);
+ try (ParcelFileDescriptor.AutoCloseOutputStream stream =
+ new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
+ stream.write("Hello".getBytes());
+ }
+ }
+ {
+ final ParcelFileDescriptor fd = mProvider.openDocument(documentId, "r", null);
+ try (ParcelFileDescriptor.AutoCloseInputStream stream =
+ new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
+ final byte[] bytes = new byte[5];
+ stream.read(bytes);
+ assertTrue(Arrays.equals("Hello".getBytes(), bytes));
+ }
+ }
+ }
+
public void testBusyDevice() throws Exception {
mMtpManager = new TestMtpManager(getContext()) {
@Override
@@ -740,6 +765,7 @@
mProvider = new MtpDocumentsProvider();
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
assertTrue(mProvider.onCreateForTesting(
+ getContext(),
mResources,
mMtpManager,
mResolver,
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
index 8611797..53dc3db 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
@@ -16,10 +16,7 @@
package com.android.mtp;
-import android.database.Cursor;
-import android.mtp.MtpObjectInfo;
import android.os.ParcelFileDescriptor;
-import android.provider.DocumentsContract.Document;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
@@ -66,64 +63,6 @@
assertDescriptorError(descriptor);
}
- public void testWriteDocument_basic() throws Exception {
- TestUtil.addTestDevice(mDatabase);
- TestUtil.addTestStorage(mDatabase, "1");
-
- final MtpObjectInfo info =
- new MtpObjectInfo.Builder().setObjectHandle(1).setName("note.txt").build();
- mDatabase.getMapper().startAddingDocuments("2");
- mDatabase.getMapper().putChildDocuments(
- 0, "2", TestUtil.OPERATIONS_SUPPORTED,
- new MtpObjectInfo[] { info },
- new long[] { 0L });
- mDatabase.getMapper().stopAddingDocuments("2");
- // Create a placeholder file which should be replaced by a real file later.
- mtpManager.setObjectInfo(0, info);
-
- // Upload testing bytes.
- final ParcelFileDescriptor descriptor = mPipeManager.writeDocument(
- getContext(),
- mtpManager,
- new Identifier(0, 0, 1, "2", MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT),
- TestUtil.OPERATIONS_SUPPORTED);
- final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
- new ParcelFileDescriptor.AutoCloseOutputStream(descriptor);
- outputStream.write(HELLO_BYTES, 0, HELLO_BYTES.length);
- outputStream.close();
- mExecutor.shutdown();
- assertTrue(mExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS));
-
- // Check if the placeholder file is removed.
- try {
- mtpManager.getObjectInfo(0, 1);
- fail(); // The placeholder file has not been deleted.
- } catch (IOException e) {
- // Expected error, as the file is gone.
- }
-
- // Confirm that the target file is created.
- final MtpObjectInfo targetDocument = mtpManager.getObjectInfo(
- 0, TestMtpManager.CREATED_DOCUMENT_HANDLE);
- assertTrue(targetDocument != null);
-
- // Confirm the object handle is updated.
- try (final Cursor cursor = mDatabase.queryDocument(
- "2", new String[] { MtpDatabaseConstants.COLUMN_OBJECT_HANDLE })) {
- assertEquals(1, cursor.getCount());
- cursor.moveToNext();
- assertEquals(TestMtpManager.CREATED_DOCUMENT_HANDLE, cursor.getInt(0));
- }
-
- // Verify uploaded bytes.
- final byte[] uploadedBytes = mtpManager.getImportFileBytes(
- 0, TestMtpManager.CREATED_DOCUMENT_HANDLE);
- assertEquals(HELLO_BYTES.length, uploadedBytes.length);
- for (int i = 0; i < HELLO_BYTES.length; i++) {
- assertEquals(HELLO_BYTES[i], uploadedBytes[i]);
- }
- }
-
public void testReadThumbnail_basic() throws Exception {
mtpManager.setThumbnail(0, 1, HELLO_BYTES);
final ParcelFileDescriptor descriptor = mPipeManager.readThumbnail(