コンテンツ URI を使用してファイルを共有するようにアプリを設定すると、他のアプリからのファイルに対するリクエストに応答できます。このようなリクエストに応答する方法の 1 つは、他のアプリが呼び出せるサーバーアプリからファイル選択インターフェースを提供することです。このアプローチにより、クライアント アプリケーションでは、ユーザーがサーバーアプリからファイルを選択して、選択されたファイルのコンテンツ URI を受信できるようになります。
このレッスンでは、ファイルのリクエストに応答するアプリでファイル選択 Activity
を作成する方法について説明します。
ファイル リクエストを受信する
クライアント アプリからファイルのリクエストを受信し、コンテンツ URI で応答するには、アプリでファイル選択 Activity
を提供する必要があります。クライアント アプリは、アクション ACTION_PICK
を含む Intent
で startActivityForResult()
を呼び出して、この 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 と権限を含む Intent
を setResult()
に渡します。定義した 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(); }
その他の関連情報については、以下をご覧ください。