После того как вы настроите приложение для обмена файлами с помощью URI контента, вы сможете отвечать на запросы других приложений. Один из способов ответить на такие запросы – предоставить интерфейс выбора файлов из серверного приложения, который могут вызывать другие приложения. Этот подход позволяет клиентскому приложению разрешить пользователям выбирать файл из серверного приложения, а затем получать URI контента выбранного файла.
В этом уроке рассказывается, как создать в приложении Activity для выбора файлов, которое будет отвечать на запросы файлов.
Как получать запросы на файлы
Чтобы получать запросы файлов от клиентских приложений и отвечать на них с помощью URI контента, ваше приложение должно предоставлять возможность выбора файлов Activity. Клиентские приложения запускают этот процесс,
Activity вызывая startActivityForResult() с Intent, содержащим действие
ACTION_PICK. Когда клиентское приложение вызывает метод startActivityForResult(), ваше приложение может вернуть ему результат в виде URI контента для выбранного пользователем файла.
Чтобы узнать, как реализовать запрос файла в клиентском приложении, ознакомьтесь с уроком Запрос общего файла.
Как создать задание на выбор файла
Чтобы настроить выбор файлов Activity, сначала укажите Activity в манифесте вместе с фильтром интентов, который соответствует действию ACTION_PICK и категориям CATEGORY_DEFAULT и CATEGORY_OPENABLE. Также добавьте фильтры MIME-типов для файлов, которые ваше приложение предоставляет другим приложениям. Во фрагменте кода ниже показано, как указать новый фильтр интентов и Activity:
<manifest xmlns:android="http://schemas.android.com/apk/res/and>roid" ..<. a>pplication ... < activity android:name=".FileSelectActivity" @ android:la>bel="File Se<lector" > inte<nt-filter action android:name>="android.intent<.action.PICK"/ category android>:name="android.i<ntent.category.DEFAULT"/ category > android:name="<android.intent.category.OPEN>ABLE"/ < data android:mimeType=">text/plain"/< > data a<ndroid:mi>meType="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 ima<geFi>les: ArrayFile // Array of filenames corresponding to imageFiles private lateinit var imageFi<lename>s: ArrayString // 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) и более поздних версий это требует особого внимания из-за изменений в модели разрешений, особенно из-за того, что разрешение READ_EXTERNAL_STORAGE стало
опасным и может отсутствовать в принимающем приложении.
Учитывая все вышесказанное, мы рекомендуем не использовать тег Uri.fromFile(), поскольку он имеет ряд недостатков. Этот метод:
- Не позволяет обмениваться файлами между профилями.
- Требует, чтобы у приложения было разрешение
WRITE_EXTERNAL_STORAGEна устройствах с ОС Android 4.4 (уровень API 19) или более ранних версий. - Требуется разрешение
READ_EXTERNAL_STORAGEдля приложений, получающих контент. Это приведет к сбоям при попытке поделиться контентом с важными приложениями, например Gmail, у которых нет такого разрешения.
Вместо Uri.fromFile() можно использовать разрешения URI, чтобы предоставить другим приложениям доступ к определенным URI. Разрешения URI не работают с URI file://, сгенерированными Uri.fromFile(), но работают с URI, связанными с поставщиками контента. API FileProvider поможет вам создать такие URI. Этот подход также работает с файлами, которые хранятся не на внешнем накопителе, а в локальном хранилище приложения, отправляющего намерение.
В onItemClick() получите объект File для имени файла выбранного файла и передайте его в качестве аргумента в getUriForFile() вместе с полномочиями, которые вы указали в элементе <provider> для FileProvider.
Полученный 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()); } ... } }); ... }
Помните, что URI контента можно создавать только для файлов, которые находятся в каталоге, указанном в файле метаданных, содержащем элемент <paths>, как описано в разделе Укажите общие каталоги. Если вы вызываете функцию getUriForFile() для элемента File в пути, который не был указан, вы получите ошибку 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() – единственный безопасный способ предоставить доступ к файлам с помощью временных разрешений. Не вызывайте метод Context.grantUriPermission() для URI контента файла, поскольку этот метод предоставляет доступ, который можно отозвать только с помощью метода Context.revokeUriPermission().
Не используйте Uri.fromFile(). Оно требует, чтобы у принимающих приложений было разрешение READ_EXTERNAL_STORAGE, не работает при попытке поделиться данными с другими пользователями, а в версиях Android ниже 4.4 (уровень API 19) требует, чтобы у вашего приложения было разрешение WRITE_EXTERNAL_STORAGE.
А действительно важные цели, например приложение Gmail, не имеют значка READ_EXTERNAL_STORAGE, из-за чего вызов завершается неудачно.
Вместо этого можно использовать разрешения URI, чтобы предоставить другим приложениям доступ к определенным URI.
Разрешения URI не работают с URI file://, которые генерируются Uri.fromFile(), но работают с URI, связанными с поставщиками контента. Вместо того чтобы создавать собственное решение, вы можете и должны использовать FileProvider, как описано в разделе Общий доступ к файлам.
Как поделиться файлом с приложением, отправившим запрос
Чтобы поделиться файлом с приложением, которое запросило его, передайте Intent, содержащий URI контента и разрешения, в setResult(). Когда определенный вами объект Activity будет создан, система отправит клиентскому приложению объект Intent, содержащий 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, _ -> ... 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(); }
Дополнительную информацию можно найти в следующих статьях:
- Как создавать URI контента
- Как реализовать разрешения для поставщика контента
- Разрешения
- Намерения и фильтры намерений