- Хранение данных и файлов
- Внешняя карта памяти
- Состояние на текущий момент
- Что делать?
- Android 11
- Storage Access Framework
- In this document show more show less
- Key classes
- Videos
- Code Samples
- See Also
- Overview
- Control Flow
- Writing a Client App
- Search for documents
- Process Results
- Examine document metadata
- Open a document
- Bitmap
- Get an InputStream
- Create a new document
- Delete a document
- Edit a document
- Persist permissions
- Writing a Custom Document Provider
- Manifest
- Supporting devices running Android 4.3 and lower
- Contracts
- Subclass DocumentsProvider
- Implement queryRoots
- Implement queryChildDocuments
- Implement queryDocument
- Implement openDocument
- Security
Хранение данных и файлов
В целом хранение файлов и данных можно условно разделить на две группы: во внутреннем или внешнем хранилище. Но разница между ними довольна тонка. В целом политика Гугла в отношение данных ужесточается с каждой версии системы.
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» (см. выше).
Источник
Storage Access Framework
In this document show more show less
Key classes
Videos
Code Samples
See Also
Android 4.4 (API level 19) introduces the Storage Access Framework (SAF). The SAF makes it simple for users to browse and open documents, images, and other files across all of their their preferred document storage providers. A standard, easy-to-use UI lets users browse files and access recents in a consistent way across apps and providers.
Cloud or local storage services can participate in this ecosystem by implementing a DocumentsProvider that encapsulates their services. Client apps that need access to a provider’s documents can integrate with the SAF with just a few lines of code.
The SAF includes the following:
- Document provider—A content provider that allows a storage service (such as Google Drive) to reveal the files it manages. A document provider is implemented as a subclass of the DocumentsProvider class. The document-provider schema is based on a traditional file hierarchy, though how your document provider physically stores data is up to you. The Android platform includes several built-in document providers, such as Downloads, Images, and Videos.
- Client app—A custom app that invokes the ACTION_OPEN_DOCUMENT and/or ACTION_CREATE_DOCUMENT intent and receives the files returned by document providers.
- Picker—A system UI that lets users access documents from all document providers that satisfy the client app’s search criteria.
Some of the features offered by the SAF are as follows:
- Lets users browse content from all document providers, not just a single app.
- Makes it possible for your app to have long term, persistent access to documents owned by a document provider. Through this access users can add, edit, save, and delete files on the provider.
- Supports multiple user accounts and transient roots such as USB storage providers, which only appear if the drive is plugged in.
Overview
The SAF centers around a content provider that is a subclass of the DocumentsProvider class. Within a document provider, data is structured as a traditional file hierarchy:
Figure 1. Document provider data model. A Root points to a single Document, which then starts the fan-out of the entire tree.
Note the following:
- Each document provider reports one or more «roots» which are starting points into exploring a tree of documents. Each root has a unique COLUMN_ROOT_ID , and it points to a document (a directory) representing the contents under that root. Roots are dynamic by design to support use cases like multiple accounts, transient USB storage devices, or user login/log out.
- Under each root is a single document. That document points to 1 to N documents, each of which in turn can point to 1 to N documents.
- Each storage backend surfaces individual files and directories by referencing them with a unique COLUMN_DOCUMENT_ID . Document IDs must be unique and not change once issued, since they are used for persistent URI grants across device reboots.
- Documents can be either an openable file (with a specific MIME type), or a directory containing additional documents (with the MIME_TYPE_DIR MIME type).
- Each document can have different capabilities, as described by COLUMN_FLAGS . For example, FLAG_SUPPORTS_WRITE , FLAG_SUPPORTS_DELETE , and FLAG_SUPPORTS_THUMBNAIL . The same COLUMN_DOCUMENT_ID can be included in multiple directories.
Control Flow
As stated above, the document provider data model is based on a traditional file hierarchy. However, you can physically store your data however you like, as long as it can be accessed through the DocumentsProvider API. For example, you could use tag-based cloud storage for your data.
Figure 2 shows an example of how a photo app might use the SAF to access stored data:
Figure 2. Storage Access Framework Flow
Note the following:
- In the SAF, providers and clients don’t interact directly. A client requests permission to interact with files (that is, to read, edit, create, or delete files).
- The interaction starts when an application (in this example, a photo app) fires the intent ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT . The intent may include filters to further refine the criteria—for example, «give me all openable files that have the ‘image’ MIME type.»
- Once the intent fires, the system picker goes to each registered provider and shows the user the matching content roots.
- The picker gives users a standard interface for accessing documents, even though the underlying document providers may be very different. For example, figure 2 shows a Google Drive provider, a USB provider, and a cloud provider.
Figure 3 shows a picker in which a user searching for images has selected a Google Drive account:
Figure 3. Picker
When the user selects Google Drive the images are displayed, as shown in figure 4. From that point on, the user can interact with them in whatever ways are supported by the provider and client app.
Figure 4. Images
Writing a Client App
On Android 4.3 and lower, if you want your app to retrieve a file from another app, it must invoke an intent such as ACTION_PICK or ACTION_GET_CONTENT . The user must then select a single app from which to pick a file and the selected app must provide a user interface for the user to browse and pick from the available files.
On Android 4.4 and higher, you have the additional option of using the ACTION_OPEN_DOCUMENT intent, which displays a picker UI controlled by the system that allows the user to browse all files that other apps have made available. From this single UI, the user can pick a file from any of the supported apps.
ACTION_OPEN_DOCUMENT is not intended to be a replacement for ACTION_GET_CONTENT . The one you should use depends on the needs of your app:
- Use ACTION_GET_CONTENT if you want your app to simply read/import data. With this approach, the app imports a copy of the data, such as an image file.
- Use ACTION_OPEN_DOCUMENT if you want your app to have long term, persistent access to documents owned by a document provider. An example would be a photo-editing app that lets users edit images stored in a document provider.
This section describes how to write client apps based on the ACTION_OPEN_DOCUMENT and ACTION_CREATE_DOCUMENT intents.
Search for documents
The following snippet uses ACTION_OPEN_DOCUMENT to search for document providers that contain image files:
Note the following:
- When the app fires the ACTION_OPEN_DOCUMENT intent, it launches a picker that displays all matching document providers.
- Adding the category CATEGORY_OPENABLE to the intent filters the results to display only documents that can be opened, such as image files.
- The statement intent.setType(«image/*») further filters to display only documents that have the image MIME data type.
Process Results
Once the user selects a document in the picker, onActivityResult() gets called. The URI that points to the selected document is contained in the resultData parameter. Extract the URI using getData() . Once you have it, you can use it to retrieve the document the user wants. For example:
Examine document metadata
Once you have the URI for a document, you gain access to its metadata. This snippet grabs the metadata for a document specified by the URI, and logs it:
Open a document
Once you have the URI for a document, you can open it or do whatever else you want to do with it.
Bitmap
Here is an example of how you might open a Bitmap :
Note that you should not do this operation on the UI thread. Do it in the background, using AsyncTask . Once you open the bitmap, you can display it in an ImageView .
Get an InputStream
Here is an example of how you can get an InputStream from the URI. In this snippet, the lines of the file are being read into a string:
Create a new document
Your app can create a new document in a document provider using the ACTION_CREATE_DOCUMENT intent. To create a file you give your intent a MIME type and a file name, and launch it with a unique request code. The rest is taken care of for you:
Once you create a new document you can get its URI in onActivityResult() , so that you can continue to write to it.
Delete a document
If you have the URI for a document and the document’s Document.COLUMN_FLAGS contains SUPPORTS_DELETE , you can delete the document. For example:
Edit a document
You can use the SAF to edit a text document in place. This snippet fires the ACTION_OPEN_DOCUMENT intent and uses the category CATEGORY_OPENABLE to to display only documents that can be opened. It further filters to show only text files:
Next, from onActivityResult() (see Process results) you can call code to perform the edit. The following snippet gets a FileOutputStream from the ContentResolver . By default it uses “write” mode. It’s best practice to ask for the least amount of access you need, so don’t ask for read/write if all you need is write:
Persist permissions
When your app opens a file for reading or writing, the system gives your app a URI permission grant for that file. It lasts until the user’s device restarts. But suppose your app is an image-editing app, and you want users to be able to access the last 5 images they edited, directly from your app. If the user’s device has restarted, you’d have to send the user back to the system picker to find the files, which is obviously not ideal.
To prevent this from happening, you can persist the permissions the system gives your app. Effectively, your app «takes» the persistable URI permission grant that the system is offering. This gives the user continued access to the files through your app, even if the device has been restarted:
There is one final step. You may have saved the most recent URIs your app accessed, but they may no longer be valid—another app may have deleted or modified a document. Thus, you should always call getContentResolver().takePersistableUriPermission() to check for the freshest data.
Writing a Custom Document Provider
If you’re developing an app that provides storage services for files (such as a cloud save service), you can make your files available through the SAF by writing a custom document provider. This section describes how to do this.
Manifest
To implement a custom document provider, add the following to your application’s manifest:
- A target of API level 19 or higher.
- A
element that declares your custom storage provider.
- In your bool.xml resources file under res/values/ , add this line:
- In your bool.xml resources file under res/values-v19/ , add this line:
Here are excerpts from a sample manifest that includes a provider:
Supporting devices running Android 4.3 and lower
The ACTION_OPEN_DOCUMENT intent is only available on devices running Android 4.4 and higher. If you want your application to support ACTION_GET_CONTENT to accommodate devices that are running Android 4.3 and lower, you should disable the ACTION_GET_CONTENT intent filter in your manifest for devices running Android 4.4 or higher. A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you support both of them simultaneously, your app will appear twice in the system picker UI, offering two different ways of accessing your stored data. This would be confusing for users.
Here is the recommended way of disabling the ACTION_GET_CONTENT intent filter for devices running Android version 4.4 or higher:
- In your bool.xml resources file under res/values/ , add this line:
- In your bool.xml resources file under res/values-v19/ , add this line:
- Add an activity alias to disable the ACTION_GET_CONTENT intent filter for versions 4.4 (API level 19) and higher. For example:
Contracts
Usually when you write a custom content provider, one of the tasks is implementing contract classes, as described in the Content Providers developers guide. A contract class is a public final class that contains constant definitions for the URIs, column names, MIME types, and other metadata that pertain to the provider. The SAF provides these contract classes for you, so you don’t need to write your own:
For example, here are the columns you might return in a cursor when your document provider is queried for documents or the root:
Subclass DocumentsProvider
The next step in writing a custom document provider is to subclass the abstract class DocumentsProvider . At minimum, you need to implement the following methods:
These are the only methods you are strictly required to implement, but there are many more you might want to. See DocumentsProvider for details.
Implement queryRoots
Your implementation of queryRoots() must return a Cursor pointing to all the root directories of your document providers, using columns defined in DocumentsContract.Root .
In the following snippet, the projection parameter represents the specific fields the caller wants to get back. The snippet creates a new cursor and adds one row to it—one root, a top level directory, like Downloads or Images. Most providers only have one root. You might have more than one, for example, in the case of multiple user accounts. In that case, just add a second row to the cursor.
Implement queryChildDocuments
Your implementation of queryChildDocuments() must return a Cursor that points to all the files in the specified directory, using columns defined in DocumentsContract.Document .
This method gets called when you choose an application root in the picker UI. It gets the child documents of a directory under the root. It can be called at any level in the file hierarchy, not just the root. This snippet makes a new cursor with the requested columns, then adds information about every immediate child in the parent directory to the cursor. A child can be an image, another directory—any file:
Implement queryDocument
Your implementation of queryDocument() must return a Cursor that points to the specified file, using columns defined in DocumentsContract.Document .
The queryDocument() method returns the same information that was passed in queryChildDocuments() , but for a specific file:
Implement openDocument
You must implement openDocument() to return a ParcelFileDescriptor representing the specified file. Other apps can use the returned ParcelFileDescriptor to stream data. The system calls this method once the user selects a file and the client app requests access to it by calling openFileDescriptor() . For example:
Security
Suppose your document provider is a password-protected cloud storage service and you want to make sure that users are logged in before you start sharing their files. What should your app do if the user is not logged in? The solution is to return zero roots in your implementation of queryRoots() . That is, an empty root cursor:
The other step is to call getContentResolver().notifyChange() . Remember the DocumentsContract ? We’re using it to make this URI. The following snippet tells the system to query the roots of your document provider whenever the user’s login status changes. If the user is not logged in, a call to queryRoots() returns an empty cursor, as shown above. This ensures that a provider’s documents are only available if the user is logged into the provider.
Источник