Merge "Use public version of AppFuse in MtpDocumentsProvider."
This commit is contained in:
committed by
Android (Google) Code Review
commit
d66ae533a9
@@ -6,7 +6,6 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
LOCAL_PACKAGE_NAME := MtpDocumentsProvider
|
||||
LOCAL_CERTIFICATE := media
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
LOCAL_JNI_SHARED_LIBRARIES := libappfuse_jni
|
||||
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
|
||||
|
||||
# Only enable asserts on userdebug/eng builds
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_SRC_FILES := \
|
||||
com_android_mtp_AppFuse.cpp
|
||||
|
||||
LOCAL_SHARED_LIBRARIES := \
|
||||
libandroid_runtime \
|
||||
libnativehelper \
|
||||
liblog
|
||||
|
||||
LOCAL_CFLAGS += -Wall -Werror -Wunused -Wunreachable-code
|
||||
LOCAL_MODULE := libappfuse_jni
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -1,571 +0,0 @@
|
||||
/*
|
||||
* 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 specic language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define LOG_TAG "AppFuseJNI"
|
||||
#include "utils/Log.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <dirent.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <linux/fuse.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "jni.h"
|
||||
#include "JNIHelp.h"
|
||||
#include "android_runtime/AndroidRuntime.h"
|
||||
#include "nativehelper/ScopedPrimitiveArray.h"
|
||||
#include "nativehelper/ScopedLocalRef.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// The numbers came from sdcard.c.
|
||||
// Maximum number of bytes to write/read in one request/one reply.
|
||||
constexpr size_t MAX_WRITE = 256 * 1024;
|
||||
constexpr size_t MAX_READ = 128 * 1024;
|
||||
|
||||
constexpr size_t NUM_MAX_HANDLES = 1024;
|
||||
|
||||
// Largest possible request.
|
||||
// The request size is bounded by the maximum size of a FUSE_WRITE request
|
||||
// because it has the largest possible data payload.
|
||||
constexpr size_t MAX_REQUEST_SIZE = sizeof(struct fuse_in_header) +
|
||||
sizeof(struct fuse_write_in) + (MAX_WRITE > MAX_READ ? MAX_WRITE : MAX_READ);
|
||||
|
||||
static jclass app_fuse_class;
|
||||
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:
|
||||
// FuseRequest and FuseResponse shares the same buffer to save memory usage, so the handlers must
|
||||
// not access input buffer after writing data to output buffer.
|
||||
struct FuseRequest {
|
||||
char buffer[MAX_REQUEST_SIZE];
|
||||
FuseRequest() {}
|
||||
const struct fuse_in_header& header() const {
|
||||
return *(const struct fuse_in_header*) buffer;
|
||||
}
|
||||
void* data() {
|
||||
return (buffer + sizeof(struct fuse_in_header));
|
||||
}
|
||||
size_t data_length() const {
|
||||
return header().len - sizeof(struct fuse_in_header);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class FuseResponse {
|
||||
size_t size_;
|
||||
T* const buffer_;
|
||||
public:
|
||||
explicit FuseResponse(void* buffer) : size_(0), buffer_(static_cast<T*>(buffer)) {}
|
||||
|
||||
void prepare_buffer(size_t size = sizeof(T)) {
|
||||
memset(buffer_, 0, size);
|
||||
size_ = size;
|
||||
}
|
||||
|
||||
void set_size(size_t size) {
|
||||
size_ = size;
|
||||
}
|
||||
|
||||
size_t size() const { return size_; }
|
||||
T* data() const { return buffer_; }
|
||||
};
|
||||
|
||||
class ScopedFd {
|
||||
int mFd;
|
||||
|
||||
public:
|
||||
explicit ScopedFd(int fd) : mFd(fd) {}
|
||||
~ScopedFd() {
|
||||
close(mFd);
|
||||
}
|
||||
operator int() {
|
||||
return mFd;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fuse implementation consists of handlers parsing FUSE commands.
|
||||
*/
|
||||
class AppFuse {
|
||||
JNIEnv* env_;
|
||||
jobject self_;
|
||||
|
||||
// Map between file handle and inode.
|
||||
std::map<uint32_t, uint64_t> handles_;
|
||||
uint32_t handle_counter_;
|
||||
|
||||
public:
|
||||
AppFuse(JNIEnv* env, jobject self) :
|
||||
env_(env), self_(self), handle_counter_(0) {}
|
||||
|
||||
void handle_fuse_request(int fd, FuseRequest* req) {
|
||||
ALOGV("Request op=%d", req->header().opcode);
|
||||
switch (req->header().opcode) {
|
||||
// TODO: Handle more operations that are enough to provide seekable
|
||||
// FD.
|
||||
case FUSE_LOOKUP:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_lookup);
|
||||
return;
|
||||
case FUSE_FORGET:
|
||||
// Return without replying.
|
||||
return;
|
||||
case FUSE_INIT:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_init);
|
||||
return;
|
||||
case FUSE_GETATTR:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_getattr);
|
||||
return;
|
||||
case FUSE_OPEN:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_open);
|
||||
return;
|
||||
case FUSE_READ:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_read);
|
||||
return;
|
||||
case FUSE_WRITE:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_write);
|
||||
return;
|
||||
case FUSE_RELEASE:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_release);
|
||||
return;
|
||||
case FUSE_FLUSH:
|
||||
invoke_handler(fd, req, &AppFuse::handle_fuse_flush);
|
||||
return;
|
||||
default: {
|
||||
ALOGV("NOTIMPL op=%d uniq=%" PRIx64 " nid=%" PRIx64 "\n",
|
||||
req->header().opcode,
|
||||
req->header().unique,
|
||||
req->header().nodeid);
|
||||
fuse_reply(fd, req->header().unique, -ENOSYS, NULL, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int handle_fuse_lookup(const fuse_in_header& header,
|
||||
const char* name,
|
||||
FuseResponse<fuse_entry_out>* out) {
|
||||
if (header.nodeid != 1) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
const int n = atoi(name);
|
||||
if (n == 0) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
int64_t size = get_file_size(n);
|
||||
if (size < 0) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
out->prepare_buffer();
|
||||
out->data()->nodeid = n;
|
||||
out->data()->attr_valid = 10;
|
||||
out->data()->entry_valid = 10;
|
||||
out->data()->attr.ino = n;
|
||||
out->data()->attr.mode = S_IFREG | 0777;
|
||||
out->data()->attr.size = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_init(const fuse_in_header&,
|
||||
const fuse_init_in* in,
|
||||
FuseResponse<fuse_init_out>* out) {
|
||||
// Kernel 2.6.16 is the first stable kernel with struct fuse_init_out
|
||||
// defined (fuse version 7.6). The structure is the same from 7.6 through
|
||||
// 7.22. Beginning with 7.23, the structure increased in size and added
|
||||
// new parameters.
|
||||
if (in->major != FUSE_KERNEL_VERSION || in->minor < 6) {
|
||||
ALOGE("Fuse kernel version mismatch: Kernel version %d.%d, "
|
||||
"Expected at least %d.6",
|
||||
in->major, in->minor, FUSE_KERNEL_VERSION);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Before writing |out|, we need to copy data from |in|.
|
||||
const uint32_t minor = in->minor;
|
||||
const uint32_t max_readahead = in->max_readahead;
|
||||
|
||||
// We limit ourselves to 15 because we don't handle BATCH_FORGET yet
|
||||
size_t response_size = sizeof(fuse_init_out);
|
||||
#if defined(FUSE_COMPAT_22_INIT_OUT_SIZE)
|
||||
// FUSE_KERNEL_VERSION >= 23.
|
||||
|
||||
// If the kernel only works on minor revs older than or equal to 22,
|
||||
// then use the older structure size since this code only uses the 7.22
|
||||
// version of the structure.
|
||||
if (minor <= 22) {
|
||||
response_size = FUSE_COMPAT_22_INIT_OUT_SIZE;
|
||||
}
|
||||
#endif
|
||||
out->prepare_buffer(response_size);
|
||||
out->data()->major = FUSE_KERNEL_VERSION;
|
||||
out->data()->minor = std::min(minor, 15u);
|
||||
out->data()->max_readahead = max_readahead;
|
||||
out->data()->flags = FUSE_ATOMIC_O_TRUNC | FUSE_BIG_WRITES;
|
||||
out->data()->max_background = 32;
|
||||
out->data()->congestion_threshold = 32;
|
||||
out->data()->max_write = MAX_WRITE;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_getattr(const fuse_in_header& header,
|
||||
const fuse_getattr_in* /* in */,
|
||||
FuseResponse<fuse_attr_out>* out) {
|
||||
out->prepare_buffer();
|
||||
out->data()->attr_valid = 10;
|
||||
out->data()->attr.ino = header.nodeid;
|
||||
if (header.nodeid == 1) {
|
||||
out->data()->attr.mode = S_IFDIR | 0777;
|
||||
out->data()->attr.size = 0;
|
||||
} else {
|
||||
int64_t size = get_file_size(header.nodeid);
|
||||
if (size < 0) {
|
||||
return -ENOENT;
|
||||
}
|
||||
out->data()->attr.mode = S_IFREG | 0777;
|
||||
out->data()->attr.size = size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_open(const fuse_in_header& header,
|
||||
const fuse_open_in* /* in */,
|
||||
FuseResponse<fuse_open_out>* out) {
|
||||
if (handles_.size() >= NUM_MAX_HANDLES) {
|
||||
// Too many open files.
|
||||
return -EMFILE;
|
||||
}
|
||||
uint32_t handle;
|
||||
do {
|
||||
handle = handle_counter_++;
|
||||
} while (handles_.count(handle) != 0);
|
||||
handles_.insert(std::make_pair(handle, header.nodeid));
|
||||
|
||||
out->prepare_buffer();
|
||||
out->data()->fh = handle;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_read(const fuse_in_header& /* header */,
|
||||
const fuse_read_in* in,
|
||||
FuseResponse<void>* out) {
|
||||
if (in->size > MAX_READ) {
|
||||
return -EINVAL;
|
||||
}
|
||||
const std::map<uint32_t, uint64_t>::iterator it = handles_.find(in->fh);
|
||||
if (it == handles_.end()) {
|
||||
return -EBADF;
|
||||
}
|
||||
uint64_t offset = in->offset;
|
||||
uint32_t size = in->size;
|
||||
|
||||
// Overwrite the size after writing data.
|
||||
out->prepare_buffer(0);
|
||||
const int64_t result = get_object_bytes(it->second, offset, size, out->data());
|
||||
if (result < 0) {
|
||||
return result;
|
||||
}
|
||||
out->set_size(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_write(const fuse_in_header& /* header */,
|
||||
const fuse_write_in* in,
|
||||
FuseResponse<fuse_write_out>* out) {
|
||||
if (in->size > MAX_WRITE) {
|
||||
return -EINVAL;
|
||||
}
|
||||
const std::map<uint32_t, uint64_t>::iterator it = handles_.find(in->fh);
|
||||
if (it == handles_.end()) {
|
||||
return -EBADF;
|
||||
}
|
||||
const uint64_t offset = in->offset;
|
||||
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(
|
||||
in->fh, it->second, offset, size, buffer, &written_size);
|
||||
if (result < 0) {
|
||||
return result;
|
||||
}
|
||||
out->prepare_buffer();
|
||||
out->data()->size = written_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_fuse_release(const fuse_in_header& /* header */,
|
||||
const fuse_release_in* in,
|
||||
FuseResponse<void>* /* out */) {
|
||||
handles_.erase(in->fh);
|
||||
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 fuse_flush_in* in,
|
||||
FuseResponse<void>* /* out */) {
|
||||
return env_->CallIntMethod(self_, app_fuse_flush_file_handle, file_handle_to_jlong(in->fh));
|
||||
}
|
||||
|
||||
template <typename T, typename S>
|
||||
void invoke_handler(int fd,
|
||||
FuseRequest* request,
|
||||
int (AppFuse::*handler)(const fuse_in_header&,
|
||||
const T*,
|
||||
FuseResponse<S>*)) {
|
||||
FuseResponse<S> response(request->data());
|
||||
const int reply_code = (this->*handler)(
|
||||
request->header(),
|
||||
static_cast<const T*>(request->data()),
|
||||
&response);
|
||||
fuse_reply(
|
||||
fd,
|
||||
request->header().unique,
|
||||
reply_code,
|
||||
request->data(),
|
||||
response.size());
|
||||
}
|
||||
|
||||
int64_t get_file_size(int inode) {
|
||||
return static_cast<int64_t>(env_->CallLongMethod(
|
||||
self_,
|
||||
app_fuse_get_file_size,
|
||||
static_cast<int>(inode)));
|
||||
}
|
||||
|
||||
int64_t get_object_bytes(
|
||||
int inode,
|
||||
uint64_t offset,
|
||||
uint32_t size,
|
||||
void* buf) {
|
||||
const jlong read_size = env_->CallLongMethod(
|
||||
self_,
|
||||
app_fuse_read_object_bytes,
|
||||
static_cast<jint>(inode),
|
||||
static_cast<jlong>(offset),
|
||||
static_cast<jlong>(size));
|
||||
if (read_size <= 0) {
|
||||
return read_size;
|
||||
}
|
||||
ScopedLocalRef<jbyteArray> array(
|
||||
env_, static_cast<jbyteArray>(env_->GetObjectField(self_, app_fuse_buffer)));
|
||||
if (array.get() == nullptr) {
|
||||
return -EFAULT;
|
||||
}
|
||||
ScopedByteArrayRO bytes(env_, array.get());
|
||||
if (bytes.get() == nullptr) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
memcpy(buf, bytes.get(), read_size);
|
||||
return read_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)));
|
||||
{
|
||||
ScopedByteArrayRW bytes(env_, array.get());
|
||||
if (bytes.get() == nullptr) {
|
||||
return -EIO;
|
||||
}
|
||||
memcpy(bytes.get(), buffer, size);
|
||||
}
|
||||
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.
|
||||
if (reply_code != 0) {
|
||||
reply_size = 0;
|
||||
}
|
||||
|
||||
struct fuse_out_header hdr;
|
||||
hdr.len = reply_size + sizeof(hdr);
|
||||
hdr.error = reply_code;
|
||||
hdr.unique = unique;
|
||||
|
||||
struct iovec vec[2];
|
||||
vec[0].iov_base = &hdr;
|
||||
vec[0].iov_len = sizeof(hdr);
|
||||
vec[1].iov_base = reply_data;
|
||||
vec[1].iov_len = reply_size;
|
||||
|
||||
const int res = writev(fd, vec, reply_size != 0 ? 2 : 1);
|
||||
if (res < 0) {
|
||||
ALOGE("*** REPLY FAILED *** %d\n", errno);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void com_android_mtp_AppFuse_start_app_fuse_loop(JNIEnv* env, jobject self, jint jfd) {
|
||||
ScopedFd fd(static_cast<int>(jfd));
|
||||
AppFuse appfuse(env, self);
|
||||
|
||||
ALOGV("Start fuse loop.");
|
||||
while (true) {
|
||||
FuseRequest request;
|
||||
|
||||
const ssize_t result = TEMP_FAILURE_RETRY(
|
||||
read(fd, request.buffer, sizeof(request.buffer)));
|
||||
if (result < 0) {
|
||||
if (errno == ENODEV) {
|
||||
ALOGV("AppFuse was unmounted.\n");
|
||||
return;
|
||||
}
|
||||
ALOGE("Failed to read bytes from FD: errno=%d\n", errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t length = static_cast<size_t>(result);
|
||||
if (length < sizeof(struct fuse_in_header)) {
|
||||
ALOGE("request too short: len=%zu\n", length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (request.header().len != length) {
|
||||
ALOGE("malformed header: len=%zu, hdr->len=%u\n",
|
||||
length, request.header().len);
|
||||
continue;
|
||||
}
|
||||
|
||||
appfuse.handle_fuse_request(fd, &request);
|
||||
}
|
||||
}
|
||||
|
||||
static const JNINativeMethod gMethods[] = {
|
||||
{
|
||||
"native_start_app_fuse_loop",
|
||||
"(I)V",
|
||||
(void *) com_android_mtp_AppFuse_start_app_fuse_loop
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
|
||||
JNIEnv* env = nullptr;
|
||||
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
|
||||
ALOGE("ERROR: GetEnv failed\n");
|
||||
return -1;
|
||||
|
||||
}
|
||||
assert(env != nullptr);
|
||||
|
||||
jclass clazz = env->FindClass("com/android/mtp/AppFuse");
|
||||
if (clazz == nullptr) {
|
||||
ALOGE("Can't find com/android/mtp/AppFuse");
|
||||
return -1;
|
||||
}
|
||||
|
||||
app_fuse_class = static_cast<jclass>(env->NewGlobalRef(clazz));
|
||||
if (app_fuse_class == nullptr) {
|
||||
ALOGE("Can't obtain global reference for com/android/mtp/AppFuse");
|
||||
return -1;
|
||||
}
|
||||
|
||||
app_fuse_get_file_size = env->GetMethodID(
|
||||
app_fuse_class, "getFileSize", "(I)J");
|
||||
if (app_fuse_get_file_size == nullptr) {
|
||||
ALOGE("Can't find getFileSize");
|
||||
return -1;
|
||||
}
|
||||
|
||||
app_fuse_read_object_bytes = env->GetMethodID(
|
||||
app_fuse_class, "readObjectBytes", "(IJJ)J");
|
||||
if (app_fuse_read_object_bytes == nullptr) {
|
||||
ALOGE("Can't find readObjectBytes");
|
||||
return -1;
|
||||
}
|
||||
|
||||
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_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;
|
||||
}
|
||||
|
||||
app_fuse_buffer = env->GetFieldID(app_fuse_class, "mBuffer", "[B");
|
||||
if (app_fuse_buffer == nullptr) {
|
||||
ALOGE("Can't find mBuffer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const jfieldID read_max_fied = env->GetStaticFieldID(app_fuse_class, "MAX_READ", "I");
|
||||
if (static_cast<int>(env->GetStaticIntField(app_fuse_class, read_max_fied)) != MAX_READ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const jfieldID write_max_fied = env->GetStaticFieldID(app_fuse_class, "MAX_WRITE", "I");
|
||||
if (static_cast<int>(env->GetStaticIntField(app_fuse_class, write_max_fied)) != MAX_WRITE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int result = android::AndroidRuntime::registerNativeMethods(
|
||||
env, "com/android/mtp/AppFuse", gMethods, NELEM(gMethods));
|
||||
if (result < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return JNI_VERSION_1_4;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ProxyFileDescriptorCallback;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
@@ -36,38 +37,13 @@ import org.junit.Test;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class AppFusePerfTest {
|
||||
final static int SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
public void testReadWriteFile() throws IOException {
|
||||
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
final StorageManager storageManager = context.getSystemService(StorageManager.class);
|
||||
final int INODE = 10;
|
||||
final int SIZE = 10 * 1024 * 1024; // 10MB
|
||||
final AppFuse appFuse = new AppFuse(
|
||||
"test",
|
||||
new TestCallback() {
|
||||
@Override
|
||||
public long getFileSize(int inode) throws FileNotFoundException {
|
||||
if (inode != INODE) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
return SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readObjectBytes(int inode, long offset, long size, byte[] bytes)
|
||||
throws IOException {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeObjectBytes(
|
||||
long fileHandle, int inode, long offset, int size, byte[] bytes) {
|
||||
return size;
|
||||
}
|
||||
});
|
||||
|
||||
appFuse.mount(storageManager);
|
||||
|
||||
final byte[] bytes = new byte[SIZE];
|
||||
final int SAMPLES = 100;
|
||||
@@ -75,22 +51,20 @@ public class AppFusePerfTest {
|
||||
final double[] writeTime = new double[SAMPLES];
|
||||
|
||||
for (int i = 0; i < SAMPLES; i++) {
|
||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||
INODE,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
final ParcelFileDescriptor fd = storageManager.openProxyFileDescriptor(
|
||||
ParcelFileDescriptor.MODE_READ_ONLY, new TestCallback());
|
||||
try (final ParcelFileDescriptor.AutoCloseInputStream stream =
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
|
||||
final long startTime = System.nanoTime();
|
||||
stream.read(bytes);
|
||||
readTime[i] = (System.nanoTime() - startTime) / 1000.0 / 1000.0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (int i = 0; i < SAMPLES; i++) {
|
||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||
INODE,
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
final ParcelFileDescriptor fd = storageManager.openProxyFileDescriptor(
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE,
|
||||
new TestCallback());
|
||||
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
|
||||
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
||||
final long startTime = System.nanoTime();
|
||||
@@ -99,8 +73,6 @@ public class AppFusePerfTest {
|
||||
}
|
||||
}
|
||||
|
||||
appFuse.close();
|
||||
|
||||
double readAverage = 0;
|
||||
double writeAverage = 0;
|
||||
double readSquaredAverage = 0;
|
||||
@@ -127,28 +99,26 @@ public class AppFusePerfTest {
|
||||
InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, results);
|
||||
}
|
||||
|
||||
private static class TestCallback implements AppFuse.Callback {
|
||||
private static class TestCallback extends ProxyFileDescriptorCallback {
|
||||
@Override
|
||||
public long getFileSize(int inode) throws FileNotFoundException {
|
||||
throw new FileNotFoundException();
|
||||
public long onGetSize() throws ErrnoException {
|
||||
return SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readObjectBytes(int inode, long offset, long size, byte[] bytes)
|
||||
throws IOException {
|
||||
throw new IOException();
|
||||
public int onRead(long offset, int size, byte[] data) throws ErrnoException {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeObjectBytes(long fileHandle, int inode, long offset, int size, byte[] bytes)
|
||||
throws IOException {
|
||||
throw new IOException();
|
||||
public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushFileHandle(long fileHandle) throws IOException {}
|
||||
public void onFsync() throws ErrnoException {}
|
||||
|
||||
@Override
|
||||
public void closeFileHandle(long fileHandle) {}
|
||||
public void onRelease() {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
/*
|
||||
* 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.WorkerThread;
|
||||
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;
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.mtp.annotations.UsedByNative;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AppFuse {
|
||||
static {
|
||||
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.
|
||||
*/
|
||||
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||
static final int MAX_READ = 128 * 1024;
|
||||
|
||||
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||
static final int MAX_WRITE = 256 * 1024;
|
||||
|
||||
private final String mName;
|
||||
private final Callback mCallback;
|
||||
|
||||
/**
|
||||
* Buffer for read bytes request.
|
||||
* Don't use the buffer from the out of AppFuseMessageThread.
|
||||
*/
|
||||
private byte[] mBuffer = new byte[Math.max(MAX_READ, MAX_WRITE)];
|
||||
|
||||
private Thread mMessageThread;
|
||||
private ParcelFileDescriptor mDeviceFd;
|
||||
|
||||
AppFuse(String name, Callback callback) {
|
||||
mName = name;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void mount(StorageManager storageManager) throws IOException {
|
||||
Preconditions.checkState(mDeviceFd == null);
|
||||
mDeviceFd = storageManager.mountAppFuse(mName);
|
||||
mMessageThread = new AppFuseMessageThread(mDeviceFd.dup().detachFd());
|
||||
mMessageThread.start();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void close() {
|
||||
try {
|
||||
// Remote side of ParcelFileDescriptor is tracking the close of mDeviceFd, and unmount
|
||||
// the corresponding fuse file system. The mMessageThread will receive ENODEV, and
|
||||
// then terminate itself.
|
||||
mDeviceFd.close();
|
||||
mMessageThread.join();
|
||||
} catch (IOException exp) {
|
||||
Log.e(MtpDocumentsProvider.TAG, "Failed to close device FD.", exp);
|
||||
} catch (InterruptedException exp) {
|
||||
Log.e(MtpDocumentsProvider.TAG, "Failed to terminate message thread.", exp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file on app fuse and returns ParcelFileDescriptor.
|
||||
*
|
||||
* @param i ID for opened file.
|
||||
* @param mode Mode for opening file.
|
||||
* @see ParcelFileDescriptor#MODE_READ_ONLY
|
||||
* @see ParcelFileDescriptor#MODE_WRITE_ONLY
|
||||
*/
|
||||
public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
|
||||
Preconditions.checkArgument(
|
||||
mode == ParcelFileDescriptor.MODE_READ_ONLY ||
|
||||
mode == (ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE));
|
||||
return ParcelFileDescriptor.open(new File(
|
||||
getMountPoint(),
|
||||
Integer.toString(i)),
|
||||
mode);
|
||||
}
|
||||
|
||||
File getMountPoint() {
|
||||
return new File("/mnt/appfuse/" + Process.myUid() + "_" + mName);
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
/**
|
||||
* Returns file size for the given inode.
|
||||
* @param inode
|
||||
* @return File size. Must not be negative.
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
long getFileSize(int inode) throws FileNotFoundException;
|
||||
|
||||
/**
|
||||
* Returns file bytes for the give inode.
|
||||
* @param inode
|
||||
* @param offset Offset for file bytes.
|
||||
* @param size Size for file bytes.
|
||||
* @param bytes Buffer to store file bytes.
|
||||
* @return Number of read bytes. Must not be negative.
|
||||
* @throws IOException
|
||||
*/
|
||||
long readObjectBytes(int inode, long offset, long size, byte[] bytes) throws IOException;
|
||||
|
||||
/**
|
||||
* Handles writing bytes for the give inode.
|
||||
* @param fileHandle
|
||||
* @param inode
|
||||
* @param offset Offset for file bytes.
|
||||
* @param size Size for file bytes.
|
||||
* @param bytes Buffer to store file bytes.
|
||||
* @return Number of read bytes. Must not be negative.
|
||||
* @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")
|
||||
@WorkerThread
|
||||
private long getFileSize(int inode) {
|
||||
try {
|
||||
return mCallback.getFileSize(inode);
|
||||
} catch (Exception error) {
|
||||
return -getErrnoFromException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||
@WorkerThread
|
||||
private long readObjectBytes(int inode, long offset, long size) {
|
||||
if (offset < 0 || size < 0 || size > MAX_READ) {
|
||||
return -OsConstants.EINVAL;
|
||||
}
|
||||
try {
|
||||
// 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 (Exception error) {
|
||||
return -getErrnoFromException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@UsedByNative("com_android_mtp_AppFuse.cpp")
|
||||
@WorkerThread
|
||||
private /* unsgined */ int writeObjectBytes(long fileHandler,
|
||||
int inode,
|
||||
/* unsigned */ long offset,
|
||||
/* unsigned */ int size,
|
||||
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 void native_start_app_fuse_loop(int fd);
|
||||
|
||||
private class AppFuseMessageThread extends Thread {
|
||||
/**
|
||||
* File descriptor used by native loop.
|
||||
* It's owned by native loop and does not need to close here.
|
||||
*/
|
||||
private final int mRawFd;
|
||||
|
||||
AppFuseMessageThread(int fd) {
|
||||
super("AppFuseMessageThread");
|
||||
mRawFd = fd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
native_start_app_fuse_loop(mRawFd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ 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.Document;
|
||||
import android.provider.DocumentsContract.Path;
|
||||
@@ -41,6 +42,7 @@ import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.provider.Settings;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
@@ -54,6 +56,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
/**
|
||||
* DocumentsProvider for MTP devices.
|
||||
*/
|
||||
@@ -84,9 +88,9 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
private RootScanner mRootScanner;
|
||||
private Resources mResources;
|
||||
private MtpDatabase mDatabase;
|
||||
private AppFuse mAppFuse;
|
||||
private ServiceIntentSender mIntentSender;
|
||||
private Context mContext;
|
||||
private StorageManager mStorageManager;
|
||||
|
||||
/**
|
||||
* Provides singleton instance to MtpDocumentsService.
|
||||
@@ -105,8 +109,8 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
|
||||
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
|
||||
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
||||
mAppFuse = new AppFuse(TAG, new AppFuseCallback());
|
||||
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.
|
||||
@@ -129,14 +133,6 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Mount AppFuse on demands.
|
||||
try {
|
||||
mAppFuse.mount(getContext().getSystemService(StorageManager.class));
|
||||
} catch (IOException error) {
|
||||
Log.e(TAG, "Failed to start app fuse.", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
resume();
|
||||
return true;
|
||||
}
|
||||
@@ -157,16 +153,9 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
|
||||
mDatabase = database;
|
||||
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
||||
mAppFuse = new AppFuse(TAG, new AppFuseCallback());
|
||||
mIntentSender = intentSender;
|
||||
mStorageManager = storageManager;
|
||||
|
||||
// TODO: Mount AppFuse on demands.
|
||||
try {
|
||||
mAppFuse.mount(storageManager);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to start app fuse.", e);
|
||||
return false;
|
||||
}
|
||||
resume();
|
||||
return true;
|
||||
}
|
||||
@@ -252,7 +241,10 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
}
|
||||
if (MtpDeviceRecord.isPartialReadSupported(
|
||||
device.operationsSupported, fileSize)) {
|
||||
return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
|
||||
|
||||
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.
|
||||
@@ -262,7 +254,9 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
// 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);
|
||||
return mStorageManager.openProxyFileDescriptor(
|
||||
modeFlag,
|
||||
new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
"The device does not support writing operation.");
|
||||
@@ -586,7 +580,6 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
mDatabase.close();
|
||||
mAppFuse.close();
|
||||
super.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -693,72 +686,92 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private class AppFuseCallback implements AppFuse.Callback {
|
||||
private final Map<Long, MtpFileWriter> mWriters = new HashMap<>();
|
||||
private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
|
||||
private final int mInode;
|
||||
private MtpFileWriter mWriter;
|
||||
|
||||
@Override
|
||||
public long getFileSize(int inode) throws FileNotFoundException {
|
||||
return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
|
||||
MtpProxyFileDescriptorCallback(int inode) {
|
||||
mInode = inode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readObjectBytes(
|
||||
int inode, long offset, long size, byte[] buffer) throws IOException {
|
||||
final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
|
||||
final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
|
||||
|
||||
if (MtpDeviceRecord.isSupported(
|
||||
record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
|
||||
return mMtpManager.getPartialObject64(
|
||||
identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
|
||||
}
|
||||
|
||||
if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
|
||||
record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
|
||||
return mMtpManager.getPartialObject(
|
||||
identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
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 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;
|
||||
}
|
||||
public long onGetSize() throws ErrnoException {
|
||||
try {
|
||||
writer.close();
|
||||
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 {
|
||||
mWriters.remove(fileHandle);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* 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.os.ParcelFileDescriptor;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
|
||||
@MediumTest
|
||||
public class AppFuseTest extends AndroidTestCase {
|
||||
public void testMount() throws ErrnoException, IOException {
|
||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||
final AppFuse appFuse = new AppFuse("test", new TestCallback());
|
||||
appFuse.mount(storageManager);
|
||||
final File file = appFuse.getMountPoint();
|
||||
assertTrue(file.isDirectory());
|
||||
assertEquals(1, Os.stat(file.getPath()).st_ino);
|
||||
appFuse.close();
|
||||
assertTrue(1 != Os.stat(file.getPath()).st_ino);
|
||||
}
|
||||
|
||||
public void testOpenFile() 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) {
|
||||
return 1024;
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
});
|
||||
appFuse.mount(storageManager);
|
||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||
INODE, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
fd.close();
|
||||
appFuse.close();
|
||||
}
|
||||
|
||||
public void testOpenFile_fileNotFound() throws IOException {
|
||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||
final int INODE = 10;
|
||||
final AppFuse appFuse = new AppFuse("test", new TestCallback());
|
||||
appFuse.mount(storageManager);
|
||||
try {
|
||||
appFuse.openFile(INODE, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
fail();
|
||||
} catch (FileNotFoundException exp) {}
|
||||
appFuse.close();
|
||||
}
|
||||
|
||||
public void testOpenFile_illegalMode() throws IOException {
|
||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||
final int INODE = 10;
|
||||
final AppFuse appFuse = new AppFuse("test", new TestCallback());
|
||||
appFuse.mount(storageManager);
|
||||
try {
|
||||
appFuse.openFile(INODE, ParcelFileDescriptor.MODE_READ_WRITE);
|
||||
fail();
|
||||
} catch (IllegalArgumentException exp) {}
|
||||
appFuse.close();
|
||||
}
|
||||
|
||||
public void testReadFile() throws IOException {
|
||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||
final int fileInode = 10;
|
||||
final byte[] fileBytes = new byte[] { 'a', 'b', 'c', 'd', 'e' };
|
||||
final AppFuse appFuse = new AppFuse(
|
||||
"test",
|
||||
new TestCallback() {
|
||||
@Override
|
||||
public long getFileSize(int inode) throws FileNotFoundException {
|
||||
if (inode == fileInode) {
|
||||
return fileBytes.length;
|
||||
}
|
||||
return super.getFileSize(inode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readObjectBytes(int inode, long offset, long size, byte[] bytes)
|
||||
throws IOException {
|
||||
if (inode == fileInode) {
|
||||
int i = 0;
|
||||
while (i < size && i + offset < fileBytes.length) {
|
||||
bytes[i] = fileBytes[(int) (i + offset)];
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return super.readObjectBytes(inode, offset, size, bytes);
|
||||
}
|
||||
});
|
||||
appFuse.mount(storageManager);
|
||||
final ParcelFileDescriptor fd = appFuse.openFile(
|
||||
fileInode, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
try (final ParcelFileDescriptor.AutoCloseInputStream stream =
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
|
||||
final byte[] buffer = new byte[1024];
|
||||
final int size = stream.read(buffer, 0, buffer.length);
|
||||
assertEquals(5, size);
|
||||
}
|
||||
appFuse.close();
|
||||
}
|
||||
|
||||
public void testWriteFile() throws IOException {
|
||||
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
|
||||
final int INODE = 10;
|
||||
final byte[] resultBytes = new byte[5];
|
||||
final AppFuse appFuse = new AppFuse(
|
||||
"test",
|
||||
new TestCallback() {
|
||||
@Override
|
||||
public long getFileSize(int inode) throws FileNotFoundException {
|
||||
if (inode != INODE) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
return resultBytes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
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];
|
||||
}
|
||||
return size;
|
||||
}
|
||||
});
|
||||
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');
|
||||
stream.write('b');
|
||||
stream.write('c');
|
||||
stream.write('d');
|
||||
stream.write('e');
|
||||
}
|
||||
final byte[] BYTES = new byte[] { 'a', 'b', 'c', 'd', 'e' };
|
||||
assertTrue(Arrays.equals(BYTES, resultBytes));
|
||||
appFuse.close();
|
||||
}
|
||||
|
||||
public void testWriteFile_writeError() 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;
|
||||
}
|
||||
});
|
||||
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');
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
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 {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readObjectBytes(int inode, long offset, long size, byte[] bytes)
|
||||
throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
@@ -557,6 +557,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
|
||||
try (ParcelFileDescriptor.AutoCloseOutputStream stream =
|
||||
new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
|
||||
stream.write("Hello".getBytes());
|
||||
fd.getFileDescriptor().sync();
|
||||
}
|
||||
}
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user