Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f4e7fa80 authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Use AppFuse to write document.

Previously MtpDocumentsProvider used pipes to transfer bytes from an
application to the provider when writing a document.  The problem was
application could not ensure that the last chunk of bytes was
successfully written to MTP device, since pipes had been already closed
when the provider transferred bytes to MTP device. Though the provider
encountered an error, the provider could not report the error to an
application.

The CL switches the method to transfer bytes from pipes to AppFuse. Now
application can flush() bytes on the file descriptor, and flush will not
complete until the provider completes writing bytes to MTP device.

Fixed: 23093747
Change-Id: I4e28f8cbf19d6c97e591943349a7535241d768f7
parent 9194f344
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -45,6 +45,8 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.system.ErrnoException;
import android.system.Os;
import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -451,7 +453,7 @@ class CopyJob extends Job {
        ParcelFileDescriptor srcFile = null;
        ParcelFileDescriptor dstFile = null;
        InputStream in = null;
        OutputStream out = null;
        ParcelFileDescriptor.AutoCloseOutputStream out = null;
        boolean success = false;

        try {
@@ -502,6 +504,8 @@ class CopyJob extends Job {
                    makeCopyProgress(len);
                }

                // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
                IoUtils.close(dstFile.getFileDescriptor());
                srcFile.checkError();
            } catch (IOException e) {
                throw new ResourceException(
+41 −17
Original line number Diff line number Diff line
@@ -52,6 +52,8 @@ 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:
@@ -307,7 +309,8 @@ private:
        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(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) {
            return result;
        }
@@ -320,13 +323,13 @@ private:
                            const fuse_release_in* in,
                            FuseResponse<void>* /* out */) {
        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 */,
                          const void* /* in */,
                          const fuse_flush_in* in,
                          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>
@@ -382,8 +385,10 @@ private:
        return read_size;
    }

    int write_object_bytes(int inode, uint64_t offset, uint32_t size, const void* buffer,
                           uint32_t* written_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)));
@@ -394,15 +399,28 @@ private:
            }
            memcpy(bytes.get(), buffer, size);
        }
        *written_size = env_->CallIntMethod(
                self_, app_fuse_write_object_bytes, inode, offset, size, array.get());
        if (env_->ExceptionCheck()) {
            env_->ExceptionClear();
            return -EIO;
        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.
@@ -511,15 +529,21 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
        return -1;
    }

    app_fuse_buffer = env->GetFieldID(app_fuse_class, "mBuffer", "[B");
    if (app_fuse_buffer == nullptr) {
        ALOGE("Can't find mBuffer");
    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_write_object_bytes = env->GetMethodID(app_fuse_class, "writeObjectBytes", "(IJI[B)I");
    if (app_fuse_write_object_bytes == nullptr) {
        ALOGE("Can't find getWriteObjectBytes");
    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;
    }

+74 −13
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ 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;
@@ -34,6 +35,8 @@ public class AppFuse {
        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.
@@ -94,7 +97,8 @@ public class AppFuse {
    public ParcelFileDescriptor openFile(int i, int mode) throws FileNotFoundException {
        Preconditions.checkArgument(
                mode == ParcelFileDescriptor.MODE_READ_ONLY ||
                mode == ParcelFileDescriptor.MODE_WRITE_ONLY);
                mode == (ParcelFileDescriptor.MODE_WRITE_ONLY |
                         ParcelFileDescriptor.MODE_TRUNCATE));
        return ParcelFileDescriptor.open(new File(
                getMountPoint(),
                Integer.toString(i)),
@@ -127,6 +131,7 @@ public class AppFuse {

        /**
         * Handles writing bytes for the give inode.
         * @param fileHandle
         * @param inode
         * @param offset Offset 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.
         * @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")
@@ -142,10 +163,8 @@ public class AppFuse {
    private long getFileSize(int inode) {
        try {
            return mCallback.getFileSize(inode);
        } catch (FileNotFoundException e) {
            return -OsConstants.ENOENT;
        } catch (UnsupportedOperationException e) {
            return -OsConstants.ENOTSUP;
        } catch (Exception error) {
            return -getErrnoFromException(error);
        }
    }

@@ -159,20 +178,62 @@ public class AppFuse {
            // 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 (IOException e) {
            return -OsConstants.EIO;
        } catch (UnsupportedOperationException e) {
            return -OsConstants.ENOTSUP;
        } catch (Exception error) {
            return -getErrnoFromException(error);
        }
    }

    @UsedByNative("com_android_mtp_AppFuse.cpp")
    @WorkerThread
    private /* unsgined */ int writeObjectBytes(int inode,
    private /* unsgined */ int writeObjectBytes(long fileHandler,
                                                int inode,
                                                /* unsigned */ long offset,
                                                /* unsigned */ int size,
                                                byte[] bytes) throws IOException {
        return mCallback.writeObjectBytes(inode, offset, size, bytes);
                                                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 boolean native_start_app_fuse_loop(int fd);
+84 −39
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.mtp;

import android.content.ContentResolver;
import android.content.Context;
import android.content.UriPermission;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
@@ -38,11 +39,16 @@ import android.provider.DocumentsContract.Root;
import android.provider.DocumentsContract;
import android.provider.DocumentsProvider;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
@@ -82,6 +88,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
    private MtpDatabase mDatabase;
    private AppFuse mAppFuse;
    private ServiceIntentSender mIntentSender;
    private Context mContext;

    /**
     * Provides singleton instance to MtpDocumentsService.
@@ -93,6 +100,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
    @Override
    public boolean onCreate() {
        sSingleton = this;
        mContext = getContext();
        mResources = getContext().getResources();
        mMtpManager = new MtpManager(getContext());
        mResolver = getContext().getContentResolver();
@@ -137,12 +145,14 @@ public class MtpDocumentsProvider extends DocumentsProvider {

    @VisibleForTesting
    boolean onCreateForTesting(
            Context context,
            Resources resources,
            MtpManager mtpManager,
            ContentResolver resolver,
            MtpDatabase database,
            StorageManager storageManager,
            ServiceIntentSender intentSender) {
        mContext = context;
        mResources = resources;
        mMtpManager = mtpManager;
        mResolver = resolver;
@@ -232,43 +242,43 @@ public class MtpDocumentsProvider extends DocumentsProvider {
        try {
            openDevice(identifier.mDeviceId);
            final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
            switch (mode) {
                case "r":
            // Turn off MODE_CREATE because openDocument does not allow to create new files.
            final int modeFlag =
                    ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
            if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
                long fileSize;
                try {
                    fileSize = getFileSize(documentId);
                } catch (UnsupportedOperationException exception) {
                    fileSize = -1;
                }
                    // MTP getPartialObject operation does not support files that are larger than
                    // 4GB. Fallback to non-seekable file descriptor.
                if (MtpDeviceRecord.isPartialReadSupported(
                        device.operationsSupported, fileSize)) {
                        return mAppFuse.openFile(
                                Integer.parseInt(documentId), ParcelFileDescriptor.MODE_READ_ONLY);
                    return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
                } else {
                    // If getPartialObject{|64} are not supported for the device, returns
                    // non-seekable pipe FD instead.
                    return getPipeManager(identifier).readDocument(mMtpManager, identifier);
                }
                case "w":
            } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
                // TODO: Clear the parent document loader task (if exists) and call notify
                // when writing is completed.
                if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
                        return getPipeManager(identifier).writeDocument(
                                getContext(), mMtpManager, identifier, device.operationsSupported);
                    return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
                } else {
                    throw new UnsupportedOperationException(
                            "The device does not support writing operation.");
                }
                case "rw":
            } else {
                // TODO: Add support for "rw" mode.
                    throw new UnsupportedOperationException(
                            "The provider does not support 'rw' mode.");
                default:
                    throw new IllegalArgumentException("Unknown mode for openDocument: " + 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) {
            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 final Map<Long, MtpFileWriter> mWriters = new HashMap<>();

        @Override
        public long getFileSize(int inode) throws FileNotFoundException {
            return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
        }

        @Override
        public long readObjectBytes(
                int inode, long offset, long size, byte[] buffer) throws IOException {
@@ -617,15 +634,43 @@ public class MtpDocumentsProvider extends DocumentsProvider {
        }

        @Override
        public long getFileSize(int inode) throws FileNotFoundException {
            return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
        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 int writeObjectBytes(int inode, long offset, int size, byte[] bytes)
                throws IOException {
            // TODO: Implement it.
            throw new IOException();
        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;
            }
            try {
                writer.close();
            } finally {
                mWriters.remove(fileHandle);
            }
        }
    }
}
+108 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
Loading