在Lollipop中从Google Drive获取文件路径(MediaStore.MediaColumns.DATA == null)

5
当用户在Google Drive中点击“发送文件”按钮并选择我的应用程序时,我希望获取该文件的文件路径,然后允许用户将其上传到另一个位置。
我查看了这些类似的SO帖子来处理KitKat手机:Get real path from URI, Android KitKat new storage access framework Android - Convert URI to file path on lollipop 但是,在Lollipop设备上,该解决方案似乎不再有效。
问题似乎是当在ContentResolver上运行查询时,MediaStore.MediaColumns.DATA返回null。

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

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

我已阅读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()方法中。这将让我接近我想要的东西,现在唯一的问题是我缺少文件扩展名。所有我做过的搜索都建议使用filepath来获取扩展名,但我无法通过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个回答

9
这里唯一的问题是我丢失了该文件的文件名。这似乎有点复杂,只是为了从驱动器获取文件路径。有没有更好的方法来做到这一点?
你似乎忽略了一个重要的问题。Linux中的文件不需要有名称。它们可以存在于内存中(例如android.os.MemoryFile),甚至可以在没有名称的目录中存在(如使用O_TMPFILE标志创建的文件)。但它们确实需要有一个文件描述符。
简要概述:文件描述符比简单文件更好,应该始终使用它们,除非在关闭它们后太过繁琐。如果您可以使用JNI,它们可以用于与File对象相同的事情,以及更多其他用途。它们由特殊的ContentProvider提供,并可以通过ContentResolver的openFileDescriptor方法访问(该方法接收与目标提供程序关联的Uri)。
也就是说,仅仅建议那些习惯于使用File对象的人将其替换为描述符听起来确实很奇怪。如果您想尝试,请阅读下面的详细说明。如果不想,请直接跳到答案底部的“简单”解决方案。
编辑:下面的答案是在Lollipop变得普遍之前编写的。现在有一个方便的类,可直接访问Linux系统调用,使使用JNI处理文件描述符成为可选项。

描述符简介

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

每个描述符由对存储在操作系统内核中的描述符表条目的引用来表示。该表的条目数量有每个进程和系统范围的限制,因此请快速关闭您的描述符,除非您希望突然失败尝试打开事物和创建新描述符。

描述符操作

在Linux中,C库函数和系统调用分为两种:一种是通过名称操作的(例如readdir()stat()chdir()chown()open()link()),另一种是通过描述符操作的:getdentsfstat()fchdir()fchown()fchownat()openat()linkat()等。只需阅读几个man手册并学习一些黑暗的JNI技巧,就可以轻松调用这些函数和系统调用。这将大大提高您软件的质量!(如果需要说明:我说的是“阅读”和“学习”,而不是一味地使用JNI。)
在Java中,有一个用于处理描述符的类:java.io.FileDescriptor。它可以与FileXXXStream类一起使用,从而间接地与所有框架IO类(包括内存映射和随机访问文件、通道和通道锁)一起使用。这是一个棘手的类。由于需要与某些专有操作系统兼容,这个跨平台类不会公开底层整数编号。甚至不能关闭!相反,您应该关闭相应的IO类,这些IO类(出于兼容性原因)彼此共享相同的底层描述符。
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();

没有支持的方法可以从FileDescriptor中获取整数值,但是你几乎可以安全地假设,在旧版本的操作系统上有一个私有整数descriptor字段,可以通过反射访问。

使用描述符射击自己的脚

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

1)它有一个finalize()方法。阅读它的javadoc以了解对性能的影响。如果您不想面对突然的IO错误,仍然需要关闭它。

2)由于它是可终结的,一旦类实例的引用超出范围,虚拟机将自动关闭它。这就是为什么一些框架类(尤其是MemoryFile)具有finalize()的错误之处,这是框架开发人员的失误:

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的引用,直到接收方完成对您的MemoryFile的操作。

传递和接收描述符

有两种方法可以传递和接收文件描述符:通过子进程继承创建者的描述符和通过进程间通信。

在Linux中,让进程的子进程继承创建者打开的文件、管道和套接字是一种常见的做法,但在Android的本机代码中需要使用fork - Runtime.exec()ProcessBuilder 在创建子进程后会关闭所有额外描述符。如果您选择自己进行fork操作,请确保关闭不必要的描述符

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

Binder允许您将ParcelFileDescriptor提供给任何接受可打包对象的东西,包括将它们放入Bundle中,从内容提供程序返回并通过AIDL调用传递到服务。

注意,大多数试图在进程外传递带有描述符的Bundle的尝试,包括调用startActivityForResult都将被系统拒绝,这很可能是因为及时关闭这些描述符会比较困难。更好的选择是创建ContentProvider(它会为您管理描述符生命周期,并通过ContentResolver发布文件)或编写AIDL接口并在传输后立即关闭描述符。还要注意,将ParcelFileDescriptor存在任何地方都没有太多意义:它只能在进程死亡之前工作,相应的整数一旦重建您的进程就很可能指向其他内容。
域套接字对于描述符传输而言是低级且有点麻烦的,特别是与提供程序和AIDL相比。然而,它们是本机进程的一个很好的(也是唯一的记录)选项。如果您被迫使用原生二进制文件打开文件或移动数据(这通常是使用root权限的应用程序的情况),请考虑不要浪费精力和CPU资源与那些二进制文件进行复杂的通信,而是编写一个开放式助手(open helper)。[厚颜无耻的广告]顺便说一下,您可以使用我写的,而不是创建自己的。[/ 厚颜无耻的广告]

确切问题的答案

我希望这个答案能让您了解MediaStore.MediaColumns.DATA存在的问题,以及为什么Android开发团队创建这个列是一个误称。

话虽如此,如果您仍然不信服,想要以任何代价获得文件名,或者只是没有阅读上面压倒性的文字墙,请使用这个已准备好的JNI函数;受Getting Filename from file descriptor in C的启发(编辑:现在有一个pure-Java version):

// 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);
}

这里是一个与之相关的类:

// com/example/FdUtil.java
public class FdUtil {
  static {
    System.loadLibrary(System.mapLibraryName("fdutil"));
  }

  public static String getFdPath(ParcelFileDescriptor fd) throws IOException {
    int intFd = fd.getFd();

    if (intFd <= 0)
      throw new IOException("Invalid fd");

      return getFdPathInternal(intFd);
  }

  private static native String getFdPathInternal(int fd) throws IOException;
}

1
哦,我的天啊,这就是我为什么喜欢stackoverflow的原因。非常感谢您抽出时间向像我这样的白痴解释这个问题 :) - android_student

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接