ファイルの共有

コンテンツ URI を使用してファイルを共有するようにアプリを設定すると、他のアプリからのファイルに対するリクエストに応答できます。このようなリクエストに応答する方法の 1 つは、他のアプリが呼び出せるサーバーアプリからファイル選択インターフェースを提供することです。このアプローチにより、クライアント アプリケーションでは、ユーザーがサーバーアプリからファイルを選択して、選択されたファイルのコンテンツ URI を受信できるようになります。

このレッスンでは、ファイルのリクエストに応答するアプリでファイル選択 Activity を作成する方法について説明します。

ファイル リクエストを受信する

クライアント アプリからファイルのリクエストを受信し、コンテンツ URI で応答するには、アプリでファイル選択 Activity を提供する必要があります。クライアント アプリは、アクション ACTION_PICK を含む IntentstartActivityForResult() を呼び出して、この Activity を開始します。クライアント アプリが startActivityForResult() を呼び出すと、アプリからクライアント アプリに、ユーザーが選択したファイルのコンテンツ URI の形式で結果を返すことができます。

ファイルに対するリクエストをクライアント アプリに実装する方法については、共有ファイルのリクエストのレッスンをご覧ください。

ファイル選択アクティビティを作成する

ファイル選択 Activity をセットアップするには、まずマニフェストで Activity を指定し、アクション ACTION_PICK とカテゴリ CATEGORY_DEFAULT および CATEGORY_OPENABLE に一致するインテント フィルタを指定します。また、アプリが他のアプリに提供するファイル用に MIME タイプのフィルタを追加します。次のスニペットは、新しい Activity とインテント フィルタを指定する方法を示しています。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
        <application>
        ...
            <activity
                android:name=".FileSelectActivity"
                android:label="@File Selector" >
                <intent-filter>
                    <action
                        android:name="android.intent.action.PICK"/>
                    <category
                        android:name="android.intent.category.DEFAULT"/>
                    <category
                        android:name="android.intent.category.OPENABLE"/>
                    <data android:mimeType="text/plain"/>
                    <data android:mimeType="image/*"/>
                </intent-filter>
            </activity>

コードでファイル選択アクティビティを定義する

次に、Activity サブクラスを定義します。内部ストレージにあるアプリの files/images/ ディレクトリから入手可能なファイルを表示し、ユーザーが目的のファイルを選択できるようにします。次のスニペットは、この Activity を定義し、ユーザーの選択に応答する方法を示しています。

Kotlin

class MainActivity : Activity() {

    // The path to the root of this app's internal storage
    private lateinit var privateRootDir: File
    // The path to the "images" subdirectory
    private lateinit var imagesDir: File
    // Array of files in the images subdirectory
    private lateinit var imageFiles: Array<File>
    // Array of filenames corresponding to imageFiles
    private lateinit var imageFilenames: Array<String>

    // Initialize the Activity
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Set up an Intent to send back to apps that request a file
        resultIntent = Intent("com.example.myapp.ACTION_RETURN_FILE")
        // Get the files/ subdirectory of internal storage
        privateRootDir = filesDir
        // Get the files/images subdirectory;
        imagesDir = File(privateRootDir, "images")
        // Get the files in the images subdirectory
        imageFiles = imagesDir.listFiles()
        // Set the Activity's result to null to begin with
        setResult(Activity.RESULT_CANCELED, null)
        /*
         * Display the file names in the ListView fileListView.
         * Back the ListView with the array imageFilenames, which
         * you can create by iterating through imageFiles and
         * calling File.getAbsolutePath() for each File
         */
        ...
    }
    ...
}

Java

public class MainActivity extends Activity {
    // The path to the root of this app's internal storage
    private File privateRootDir;
    // The path to the "images" subdirectory
    private File imagesDir;
    // Array of files in the images subdirectory
    File[] imageFiles;
    // Array of filenames corresponding to imageFiles
    String[] imageFilenames;
    // Initialize the Activity
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Set up an Intent to send back to apps that request a file
        resultIntent =
                new Intent("com.example.myapp.ACTION_RETURN_FILE");
        // Get the files/ subdirectory of internal storage
        privateRootDir = getFilesDir();
        // Get the files/images subdirectory;
        imagesDir = new File(privateRootDir, "images");
        // Get the files in the images subdirectory
        imageFiles = imagesDir.listFiles();
        // Set the Activity's result to null to begin with
        setResult(Activity.RESULT_CANCELED, null);
        /*
         * Display the file names in the ListView fileListView.
         * Back the ListView with the array imageFilenames, which
         * you can create by iterating through imageFiles and
         * calling File.getAbsolutePath() for each File
         */
         ...
    }
    ...
}

ファイル選択に応答する

ユーザーが共有ファイルを選択したら、アプリは選択されたファイルを特定し、そのファイルのコンテンツ URI を生成する必要があります。Activity は利用可能なファイルのリストを ListView に表示するため、ユーザーがファイル名をクリックすると、メソッド onItemClick() が呼び出され、選択されたファイルを取得できます。

インテントを使用してアプリ間でファイルの URI を送信する場合は、他のアプリが読み取れる URI を取得するよう注意が必要です。Android 6.0(API レベル 23)以降を搭載したデバイスでこれを行うには、そのバージョンの Android の権限モデルが変更されるため、特別な注意が必要です。特に READ_EXTERNAL_STORAGE 危険な権限になり、受信側のアプリにはこの権限がない可能性があります。

これらの点を念頭に置いて、Uri.fromFile() は使用しないことをおすすめします。これにはいくつかの欠点があります。このメソッドは次の処理を行います。

  • プロファイル間でのファイル共有が許可されません。
  • Android 4.4(API レベル 19)以下を搭載したデバイスでは、アプリに WRITE_EXTERNAL_STORAGE 権限が必要です。
  • 受信側アプリに READ_EXTERNAL_STORAGE 権限が必要です。この権限がない重要な共有ターゲット(Gmail など)では失敗します。

Uri.fromFile() を使用する代わりに URI 権限を使用して、他のアプリに特定の URI へのアクセス権を付与できます。URI 権限は、Uri.fromFile() によって生成された file:// URI では機能しませんが、コンテンツ プロバイダに関連付けられた URI では機能します。FileProvider API は、このような URI の作成に役立ちます。この方法は、外部ストレージではなく、インテントを送信するアプリのローカル ストレージにあるファイルに対しても機能します。

onItemClick() で、選択したファイルの名前の File オブジェクトを取得し、FileProvider<provider> 要素で指定したオーソリティとともに、引数として getUriForFile() に渡します。 結果として得られるコンテンツ URI には、オーソリティ、ファイルのディレクトリに対応するパスセグメント(XML メタデータで指定)、ファイル名(拡張子を含む)が含まれます。FileProvider が XML メタデータに基づいてディレクトリをパスセグメントにマッピングする方法については、共有可能なディレクトリを指定するをご覧ください。

次のスニペットは、選択したファイルを検出してそのコンテンツ URI を取得する方法を示しています。

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            /*
             * Get a File for the selected file name.
             * Assume that the file names are in the
             * imageFilename array.
             */
            val requestFile = File(imageFilenames[position])
            /*
             * Most file-related method calls need to be in
             * try-catch blocks.
             */
            // Use the FileProvider to get a content URI
            val fileUri: Uri? = try {
                FileProvider.getUriForFile(
                        this@MainActivity,
                        "com.example.myapp.fileprovider",
                        requestFile)
            } catch (e: IllegalArgumentException) {
                Log.e("File Selector",
                        "The selected file can't be shared: $requestFile")
                null
            }
            ...
        }
        ...
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            /*
             * When a filename in the ListView is clicked, get its
             * content URI and send it to the requesting app
             */
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                /*
                 * Get a File for the selected file name.
                 * Assume that the file names are in the
                 * imageFilename array.
                 */
                File requestFile = new File(imageFilename[position]);
                /*
                 * Most file-related method calls need to be in
                 * try-catch blocks.
                 */
                // Use the FileProvider to get a content URI
                try {
                    fileUri = FileProvider.getUriForFile(
                            MainActivity.this,
                            "com.example.myapp.fileprovider",
                            requestFile);
                } catch (IllegalArgumentException e) {
                    Log.e("File Selector",
                          "The selected file can't be shared: " + requestFile.toString());
                }
                ...
            }
        });
        ...
    }

共有可能なディレクトリを指定するで説明しているように、<paths> 要素を含むメタデータ ファイルで指定したディレクトリに存在するファイルについてのみ、コンテンツ URI を生成できます。指定していないパスで File に対して getUriForFile() を呼び出すと、IllegalArgumentException が返されます。

ファイルの権限を付与する

別のアプリと共有するファイルのコンテンツ URI を取得したら、クライアント アプリがファイルにアクセスできるようにする必要があります。アクセスを許可するには、コンテンツ URI を Intent に追加し、Intent に権限フラグを設定して、クライアント アプリに権限を付与します。付与する権限は一時的なものであり、受信側アプリのタスクスタックが終了すると自動的に期限切れになります。

次のコード スニペットは、ファイルの読み取り権限を設定する方法を示しています。

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            ...
            if (fileUri != null) {
                // Grant temporary read permission to the content URI
                resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                ...
            }
            ...
        }
        ...
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                ...
                if (fileUri != null) {
                    // Grant temporary read permission to the content URI
                    resultIntent.addFlags(
                        Intent.FLAG_GRANT_READ_URI_PERMISSION);
                }
                ...
             }
             ...
        });
    ...
    }

注意: 一時的なアクセス権限を使用してファイルへのアクセス権を安全に付与する唯一の方法は、setFlags() を呼び出すことです。ファイルのコンテンツ URI に対して Context.grantUriPermission() メソッドを呼び出さないでください。このメソッドで付与されるアクセス権は、Context.revokeUriPermission() の呼び出しによってのみ取り消すことができるためです。

Uri.fromFile() は使用しないでください。この場合、受信側アプリに強制的に READ_EXTERNAL_STORAGE 権限が要求されます。ユーザー間で共有する場合にはまったく機能せず、Android 4.4(API レベル 19)より前のバージョンではアプリに WRITE_EXTERNAL_STORAGE が必要です。また、Gmail アプリなどの非常に重要な共有ターゲットには READ_EXTERNAL_STORAGE がないため、この呼び出しは失敗します。代わりに、URI 権限を使用して、他のアプリに特定の URI へのアクセスを許可できます。URI 権限は、Uri.fromFile() によって生成された file:// URI では機能しませんが、コンテンツ プロバイダに関連付けられた URI では機能します。このために独自のツールを実装するのではなく、ファイル共有で説明されているように FileProvider を使用できます。

リクエスト元のアプリとファイルを共有する

このファイルをリクエスト元のアプリと共有するには、コンテンツ URI と権限を含む IntentsetResult() に渡します。定義した Activity が終了すると、コンテンツ URI を含む Intent がクライアント アプリに送信されます。次のコード スニペットは、その方法を示しています。

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            ...
            if (fileUri != null) {
                ...
                // Put the Uri and MIME type in the result Intent
                resultIntent.setDataAndType(fileUri, contentResolver.getType(fileUri))
                // Set the result
                setResult(Activity.RESULT_OK, resultIntent)
            } else {
                resultIntent.setDataAndType(null, "")
                setResult(RESULT_CANCELED, resultIntent)
            }
        }
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                ...
                if (fileUri != null) {
                    ...
                    // Put the Uri and MIME type in the result Intent
                    resultIntent.setDataAndType(
                            fileUri,
                            getContentResolver().getType(fileUri));
                    // Set the result
                    MainActivity.this.setResult(Activity.RESULT_OK,
                            resultIntent);
                    } else {
                        resultIntent.setDataAndType(null, "");
                        MainActivity.this.setResult(RESULT_CANCELED,
                                resultIntent);
                    }
                }
        });

ファイルを選択したらすぐにクライアント アプリに戻る方法をユーザーに提供する。たとえば、チェックマークまたは完了ボタンを提供します。ボタンの android:onClick 属性を使用して、メソッドをボタンに関連付けます。このメソッドで finish() を呼び出します。次に例を示します。

Kotlin

    fun onDoneClick(v: View) {
        // Associate a method with the Done button
        finish()
    }

Java

    public void onDoneClick(View v) {
        // Associate a method with the Done button
        finish();
    }

その他の関連情報については、以下をご覧ください。