Scoped Storage — це модель доступу до файлової системи, введена в Android 10 (API 29), яка обмежує довільний доступ додатків до загального сховища пристрою. Згідно з Google Android Developer Documentation (2024), Scoped Storage замінює стару модель дозволів READ_EXTERNAL_STORAGE та WRITE_EXTERNAL_STORAGE, надаючи додаткам доступ лише до їхніх власних файлів в ізольованій пісочниці та до спільних колекцій через MediaStore. Ця модель підвищує конфіденційність користувацьких даних і запобігає несанкціонованому збору інформації додатками.
Головне
Scoped Storage — це архітектурна зміна в Android 10 (API 29), яка фундаментально змінює спосіб доступу додатків до файлової системи пристрою. Замість повного доступу до загального сховища додаток отримує доступ лише до своєї пісочниці та до певних колекцій медіафайлів через MediaStore.
До Scoped Storage будь-який додаток із дозволом READ_EXTERNAL_STORAGE міг читати всі файли в загальному сховищі — фотографії, документи, завантаження, дані інших додатків. Це створювало ризики конфіденційності: недобросовісні додатки могли збирати інформацію про користувача без його відома.
За даними Google I/O 2019, понад 65% користувачів Android вважають конфіденційність даних критичним фактором при виборі додатку. Scoped Storage — пряма відповідь на цей запит: кожен додаток працює в ізольованому середовищі та отримує доступ до чужих даних лише за явною згодою користувача.
Важливо: Scoped Storage в Android 10 був опціональним (додаток міг вимкнути через requestLegacyExternalStorage). Починаючи з Android 11 (API 30), Scoped Storage став обов'язковим для всіх додатків, незалежно від target SDK.
Google ввів Scoped Storage для вирішення трьох фундаментальних проблем старої моделі доступу до файлів: конфіденційність, управління дозволами та очищення залишкових файлів.
У старій моделі додаток із READ_EXTERNAL_STORAGE міг сканувати все загальне сховище та збирати метадані про файли користувача — географічні координати фотографій, імена документів, структуру директорій. Scoped Storage усуває цю можливість: навіть маючи дозвіл, додаток бачить лише файли, створені ним самим, та явно вибрані користувачем через SAF.
До Scoped Storage додаток міг залишати файли в загальному сховищі після видалення. З часом накопичувалися сміттєві директорії. Scoped Storage вирішує це: всі файли в пісочниці додатку видаляються разом із додатком, а файли через MediaStore мають власника і можуть бути очищені системою.
| Аспект | Стара модель (Legacy) | Scoped Storage |
|---|---|---|
| Доступ до загального сховища | Повний (з дозволом) | Тільки своя пісочниця |
| Медіафайли | Прямий file path | Через MediaStore URI |
| Чужі файли | Доступні завжди | Тільки через SAF |
| Очищення при видаленні | Файли залишаються | Пісочниця видаляється |
| Контроль користувача | Мінімальний | Явна згода на кожен файл |
Scoped Storage — це частина загальної стратегії Google з посилення конфіденційності в Android, яка також включає дозволи «тільки під час використання», індикатори доступу до камери та мікрофону, і Privacy Dashboard.
Кожен додаток в Android отримує власну приватну директорію, доступну через Context.getFilesDir() та context.getCacheDir(). Для доступу до зовнішньої (спільної) приватної директорії використовується Context.getExternalFilesDir().
У Scoped Storage додаток має повний доступ до своєї зовнішньої приватної директорії без будь-яких дозволів. Це основне місце для зберігання файлів, які не повинні бути доступні іншим додаткам або які додаток створює для власного використання.
import android.os.Environment
import java.io.File
import java.io.IOException
class FileManager {
fun saveToAppStorage(context: Context, fileName: String, data: ByteArray) {
val appDir = context.getExternalFilesDir(null)
?: return
val file = File(appDir, fileName)
file.writeBytes(data)
}
fun readFromAppStorage(context: Context, fileName: String): ByteArray? {
val appDir = context.getExternalFilesDir(null)
?: return null
val file = File(appDir, fileName)
return file.takeIf { it.exists() }?.readBytes()
}
}
Шлях до зовнішньої приватної директорії: /storage/emulated/0/Android/data/{packageName}/files/. Починаючи з Android 11, прямий file path недоступний — тільки через API. Це ще одне посилення безпеки Scoped Storage.
MediaStore — це основний API для доступу до спільних медіафайлів (зображення, відео, аудіо) в Scoped Storage. Додаток працює не з file paths, а з content URIs, які надаються MediaStore provider.
MediaStore розділений на три основні колекції: Images, Video та Audio. Кожна колекція підтримує CRUD-операції через ContentResolver. Для вставки нового файлу в колекцію Images використовується MediaStore.Images.Media, для запиту існуючих — query з відповідним URI.
import android.content.ContentValues
import android.provider.MediaStore
import android.os.Environment
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, title: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/MyApp"
)
}
val uri = context.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: return
context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
95,
output
)
}
}
Важливо: MediaStore не підтримує довільні file paths — тільки content URIs. Спроба отримати file path з URI через MediaStore.Files.getContentUri() не дасть прямого шляху до файлу на Android 11+. Натомість використовуй ContentResolver.openInputStream() та openOutputStream() для роботи з вмістом.
Storage Access Framework (SAF) — це API для доступу до довільних файлів та директорій за межами пісочниці додатку. SAF надає користувачеві інтерфейс вибору файлу або директорії, після чого додаток отримує content URI з тимчасовим доступом.
SAF використовується для сценаріїв, які не покриваються MediaStore: робота з довільними документами (PDF, ZIP, APK), доступ до директорій на SD-картці, імпорт та експорт файлів з інших додатків. Користувач явно вибирає файл через системний файловий пікер — це гарантує його згоду на доступ.
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
const val REQUEST_CODE_PICK_DIR = 1001
fun pickDirectory(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE_PICK_DIR)
}
fun handlePickResult(requestCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_DIR && data != null) {
val treeUri: Uri = data.data ?: return
// Take persistent URI permission
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF дає додатку доступ до вибраної користувачем директорії до перезавантаження пристрою (при використанні takePersistableUriPermission). Це єдиний спосіб отримати доступ до довільних файлів у загальному сховищі на Android 11+.
Міграція існуючого Android-додатку на Scoped Storage вимагає змін у кількох ключових областях. Google рекомендує поетапний підхід із тестуванням на Android 11+.
Заміни всі прямі File операції в загальному сховищі на ContentResolver.openInputStream / openOutputStream. Для файлів у пісочниці додатку (getExternalFilesDir) file paths продовжують працювати.
Видали READ_EXTERNAL_STORAGE та WRITE_EXTERNAL_STORAGE, якщо вони не потрібні для специфічних сценаріїв (наприклад, керування файлами — для цього є спеціальний дозвіл MANAGE_EXTERNAL_STORAGE).
| Сценарій | Старий підхід | Scoped Storage підхід |
|---|---|---|
| Збереження фото | File(path).writeBytes() | MediaStore + ContentResolver |
| Читання PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| Свої файли | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Кеш | File(cacheDir).writeBytes() | context.cacheDir (без змін) |
Після міграції протестуй додаток на Android 11+ (API 30), увімкнувши в build.gradle targetSdk = 30 або вище. Переконайся, що всі операції з файлами в загальному сховищі працюють через MediaStore або SAF, а не через прямі file paths.
import android.os.Build
fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun compatReadFile(context: Context, uri: Uri): ByteArray? {
return if (isScopedStorage()) {
context.contentResolver
.openInputStream(uri)?.readBytes()
} else {
File(uri.path ?: return null)
.takeIf { it.exists() }
?.readBytes()
}
}
За даними Google Play Console (2024), понад 78% активних Android-пристроїв працюють на Android 10+ з Scoped Storage. Міграція — обов'язковий крок для публікації оновлень у Google Play: нові додатки повинні target API 31+, оновлення — API 30+.
Часто задавані питання
Ні, починаючи з Android 11 (API 30) Scoped Storage обов'язковий для всіх додатків. Флаг requestLegacyExternalStorage, доступний в Android 10, не працює на API 30+. Єдиний спосіб отримати широкий доступ до файлової системи — дозвіл MANAGE_EXTERNAL_STORAGE, але він призначений тільки для файлових менеджерів та антивірусів.
MANAGE_EXTERNAL_STORAGE — спеціальний дозвіл для додатків, яким потрібен повний доступ до файлової системи (файлові менеджери, backup-інструменти). Запит дозволу відкриває системний екран з попередженням для користувача. При публікації в Google Play потрібно заповнити форму Declaration про необхідність цього дозволу.
У Scoped Storage на Android 11+ прямий file path з content URI отримати неможливо. Натомість використовуй ContentResolver.openInputStream() для читання та openOutputStream() для запису. Якщо тобі потрібен file path для сумісності зі сторонньою бібліотекою — створи копію файлу в getCacheDir() і працюй із копією.
FileProvider продовжує працювати без змін — він використовується для надання доступу до файлів із пісочниці додатку іншим додаткам через content URIs. Scoped Storage не впливає на FileProvider, оскільки FileProvider працює на рівні content URIs, а не прямих file paths.
Перевір Build.VERSION.SDK_INT: якщо >= Build.VERSION_CODES.Q (29), то пристрій підтримує Scoped Storage. Однак на Android 10 Scoped Storage може бути вимкнений через requestLegacyExternalStorage. На Android 11+ перевір, чи встановлено флаг-маніфест requestLegacyExternalStorage — якщо ні, Scoped Storage активний.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також