Задача — создать файл в каталоге 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
Спасибо, очень ценный комментарий!