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

and*_*ent 5 android android-intent google-drive-api storage-access-framework

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

我为kitkat手机检查了这些类似的SO帖子:从URI,Android KitKat新的存储访问框架获取真实路径

Android - 将URI转换为棒棒糖上的文件路径

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

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

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

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

我已经阅读了CommonsWare的这篇博文,其中建议我"尝试直接使用Uri直接使用ContentResolver",这是我不明白的.如何直接在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 "";
}
Run Code Online (Sandbox Code Playgroud)

这里唯一的问题是我丢失了该文件的文件名.为了从驱动器获取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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是,我并不完全相信这是"正确"的做法吗?

use*_*723 8

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

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


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

也就是说,只是说用来File对象的人用描述符替换它们听起来确实听起来很奇怪.如果你想尝试一下,请阅读下面的详细说明.如果不这样做,只需跳到答案的底部即可获得"简单"解决方案.

编辑:下面的答案是在棒棒糖广泛传播之前编写的.现在有一个方便的类可以直接访问Linux系统调用,这使得使用JNI处理文件描述符是可选的.


关于描述符的快速简报

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

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

对描述符进行操作

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

在Java中有一个用于处理描述符的类:java.io.FileDescriptor.它可以FileXXXStream一起使用,因此可以与所有框架IO类间接使用,包括内存映射和随机访问文件,通道和通道锁.这是一个棘手的课程.由于要求与某些专有OS兼容,因此该跨平台类不会暴露基础整数.它甚至无法关闭!相反,您需要关闭相应的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();
Run Code Online (Sandbox Code Playgroud)

没有支持的方法来获取整数值FileDescriptor,但您可以(几乎)安全地假设,在较旧的OS版本上有一个私有整数descriptor字段,可以通过反射访问.

用描述符射击自己的脚

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

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

2)由于可以最终化,一旦对类实例的引用超出范围,它将由虚拟机自动关闭.这就是为什么有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
Run Code Online (Sandbox Code Playgroud)

幸运的是,有一种解决这种恐怖的方法:一个神奇的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());
  }
}
Run Code Online (Sandbox Code Playgroud)

dup系统调用克隆整数文件描述符,这使得相应的FileDescriptor独立于原始之一.请注意,跨进程传递描述符不需要手动复制:接收的描述符独立于源进程.传递描述符MemoryFile(如果你用反射获得它)确实需要调用dup:在原始进程中销毁共享内存区域将使每个人都无法访问它.此外,您必须执行dup本机代码或保持对创建的引用,ParcelFileDescriptor直到接收器完成您的MemoryFile.

给予和接受描述符

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

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

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

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

请注意,大多数尝试在进程外传递带有描述符的Bundles,包括startActivityForResult系统拒绝调用,可能是因为及时关闭这些描述符会太难.更好的选择是创建一个ContentProvider(它为您管理描述符生命周期,并通过发布文件ContentResolver)或编写AIDL接口并在传输后立即关闭描述符.还要注意,持久化ParcelFileDescriptor 任何地方都没有多大意义:只有在重新创建进程后,它才会工作,直到进程死亡和相应的整数最有可能指向其他内容.

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


回答确切的问题

我希望,这个答案给了你一个好主意,MediaStore.MediaColumns.DATA有什么问题,以及为什么创建这个专栏是Android开发团队的一个误称.

也就是说,如果您仍然不相信,不惜一切代价想要这个文件名,或者根本无法阅读上面的压倒性的文本墙,这里 - 有一个随时可用的JNI函数; 灵感来自C中文件描述符获取文件名(编辑:现在有一个纯Java版本):

// 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);
}
Run Code Online (Sandbox Code Playgroud)

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

// 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;
}
Run Code Online (Sandbox Code Playgroud)