В работе над своим проектом больше всего времени я убил на то, чтобы разобраться, как правильно сохранить файл в общедоступную папку, например, Download. Мне не удалось найти четкого и ясного объяснения в интернете. Собирал информацию по частям и доходил до результата методом проб и ошибок.
Виной всей этой сложности - множество факторов. Языковой барьер: русский - английский, Kotlin - Java. Различия в способах копирования в разных версиях Android. Разобраться было не просто. В итоге - пишу этот гайд, чтобы облегчить жизнь тем, кто пойдет за мной следом.
Задача
Мне нужно было реализовать в моем приложении возможность делать резервную копию базы данных так, чтобы пользователь мог получить доступ к копии из другого приложения. Например, отправить файл резервной копии себе на электронную почту или скачать по проводу на стационарный компьютер.
Я понял, что все данные приложения, в том числе и файл базы данных находятся во внутренней папке, к которой доступ из-вне получить невозможно. Посему, нужно каким-то образом сделать копию файла базы данных из внутреннего хранилища в общедоступную папку. Я выбрал папку Download.
Решение
1. Правлю файл Манифеста
Указываю в Манифесте разрешение на доступ к внешнему хранилищу: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<manifest
...
package="имя.вашего.проекта">
...
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
...
</application>
</manifest>
Я так понял, что этого достаточно. При разрешенном доступе для записи, доступ для чтения можно не получать.
2. Обрабатываю исключение (exeption)
Весь код, отвечающий за копирование, упаковываю в try. Если что-то пойдет не так, то приложение "не вылетит", выдав пользователю сообщение об ошибке, а выполнится код в catch.
try {
// копирую данные
...
} catch (e: Exception) {
e.printStackTrace()
// если что-то пошло не так
...
}
3. Проверяю версию Android
Смотрю информацию о версии в файле build.gradle (Module)
android {
...
defaultConfig {
applicationId "имя.вашего.проекта"
minSdkVersion 23
targetSdkVersion 30
...
}
...
}
В моем проекте указано, что целевая версия - 30, минимальная - 23. Это значит, что устройства с такой минимальной версией должны справляться с моим приложением успешно. Для большей ясности о версиях Android можно посмотреть на следующий скриншот:
![Нумерация версий Android с их сладкими названиями Нумерация версий Android с их сладкими названиями](https://habrastorage.org/getpro/habr/upload_files/dbd/d21/ad2/dbdd21ad2b89b29d865a4810e779e52d.png)
В табличке выше смотрю столбец API Level. Android 6.0 Marshmellow - минимальная версия Android, 84,9% устройств будет поддерживаться.
Android 10 в коде имеет маркировку "Q" (Build.VERSION_CODES.Q) Оказывается, что копирование файлов, а именно организация доступа к общедоступным папкам, с этой версии Android и выше выглядит совершенно иначе!
![Кусок кода с проверкой версии Android и некоторой лирикой от разработчиков Google Кусок кода с проверкой версии Android и некоторой лирикой от разработчиков Google](https://habrastorage.org/getpro/habr/upload_files/8a1/19b/393/8a119b3934f184a7600be796ad3884a9.png)
Зачем? Чтобы дать вам почувствовать вкус вашего будущего, предварительный просмотр того, что грядет. Con permiso, Capitan. Зал арендован, оркестр задействован. Пришло время посмотреть, умеешь ли ты танцевать.
Видимо я танцевать научился, иначе бы не писал эту статью. Я понял, что код для копирования файла в общедоступную папку для Android 10+ должен быть другим.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// version >= 29 (Android 10, 11, ...)
...
} else {
// version < 29 (Android ..., 7,8,9)
...
}
Засим в обязательном порядке проверяю версию и готовлюсь писать разный код. Печаль...
4. Копирую, если версия Android >= 10 (SdkVersion >= 29)
В этом случае никаких разрешений получать не надо. По-моему, даже в Манифесте можно ничего не указывать. Тут и правда мне пришлось, как у нас говорят - "потанцевать с бубном", пока я не разобрался что к чему.
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, dbFileName)
}
val dstUri = applicationContext.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (dstUri != null) {
val src = FileInputStream(applicationContext.getDatabasePath(DbName.DATABASE_NAME))
val dst = applicationContext.contentResolver.openOutputStream(dstUri)
src.copyTo(dst!!)
src.close()
dst.close()
backupOk(dbFileName)
} else backupFail()
Здесь я пользуюсь компонентом MediaStore. Его использование похоже на работу с базой данных. Сначала "запись" со всеми ее полями прописывается в contentValues. Я пишу тут только имя файла в колонке DISPLAY_NAME, хотя в примерах видел, что указывают еще размер SIZE и некоторые другие поля. Но у меня работает и так.
Потом при помощи contentResolver вставляю свою запись в таблицу MediaStore.Downloads.EXTERNAL_CONTENT_URI. В итоге получаю путь к файлу в формате Uri и только после этого по нему копирую данные.
О различных способах копирования можно почитать тут: https://stackoverflow.com/questions/9292954/how-to-make-a-copy-of-a-file-in-android Я несколько из них попробовал. Все рабочие, но короче и лаконичнее, конечно, выглядит src.copyTo(dst)
5. Проверяю, получены ли разрешения
Для Android 9 и ниже мало указать в Манифесте разрешения для доступа. Их еще нужно получить. Доступ к общим папкам приложению может дать пользователь, если его об этом запросить или он сам в настройках указал разрешения.
![Настройки -> Приложения -> Разрешения Настройки -> Приложения -> Разрешения](https://habrastorage.org/getpro/habr/upload_files/5cb/152/f50/5cb152f50b1a85b8881f09fbe8342f61.png)
Для того, чтобы проверить, есть ли у моего приложения разрешение доступа к хранилищу, пишу следующий код:
if (ContextCompat.checkSelfPermission(applicationContext,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
// разрешение есть
...
} else {
// разрешения нет
...
}
Попытки копировать без разрешения не увенчаются успехом.
6. Запрашиваю разрешение, если его нет
Можно попросить пользователя дать разрешение приложению на копирование файлов в общие папки. Выглядеть оно может примерно так:
![Предоставить приложению доступ к фотографиям, медиа и файлам на устройстве? Предоставить приложению доступ к фотографиям, медиа и файлам на устройстве?](https://habrastorage.org/getpro/habr/upload_files/c4c/acb/7a7/c4cacb7a7aaf554f40688c7440a4193d.png)
Код запроса состоит из двух частей. Первая - спрашивает. Вторая - получает ответ (callback).
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
Const.REQUEST_CODE)
Этот нехитрый код покажет пользователю окно с вопросом из скриншота выше и предложит разрешить (allow) или запретить (deny) доступ. Request_code - любое число, с которым в последствии нужно свериться, чтобы убедиться, что пришел ответ именно для вашего приложения.
7. Обрабатываю ответ пользователя
Ответ буду получать в другой части программы при помощи функции onRequestPermissionsResult. Она запускается при нажатии пользователем "Allow" или "Deny".
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == Const.REQUEST_CODE && permissions[0] == Manifest.permission.WRITE_EXTERNAL_STORAGE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
try {
copyFile()
} catch (e: Exception) {
e.printStackTrace()
backupFail()
}
} else backupFail()
}
}
Код выше даю в том виде, как он написан у меня без пропусков. Проверяю, что пришло именно разрешение WRITE_EXTERNAL_STORAGE и requestCode тоже совпадает. Тогда только смотрю результат. Если он - PERMISSION_GRANTED, то копирую, не забыв упаковать копирование в try.
8. Копирую, если версия Android < 10 (SdkVersion < 29)
Код копирования в данном случае упаковал в отдельную функцию, т.к. он запускается из разных частей программы. Если разрешение получено, то сразу копирую и после получения разрешения, если его еще не было.
private fun copyFile() {
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (downloadDir.canWrite()) {
val dbFileName = Const.BACKUP_FILENAME_MASK +
fHelper.dateFromLong(applicationContext,
fHelper.dateFromToday(),
Const.YEAR_MONTH_DAY) + ".db"
val src = FileInputStream(applicationContext.getDatabasePath(DbName.DATABASE_NAME))
val dst = FileOutputStream(File(downloadDir, dbFileName))
src.copyTo(dst)
src.close()
dst.close()
backupOk(dbFileName)
} else backupFail()
}
Ответ
Создание копии базы данных работает на всех устройствах Android. Мое приложение не поддерживает версии меньше 6.0 Marshmellow (SdkVersion 23), поэтому на самых ранних версиях я и не проверял.
Очень надеюсь, что мое описание процесса сэкономит вам время, ибо я его в таком полном виде в интернете не встречал.
? Творите добро, пусть мир будет лучше!
? Приложение, над которым я работаю скачать можно по ссылке: https://play.google.com/store/apps/details?id=ru.keytomyself.customeraccounting
? Задавайте вопросы, пишите комментарии. Нужно ли что-нибудь пояснить более подробно?