从Lollipop的google驱动器获取文件路径(MediaStore.MediaColumns.DATA == null)

时间:2015-03-31 21:11:04

标签: android android-intent google-drive-api storage-access-framework

当用户点击"发送文件"谷歌驱动器中的按钮,并选择我的应用程序。我想获取该文件的文件路径,然后允许用户将其上传到其他位置。

我检查了这些类似的针对kitkat手机的SO帖子:Get real path from URI, Android KitKat new storage access framework

Android - Convert URI to file path on lollipop

然而,解决方案似乎不再适用于Lollipop设备。

问题似乎是MediaStore.MediaColumns.DATA在ContentResolver上运行查询时返回null。

  

https://code.google.com/p/android/issues/detail?id=63651

     

您应该使用ContentResolver.openFileDescriptor()而不是尝试获取原始文件系统路径。 " _data" column不是CATEGORY_OPENABLE合约的一部分,因此不需要Drive返回它。

我已阅读此blog post by CommonsWare,其中建议我尝试直接在ContentResolver"中使用Uri。我不明白。如何直接在ContentResolvers中使用URI?

但是,我还不清楚如何最好地处理这些类型的URI。

我能找到的最佳解决方案是调用openFileDescriptor,然后将文件流复制到新文件中,然后将新文件路径传递给我的上传活动。

 private static String getDriveFileAbsolutePath(Activity context, Uri uri) {
    if (uri == null) return null;
    ContentResolver resolver = context.getContentResolver();
    FileInputStream input = null;
    FileOutputStream output = null;
    String outputFilePath = new File(context.getCacheDir(), fileName).getAbsolutePath();
    try {
        ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
        FileDescriptor fd = pfd.getFileDescriptor();
        input = new FileInputStream(fd);
        output = new FileOutputStream(outputFilePath);
        int read = 0;
        byte[] bytes = new byte[4096];
        while ((read = input.read(bytes)) != -1) {
            output.write(bytes, 0, read);
        }
        return new File(outputFilePath).getAbsolutePath();
    } catch (IOException ignored) {
        // nothing we can do
    } finally {
            input.close();
            output.close();
    }
    return "";
}

这里唯一的问题是我丢失了该文件的文件名。为了从驱动器获取filePath,这似乎有点复杂。有更好的方法吗?

感谢。

编辑: 所以我可以使用普通查询来获取文件名。然后我可以将它传递给我的getDriveAbsolutePath()方法。这将使我非常接近我想要的,现在唯一的问题是我缺少文件扩展名。我所做的所有搜索建议使用文件路径来获取扩展,我可以使用openFileDescriptor()。有什么帮助吗?

    String filename = "";
    final String[] projection = {
            MediaStore.MediaColumns.DISPLAY_NAME
    };
    ContentResolver cr = context.getApplicationContext().getContentResolver();
    Cursor metaCursor = cr.query(uri, projection, null, null, null);
    if (metaCursor != null) {
        try {
            if (metaCursor.moveToFirst()) {
                filename = metaCursor.getString(0);
            }
        } finally {
            metaCursor.close();
        }
    }

但是,我并不完全相信这是"对"这样做的方法?

1 个答案:

答案 0 :(得分:8)

  

这里唯一的问题是我丢失了该文件的文件名。为了从驱动器获取filePath,这似乎有点复杂。有更好的方法吗?

你似乎错过了一个重要的观点。 Linux中的文件不需要具有名称。它们可能存在于内存中(例如android.os.MemoryFile),甚至可能驻留在目录中而没有名称(例如创建的文件) 带O_TMPFILE标志)。他们需要的是文件描述符

简短摘要:文件描述符比简单文件更好,应该总是使用它们,除非在你自己之后关闭它们是太多的负担。如果你可以使用JNI,它们可以用于File个对象,以及更多。它们由特殊的ContentProvider提供,可以通过ContentResolver的openFileDescriptor方法访问(它接收与目标提供者关联的Uri)。

那就是说,只是说用于File对象的人用描述符替换它们肯定听起来很奇怪。如果你想尝试一下,请阅读下面的详细说明。如果你不这样做,只需跳到答案的底部即可。"简单"溶液

编辑:以下答案是在棒棒糖广为流传之前编写的。现在有a handy class可以直接访问Linux系统调用,这使得使用JNI处理文件描述符是可选的。

关于描述符的快速简报

文件描述符来自Linux open系统调用和C库中相应的open()函数。您不需要访问文件来操作它的描述符。大多数访问检查将被简单地跳过,但是一些关键信息,例如访问类型(读/写/读写等)是"硬编码"进入描述符,创建后无法更改。文件描述符由非负整数表示,从0开始。这些数字对于每个进程都是本地的,并且不具有任何持久性或系统范围的含义,它们仅仅区分句柄 给定进程的文件彼此相同(0,1和2传统上引用stdinstdoutstderr)。

每个描述符由对描述符表中的条目的引用表示,存储在OS内核中。对于该表中的条目数,每个进程和系统范围limits,因此快速关闭描述符,除非您希望尝试打开内容并创建新描述符以突然失败。

对描述符进行操作

在Linux中有两种C库函数和系统调用:使用名称(例如readdir()stat()chdir()chown(),{{1 }},open())并对描述符进行操作:link()getdentsfstat()fchdir()fchown()fchownat()openat()等。在阅读了几本手册页并研究一些黑暗的JNI魔法之后,您可以轻松地调用这些函数和系统调用。这将通过屋顶提高您的软件质量! (以防万一:我说的是阅读学习,而不是一直盲目地使用JNI。)

在Java中,有一个用于处理描述符的类:linkat()。它can be used具有java.io.FileDescriptor类,因此间接与所有框架IO类,包括内存映射和随机访问文件,通道和通道锁。这是一个棘手的课程。由于要求与某些专有OS兼容,因此该跨平台类不会暴露基础整数。它甚至无法关闭!相反,您应该关闭相应的IO类,这些类(再次出于兼容性原因)彼此共享相同的底层描述符:

FileXXXStream

没有受支持的方法可以从FileInputStream fileStream1 = new FileInputStream("notes.db"); FileInputStream fileStream2 = new FileInputStream(fileStream1.getFD()); WritableByteChannel aChannel = fileStream1.getChannel(); // pass fileStream1 and aChannel to some methods, written by clueless people ... // surprise them (or get surprised by them) fileStream2.close(); 中获取整数值,但您可以(几乎)安全地假设,在较旧的OS版本上有一个私有整数FileDescriptor字段,可以访问通过反思。

用描述符射击自己的脚

在Android框架中,有一个用于处理Linux文件描述符的专用类:descriptor。不幸的是,它几乎和FileDescriptor一样糟糕。为什么?有两个原因:

1)它有一个android.os.ParcelFileDescriptor方法。阅读它的javadoc来学习,这对你的表现意味着什么。如果你不想面对突然的IO错误,你仍然需要关闭它。

2)由于可以最终化,一旦对类实例的引用超出范围,它将由虚拟机自动关闭。这就是为什么在某些框架类上使用finalize()特别是 finalize()是框架开发人员的一个错误:

MemoryFile

幸运的是,这种恐怖行为有一种补救方法:一个神奇的public FileOutputStream giveMeAStream() { ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY); return new FileInputStream(fd.getDescriptor()); } ... FileInputStream aStream = giveMeAStream(); // enjoy having aStream suddenly closed during garbage collection 系统调用:

dup

public FileOutputStream giveMeAStream() { ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY); return new FileInputStream(fd.dup().getDescriptor()); } ... FileInputStream aStream = giveMeAStream(); // you are perfectly safe now... // Just kidding! Also close original ParcelFileDescriptor like this: public FileOutputStream giveMeAStreamProperly() { // Use try-with-resources block, because closing things in Java is hard. // You can employ Retrolambda for backward compatibility, // it can handle those too! try (ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY)) { return new FileInputStream(fd.dup().getDescriptor()); } } 系统调用克隆整数文件描述符,使相应的dup独立于原始文件描述符。请注意,跨进程传递描述符不需要手动复制:已接收 描述符独立于源进程。传递FileDescriptor的描述符(如果你用反射获得它)确实需要调用MemoryFile:在原始文件中销毁共享内存区域 过程将使每个人都无法访问。此外,您必须在原生代码中执行dup或保留对已创建dup的引用,直到接收者完成您的ParcelFileDescriptor

给予和接收描述符

有两种方式来提供和接收文件描述符:让子进程继承创建者的描述符并通过进程间通信。

允许进程的子进程继承文件,管道和套接字,由创建者打开,这是Linux中的常见做法,但需要在Android上的本机代码中分叉 - MemoryFileRuntime.exec()关闭所有额外的 创建子进程后的描述符。如果您愿意,也一定要close unnecessary descriptorsProcessBuilder你自己。

目前支持在Android上传递文件描述符的唯一IPC工具是Binder和Linux域套接字。

Binder允许您向接受可分配对象的任何内容提供fork,包括将它们放入Bundles,从内容提供程序返回并通过AIDL调用传递给服务。

请注意,大多数尝试在进程外传递包含描述符的Bundles,包括调用ParcelFileDescriptor将被系统拒绝,可能是因为及时关闭这些描述符会太难。更好的选择是创建ContentProvider(它为您管理描述符生命周期,并通过startActivityForResult发布文件)或编写AIDL接口并在传输后立即关闭描述符。另请注意,持续ContentResolver 任何地方都没有多大意义:只有在重建过程之后,它才会有效,直到过程死亡和相应的整数最有可能指向别的东西。

域套接字是低级的,用于描述符传输有点痛苦,特别是与提供者和AIDL相比。但是,它们是本机进程的一个好的(并且是唯一记录的)选项。如果你是 强制打开文件和/或使用本机二进制文件移动数据(这通常是应用程序的情况,使用root权限),考虑不要浪费你的努力和CPU资源与复杂的通信 那些二进制文件,而是写一个打开助手。 [无耻广告]顺便说一下,您可以使用the one I wrote,而不是创建自己的广告。 [/无耻广告]

回答确切的问题

我希望,这个答案给了你一个好主意,MediaStore.MediaColumns.DATA有什么问题,以及为什么创建这个专栏对Android开发团队来说是个用词不当。

也就是说,如果您仍然不相信,想要不惜一切代价 ,或者根本无法阅读上面的压倒性文本墙,这里 - 有一个随时可用的JNI功能;灵感来自Getting Filename from file descriptor in C编辑:现在有一个pure-Java version):

ParcelFileDescriptor

这是一个与之相关的课程:

// src/main/jni/fdutil.c
JNIEXPORT jstring Java_com_example_FdUtil_getFdPathInternal(JNIEnv *env, jint descriptor)
{
  // The filesystem name may not fit in PATH_MAX, but all workarounds
  // (as well as resulting strings) are prone to OutOfMemoryError.
  // The proper solution would, probably, include writing a specialized   
  // CharSequence. Too much pain, too little gain.
  char buf[PATH_MAX + 1] = { 0 };

  char procFile[25];

  sprintf(procFile, "/proc/self/fd/%d", descriptor);

  if (readlink(procFile, buf, sizeof(buf)) == -1) {
    // the descriptor is no more, became inaccessible etc.
    jclass exClass = (*env) -> FindClass(env, "java/io/IOException");

    (*env) -> ThrowNew(env, exClass, "readlink() failed");

    return NULL;
  }

  if (buf[PATH_MAX] != 0) {
    // the name is over PATH_MAX bytes long, the caller is at fault
    // for dealing with such tricky descriptors
    jclass exClass = (*env) -> FindClass(env, "java/io/IOException");

    (*env) -> ThrowNew(env, exClass, "The path is too long");

    return NULL;
  }

  if (buf[0] != '/') {
    // the name is not in filesystem namespace, e.g. a socket,
    // pipe or something like that
    jclass exClass = (*env) -> FindClass(env, "java/io/IOException");

    (*env) -> ThrowNew(env, exClass, "The descriptor does not belong to file with name");

    return NULL;
  }

  // doing stat on file does not give any guarantees, that it
  // will remain valid, and on Android it likely to be
  // inaccessible to us anyway let's just hope
  return (*env) -> NewStringUTF(env, buf);
}