- Хранение данных и файлов
- Внешняя карта памяти
- Состояние на текущий момент
- Что делать?
- Android 11
- How to Correctly Store App-Specific Files in Android
- App-specific files vs. app-independent files
- App-independent files
- App-specific files
- Internal storage versus external storage
- Internal storage space can be limited
- Permissions for writing to external storage
- External storage might be unavailable
- Internal app-specific directories
- External app-specific directories
- Older devices
- Cache
- Naming of the folder
- Be aware of the «.nomedia»-switch
- Lessons learned
Хранение данных и файлов
В целом хранение файлов и данных можно условно разделить на две группы: во внутреннем или внешнем хранилище. Но разница между ними довольна тонка. В целом политика Гугла в отношение данных ужесточается с каждой версии системы.
Android поддерживает различные варианты хранения данных и файлов.
- Специфичные для приложения файлы. Доступ к файлам имеет только приложение, их создавшее. Файлы могут находиться во внутреннем и внешнем хранилище. У других приложений нет доступа (кроме случаев, когда файлы хранятся на внешнем хранилище). Методы getFilesDir(), getCacheDir(), getExternalFilesDir(), getExternalCacheDir(). Разрешений на доступ не требуется. Файлы удаляются, когда приложение удаляется пользователем.
- Разделяемое хранилище. Приложение может создавать файлы, которыми готово поделиться с другими приложениями — медиафайлы (картинки, видео, аудио), документы. Для медифайлов требуется разрешение READ_EXTERNAL_STORAGE или WRITE_EXTERNAL_STORAGE.
- Настройки. Хранение простых данных по принципу ключ-значение. Доступно внутри приложения. Реализовано через Jetpack Preferences. Настройки удаляются, когда приложение удаляется пользователем.
- Базы данных. Хранение данных в SQLite. На данный момент реализовано через библиотеку Room. Доступ только у родного приложения.
В зависимости от ваших потребностей, нужно выбрать нужный вариант хранения данных.
Следует быть осторожным при работе с внутренним и внешним хранилищем. Внутренне хранилище всегда есть в системе, но оно может быть не слишком большим по объёму. Вдобавок к внутреннему хранилищу, устройство может иметь внешнее хранилище. В старых моделях таким хранилищем выступала съёмная SD-карта. Сейчас чаще используют встроенную и недоступную для извлечения флеш-память. Если ваше приложение слишком большое, можно попросить систему устанавливать программу во внешнее хранилище, указав просьбу в манифесте.
В разных версиях Android требования к разрешению для работы с внешним хранилищем постоянно менялись. На данный момент (Android 10, API 29) требования выглядят следующим образом.
Приложение может иметь доступ к собственным файлам, которые находятся во внешнем хранилище. Также может получить доступ к определённым общим файлам на внешнем хранилище.
Доступ к общим файлам достигается через FileProvider API или контент-провайдеры.
Для просмотра файлов через студию используйте инструмент Device File Explorer.
Внешняя карта памяти
Когда появились первые устройства на Android, то практически у всех были внешние карточки памяти, которые вставлялись в телефон. Обычно там хранили фотки, видео и свои файлы. Всё было понятно — были различные методы для доступа к файловой системе. А потом началась чехарда. В телефонах также была и собственная «внешняя» память. Она вроде как и внешняя, но вставлена на заводе и вытащить её пользователь не мог, т.е. практически внутренняя. Затем пошла мода на телефоны, у которых была только такая внутреннее-внешняя карта. Пользователи поворчали, но привыкли. Сейчас встречаются оба варианта. Как правило, у телефонов с спрятанной картой больше памяти и выше степень водонепроницаемости.
Подобные фокусы с картой породили и другую проблему — Гугл озаботился безопасностью файлов и стала думать, как осложнить жизнь разработчику. С выходом каждой новой версии системы компания то давала добро на полный доступ к карточке, то ограничивала, то давала права с ограничениями, то откатывала свои решения назад. Короче, запутались сами и запутали всех.
Попробуем немного разобраться с этим зоопарком. Но помните, что процесс путаницы продолжается.
При подготовке материала я опирался на письма некоторых читателей сайта, которые присылали свои мысли по этому поводу. Спасибо им за структуризацию материала.
Вот что я (кажется) понял, попытавшись загрузить картинку с внешней SD карточки.
External это не External
«EXTERNAL_STORAGE» называется так не потому, что это внешняя память по отношению к устройству, а потому что она выглядит как внешняя память для компьютера, если устройство подключить кабелем к компьютеру. Причём именно выглядит, потому что обмен идёт по протоколу MTP – устройство только показывает компьютеру список папок и файлов, а при необходимости открыть или скопировать файл он специально загружается на компьютер, в отличие от настоящей флешки, файлы которой становятся файлами в файловой системе самого компьютера. Обмен по MTP позволяет устройству продолжать работать, когда оно подключено к компьютеру.
Emulated это не Emulated
Сначала я пытался прочесть файл с карточки на эмуляторе (из этого так ничего и не вышло). Функция getExternalStorageDirectory() давала мне /storage/emulated/0, и я думал, что «emulated» – это потому что на эмуляторе. Но когда я подцепил реальный планшет, слово «emulated» никуда не исчезло. Я стал рыться в интернете и обнаружил, что «Emulated storage is provided by exposing a portion of internal storage through an emulation layer and has been available since Android 3.0.» – то есть это просто кусок внутренней памяти, которая путём какой-то эмуляции делается доступной для пользователя, в отличие от собственно внутренней памяти.
При этом с точки зрения системы доступная для пользователя папка называется /storage/emulated/0, а при подключении к компьютеру по USB это просто одна из двух главных папок устройства – у меня в Windows Explorer она называется Tablet. Вторая папка у меня называется Card, и это и есть настоящая внешняя карточка.
Нет стандартных средств добраться из приложения до файлов на внешней карточке. Все попытки добраться до настоящей внешней карточки делаются с помощью неких трюков. Самое интересное, что я нашел, это статья на http://futurewithdreams.blogspot.com/2014/01/get-external-sdcard-location-in-android.html — парень читает таблицу смонтированных устройств /proc/mounts, таблицу volume daemons /system/etc/vold.fstab, сравнивает их и выбирает те тома, которые оказываются съёмными (с помощью Environment.isExternalStorageRemovable()).
Оказалось, что несистемным приложениям в принципе запрещено напрямую обращаться к съёмной карточке! Похоже, что это было так всегда, но вот начиная с версии Android 6 Marshmallow написано: внешняя карточка может быть определена как Portable либо Adoptable. Adoptable – это как бы «усыновляемая» память которая может быть «adopted», то есть взята в систему (примерно как кот с улицы в дом – это тоже называется to adopt) и использована как внутренняя. Для этого ее надо особым образом отформатировать и не вынимать, иначе не факт, что система продолжит нормально работать.
Portable – это нормальная съёмная карточка, но несистемным приложениям запрещено обращаться из программ к файлам на ней! Вот что написано в https://source.android.com/devices/storage/traditional.html:
Android 6.0 supports portable storage devices which are only connected to the device for a short period of time, like USB flash drives. When a user inserts a new portable device, the platform shows a notification to let them copy or manage the contents of that device. In Android 6.0, any device that is not adopted is considered portable. Because portable storage is connected for only a short time, the platform avoids heavy operations such as media scanning. Third-party apps must go through the Storage Access Framework to interact with files on portable storage; direct access is explicitly blocked for privacy and security reasons.
Если я правильно понял, этот самый Storage Access Framework позволяет работать с документом на карточке через диалог (открыть файл/сохранить файл), а вот прочитать или записать файл на карточке непосредственно из программы невозможно.
Общий вывод – реально из программы можно работать только с файлами на предоставляемой пользователю части встроенной памяти устройства, а на съёмной карточке – нет.
Это напоминает войну Microsoft с пользователями и разработчиками по поводу диска C:, компания уговаривала не устраивать беспорядок в корне этого диска, а ещё лучше — перенести свои файлы на другой диск. Но явных запретов не было.
Состояние на текущий момент
Гугл утверждает, что с версии Android 10 Q стандартный доступ к файлам будет прекращён. Ещё в Android 4.4 появился Storage Access Framework, который и должен стать заменой для работы с файлами.
Методы Environment.getExternalStorageDirectory() и Environment.getExternalStoragePublicDirectory() признаны устаревшими и будут недоступны. Даже если они будут возвращать корректные значения, ими вы не сможете воспользоваться.
В Android 7.0 добавили исключение FileUriExposedException, чтобы разработчики перестали использовать схему file://Uri.
Можно создавать файлы в корневой папке карточки при помощи Environment.getExternalStorageDirectory(), а также папки с вложенными файлами. Если папка уже существует, то у вас не будет доступа на запись (если это не ваша папка).
Если вы что-то записали, то сможете и прочитать. Чужое читать нельзя.
Кстати, разрешения на чтение и запись файлов не требуются, а READ_EXTERNAL_STORAGE и WRITE_EXTERNAL_STORAGE объявлены устаревшими.
Другие приложения не могут получить доступ к файлам вашего приложения. Файлы, которые вы создали через getExternalFilesDir(), доступны через Storage Access Framework, кроме файлов, созданных в корне карточки (что-то я совсем запутался). Ещё можно дать доступ через FileProvider.
При подключении USB-кабеля через getExternalFilesDir(), вы можете увидеть свои файлы и папки, а также файлы и папки пользователя. При этом файлы и папки пользователя на корневой папке вы не увидите. Вам не поможет даже adb или Device File Explorer студии.
Что делать?
Пользуйтесь методами класса Context, типа getExternalFilesDir(), getExternalCacheDir(), getExternalMediaDirs(), getObbDir() и им подобными, чтобы найти место для записи.
Используйте Storage Access Framework.
Используйте MediaStore для мультимедийных файлов.
Используйте FileProvider, чтобы файлы были видимы другим приложениям через ACTION_VIEW/ACTION_SEND.
Android 10: Появился новый флаг android:allowExternalStorageSandbox=»false» и метод Environment.isExternalStorageSandboxed() для работы с песочницей. Флаг android:requestLegacyExternalStorage=»true» для приложений, которые ещё используют старую модель доступа к файлам.
Как временное решение можно добавить в блок манифеста application атрибут android:requestLegacyExternalStorage=»true», чтобы доступ к файлам был как раньше в Android 4.4-9.0.
Android 11
Если вы создаёте файловый менеджер, то ему нужны возможности для просмотра файлов. Для этого следует установить разрешение MANAGE_EXTERNAL_STORAGE или использовать атрибут android:requestLegacyExternalStorage=»true» (см. выше).
Источник
How to Correctly Store App-Specific Files in Android
Christophe Versieux (Waza_be) posted a rant about android developers’ bad habit to store files directly on the root of the sd card. I completely agree with the post. It’s bad usage to create app-specific folders directly at the root. If you install a lot of apps, the sd card’s root gets cluttered fast.
One comment also mentioned that most tutorials do not cover the app-specific folders, so let me correct that with a short tutorial on how to do it correctly.
App-specific files vs. app-independent files
If you need to store files there are generally two usage types:
- App independent data
- App specific data
I will cover both types in more detail in the following sections. But in short I would characterize those types as follows: App-specific files are those that only are useful for as long as the app is installed (e.g. ebooks in a proprietary format). App-independent files on the other hand are those that the user cares about regardless of the specific app that created them (e.g. photos).
App-independent files
This type of data is stuff your user very likely cares about, even if your app is no longer installed on the device. Examples are photos shot, images processed or sketched, code-files edited, audio files bought and so on.
For most of these types, Android provides special directories. A full list of directories that Android provides out of the box can be seen in the documentation of the Environment class. Those fields all start with «DIRECTORY». E.g. DIRECTORY_MUSIC or DIRECTORY_PICTURES.
Those files always have to be stored on the sd card (or the equivalent partition for devices that have no sd card slot like the Google Nexus line). The reason is, that those files tend to be quite large, that they need to be world-readable and that they must not be stored in a directory that get’s cleaned up when your app gets uninstalled. I will cover external storage in more detail in the following sections.
You can get access to the root of the sd card by calling the getExternalStorageDirectory() method on the Environment class.
And you can use getExternalStoragePublicDirectory(String type) to directly get a File object for any of the supported types:
It’s the usual Java IO API from here on.
App-specific files
This type of files is for any kind of data that only this specific app can or should make use of. This could be proprietary files like ebooks, media files that should not be available through the normal media players (e.g. thumbnails for CD covers), downloaded magazines, database files, preferences and so on.
App-specific files can be stored internally or externally (on the sd card) and the Android API helps you to find the appropriate directories.
What’s nice for app-specific folders that follow a certain naming convention is Android’s cleanup mechanism. Android takes care to delete these folder when users uninstall your app. This way Android gets rid of unnecessary files and users do not have to clean up manually after any deinstallation.
Internal storage versus external storage
You should know that there are two app-specific folders for any app. The internal one which you can use for private files and the external one. External storage refers to the sd card of Android devices or the equivalent partition that devices with no sd card option offer (e.g. the Nexus line).
Internal storage space can be limited
Especially for larger files you should prefer the external storage option. You should do so because internal storage space can be very limited depending on the device of your user. A probably extreme example is my old LG Optimus One that has only about 300 MB of internal storage. But with a 16 GB sd card I have plenty of external storage. Even if this device is one of the worst examples regarding internal storage, there are plenty of devices out there that also come with little internal storage. Not everyone uses high-end phones.
Permissions for writing to external storage
Whenever you want to access files on the external storage you need permissions to do so. Add this permissions to your manifest file:
The need to declare this permissions is a slight drawback compared to internal storage. Some users may be wary — especially if this adds to an already long list of permissions. But if you explain this in your app description you should be fine.
Note: For reading files of the sd card no permissions were needed prior to Jelly Bean. So you can leave this one out if your build target is lower than API level 16.
External storage might be unavailable
The biggest problem with external storage is, that it might be unmounted when you need it. That’s obviously the case when the sd card is ejected but also when your device is mounted for file access to your computer. Because of this, you always have to check, if the external storage is currently available:
Sometimes the external storage might be mounted read-only. If you only need to read data the following check is better suited for you:
This works since the value of the final field Environment.MEDIA_MOUNTED_READ_ONLY is «mounted_ro» . I actually do not like code, that uses knowledge of final fields’ value. In my opinion it would have been better, had Google chosen to use an integer so that we could use final fields as bitmasks to test for the state.
Internal app-specific directories
Android creates a directory private to your app for you. Your shared preferences go in here, as well as your SQLite databases, native libraries or cached files.
All app-specific files are within a folder named
Within this folder some common sub-folders might exist — depending on what your app needs:
- databases — for SQLite databases
- shared_prefs — for your preferences
- cache — for cache files and data
- lib — for native libraries
- files — for files that do not fit into other categories
The Context class provides some methods you can use to create new directories, open InputStreams and so on. The following table lists these methods:
Method | When to use |
---|---|
deleteFile(String name) | Deletes the file with the given name |
fileList() | Returns a list of files |
getDir(String name, int mode) | Returns a file object to this directory. If the directory doesn’t exist yet, it gets created. |
getFilesDir() | Returns a File object pointing to the files directory |
openFileInput(String name) | Opens an InputStream object to the file with the given name |
openFileOutput(String name, int mode) | Opens an OutputStream object to the file with the given name. The file gets created if it does not exists |
Some methods use a mode parameter. This can be any of the following constants of the Context class:
- MODE_APPEND
- MODE_PRIVATE
- MODE_WORLD_READABLE
- MODE_WORLD_WRITEABLE
These are int values and you can use the or operator («|») to combine them — e.g. to append to a world readable file:
External app-specific directories
This is where Waza_be’s rant comes into play — because too many apps ignore the correct handling of app-specific directories on external storage.
All external app-specific files should be stored within a folder named
Note that I use a relative path. This path is relative to the root of the sdcard. The convention of where sdcards are mounted, changed between Android releases.
It’s always good practice to use API calls instead of hard-coded values, but the fact that the mount-point has changed in the past should make you even more cautious.
Now for external files there exists only one method you can use:
If you pass in a null value the File object will point to the files directory. If you add any of the directory constants of the Environment class, you will get a File object pointing to a sub-directory within your files directory. If the directory doesn’t exits yet, Android creates it for you. If the external media is not mounted, the method returns null.
Note: This method has only been introduced with API Level 8 (that is Froyo or Android 2.2). In the next section I briefly touch on the issues you face when dealing with older devices.
Older devices
There are still devices out there on older versions, which you might want to support. In this case using the naming convention shown above is still a good idea.
Alas, neither the method getExternalFilesDir(String type) exists, nor does Android clean up after an app uninstall. But using the same naming convention still avoids too many irritating folders on the root of your sd card.
Cache
Many times you need to cache data you downloaded from the net or created within your app. Android allows you to use internal as well as external storage space to use the cache. But using the external storage can be risky, since your cache might be unavailable when you need it.
The Context object has two methods to get a File object for either the internal or the external cache directory:
You have to take care of the cache size yourself. Android deletes all files in both directories on an uninstallation of your app, but otherwise it’s up to you to clean up any cache files no longer needed.
If Android is running low on internal storage it cleans up cache files first but the API states explicitly that you should not rely on Android to clean up for you!
With the external cache storage Android doesn’t care at all. Even if the external storage is full, no cache files will be deleted.
Naming of the folder
The official naming convention for the folder contains your package name. Christophe Versieux (Waza_be) himself mentioned that he used to use the app name instead, since users are more familiar with the package name of the app.
Even though familiarity is something to consider, I do not agree with this statement. First of all the API call uses the package name, so why not use it. Only with this method you can rely to be on the safe side. Secondly Android only cleans up a folder using the package name. And finally you could get screwed since app names do not have to be unique. In this case you might end up doing stuff in your folder that clashes with the intentions of the other app.
Be aware of the «.nomedia»-switch
Android’s MediaScanner regularly scans the sd card for any media files and adds these to the public list of media files. Thus images will show up in the Gallery app or music files in audio players.
But that’s not always what you want. Sometimes those files really should be presented by your app only. That’s where «.nomedia» comes into play. If a folder contains a file named «.nomedia» it will be skipped by the MediaScanner and any media files will thus not show up in the public media list.
That’s another reason to use the standard app-specific folder. It contains the file «.nomedia» within the data directory so that any media files you add to your app-specific directory will not show up.
Lessons learned
In this tutorial you have heard about the difference between app-specific and app-independent files and how to apply this knowledge to Android.
Also you have seen how to use app-specific files on Android, and how to leverage the internal storage as well as the external storage.
In a follow up post I will cover how to add app-independent media files to the corresponding content providers, so that they show up immediately in the list of public media files. Stay tuned.
Edited:
Minor changes due to comments by +Alexandre Roman and +Cyril Mottier to my G+ announcement of this post.
Wolfram Rittmeyer lives in Germany and has been developing with Java for many years.
He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.
Источник