ファイルの共有

コンテンツ URI を使用してファイルを共有するようにアプリをセットアップすると、そのファイルに対する他のアプリからのリクエストに応答できます。このリクエストに応答できるようにするには、たとえば、他のアプリが起動できるサーバーアプリからファイル選択インターフェースを提供します。このアプローチにより、クライアント アプリを使って、ユーザーがサーバーアプリから選択したファイルのコンテンツ URI を受信できるようになります。

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

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

クライアント アプリからファイルのリクエストを受信し、コンテンツ URI で応答するには、アプリでファイル選択 Activity を提供する必要があります。クライアント アプリがこの Activity を開始するには、アクション ACTION_PICK を含む IntentstartActivityForResult() を呼び出します。クライアント アプリによって 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>

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

次に、内部ストレージのアプリの files/images/ ディレクトリから使用可能なファイルを表示する Activity サブクラスを定義し、ユーザーが目的のファイルを選択できるようにします。次のスニペットは、この 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 では機能します。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();
        }
    

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