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