5

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

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

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 一起使用”,但我不明白。如何直接将 URI 与 ContentResolvers 一起使用?

但是,我仍然不清楚如何最好地处理这些类型的 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 "";
}

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

谢谢。

编辑:所以我可以使用普通查询来获取文件名。然后我可以将它传递给我的 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();
        }
    }

但是,我并不完全相信这是做到这一点的“正确”方式?

4

1 回答 1

9

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

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


简短的总结:文件描述符比简单文件更好,应该始终使用,除非你自己关闭它们负担过重。如果您可以使用 JNI,它们可以用于与对象相同的事物File,甚至更多。它们由特殊的 ContentProvider 提供,并且可以通过openFileDescriptorContentResolver 的方法访问(它接收 Uri,与目标提供程序相关联)。

也就是说,只是说习惯于File对象的人用描述符代替它们听起来很奇怪。如果您想尝试一下,请阅读下面的详细说明。如果您不这样做,请跳至“简单”解决方案的答案底部。

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


关于描述符的快速简报

文件描述符来自 Linuxopen系统调用和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 类一起使用,包括内存映射和随机访问文件、通道和通道锁。这是一门棘手的课。由于需要与某些专有操作系统兼容,此跨平台类不公开底层整数。它甚至不能关闭!相反,您应该关闭相应的 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)由于是可终结的,一旦对类实例的引用超出范围,它将被虚拟机自动关闭。这就是为什么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在本机代码中执行或保留对 created 的引用,ParcelFileDescriptor直到接收器完成您的MemoryFile.

提供和接收描述符

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

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

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

Binder 允许您提供ParcelFileDescriptor任何接受可打包对象的东西,包括将它们放入 Bundles、从内容提供者返回以及通过 AIDL 调用传递给服务。

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

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


回答确切的问题

我希望这个答案能给您一个好主意,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);
}

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

// 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;
}
于 2015-05-17T05:44:25.420 回答