Unmount appfuse when the device FD is closed.

The CL lets MountService to observe device FD, and request unmount to
vold when the device FD was closed, or remote application providing
appfuse is crashed.

BUG=25756420

Change-Id: I7990694d32affa7f89e3f40badb25098d74d744d
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 135f369..d5491d3 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -233,6 +233,19 @@
         final FileDescriptor fd = openInternal(file, mode);
         if (fd == null) return null;
 
+        return fromFd(fd, handler, listener);
+    }
+
+    /** {@hide} */
+    public static ParcelFileDescriptor fromFd(
+            FileDescriptor fd, Handler handler, final OnCloseListener listener) throws IOException {
+        if (handler == null) {
+            throw new IllegalArgumentException("Handler must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener must not be null");
+        }
+
         final FileDescriptor[] comm = createCommSocketPair();
         final ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd, comm[0]);
         final MessageQueue queue = handler.getLooper().getQueue();
diff --git a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
index 9267f4c..f592a1f 100644
--- a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
+++ b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
@@ -62,6 +62,19 @@
     }
 };
 
+class ScopedFd {
+    int mFd;
+
+public:
+    explicit ScopedFd(int fd) : mFd(fd) {}
+    ~ScopedFd() {
+        close(mFd);
+    }
+    operator int() {
+        return mFd;
+    }
+};
+
 /**
  * The class is used to access AppFuse class in Java from fuse handlers.
  */
@@ -70,24 +83,26 @@
     AppFuse(JNIEnv* /*env*/, jobject /*self*/) {
     }
 
-    void handle_fuse_request(int fd, const FuseRequest& req) {
+    bool handle_fuse_request(int fd, const 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_INIT:
                 invoke_handler(fd, req, &AppFuse::handle_fuse_init);
-                break;
+                return true;
             case FUSE_GETATTR:
                 invoke_handler(fd, req, &AppFuse::handle_fuse_getattr);
-                break;
+                return true;
+            case FUSE_FORGET:
+                return false;
             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);
-                break;
+                return true;
             }
         }
     }
@@ -198,7 +213,7 @@
 
 jboolean com_android_mtp_AppFuse_start_app_fuse_loop(
         JNIEnv* env, jobject self, jint jfd) {
-    const int fd = static_cast<int>(jfd);
+    ScopedFd fd(dup(static_cast<int>(jfd)));
     AppFuse appfuse(env, self);
 
     ALOGD("Start fuse loop.");
@@ -209,7 +224,7 @@
         if (result < 0) {
             if (errno == ENODEV) {
                 ALOGE("Someone stole our marbles!\n");
-                return false;
+                return JNI_FALSE;
             }
             ALOGE("Failed to read bytes from FD: errno=%d\n", errno);
             continue;
@@ -227,7 +242,9 @@
             continue;
         }
 
-        appfuse.handle_fuse_request(fd, request);
+        if (!appfuse.handle_fuse_request(fd, request)) {
+            return JNI_TRUE;
+        }
     }
 }
 
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
index e9edeb9..2c09ad1 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
@@ -18,10 +18,13 @@
 
 import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.IOException;
+
 import android.os.Process;
 
 /**
@@ -55,6 +58,21 @@
     }
 
     @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 FUSE_FORGET, 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);
+        }
+    }
+
+    @VisibleForTesting
     File getMountPoint() {
         return new File("/mnt/appfuse/" + Process.myUid() + "_" + mName);
     }
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
index a145756..b66d8eb 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
@@ -17,6 +17,8 @@
 package com.android.mtp;
 
 import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -26,12 +28,17 @@
 public class AppFuseTest extends AndroidTestCase {
     /**
      * TODO: Enable this test after adding SELinux policies for appfuse.
+     * @throws ErrnoException
+     * @throws InterruptedException
      */
-    public void testBasic() {
+    public void disabled_testBasic() throws ErrnoException, InterruptedException {
         final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
         final AppFuse appFuse = new AppFuse("test");
         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);
     }
 }
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index fbb31a5..96a8db8 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2812,17 +2812,32 @@
     }
 
     @Override
-    public ParcelFileDescriptor mountAppFuse(String name) throws RemoteException {
+    public ParcelFileDescriptor mountAppFuse(final String name) throws RemoteException {
         try {
+            final int uid = Binder.getCallingUid();
             final NativeDaemonEvent event =
-                    mConnector.execute("appfuse", "mount", Binder.getCallingUid(), name);
+                    mConnector.execute("appfuse", "mount", uid, name);
             if (event.getFileDescriptors() == null) {
-                Log.e(TAG, "AppFuse FD from vold is null.");
-                return null;
+                throw new RemoteException("AppFuse FD from vold is null.");
             }
-            return new ParcelFileDescriptor(event.getFileDescriptors()[0]);
+            return ParcelFileDescriptor.fromFd(
+                    event.getFileDescriptors()[0],
+                    mHandler,
+                    new ParcelFileDescriptor.OnCloseListener() {
+                        @Override
+                        public void onClose(IOException e) {
+                            try {
+                                final NativeDaemonEvent event = mConnector.execute(
+                                        "appfuse", "unmount", uid, name);
+                            } catch (NativeDaemonConnectorException unmountException) {
+                                Log.e(TAG, "Failed to unmount appfuse.");
+                            }
+                        }
+                    });
         } catch (NativeDaemonConnectorException e) {
             throw e.rethrowAsParcelableException();
+        } catch (IOException e) {
+            throw new RemoteException(e.getMessage());
         }
     }