LOCAL_CERTIFICATE := media
LOCAL_PRIVILEGED_MODULE := true
LOCAL_JNI_SHARED_LIBRARIES := libappfuse_jni
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
include $(BUILD_PACKAGE)
include $(call all-makefiles-under, $(LOCAL_PATH))
--- /dev/null
+# Keeps methods that are invoked by JNI.
+-keepclassmembers class * {
+ @com.android.mtp.annotations.UsedByNative *;
+}
import android.os.storage.StorageManager;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.mtp.annotations.UsedByNative;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-/**
- * TODO: Remove VisibleForTesting class.
- */
-@VisibleForTesting
public class AppFuse {
static {
System.loadLibrary("appfuse_jni");
}
+ /**
+ * Max read amount specified at the FUSE kernel implementation.
+ * The value is copied from sdcard.c.
+ */
+ static final int MAX_READ = 128 * 1024;
+
private final String mName;
private final Callback mCallback;
private final Thread mMessageThread;
private ParcelFileDescriptor mDeviceFd;
- @VisibleForTesting
AppFuse(String name, Callback callback) {
mName = name;
mCallback = callback;
});
}
- @VisibleForTesting
void mount(StorageManager storageManager) {
mDeviceFd = storageManager.mountAppFuse(mName);
mMessageThread.start();
}
}
- /**
- * @param i
- * @throws FileNotFoundException
- */
- @VisibleForTesting
public ParcelFileDescriptor openFile(int i) throws FileNotFoundException {
return ParcelFileDescriptor.open(new File(
getMountPoint(),
byte[] getObjectBytes(int inode, long offset, int size) throws IOException;
}
- @VisibleForTesting
+ @UsedByNative("com_android_mtp_AppFuse.cpp")
private long getFileSize(int inode) {
try {
return mCallback.getFileSize(inode);
}
}
- @VisibleForTesting
+ @UsedByNative("com_android_mtp_AppFuse.cpp")
private byte[] getObjectBytes(int inode, long offset, int size) {
+ if (offset < 0 || size < 0 || size > MAX_READ) {
+ return null;
+ }
try {
return mCallback.getObjectBytes(inode, offset, size);
} catch (IOException e) {
package com.android.mtp;
+import static com.android.internal.util.Preconditions.checkArgument;
+
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.mtp.MtpObjectInfo;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
+import android.os.storage.StorageManager;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsContract;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
import java.io.FileNotFoundException;
import java.io.IOException;
private RootScanner mRootScanner;
private Resources mResources;
private MtpDatabase mDatabase;
+ private AppFuse mAppFuse;
/**
* Provides singleton instance to MtpDocumentsService.
mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
+ mAppFuse = new AppFuse(TAG, new AppFuseCallback());
+ // TODO: Mount AppFuse on demands.
+ mAppFuse.mount(getContext().getSystemService(StorageManager.class));
resume();
return true;
}
try {
switch (mode) {
case "r":
- return getPipeManager(identifier).readDocument(mMtpManager, identifier);
+ final long fileSize = getFileSize(documentId);
+ // MTP getPartialObject operation does not support files that are larger than 4GB.
+ // Fallback to non-seekable file descriptor.
+ // TODO: Use getPartialObject64 for MTP devices that support Android vendor
+ // extension.
+ if (fileSize <= 0xffffffff) {
+ return mAppFuse.openFile(Integer.parseInt(documentId));
+ } else {
+ return getPipeManager(identifier).readDocument(mMtpManager, identifier);
+ }
case "w":
// TODO: Clear the parent document loader task (if exists) and call notify
// when writing is completed.
return getPipeManager(identifier).writeDocument(
getContext(), mMtpManager, identifier);
- default:
- // TODO: Add support for seekable files.
+ case "rw":
+ // TODO: Add support for "rw" mode.
throw new UnsupportedOperationException(
- "The provider does not support seekable file.");
+ "The provider does not support 'rw' mode.");
+ default:
+ throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
}
} catch (IOException error) {
throw new FileNotFoundException(error.getMessage());
return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
}
+ private long getFileSize(String documentId) throws FileNotFoundException {
+ final Cursor cursor = mDatabase.queryDocument(
+ documentId,
+ MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
+ try {
+ if (cursor.moveToNext()) {
+ return cursor.getLong(0);
+ } else {
+ throw new FileNotFoundException();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
private static class DeviceToolkit {
public final PipeManager mPipeManager;
public final DocumentLoader mDocumentLoader;
mDocumentLoader = new DocumentLoader(manager, resolver, database);
}
}
+
+ private class AppFuseCallback implements AppFuse.Callback {
+ final byte[] mBytes = new byte[AppFuse.MAX_READ];
+
+ @Override
+ public byte[] getObjectBytes(int inode, long offset, int size) throws IOException {
+ final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
+ mMtpManager.getPartialObject(
+ identifier.mDeviceId, identifier.mObjectHandle, (int) offset, size, mBytes);
+ return mBytes;
+ }
+
+ @Override
+ public long getFileSize(int inode) throws FileNotFoundException {
+ return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
+ }
+ }
}
--- /dev/null
+/*
+ * 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that shows the method is used by JNI.
+ */
+@Target(ElementType.METHOD)
+public @interface UsedByNative {
+ /**
+ * JNI file name that uses the method.
+ */
+ String value();
+}
import java.io.IOException;
import java.util.Arrays;
-/**
- * TODO: Enable this test after adding SELinux policies for appfuse.
- */
@MediumTest
public class AppFuseTest extends AndroidTestCase {
-
- public void disabled_testMount() throws ErrnoException, InterruptedException {
+ public void testMount() throws ErrnoException {
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
final AppFuse appFuse = new AppFuse("test", new TestCallback());
appFuse.mount(storageManager);
assertTrue(1 != Os.stat(file.getPath()).st_ino);
}
- public void disabled_testOpenFile() throws IOException {
+ public void testOpenFile() throws IOException {
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
final int INODE = 10;
final AppFuse appFuse = new AppFuse(
appFuse.close();
}
- public void disabled_testOpenFile_error() {
+ public void testOpenFile_error() {
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
final int INODE = 10;
final AppFuse appFuse = new AppFuse("test", new TestCallback());
appFuse.close();
}
- public void disabled_testReadFile() throws IOException {
+ public void testReadFile() throws IOException {
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
final int INODE = 10;
final byte[] BYTES = new byte[] { 'a', 'b', 'c', 'd', 'e' };