- Android Persistence with preferences and files — Tutorial
- 1. File based persistence
- 1.1. Methods of local data persistence
- 1.2. Internal vs. external storage
- 1.3. Application on external storage
- 2. Preferences
- 2.1. Storing key-value pairs
- 2.2. Preference Listener
- 2.3. User interface for preferences
- 3. Exercise: Prerequisites
- 4. Exercise: Preference setting for the RSS feed
- 4.1. Create preference file
- 4.2. Create settings fragment
- 4.3. Connect your settings fragment
- 4.4. Use the preference value to load the RSS feed
- 4.5. Validate
- 4.6. Optional: Show the current value in the settings
- 5. Android File API
- 5.1. Using the file API
- 5.2. External storage
- Обновляемся на новую версию API Android по наставлению Google
- Что происходит
- Переход на новую версию
Android Persistence with preferences and files — Tutorial
File based persistence in Android. This tutorial describes how to save key-value pairs using the preference API in Android. It also explains how to read and write files in Android.
1. File based persistence
1.1. Methods of local data persistence
Android allows to persists application data via the file system. For each application the Android system creates a data/data/[application package] directory.
Android supports the following ways of storing data in the local file system:
Files — You can create and update files
Preferences — Android allows you to save and retrieve persistent key-value pairs of primitive data type.
SQLite database — instances of SQLite databases are also stored on the local file system.
Files are saved in the files folder and application settings are saved as XML files in the shared_prefs folder.
If your application creates an SQLite database this database is saved in the main application directory under the databases folder.
The following screenshot shows a file system which contains file, cache files and preferences.
Only the application can write into its application directory. It can create additional sub-directories in this application directory. For these sub-directories, the application can grant read or write permissions for other applications.
1.2. Internal vs. external storage
Android has internal storage and external storage. External storage is not private and may not always be availale. If, for example, the Android device is connected with a computer, the computer may mount the external system via USB and that makes this external storage not avaiable for Android applications.
1.3. Application on external storage
As of Android 8 SDK level it is possible to define that the application can or should be placed on external storage. For this set the android:installLocation to preferExternal or auto.
In this case certain application components may be stored on an encrypted external mount point. Database and other private data will still be stored in the internal storage system.
2. Preferences
2.1. Storing key-value pairs
The SharedPreferences class allows to persists key-value pairs of primitive data types in the Android file system.
The PreferenceManager class provides methods to get access to these preferences. The following code shows how to access preferences from a certain file
Preferences should be created private for the application. They can be accessed via all application components.
A default store for preferences can be accessed via the PreferenceManager.getDefaultSharedPreferences(this) method call. Preference value are accessed via the key and the instance of the SharedPreferences class, as demonstrated in the following listing.
To create or change preferences you have to call the edit() method on the SharedPreferences object. Once you have changed the value you have to call the apply() method to apply your asynchronously to the file system.
The usage of the commit() method is discouraged, as it write the changes synchronously to the file system.
2.2. Preference Listener
You can listen to changes in the preferences via the registerOnSharedPreferenceChangeListener() method on SharedPreferences .
One watch out is that SharedPreferences keeps listeners in a WeakHashMap hence listener may be recycled if your code does not hold a reference to it.
2.3. User interface for preferences
Android allows you to create XML resource files which describes preference key-values. An instance of PreferenceActivity or PreferenceFragment can generate an user interface for these files. The user interfaces takes care of persisting the key-value pairs.
To create a preference resource file of type XML.
Android provides the PreferenceFragment class which simplifies the creation of an user interface for maintaining preference values. This fragment can load an XML preference definition file via the method addPreferencesFromResource() .
3. Exercise: Prerequisites
The following exercise assumes that you have created an Android project with the top-level package com.example.android.rssfeed. This application has at least one entry in the toolbar with the R.id.action_settings id. Once this toolbar entry is selected, an existing fragment is replaced.
4. Exercise: Preference setting for the RSS feed
In this exercise you allow the user to enter his preferred URL for an RSS feed via another fragment. This fragment uses a preference xml file to define the user interface.
4.1. Create preference file
Create an Android XML resource called mypreferences.xml in the xml folder. Add entries to this file similar to the following listing.
4.2. Create settings fragment
Create the class SettingsFragment . It should extends PreferenceFragment . This fragment loads the preference file and allows the user to change the values.
4.3. Connect your settings fragment
Ensure that you open the preference fragment via the onOptionsItemSelected() method. The relevant code is demonstrated in the following listing.
4.4. Use the preference value to load the RSS feed
The following code snippet demonstrates how you can access the preference value in a method.
4.5. Validate
Run your application. Select from your action bar the Settings action. You should be able to enter a URL. If you press the back button and press the refresh button, ensure that the value of the url preference is used in your activity.
4.6. Optional: Show the current value in the settings
The following code snippet demonstrates how to show the current value in the preference screen.
5. Android File API
5.1. Using the file API
Access to the file system is performed via the standard java.io classes. Android provides also helper classes for creating and accessing new files and directories. For example the getDir(String, int) method would create or access a directory. The openFileInput(String s) method provides access to an FileInputStream for the file. The openFileOutput(String s, Context.MODE_PRIVATE) method provides access to an FileOutputStream for the file.
All modes except Context.MODE_PRIVATE are deprecated, files should be private to the application.
The following example shows the API usage.
5.2. External storage
Android supports also access to an external storage system, e.g., the SD card. All files and directories on the external storage system are readable for all applications with the correct permission.
To read from external storage your application need to have the android.permission.READ_EXTERNAL_STORAGE permission.
To write to the external storage system your application needs the android.permission.WRITE_EXTERNAL_STORAGE permission. You get the path to the external storage system via the Environment.getExternalStorageDirectory() method.
Via the following method call you can check the state of the external storage system. If the Android device is connected via USB to a computer, external storage might not be available.
The following shows an example for reading from the external storage system.
Источник
Обновляемся на новую версию API Android по наставлению Google
Скоро выходит Android 12, но в этом августе уже с 11-й версии разработчикам придётся использовать новые стандарты доступа приложений к внешним файлам. Если раньше можно было просто поставить флаг, что ваше приложение не поддерживает нововведения, то скоро они станут обязательными для всех. Главный фокус — повышение безопасности.
Переход на новую версию API — довольно трудоёмкая операция, требующая больших затрат на её поддержку при введении крупных апдейтов. Далее расскажу немного про наш переход и возникшие при этом трудности.
Что происходит
Если вы уже знакомы с теорией, то этот раздел можно пропустить — тут я хочу поверхностно сравнить подходы к предмету в разных версиях операционной системы.
В Android есть внутреннее Internal Storage (IS) и внешнее хранилище External Storage (ES). Исторически это были встроенная память в телефоне и внешняя SD-карта, поэтому ES был больше, но медленнее и дешевле. Отсюда и разделение — настройки и критически важное записывали в IS, а в ES хранили данные и большие файлы, например, медиа. Потом ES тоже стал встраиваться в телефон, но разделение, по крайней мере логическое, осталось.
У приложения всегда есть доступ к IS, и там оно может делать что угодно. Но эта папка только для конкретного приложения и она ограничена в памяти. К ES нужно было получать доступ и, кроме манипуляции со своими данными, можно было получить доступ к данным других приложений и производить с ними любые действия (редактировать, удалять или украсть).
Но после разделения на внутреннее и внешнее хранилища все равно оставались проблемы. Многие приложения могли хранить чувствительную информацию не только в IS, но и в ES — то есть ответственность лежала целиком на разработчиках и на том, кто хочет завладеть файлами.
В Android решили всё это переделать ещё в 10-й версии, а в 11-й это стало обязательным.
Чтобы минимизировать риски для пользователя в Google решили внедрить Scoped Storage (SS) в ES. Возможность проникнуть в папки других приложений убрали, а доступ есть только к своим данным — теперь это сугубо личная папка. А IS с 10-й версии ещё и зашифрована по умолчанию.
В Android 11 Google зафорсировала использование SS — когда таргет-версия SDK повышается до 30-й версии API, то нужно использовать SS, иначе будут ошибки, связанные с доступом к файлам. Фишка Android в том, что можно заявить совместимость с определённой версией ОС. Те, кто не переходили на 11, просто говорили, что пока не совместимы с этой версий, но теперь нужно начать поддерживать нововведения всем. С осени не получится заливать апдейты, если не поддерживаешь Android 11, а с августа нельзя будет заливать новые приложения.
Если SS не поддерживается (для девайсов ниже 10-й версии), то для доступа к данным других приложений требуется получить доступ к чтению и записи в память. Иначе придётся получать доступ к файлам через Media Content, Storage Access Framework или новый, появившийся в 11-м Android, фреймворк Datasets в зависимости от типа данных. Здесь тоже придётся получать разрешение доступа к файлу, но по более интересной схеме. Когда расшариваемый файл создаёшь сам, то доступ к нему не нужен. Но если переустановить приложение — доступ к нему опять потребуется. К каждому файлу система привязывает приложение, поэтому когда запрашиваешь доступ, его может не оказаться. Особо беспокоиться не нужно, это сложно отследить, поэтому лучше просто сразу запрашивать пермишен.
Media Content, SAF и Datasets относятся к Shared Storage (ShS). При удалении приложения расшаренные данные не удаляются. Это полезно, если не хочется потерять нужный контент.
Хотя даже при наличии SS можно дать доступ к своим файлам по определённой технологии — через FileProvider можно указать возможность получения доступа к своим файлам из другого приложения. Это нормально, потому что файлы расшаривает сам разработчик.
Также добавилась фича — если приложение не использовалось несколько месяцев, то снимаются все пермишены и доступы к системным элементам. По best practice разрешение запрашивается по необходимости (то есть непосредственно перед использованием того, на что спрашиваем разрешение), поэтому мы просто перед выполнением какого-либо действия проверяем, есть ли у нас пермишены. Если нет, то запрашиваем.
В то же время перекрыли доступы к приложениям внутри девайса. Если раньше можно было отследить, что установлены определённые приложения и отправлять к ним соответствующие интенты, то сейчас мы должны прямо в манифесте прописать, что работаем именно с этими приложениями, и только после этого получить доступ.
В качестве примера можем взять шаринг — мы шарим множество приложений, и их всех нужно указывать в манифесте, иначе они не обнаружатся. Начнём перебирать пакет установленных приложений — будет информация, что не указанного в манифесте приложения нет и при шаринге всё отвалится.
Перейдём к практике.
Переход на новую версию
Основная функциональность по работе с файлами в приложении iFunny представлена в виде сохранения мемов в память и расшаривания их между приложениями. Это было первое, что требовалось починить.
Для этого выделили в общий интерфейс работу с файлами, реализация которого зависела от версии API.
FilesManipulator представляет собой интерфейс, который знает, как работать с файлами и предоставляет разработчику API для записи информации в файл. Copier — это интерфейс, который разработчик должен реализовать, и в который передаётся поток вывода. Грубо говоря, мы не заботимся о том, как создаются файлы, мы работаем только с потоком вывода. Под капотом до 10-й версии Android в FilesManipulator происходит работа с File API, после 10-й (и включая её) — MediaStore API.
Рассмотрим на примере сохранения картинки.
Так как операция сохранения медиафайлов достаточно длительная, то целесообразно использовать MediaStore.Images.Media.IS_PENDING , которая при установлении значения 0 не дает видеть файл приложениям, отличного от текущего.
По сути, вся работа с файлами реализована через эти классы. Шаринг в другие приложения автоматически сохраняют медиа в память устройства и последующая работа с URI уже происходит по новому пути. Но есть такие SDK, которые ещё не успели перестроиться под новые реалии и до сих пор используют File API для проверки медиа. В этом случае используем кеш из External Storage и при необходимости провайдим доступ к файлу через FileProvider API.
Помимо ограничений с памятью в приложениях, таргетированных на 30-ю версию API, появилось ограничение на видимость приложения. Так как iFunny использует шаринг во множество приложений, то данная функциональность была сломана полностью. К счастью, достаточно добавить в манифест query, открывающую область видимости к приложению, и можно будет также полноценно использовать SDK.
Для неявных интентов тоже приходится добавлять код в манифест, чтобы задекларировать то, с чем будет работать приложение. В качестве примера выложу часть кода, добавленного в манифест.
После проверок запуска UI-тестов на девайсах с версиями API 29-30 было выявлено, что они также перестали корректно отрабатываться.
Первоначально в LogCat обнаружил, что приложение не может приконнектиться к процессу Orchestrator и выдает ошибку java.lang.RuntimeException: Cannot connect to androidx.test.orchestrator.OrchestratorService.
Эта проблема из разряда видимости других приложений, поэтому достаточно было добавить строку
Тест удачно запустился, но возникла другая ошибка — Allure не может сохранить отчёт в память устройства, падает с ошибкой.
Очевидно из-за Scoped Storage стало невозможно сохранять файлы в другие папки, поэтому снова почитав документацию по управлению файлами в памяти на девайсе, обнаружил интересный раздел. Там рассказано, как для нужд тестов открыть доступ к папкам девайса, но с существенными ограничениями, которые можно почитать тут.
Так как нам нужно использовать этот пермишен только для тестов, то нам условия подходят. Поэтому я быстренько написал свой ShellCommandExecutor, который выполняет команду adb shell appops set —uid PACKAGE_NAME MANAGE_EXTERNAL_STORAGE allow на создании раннера тестов.
На Android 11 тесты удачно запустились и стали проходить без ошибок.
После попытки запуска на 10-й версии Android обнаружил, что отчет Allure также перестал сохраняться в память девайса. Посмотрев issue Allure, обнаружил, что проблема известная, как и с 11-й версией. Достаточно выполнить команду adb shell appops set —uid PACKAGE_NAME LEGACY_STORAGE allow . Сказано, сделано.
Запустил тесты — всё еще не происходит сохранения в память отчёта. Тогда я обнаружил, что в манифесте WRITE_EXTERNAL_STORAGE ограничен верхней планкой до 28 версии API, то есть запрашивая работу памятью мы не предоставили все разрешения. После изменения верхней планки (конечно, для варианта debug) и запроса пермишена на запись тесты удачно запустились и отчёт Allure сохранился в память устройства.
Добавлены следующие определения пермишенов для debug-сборки.
После всех вышеописанных манипуляций с приложением, можно спокойно устанавливать targetSdkVersion 30, загружать в Google Play и не беспокоиться про дедлайн, после которого загружать приложения версией ниже станет невозможно.
Источник