5

ユーザーが Google ドライブの [ファイルを送信] ボタンをクリックしてアプリを選択したとき。そのファイルのファイルパスを取得し、ユーザーが別の場所にアップロードできるようにしたいと考えています。

キットカットの電話については、これらの同様の SO 投稿を確認します: Get real path from URI, Android KitKat new storage access framework

Android - lollipop で URI をファイル パスに変換する

ただし、その解決策は Lollipop デバイスでは機能しないようです。

ContentResolver でクエリを実行すると、MediaStore.MediaColumns.DATA が null を返すことが問題のようです。

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

生のファイルシステム パスを取得しようとする代わりに、 ContentResolver.openFileDescriptor() を使用する必要があります。「_data」列は CATEGORY_OPENABLE コントラクトの一部ではないため、ドライブはそれを返す必要はありません。

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

しかし、これがこれを行う「正しい」方法であると完全に確信しているわけではありませんか?

4

1 に答える 1

9

ここでの唯一の問題は、そのファイルのファイル名を失うことです。これは、ドライブから filePath を取得するためだけに少し複雑に思えます。これを行うより良い方法はありますか?

ここで重要な点を見逃しているようです。Linux のファイルには名前が必要ありません。それらはメモリ内に存在する場合 (例: android.os.MemoryFile)、または名前を持たずにディレクトリに存在する場合もあります (O_TMPFILEフラグで作成されたファイルなど)。彼らが持っている必要があるのはファイル記述子です。


簡単な要約: ファイル記述子は単純なファイルよりも優れており、自分でそれらを閉じることが負担になりすぎない限り、代わりに常に使用する必要があります。FileJNI を使用できる場合は、オブジェクトと同じこと、またはそれ以上の目的で使用できます。それらは特別な ContentProvider によって利用可能になりopenFileDescriptor、ContentResolver (ターゲット プロバイダーに関連付けられた Uri を受け取る) のメソッドを介してアクセスできます。

Fileとはいえ、オブジェクトに慣れている人々がそれらを記述子に置き換えると言っただけでは、確かに奇妙に聞こえます。試してみたい場合は、以下の詳細な説明をお読みください。そうでない場合は、「単純な」解決策の回答の最後までスキップしてください。

編集:以下の回答は、Lollipop が普及する前に書かれたものです。最近では、Linux システム コールに直接アクセスするための便利なクラスがあり、JNI を使用してファイル ディスクリプタを操作することがオプションになります。


記述子に関する簡単な説明

ファイル記述子は、Linuxopenシステム コールとopen()C ライブラリの対応する関数から取得されます。ファイルの記述子を操作するためにファイルにアクセスする必要はありません。ほとんどのアクセス チェックは単純にスキップされますが、アクセス タイプ (読み取り/書き込み/読み取りと書き込みなど) などの重要な情報は記述子に「ハードコード」されており、作成後に変更することはできません。ファイル記述子は、0 から始まる非負の整数で表されます。これらの数値は各プロセスにローカルであり、永続的またはシステム全体の意味はありません。特定のプロセス (0、 1 と 2 は伝統的stdinに 、stdoutおよびstderr) を参照します。

各記述子は、OS カーネルに格納されている記述子テーブルのエントリへの参照によって表されます。そのテーブルのエントリ数にはプロセスごとおよびシステム全体の制限があるため、何かを開いて新しい記述子を作成しようとして突然失敗したくない場合を除き、記述子をすばやく閉じてください。

記述子の操作

Linux には、2 種類の C ライブラリ関数とシステム コールがあります。名前 ( readdir()stat()chdir()chown()open()など) を操作するものと、、、、、などのlink()記述子を操作するものです。これらの関数とシステム コールは、いくつかの man ページを読み、いくつかの暗い JNI マジックを研究しています。これにより、ソフトウェアの品質が大幅に向上します。(念のため: JNI を常に盲目的に使用するだけでなく、 を読ん勉強することについて話している)。getdentsfstat()fchdir()fchown()fchownat()openat()linkat()

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();

から整数値を取得するサポートされている方法はありませんが、古い OS バージョンには、リフレクションを介してアクセスできるFileDescriptorプライベート整数フィールドがあると (ほぼ) 安全に想定できます。descriptor

記述子で足を撃つ

Android フレームワークには、Linux ファイル記述子を操作するための特殊なクラスがあります: android.os.ParcelFileDescriptor. 残念ながら、これは FileDescriptor とほぼ同じくらい悪いものです。なんで?理由は 2 つあります。

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

dupsyscall は整数のファイル記述子を複製します。これにより、対応するファイル記述子FileDescriptorが元のファイル記述子から独立します。プロセス間で記述子を渡す場合、手動で複製する必要がないことに注意してください。受信した記述子は、ソース プロセスから独立しています。MemoryFile(リフレクションで取得した場合)の記述子を渡すには、への呼び出しdup必要です。元のプロセスで共有メモリ領域が破棄されると、誰もがアクセスできなくなります。dupさらに、ネイティブ コードで実行するかParcelFileDescriptor、レシーバーがMemoryFile.

記述子の授受

ファイル記述子を授受するには、作成者の記述子を子プロセスに継承させる方法と、プロセス間通信を使用する方法の 2 つがあります。

プロセスの子にファイル、パイプ、およびソケットを継承させ、作成者が開くようにすることは、Linux では一般的な方法ですが、Android ではネイティブ コードで fork する必要があります。また、子プロセスの作成後にすべての余分な記述子Runtime.exec()を閉じる必要があります。自分で選択した場合は、不要な記述子ProcessBuilderも必ず閉じてください。fork

現在 Android でファイル記述子の受け渡しをサポートしている唯一の IPC 機能は、Binder と Linux ドメイン ソケットです。

Binder を使用するとParcelFileDescriptor、バンドルに入れたり、コンテンツ プロバイダーから戻ったり、AIDL 呼び出しを介してサービスに渡したりするなど、分割可能なオブジェクトを受け入れるあらゆるものに与えることができます。

プロセス外で記述子を使用して Bundles を渡そうとするほとんどの試みは、startActivityForResultwill の呼び出しを含め、システムによって拒否されることに注意してください。ContentProvider (記述子のライフサイクルを管理し、 を介してファイルを公開するContentResolver) を作成するか、AIDL インターフェースを作成し、転送直後に記述子を閉じることをお勧めします。ParcelFileDescriptor また、どこにでも永続化することはあまり意味がないことに注意してください。プロセスが終了するまでしか機能せず、プロセスが再作成されると、対応する整数が別のものを指す可能性が高くなります。

ドメイン ソケットは低レベルであり、特にプロバイダーや AIDL と比較して、記述子の転送に使用するのは少し面倒です。ただし、これらはネイティブ プロセスの優れた (そして唯一文書化された) オプションです。ネイティブ バイナリを使用してファイルを開いたりデータを移動したりする必要がある場合 (これは通常、root 権限を使用するアプリケーションの場合です)、それらのバイナリとの複雑な通信に労力と CPU リソースを無駄にしないことを検討し、代わりにopen ヘルパーを記述します。 . 【恥知らずな宣伝】 ちなみに自作ではなく、私が書いたものを使っても構いません。[/恥知らずな広告]


正確な質問への回答

この回答で、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 に答える