Adding FUSE loop thread in app side.
The CL adds FuseAppLoop internal class to the framework. It parses FUSE commands
from the proxy in the system service and invokes a callback provided by
application.
Bug: 29970149
Test: None, will be tested with CTS for StorageManager#openProxyFileDescriptor.
Change-Id: I10b2add4c2743fb91eae3cb194f55843a74fb668
diff --git a/core/java/android/os/IProxyFileDescriptorCallback.java b/core/java/android/os/IProxyFileDescriptorCallback.java
new file mode 100644
index 0000000..e41e194
--- /dev/null
+++ b/core/java/android/os/IProxyFileDescriptorCallback.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.os;
+
+import android.system.ErrnoException;
+
+/**
+ * Callback that handles file system requests from ProxyFileDescriptor.
+ * @hide
+ */
+public interface IProxyFileDescriptorCallback {
+ /**
+ * Returns size of bytes provided by the file descriptor.
+ * @return Size of bytes
+ * @throws ErrnoException
+ */
+ long onGetSize() throws ErrnoException;
+
+ /**
+ * Provides bytes read from file descriptor.
+ * It needs to return exact requested size of bytes unless it reaches file end.
+ * @param offset Where to read bytes from.
+ * @param size Size for read bytes.
+ * @param data Byte array to store read bytes.
+ * @return Size of bytes returned by the function.
+ * @throws ErrnoException
+ */
+ int onRead(long offset, int size, byte[] data) throws ErrnoException;
+
+ /**
+ * Handles bytes written to file descriptor.
+ * @param offset Where to write bytes to.
+ * @param size Size for write bytes.
+ * @param data Byte array to be written to somewhere.
+ * @return Size of bytes processed by the function.
+ * @throws ErrnoException
+ */
+ int onWrite(long offset, int size, byte[] data) throws ErrnoException;
+
+ /**
+ * Processes fsync request.
+ * @throws ErrnoException
+ */
+ void onFsync() throws ErrnoException;
+}
diff --git a/core/java/com/android/internal/os/FuseAppLoop.java b/core/java/com/android/internal/os/FuseAppLoop.java
new file mode 100644
index 0000000..34253ce
--- /dev/null
+++ b/core/java/com/android/internal/os/FuseAppLoop.java
@@ -0,0 +1,222 @@
+/*
+ * 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.internal.os;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IProxyFileDescriptorCallback;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class FuseAppLoop {
+ private static final String TAG = "FuseAppLoop";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ public static final int ROOT_INODE = 1;
+ private static final int MIN_INODE = 2;
+
+ private final Object mLock = new Object();
+ private final File mParent;
+
+ @GuardedBy("mLock")
+ private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private boolean mActive = true;
+
+ /**
+ * Sequential number can be used as file name and inode in AppFuse.
+ * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
+ */
+ @GuardedBy("mLock")
+ private int mNextInode = MIN_INODE;
+
+ private FuseAppLoop(@NonNull File parent) {
+ mParent = parent;
+ }
+
+ public static @NonNull FuseAppLoop open(
+ @NonNull File parent, @NonNull ParcelFileDescriptor fd) {
+ Preconditions.checkNotNull(parent);
+ Preconditions.checkNotNull(fd);
+ final FuseAppLoop bridge = new FuseAppLoop(parent);
+ final int rawFd = fd.detachFd();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ bridge.native_start_loop(rawFd);
+ }
+ }, TAG).start();
+ return bridge;
+ }
+
+ public @NonNull ParcelFileDescriptor openFile(int mode, IProxyFileDescriptorCallback callback)
+ throws UnmountedException, IOException {
+ int id;
+ synchronized (mLock) {
+ if (!mActive) {
+ throw new UnmountedException();
+ }
+ if (mCallbackMap.size() >= Integer.MAX_VALUE - MIN_INODE) {
+ throw new IOException("Too many opened files.");
+ }
+ while (true) {
+ id = mNextInode;
+ mNextInode++;
+ if (mNextInode < 0) {
+ mNextInode = MIN_INODE;
+ }
+ if (mCallbackMap.get(id) == null) {
+ break;
+ }
+ }
+
+ // Register callback after we succeed to create pfd.
+ mCallbackMap.put(id, new CallbackEntry(callback));
+ }
+ try {
+ return ParcelFileDescriptor.open(new File(mParent, String.valueOf(id)), mode);
+ } catch (FileNotFoundException error) {
+ synchronized (mLock) {
+ mCallbackMap.remove(id);
+ }
+ throw error;
+ }
+ }
+
+ public @Nullable File getMountPoint() {
+ synchronized (mLock) {
+ return mActive ? mParent : null;
+ }
+ }
+
+ private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
+ final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
+ if (entry != null) {
+ return entry;
+ } else {
+ throw new ErrnoException("getCallbackEntry", OsConstants.ENOENT);
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private long onGetSize(long inode) {
+ synchronized(mLock) {
+ try {
+ return getCallbackEntryOrThrowLocked(inode).callback.onGetSize();
+ } catch (ErrnoException exp) {
+ return -exp.errno;
+ }
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private int onOpen(long inode) {
+ synchronized(mLock) {
+ try {
+ final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
+ if (entry.opened) {
+ throw new ErrnoException("onOpen", OsConstants.EMFILE);
+ }
+ entry.opened = true;
+ // Use inode as file handle. It's OK because AppFuse does not allow to open the same
+ // file twice.
+ return (int) inode;
+ } catch (ErrnoException exp) {
+ return -exp.errno;
+ }
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private int onFsync(long inode) {
+ synchronized(mLock) {
+ try {
+ getCallbackEntryOrThrowLocked(inode).callback.onFsync();
+ return 0;
+ } catch (ErrnoException exp) {
+ return -exp.errno;
+ }
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private int onRelease(long inode) {
+ synchronized(mLock) {
+ mCallbackMap.remove(checkInode(inode));
+ if (mCallbackMap.size() == 0) {
+ mActive = false;
+ return -1;
+ }
+ return 0;
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private int onRead(long inode, long offset, int size, byte[] bytes) {
+ synchronized(mLock) {
+ try {
+ return getCallbackEntryOrThrowLocked(inode).callback.onRead(offset, size, bytes);
+ } catch (ErrnoException exp) {
+ return -exp.errno;
+ }
+ }
+ }
+
+ // Called by JNI.
+ @SuppressWarnings("unused")
+ private int onWrite(long inode, long offset, int size, byte[] bytes) {
+ synchronized(mLock) {
+ try {
+ return getCallbackEntryOrThrowLocked(inode).callback.onWrite(offset, size, bytes);
+ } catch (ErrnoException exp) {
+ return -exp.errno;
+ }
+ }
+ }
+
+ native boolean native_start_loop(int fd);
+
+ private static int checkInode(long inode) {
+ Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
+ return (int) inode;
+ }
+
+ public static class UnmountedException extends Exception {}
+
+ private static class CallbackEntry {
+ final IProxyFileDescriptorCallback callback;
+ boolean opened;
+ CallbackEntry(IProxyFileDescriptorCallback callback) {
+ Preconditions.checkNotNull(callback);
+ this.callback = callback;
+ }
+ }
+}
diff --git a/core/jni/Android.mk b/core/jni/Android.mk
index a4e9576..be2e404 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -183,6 +183,7 @@
android_content_res_Configuration.cpp \
android_animation_PropertyValuesHolder.cpp \
com_android_internal_net_NetworkStatsFactory.cpp \
+ com_android_internal_os_FuseAppLoop.cpp \
com_android_internal_os_PathClassLoaderFactory.cpp \
com_android_internal_os_Zygote.cpp \
com_android_internal_util_VirtualRefBasePtr.cpp \
@@ -201,6 +202,7 @@
$(TOP)/frameworks/base/media/jni \
$(TOP)/system/core/base/include \
$(TOP)/system/core/include \
+ $(TOP)/system/core/libappfuse/include \
$(TOP)/system/media/camera/include \
$(TOP)/system/netd/include \
external/giflib \
@@ -230,6 +232,7 @@
LOCAL_SHARED_LIBRARIES := \
libmemtrack \
libandroidfw \
+ libappfuse \
libbase \
libexpat \
libnativehelper \
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index cdaa4dc..1f810ac 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -201,6 +201,7 @@
extern int register_android_animation_PropertyValuesHolder(JNIEnv *env);
extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env);
extern int register_com_android_internal_net_NetworkStatsFactory(JNIEnv *env);
+extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
extern int register_com_android_internal_os_PathClassLoaderFactory(JNIEnv* env);
extern int register_com_android_internal_os_Zygote(JNIEnv *env);
extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env);
@@ -1419,7 +1420,7 @@
REG_JNI(register_android_animation_PropertyValuesHolder),
REG_JNI(register_com_android_internal_content_NativeLibraryHelper),
REG_JNI(register_com_android_internal_net_NetworkStatsFactory),
-
+ REG_JNI(register_com_android_internal_os_FuseAppLoop),
};
/*
diff --git a/core/jni/com_android_internal_os_FuseAppLoop.cpp b/core/jni/com_android_internal_os_FuseAppLoop.cpp
new file mode 100644
index 0000000..92a6934
--- /dev/null
+++ b/core/jni/com_android_internal_os_FuseAppLoop.cpp
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "FuseAppLoopJNI"
+#define LOG_NDEBUG 0
+
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include <android_runtime/Log.h>
+#include <android-base/logging.h>
+#include <android-base/unique_fd.h>
+#include <jni.h>
+#include <libappfuse/FuseAppLoop.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+namespace {
+
+constexpr const char* CLASS_NAME = "com/android/internal/os/FuseAppLoop";
+
+jclass gFuseAppLoopClass;
+jmethodID gOnGetSizeMethod;
+jmethodID gOnOpenMethod;
+jmethodID gOnFsyncMethod;
+jmethodID gOnReleaseMethod;
+jmethodID gOnReadMethod;
+jmethodID gOnWriteMethod;
+
+class Callback : public fuse::FuseAppLoopCallback {
+private:
+ static constexpr size_t kBufferSize = std::max(fuse::kFuseMaxWrite, fuse::kFuseMaxRead);
+ static_assert(kBufferSize <= INT32_MAX, "kBufferSize should be fit in int32_t.");
+
+ JNIEnv* const mEnv;
+ jobject const mSelf;
+ ScopedLocalRef<jbyteArray> mJniBuffer;
+ bool mActive;
+
+ template <typename T>
+ T checkException(T result) const {
+ if (mEnv->ExceptionCheck()) {
+ LOGE_EX(mEnv, nullptr);
+ mEnv->ExceptionClear();
+ return -EIO;
+ }
+ return result;
+ }
+
+public:
+ Callback(JNIEnv* env, jobject self) :
+ mEnv(env),
+ mSelf(self),
+ mJniBuffer(env, nullptr),
+ mActive(true) {}
+
+ bool Init() {
+ mJniBuffer.reset(mEnv->NewByteArray(kBufferSize));
+ return mJniBuffer.get();
+ }
+
+ bool IsActive() override {
+ return mActive;
+ }
+
+ int64_t OnGetSize(uint64_t inode) override {
+ return checkException(mEnv->CallLongMethod(mSelf, gOnGetSizeMethod, inode));
+ }
+
+ int32_t OnOpen(uint64_t inode) override {
+ return checkException(mEnv->CallIntMethod(mSelf, gOnOpenMethod, inode));
+ }
+
+ int32_t OnFsync(uint64_t inode) override {
+ return checkException(mEnv->CallIntMethod(mSelf, gOnFsyncMethod, inode));
+ }
+
+ int32_t OnRelease(uint64_t inode) override {
+ if (checkException(mEnv->CallIntMethod(mSelf, gOnReleaseMethod, inode)) == -1) {
+ mActive = false;
+ }
+ return fuse::kFuseSuccess;
+ }
+
+ int32_t OnRead(uint64_t inode, uint64_t offset, uint32_t size, void* buffer) override {
+ CHECK_LE(size, static_cast<uint32_t>(kBufferSize));
+ const int32_t result = checkException(mEnv->CallIntMethod(
+ mSelf, gOnReadMethod, inode, offset, size, mJniBuffer.get()));
+ if (result <= 0) {
+ return result;
+ }
+ if (result > static_cast<int32_t>(size)) {
+ LOG(ERROR) << "Returned size is too large.";
+ return -EIO;
+ }
+
+ mEnv->GetByteArrayRegion(mJniBuffer.get(), 0, result, static_cast<jbyte*>(buffer));
+ CHECK(!mEnv->ExceptionCheck());
+
+ return checkException(result);
+ }
+
+ int32_t OnWrite(uint64_t inode, uint64_t offset, uint32_t size, const void* buffer) override {
+ CHECK_LE(size, static_cast<uint32_t>(kBufferSize));
+
+ mEnv->SetByteArrayRegion(mJniBuffer.get(), 0, size, static_cast<const jbyte*>(buffer));
+ CHECK(!mEnv->ExceptionCheck());
+
+ return checkException(mEnv->CallIntMethod(
+ mSelf, gOnWriteMethod, inode, offset, size, mJniBuffer.get()));
+ }
+};
+
+jboolean com_android_internal_os_FuseAppLoop_start_loop(JNIEnv* env, jobject self, jint jfd) {
+ base::unique_fd fd(jfd);
+ Callback callback(env, self);
+
+ if (!callback.Init()) {
+ LOG(ERROR) << "Failed to init callback";
+ return JNI_FALSE;
+ }
+
+ return fuse::StartFuseAppLoop(fd.release(), &callback);
+}
+
+const JNINativeMethod methods[] = {
+ {
+ "native_start_loop",
+ "(I)Z",
+ (void *) com_android_internal_os_FuseAppLoop_start_loop
+ }
+};
+
+} // namespace
+
+int register_com_android_internal_os_FuseAppLoop(JNIEnv* env) {
+ gFuseAppLoopClass = MakeGlobalRefOrDie(env, FindClassOrDie(env, CLASS_NAME));
+ gOnGetSizeMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onGetSize", "(J)J");
+ gOnOpenMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onOpen", "(J)I");
+ gOnFsyncMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onFsync", "(J)I");
+ gOnReleaseMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onRelease", "(J)I");
+ gOnReadMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onRead", "(JJI[B)I");
+ gOnWriteMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onWrite", "(JJI[B)I");
+ RegisterMethodsOrDie(env, CLASS_NAME, methods, NELEM(methods));
+ return 0;
+}
+
+} // namespace android