Задача — создать файл в каталоге Downloads из вашего приложения на платформе Android.
Пусть это будет какой то текстовый файл, который наше приложение умеет генерировать. Формат файла — XML.
Наша задача решается по-разному для андроидов 10+ и андроидов 9-. Потому скелет кода будет выглядеть следующим образом (в контексте вашего activity):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// пользователь нажал какую то кнопку и // и мы запускаем экспорт fun onButtomClick(view: View) { if (hasWriteStoragePermission()) { xmlFileExport() } } // логика проверки разрешения private fun hasWriteStoragePermission(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // API version >= 29 (Android 10, 11, ...) return true } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // API version >= 23 (Android 6, 7, ...) if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions( arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), XMLWRITEFILE ) return false } } // для более древних API не требуется проверки return true } // функция записи файла, после получения нужных разрешений fun xmlFileExport() { val fileName = "myFileName.xml" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // API version >= 29 (Android 10, 11, ...) ... } else { // API version < 29 (Android ..., 7,8,9) ... } } companion object { const val XMLWRITEFILE: Int = 2000 } |
Далее рассмотрим оба кейса.
Сценарий для Android 6 — 9
Здесь используется специальный механизм получения разрешения на запись. В нашем случае это WRITE_EXTERNAL_STORAGE. Если разрешение уже выдано при установке программы или в ходе её работы, то проверка проходит успешно. В противном случае запускается диалог, где разрешение запрашивается у пользователя.
В этот диалог, в том числе, отправляется ваш маркер, по которому вы потом сможете различить этот запрос среди прочих. У меня для этого служит константа XMLWRITEFILE.
Ваше activity должно реализовать callback метод onRequestPermissionsResult, который получает ответ пользователя из описанного выше диалога.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { XMLWRITEFILE -> { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { xmlFileExport() } else { Toast.makeText(this, "Feature is not available, because you didn't permit to crate an export file.", Toast.LENGTH_LONG) } } } } |
В обработчике ответа мы, при получении требуемых прав, запускаем экспорт данных. Иначе выводим сообщение, что создание файла не возможно.
В манифесте приложения, также потребуется внести некоторые изменения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="YOUR PACKAGE NAME"> <!-- Without this entry storage-permission entry will not be visible under app-info permissions list Android-10 and below --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <application ... android:requestLegacyExternalStorage="true"> ... |
Нам важен здесь тег — uses-permission и дополнительный атрибут в теге application.
Создание и запись файла в Downloads под Android 9 и ниже
Все разрешения получены, теперь можно рассмотреть как создать файл в папке Downloads.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// функция записи файла, после получения нужных разрешений fun xmlFileExport() { val fileName = "myFileName.xml" val fileContent = "My text/xml file content"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // API version >= 29 (Android 10, 11, ...) ... } else { // API version < 29 (Android ..., 7,8,9) val outputFile = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName ) outputFile.writeText(fileContent) } } |
Довольно простой кейс — вы создаёте файл в DIRECTORY_DOWNLOADS, и пишите туда нужное вам содержимое.
Создание и запись файла в Downloads под Android 10 и выше
Здесь поток записи в файл получается через регистрацию нашего файла в медиа хранилище. Сначала нужно подготовить описание файла, это объект типа ContentValues.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// функция записи файла, после получения нужных разрешений fun xmlFileExport() { val fileName = "myFileName.xml" val fileContent = "My text/xml file content"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // API version >= 29 (Android 10, 11, ...) val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "application/xml") put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } val resolver = this.contentResolver // регистрация файла val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) Toast.makeText(this, uri.toString(), Toast.LENGTH_LONG).show() if (uri != null) { val fileOutputStream = resolver.openOutputStream(uri) fileOutputStream?.write(fileContent.toByteArray()) fileOutputStream?.close() } else { Toast.makeText(this, "Can\'t resolve media path", Toast.LENGTH_LONG).show() } } else { // API version < 29 (Android ..., 7,8,9) ... } } |
На основе описания, медиа хранилище регистрирует файл и подготавливает вам Uri дескриптор. Остаётся только произвести запись данных.
Если resolver.insert возвращает null
Из документации не очень понятно как обработать такое событие. У меня эта ситуация возникала, когда я удалял ранее созданные файлы и снова пытался произвести экспорт. В итоге, это удалось решить генерацией файла с уникальным именем
1 |
val fileName = "%s-%s.xml".format(XMLFILENAME, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)) |
Т.е. я добавил дату-время в имя файла.
По всей видимости, реестр не хотел добавлять файлы с ранее существовавшими именами. Как этого избежать, мне пока не ясно, если у вас есть решение — поделитесь в комментах.
Android 7
outputFile.writeText(fileContent)
файл сохраняется, и читается приложением. Но! не читается встроенным проводником и не виден по USB в папке Downloads до перезагрузки устройства.
После перегрузки становится доступен.
Что ещё нужно подкинуть системе, чтоб разблокировать файл? Отвязать от приложения без перегрузки.
Android Studio Koala | 2024.1.1 Patch 1
разобрался с предыдущим своим комментарием, хотя он тут и не отобразился.
после: outputFile.writeText(fileContent)
необходимо поместить файл в библиотеку:
MediaScannerConnection.scanFile(
applicationContext, arrayOf(outputFile.toString()),
null, null)
тогда он будет сразу доступен для чтения, и виден по USB
Спасибо, очень ценный комментарий!