Merge "Use AppFuse to write document." into nyc-dev
am: c124427
* commit 'c124427880cf51e27185cd525ec332f4ad312c34':
Use AppFuse to write document.
Change-Id: I36769d0ec7fbe73f4eb3aec66513a2806c1b5c5e
This commit is contained in:
@@ -45,6 +45,8 @@ import android.os.ParcelFileDescriptor;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.DocumentsContract.Document;
|
import android.provider.DocumentsContract.Document;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
@@ -451,7 +453,7 @@ class CopyJob extends Job {
|
|||||||
ParcelFileDescriptor srcFile = null;
|
ParcelFileDescriptor srcFile = null;
|
||||||
ParcelFileDescriptor dstFile = null;
|
ParcelFileDescriptor dstFile = null;
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
OutputStream out = null;
|
ParcelFileDescriptor.AutoCloseOutputStream out = null;
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -502,6 +504,8 @@ class CopyJob extends Job {
|
|||||||
makeCopyProgress(len);
|
makeCopyProgress(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
|
||||||
|
IoUtils.close(dstFile.getFileDescriptor());
|
||||||
srcFile.checkError();
|
srcFile.checkError();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ResourceException(
|
throw new ResourceException(
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ static jclass app_fuse_class;
|
|||||||
static jmethodID app_fuse_get_file_size;
|
static jmethodID app_fuse_get_file_size;
|
||||||
static jmethodID app_fuse_read_object_bytes;
|
static jmethodID app_fuse_read_object_bytes;
|
||||||
static jmethodID app_fuse_write_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;
|
static jfieldID app_fuse_buffer;
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
@@ -307,7 +309,8 @@ private:
|
|||||||
const uint32_t size = in->size;
|
const uint32_t size = in->size;
|
||||||
const void* const buffer = reinterpret_cast<const uint8_t*>(in) + sizeof(fuse_write_in);
|
const void* const buffer = reinterpret_cast<const uint8_t*>(in) + sizeof(fuse_write_in);
|
||||||
uint32_t written_size;
|
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) {
|
if (result < 0) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -320,13 +323,13 @@ private:
|
|||||||
const fuse_release_in* in,
|
const fuse_release_in* in,
|
||||||
FuseResponse<void>* /* out */) {
|
FuseResponse<void>* /* out */) {
|
||||||
handles_.erase(in->fh);
|
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 */,
|
int handle_fuse_flush(const fuse_in_header& /* header */,
|
||||||
const void* /* in */,
|
const fuse_flush_in* in,
|
||||||
FuseResponse<void>* /* out */) {
|
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>
|
template <typename T, typename S>
|
||||||
@@ -382,8 +385,10 @@ private:
|
|||||||
return read_size;
|
return read_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
int write_object_bytes(int inode, uint64_t offset, uint32_t size, const void* buffer,
|
int write_object_bytes(uint64_t handle, int inode, uint64_t offset, uint32_t size,
|
||||||
uint32_t* written_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(
|
ScopedLocalRef<jbyteArray> array(
|
||||||
env_,
|
env_,
|
||||||
static_cast<jbyteArray>(env_->GetObjectField(self_, app_fuse_buffer)));
|
static_cast<jbyteArray>(env_->GetObjectField(self_, app_fuse_buffer)));
|
||||||
@@ -394,15 +399,28 @@ private:
|
|||||||
}
|
}
|
||||||
memcpy(bytes.get(), buffer, size);
|
memcpy(bytes.get(), buffer, size);
|
||||||
}
|
}
|
||||||
*written_size = env_->CallIntMethod(
|
const int result = env_->CallIntMethod(
|
||||||
self_, app_fuse_write_object_bytes, inode, offset, size, array.get());
|
self_,
|
||||||
if (env_->ExceptionCheck()) {
|
app_fuse_write_object_bytes,
|
||||||
env_->ExceptionClear();
|
file_handle_to_jlong(handle),
|
||||||
return -EIO;
|
inode,
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
array.get());
|
||||||
|
if (result < 0) {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
*written_size = result;
|
||||||
return 0;
|
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,
|
static void fuse_reply(int fd, int unique, int reply_code, void* reply_data,
|
||||||
size_t reply_size) {
|
size_t reply_size) {
|
||||||
// Don't send any data for error case.
|
// Don't send any data for error case.
|
||||||
@@ -511,15 +529,21 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
app_fuse_buffer = env->GetFieldID(app_fuse_class, "mBuffer", "[B");
|
app_fuse_write_object_bytes = env->GetMethodID(app_fuse_class, "writeObjectBytes", "(JIJI[B)I");
|
||||||
if (app_fuse_buffer == nullptr) {
|
if (app_fuse_write_object_bytes == nullptr) {
|
||||||
ALOGE("Can't find mBuffer");
|
ALOGE("Can't find writeObjectBytes");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
app_fuse_write_object_bytes = env->GetMethodID(app_fuse_class, "writeObjectBytes", "(IJI[B)I");
|
app_fuse_flush_file_handle = env->GetMethodID(app_fuse_class, "flushFileHandle", "(J)I");
|
||||||
if (app_fuse_write_object_bytes == nullptr) {
|
if (app_fuse_flush_file_handle == nullptr) {
|
||||||
ALOGE("Can't find getWriteObjectBytes");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.annotation.WorkerThread;
|
|||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.os.storage.StorageManager;
|
import android.os.storage.StorageManager;
|
||||||
|
import android.system.ErrnoException;
|
||||||
import android.system.OsConstants;
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
@@ -34,6 +35,8 @@ public class AppFuse {
|
|||||||
System.loadLibrary("appfuse_jni");
|
System.loadLibrary("appfuse_jni");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max read amount specified at the FUSE kernel implementation.
|
* Max read amount specified at the FUSE kernel implementation.
|
||||||
* The value is copied from sdcard.c.
|
* The value is copied from sdcard.c.
|
||||||
@@ -94,7 +97,8 @@ public class AppFuse {
|
|||||||
public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
|
public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
|
||||||
Preconditions.checkArgument(
|
Preconditions.checkArgument(
|
||||||
mode == ParcelFileDescriptor.MODE_READ_ONLY ||
|
mode == ParcelFileDescriptor.MODE_READ_ONLY ||
|
||||||
mode == ParcelFileDescriptor.MODE_WRITE_ONLY);
|
mode == (ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||||
|
ParcelFileDescriptor.MODE_TRUNCATE));
|
||||||
return ParcelFileDescriptor.open(new File(
|
return ParcelFileDescriptor.open(new File(
|
||||||
getMountPoint(),
|
getMountPoint(),
|
||||||
Integer.toString(i)),
|
Integer.toString(i)),
|
||||||
@@ -127,6 +131,7 @@ public class AppFuse {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles writing bytes for the give inode.
|
* Handles writing bytes for the give inode.
|
||||||
|
* @param fileHandle
|
||||||
* @param inode
|
* @param inode
|
||||||
* @param offset Offset for file bytes.
|
* @param offset Offset for file bytes.
|
||||||
* @param size Size for file bytes.
|
* @param size Size for file bytes.
|
||||||
@@ -134,7 +139,23 @@ public class AppFuse {
|
|||||||
* @return Number of read bytes. Must not be negative.
|
* @return Number of read bytes. Must not be negative.
|
||||||
* @throws IOException
|
* @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")
|
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||||
@@ -142,10 +163,8 @@ public class AppFuse {
|
|||||||
private long getFileSize(int inode) {
|
private long getFileSize(int inode) {
|
||||||
try {
|
try {
|
||||||
return mCallback.getFileSize(inode);
|
return mCallback.getFileSize(inode);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (Exception error) {
|
||||||
return -OsConstants.ENOENT;
|
return -getErrnoFromException(error);
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
return -OsConstants.ENOTSUP;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,20 +178,62 @@ public class AppFuse {
|
|||||||
// It's OK to share the same mBuffer among requests because the requests are processed
|
// It's OK to share the same mBuffer among requests because the requests are processed
|
||||||
// by AppFuseMessageThread sequentially.
|
// by AppFuseMessageThread sequentially.
|
||||||
return mCallback.readObjectBytes(inode, offset, size, mBuffer);
|
return mCallback.readObjectBytes(inode, offset, size, mBuffer);
|
||||||
} catch (IOException e) {
|
} catch (Exception error) {
|
||||||
return -OsConstants.EIO;
|
return -getErrnoFromException(error);
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
return -OsConstants.ENOTSUP;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private /* unsgined */ int writeObjectBytes(int inode,
|
private /* unsgined */ int writeObjectBytes(long fileHandler,
|
||||||
|
int inode,
|
||||||
/* unsigned */ long offset,
|
/* unsigned */ long offset,
|
||||||
/* unsigned */ int size,
|
/* unsigned */ int size,
|
||||||
byte[] bytes) throws IOException {
|
byte[] bytes) {
|
||||||
return mCallback.writeObjectBytes(inode, offset, size, 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);
|
private native boolean native_start_app_fuse_loop(int fd);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package com.android.mtp;
|
package com.android.mtp;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.UriPermission;
|
import android.content.UriPermission;
|
||||||
import android.content.res.AssetFileDescriptor;
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -38,11 +39,16 @@ import android.provider.DocumentsContract.Root;
|
|||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.DocumentsProvider;
|
import android.provider.DocumentsProvider;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.GuardedBy;
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -82,6 +88,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
private MtpDatabase mDatabase;
|
private MtpDatabase mDatabase;
|
||||||
private AppFuse mAppFuse;
|
private AppFuse mAppFuse;
|
||||||
private ServiceIntentSender mIntentSender;
|
private ServiceIntentSender mIntentSender;
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides singleton instance to MtpDocumentsService.
|
* Provides singleton instance to MtpDocumentsService.
|
||||||
@@ -93,6 +100,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
sSingleton = this;
|
sSingleton = this;
|
||||||
|
mContext = getContext();
|
||||||
mResources = getContext().getResources();
|
mResources = getContext().getResources();
|
||||||
mMtpManager = new MtpManager(getContext());
|
mMtpManager = new MtpManager(getContext());
|
||||||
mResolver = getContext().getContentResolver();
|
mResolver = getContext().getContentResolver();
|
||||||
@@ -137,12 +145,14 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
boolean onCreateForTesting(
|
boolean onCreateForTesting(
|
||||||
|
Context context,
|
||||||
Resources resources,
|
Resources resources,
|
||||||
MtpManager mtpManager,
|
MtpManager mtpManager,
|
||||||
ContentResolver resolver,
|
ContentResolver resolver,
|
||||||
MtpDatabase database,
|
MtpDatabase database,
|
||||||
StorageManager storageManager,
|
StorageManager storageManager,
|
||||||
ServiceIntentSender intentSender) {
|
ServiceIntentSender intentSender) {
|
||||||
|
mContext = context;
|
||||||
mResources = resources;
|
mResources = resources;
|
||||||
mMtpManager = mtpManager;
|
mMtpManager = mtpManager;
|
||||||
mResolver = resolver;
|
mResolver = resolver;
|
||||||
@@ -232,43 +242,43 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
try {
|
try {
|
||||||
openDevice(identifier.mDeviceId);
|
openDevice(identifier.mDeviceId);
|
||||||
final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
|
final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
|
||||||
switch (mode) {
|
// Turn off MODE_CREATE because openDocument does not allow to create new files.
|
||||||
case "r":
|
final int modeFlag =
|
||||||
long fileSize;
|
ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
|
||||||
try {
|
if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
|
||||||
fileSize = getFileSize(documentId);
|
long fileSize;
|
||||||
} catch (UnsupportedOperationException exception) {
|
try {
|
||||||
fileSize = -1;
|
fileSize = getFileSize(documentId);
|
||||||
}
|
} catch (UnsupportedOperationException exception) {
|
||||||
// MTP getPartialObject operation does not support files that are larger than
|
fileSize = -1;
|
||||||
// 4GB. Fallback to non-seekable file descriptor.
|
}
|
||||||
if (MtpDeviceRecord.isPartialReadSupported(
|
if (MtpDeviceRecord.isPartialReadSupported(
|
||||||
device.operationsSupported, fileSize)) {
|
device.operationsSupported, fileSize)) {
|
||||||
return mAppFuse.openFile(
|
return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
|
||||||
Integer.parseInt(documentId), ParcelFileDescriptor.MODE_READ_ONLY);
|
} else {
|
||||||
} else {
|
// If getPartialObject{|64} are not supported for the device, returns
|
||||||
return getPipeManager(identifier).readDocument(mMtpManager, identifier);
|
// non-seekable pipe FD instead.
|
||||||
}
|
return getPipeManager(identifier).readDocument(mMtpManager, identifier);
|
||||||
case "w":
|
}
|
||||||
// TODO: Clear the parent document loader task (if exists) and call notify
|
} else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
|
||||||
// when writing is completed.
|
// TODO: Clear the parent document loader task (if exists) and call notify
|
||||||
if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
|
// when writing is completed.
|
||||||
return getPipeManager(identifier).writeDocument(
|
if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
|
||||||
getContext(), mMtpManager, identifier, device.operationsSupported);
|
return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException(
|
|
||||||
"The device does not support writing operation.");
|
|
||||||
}
|
|
||||||
case "rw":
|
|
||||||
// TODO: Add support for "rw" mode.
|
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
"The provider does not support 'rw' mode.");
|
"The device does not support writing operation.");
|
||||||
default:
|
}
|
||||||
throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
|
} 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) {
|
} catch (IOException error) {
|
||||||
Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
|
Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
|
||||||
throw new FileNotFoundException(error.getMessage());
|
throw new IllegalStateException(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +605,13 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class AppFuseCallback implements AppFuse.Callback {
|
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
|
@Override
|
||||||
public long readObjectBytes(
|
public long readObjectBytes(
|
||||||
int inode, long offset, long size, byte[] buffer) throws IOException {
|
int inode, long offset, long size, byte[] buffer) throws IOException {
|
||||||
@@ -617,15 +634,43 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFileSize(int inode) throws FileNotFoundException {
|
public int writeObjectBytes(
|
||||||
return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
|
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
|
@Override
|
||||||
public int writeObjectBytes(int inode, long offset, int size, byte[] bytes)
|
public void flushFileHandle(long fileHandle) throws IOException, ErrnoException {
|
||||||
throws IOException {
|
final MtpFileWriter writer = mWriters.get(fileHandle);
|
||||||
// TODO: Implement it.
|
if (writer == null) {
|
||||||
throw new IOException();
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,13 +16,9 @@
|
|||||||
|
|
||||||
package com.android.mtp;
|
package com.android.mtp;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.mtp.MtpObjectInfo;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -52,15 +48,6 @@ class PipeManager {
|
|||||||
return task.getReadingFileDescriptor();
|
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 {
|
ParcelFileDescriptor readThumbnail(MtpManager model, Identifier identifier) throws IOException {
|
||||||
final Task task = new GetThumbnailTask(model, identifier);
|
final Task task = new GetThumbnailTask(model, identifier);
|
||||||
mExecutor.execute(task);
|
mExecutor.execute(task);
|
||||||
@@ -81,10 +68,6 @@ class PipeManager {
|
|||||||
ParcelFileDescriptor getReadingFileDescriptor() {
|
ParcelFileDescriptor getReadingFileDescriptor() {
|
||||||
return mDescriptors[0];
|
return mDescriptors[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
ParcelFileDescriptor getWritingFileDescriptor() {
|
|
||||||
return mDescriptors[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ImportFileTask extends Task {
|
private static class ImportFileTask extends Task {
|
||||||
@@ -108,85 +91,6 @@ class PipeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private static class GetThumbnailTask extends Task {
|
||||||
GetThumbnailTask(MtpManager model, Identifier identifier) throws IOException {
|
GetThumbnailTask(MtpManager model, Identifier identifier) throws IOException {
|
||||||
super(model, identifier);
|
super(model, identifier);
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import android.system.Os;
|
|||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
import android.test.suitebuilder.annotation.MediumTest;
|
import android.test.suitebuilder.annotation.MediumTest;
|
||||||
|
|
||||||
|
import libcore.io.IoUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -143,7 +145,8 @@ public class AppFuseTest extends AndroidTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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++) {
|
for (int i = 0; i < size; i++) {
|
||||||
resultBytes[(int)(offset + i)] = bytes[i];
|
resultBytes[(int)(offset + i)] = bytes[i];
|
||||||
}
|
}
|
||||||
@@ -152,7 +155,7 @@ public class AppFuseTest extends AndroidTestCase {
|
|||||||
});
|
});
|
||||||
appFuse.mount(storageManager);
|
appFuse.mount(storageManager);
|
||||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||||
INODE, ParcelFileDescriptor.MODE_WRITE_ONLY);
|
INODE, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
|
||||||
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
|
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
|
||||||
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
||||||
stream.write('a');
|
stream.write('a');
|
||||||
@@ -182,7 +185,7 @@ public class AppFuseTest extends AndroidTestCase {
|
|||||||
});
|
});
|
||||||
appFuse.mount(storageManager);
|
appFuse.mount(storageManager);
|
||||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||||
INODE, ParcelFileDescriptor.MODE_WRITE_ONLY);
|
INODE, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
|
||||||
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
|
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
|
||||||
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
||||||
stream.write('a');
|
stream.write('a');
|
||||||
@@ -192,6 +195,46 @@ public class AppFuseTest extends AndroidTestCase {
|
|||||||
appFuse.close();
|
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 {
|
private static class TestCallback implements AppFuse.Callback {
|
||||||
@Override
|
@Override
|
||||||
public long getFileSize(int inode) throws FileNotFoundException {
|
public long getFileSize(int inode) throws FileNotFoundException {
|
||||||
@@ -205,9 +248,15 @@ public class AppFuseTest extends AndroidTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
throws IOException {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flushFileHandle(long fileHandle) throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closeFileHandle(long fileHandle) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import android.mtp.MtpConstants;
|
|||||||
import android.mtp.MtpObjectInfo;
|
import android.mtp.MtpObjectInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||||
import android.os.storage.StorageManager;
|
import android.os.storage.StorageManager;
|
||||||
import android.provider.DocumentsContract.Document;
|
import android.provider.DocumentsContract.Document;
|
||||||
import android.provider.DocumentsContract.Root;
|
import android.provider.DocumentsContract.Root;
|
||||||
@@ -533,6 +534,30 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
public void testBusyDevice() throws Exception {
|
||||||
mMtpManager = new TestMtpManager(getContext()) {
|
mMtpManager = new TestMtpManager(getContext()) {
|
||||||
@Override
|
@Override
|
||||||
@@ -740,6 +765,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
|
|||||||
mProvider = new MtpDocumentsProvider();
|
mProvider = new MtpDocumentsProvider();
|
||||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||||
assertTrue(mProvider.onCreateForTesting(
|
assertTrue(mProvider.onCreateForTesting(
|
||||||
|
getContext(),
|
||||||
mResources,
|
mResources,
|
||||||
mMtpManager,
|
mMtpManager,
|
||||||
mResolver,
|
mResolver,
|
||||||
|
|||||||
@@ -16,10 +16,7 @@
|
|||||||
|
|
||||||
package com.android.mtp;
|
package com.android.mtp;
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.mtp.MtpObjectInfo;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.DocumentsContract.Document;
|
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
import android.test.suitebuilder.annotation.MediumTest;
|
import android.test.suitebuilder.annotation.MediumTest;
|
||||||
|
|
||||||
@@ -66,64 +63,6 @@ public class PipeManagerTest extends AndroidTestCase {
|
|||||||
assertDescriptorError(descriptor);
|
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 {
|
public void testReadThumbnail_basic() throws Exception {
|
||||||
mtpManager.setThumbnail(0, 1, HELLO_BYTES);
|
mtpManager.setThumbnail(0, 1, HELLO_BYTES);
|
||||||
final ParcelFileDescriptor descriptor = mPipeManager.readThumbnail(
|
final ParcelFileDescriptor descriptor = mPipeManager.readThumbnail(
|
||||||
|
|||||||
Reference in New Issue
Block a user