1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mtp;
18
19import android.annotation.WorkerThread;
20import android.os.ParcelFileDescriptor;
21import android.os.Process;
22import android.os.storage.StorageManager;
23import android.system.ErrnoException;
24import android.system.OsConstants;
25import android.util.Log;
26import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.util.Preconditions;
28import com.android.mtp.annotations.UsedByNative;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.IOException;
32
33public class AppFuse {
34    static {
35        System.loadLibrary("appfuse_jni");
36    }
37
38    private static final boolean DEBUG = false;
39
40    /**
41     * Max read amount specified at the FUSE kernel implementation.
42     * The value is copied from sdcard.c.
43     */
44    @UsedByNative("com_android_mtp_AppFuse.cpp")
45    static final int MAX_READ = 128 * 1024;
46
47    @UsedByNative("com_android_mtp_AppFuse.cpp")
48    static final int MAX_WRITE = 256 * 1024;
49
50    private final String mName;
51    private final Callback mCallback;
52
53    /**
54     * Buffer for read bytes request.
55     * Don't use the buffer from the out of AppFuseMessageThread.
56     */
57    private byte[] mBuffer = new byte[Math.max(MAX_READ, MAX_WRITE)];
58
59    private Thread mMessageThread;
60    private ParcelFileDescriptor mDeviceFd;
61
62    AppFuse(String name, Callback callback) {
63        mName = name;
64        mCallback = callback;
65    }
66
67    void mount(StorageManager storageManager) throws IOException {
68        Preconditions.checkState(mDeviceFd == null);
69        mDeviceFd = storageManager.mountAppFuse(mName);
70        mMessageThread = new AppFuseMessageThread(mDeviceFd.dup().detachFd());
71        mMessageThread.start();
72    }
73
74    @VisibleForTesting
75    void close() {
76        try {
77            // Remote side of ParcelFileDescriptor is tracking the close of mDeviceFd, and unmount
78            // the corresponding fuse file system. The mMessageThread will receive ENODEV, and
79            // then terminate itself.
80            mDeviceFd.close();
81            mMessageThread.join();
82        } catch (IOException exp) {
83            Log.e(MtpDocumentsProvider.TAG, "Failed to close device FD.", exp);
84        } catch (InterruptedException exp) {
85            Log.e(MtpDocumentsProvider.TAG, "Failed to terminate message thread.", exp);
86        }
87    }
88
89    /**
90     * Opens a file on app fuse and returns ParcelFileDescriptor.
91     *
92     * @param i ID for opened file.
93     * @param mode Mode for opening file.
94     * @see ParcelFileDescriptor#MODE_READ_ONLY
95     * @see ParcelFileDescriptor#MODE_WRITE_ONLY
96     */
97    public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
98        Preconditions.checkArgument(
99                mode == ParcelFileDescriptor.MODE_READ_ONLY ||
100                mode == (ParcelFileDescriptor.MODE_WRITE_ONLY |
101                         ParcelFileDescriptor.MODE_TRUNCATE));
102        return ParcelFileDescriptor.open(new File(
103                getMountPoint(),
104                Integer.toString(i)),
105                mode);
106    }
107
108    File getMountPoint() {
109        return new File("/mnt/appfuse/" + Process.myUid() + "_" + mName);
110    }
111
112    static interface Callback {
113        /**
114         * Returns file size for the given inode.
115         * @param inode
116         * @return File size. Must not be negative.
117         * @throws FileNotFoundException
118         */
119        long getFileSize(int inode) throws FileNotFoundException;
120
121        /**
122         * Returns file bytes for the give inode.
123         * @param inode
124         * @param offset Offset for file bytes.
125         * @param size Size for file bytes.
126         * @param bytes Buffer to store file bytes.
127         * @return Number of read bytes. Must not be negative.
128         * @throws IOException
129         */
130        long readObjectBytes(int inode, long offset, long size, byte[] bytes) throws IOException;
131
132        /**
133         * Handles writing bytes for the give inode.
134         * @param fileHandle
135         * @param inode
136         * @param offset Offset for file bytes.
137         * @param size Size for file bytes.
138         * @param bytes Buffer to store file bytes.
139         * @return Number of read bytes. Must not be negative.
140         * @throws IOException
141         */
142        int writeObjectBytes(long fileHandle, int inode, long offset, int size, byte[] bytes)
143                throws IOException, ErrnoException;
144
145        /**
146         * Flushes bytes for file handle.
147         * @param fileHandle
148         * @throws IOException
149         * @throws ErrnoException
150         */
151        void flushFileHandle(long fileHandle) throws IOException, ErrnoException;
152
153        /**
154         * Closes file handle.
155         * @param fileHandle
156         * @throws IOException
157         */
158        void closeFileHandle(long fileHandle) throws IOException, ErrnoException;
159    }
160
161    @UsedByNative("com_android_mtp_AppFuse.cpp")
162    @WorkerThread
163    private long getFileSize(int inode) {
164        try {
165            return mCallback.getFileSize(inode);
166        } catch (Exception error) {
167            return -getErrnoFromException(error);
168        }
169    }
170
171    @UsedByNative("com_android_mtp_AppFuse.cpp")
172    @WorkerThread
173    private long readObjectBytes(int inode, long offset, long size) {
174        if (offset < 0 || size < 0 || size > MAX_READ) {
175            return -OsConstants.EINVAL;
176        }
177        try {
178            // It's OK to share the same mBuffer among requests because the requests are processed
179            // by AppFuseMessageThread sequentially.
180            return mCallback.readObjectBytes(inode, offset, size, mBuffer);
181        } catch (Exception error) {
182            return -getErrnoFromException(error);
183        }
184    }
185
186    @UsedByNative("com_android_mtp_AppFuse.cpp")
187    @WorkerThread
188    private /* unsgined */ int writeObjectBytes(long fileHandler,
189                                                int inode,
190                                                /* unsigned */ long offset,
191                                                /* unsigned */ int size,
192                                                byte[] bytes) {
193        try {
194            return mCallback.writeObjectBytes(fileHandler, inode, offset, size, bytes);
195        } catch (Exception error) {
196            return -getErrnoFromException(error);
197        }
198    }
199
200    @UsedByNative("com_android_mtp_AppFuse.cpp")
201    @WorkerThread
202    private int flushFileHandle(long fileHandle) {
203        try {
204            mCallback.flushFileHandle(fileHandle);
205            return 0;
206        } catch (Exception error) {
207            return -getErrnoFromException(error);
208        }
209    }
210
211    @UsedByNative("com_android_mtp_AppFuse.cpp")
212    @WorkerThread
213    private int closeFileHandle(long fileHandle) {
214        try {
215            mCallback.closeFileHandle(fileHandle);
216            return 0;
217        } catch (Exception error) {
218            return -getErrnoFromException(error);
219        }
220    }
221
222    private static int getErrnoFromException(Exception error) {
223        if (DEBUG) {
224            Log.e(MtpDocumentsProvider.TAG, "AppFuse callbacks", error);
225        }
226        if (error instanceof FileNotFoundException) {
227            return OsConstants.ENOENT;
228        } else if (error instanceof IOException) {
229            return OsConstants.EIO;
230        } else if (error instanceof UnsupportedOperationException) {
231            return OsConstants.ENOTSUP;
232        } else if (error instanceof IllegalArgumentException) {
233            return OsConstants.EINVAL;
234        } else {
235            return OsConstants.EIO;
236        }
237    }
238
239    private native void native_start_app_fuse_loop(int fd);
240
241    private class AppFuseMessageThread extends Thread {
242        /**
243         * File descriptor used by native loop.
244         * It's owned by native loop and does not need to close here.
245         */
246        private final int mRawFd;
247
248        AppFuseMessageThread(int fd) {
249            super("AppFuseMessageThread");
250            mRawFd = fd;
251        }
252
253        @Override
254        public void run() {
255            native_start_app_fuse_loop(mRawFd);
256        }
257    }
258}
259