Scoped Storage — to model dostępu do systemu plików wprowadzony w Android 10 (API 29), który ogranicza dowolny dostęp aplikacji do współdzielonej pamięci urządzenia. Według Google Android Developer Documentation (2024), Scoped Storage zastępuje stary model uprawnień READ_EXTERNAL_STORAGE i WRITE_EXTERNAL_STORAGE, zapewniając aplikacjom dostęp tylko do ich własnych plików w izolowanej piaskownicy oraz do wspólnych kolekcji przez MediaStore. Model ten zwiększa prywatność danych użytkownika i zapobiega nieautoryzowanemu zbieraniu informacji przez aplikacje.
Najważniejsze
Scoped Storage — to zmiana architektoniczna w Android 10 (API 29), która fundamentalnie zmienia sposób dostępu aplikacji do systemu plików urządzenia. Zamiast pełnego dostępu do współdzielonej pamięci, aplikacja otrzymuje dostęp tylko do swojej piaskownicy i do określonych kolekcji plików multimedialnych przez MediaStore.
Przed Scoped Storage każda aplikacja z uprawnieniem READ_EXTERNAL_STORAGE mogła czytać wszystkie pliki we współdzielonej pamięci — zdjęcia, dokumenty, pliki do pobrania, dane innych aplikacji. Stwarzało to ryzyko dla prywatności: nieuczciwe aplikacje mogły zbierać informacje o użytkowniku bez jego wiedzy.
Według Google I/O 2019, ponad 65% użytkowników Androida uważa prywatność danych za krytyczny czynnik przy wyborze aplikacji. Scoped Storage jest bezpośrednią odpowiedzią na to zapotrzebowanie: każda aplikacja działa w izolowanym środowisku i uzyskuje dostęp do danych innych aplikacji tylko za wyraźną zgodą użytkownika.
Ważne: Scoped Storage w Android 10 był opcjonalny (aplikacja mogła go wyłączyć przez requestLegacyExternalStorage). Począwszy od Android 11 (API 30), Scoped Storage stał się obowiązkowy dla wszystkich aplikacji, niezależnie od target SDK.
Google wprowadził Scoped Storage, aby rozwiązać trzy fundamentalne problemy starego modelu dostępu do plików: prywatność, zarządzanie uprawnieniami i czyszczenie pozostałych plików.
W starym modelu aplikacja z READ_EXTERNAL_STORAGE mogła skanować całą współdzieloną pamięć i zbierać metadane o plikach użytkownika — współrzędne geograficzne zdjęć, nazwy dokumentów, strukturę katalogów. Scoped Storage eliminuje tę możliwość: nawet mając uprawnienie, aplikacja widzi tylko pliki przez siebie utworzone oraz jawnie wybrane przez użytkownika przez SAF.
Przed Scoped Storage aplikacja mogła pozostawiać pliki we współdzielonej pamięci po swoim usunięciu. Z czasem gromadziły się śmieciowe katalogi. Scoped Storage rozwiązuje to: wszystkie pliki w piaskownicy aplikacji są usuwane wraz z aplikacją, a pliki przez MediaStore mają właściciela i mogą być wyczyszczone przez system.
| Aspekt | Stary model (Legacy) | Scoped Storage |
|---|---|---|
| Dostęp do współdzielonej pamięci | Pełny (z uprawnieniem) | Tylko własna piaskownica |
| Pliki multimedialne | Bezpośrednia ścieżka pliku | Przez MediaStore URI |
| Cudze pliki | Zawsze dostępne | Tylko przez SAF |
| Czyszczenie przy usuwaniu | Pliki pozostają | Piaskownica jest usuwana |
| Kontrola użytkownika | Minimalna | Wyraźna zgoda na każdy plik |
Scoped Storage jest częścią ogólnej strategii Google na rzecz wzmocnienia prywatności w Androidzie, która obejmuje również uprawnienia „tylko podczas używania”, wskaźniki dostępu do kamery i mikrofonu oraz Privacy Dashboard.
Każda aplikacja w Androidzie otrzymuje własny prywatny katalog, dostępny przez Context.getFilesDir() i context.getCacheDir(). Do dostępu do zewnętrznego (współdzielonego) prywatnego katalogu używany jest Context.getExternalFilesDir().
W Scoped Storage aplikacja ma pełny dostęp do swojego zewnętrznego prywatnego katalogu bez żadnych uprawnień. Jest to główne miejsce do przechowywania plików, które nie powinny być dostępne dla innych aplikacji lub które aplikacja tworzy na własny użytek.
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()
}
}
Ścieżka do zewnętrznego prywatnego katalogu: /storage/emulated/0/Android/data/{packageName}/files/. Począwszy od Android 11 bezpośrednia ścieżka pliku jest niedostępna — tylko przez API. To kolejne wzmocnienie bezpieczeństwa Scoped Storage.
MediaStore — to podstawowe API dostępu do współdzielonych plików multimedialnych (obrazy, wideo, audio) w Scoped Storage. Aplikacja pracuje nie ze ścieżkami plików, ale z content URIs dostarczanymi przez MediaStore provider.
MediaStore jest podzielony na trzy główne kolekcje: Images, Video i Audio. Każda kolekcja obsługuje operacje CRUD przez ContentResolver. Do wstawienia nowego pliku do kolekcji Images używany jest MediaStore.Images.Media, do zapytania istniejących — query z odpowiednim 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
)
}
}
Ważne: MediaStore nie obsługuje dowolnych ścieżek plików — tylko content URIs. Próba uzyskania ścieżki pliku z URI przez MediaStore.Files.getContentUri() nie da bezpośredniej ścieżki do pliku na Android 11+. Zamiast tego używaj ContentResolver.openInputStream() i openOutputStream() do pracy z zawartością.
Storage Access Framework (SAF) — to API dostępu do dowolnych plików i katalogów poza piaskownicą aplikacji. SAF udostępnia użytkownikowi interfejs wyboru pliku lub katalogu, po czym aplikacja otrzymuje content URI z tymczasowym dostępem.
SAF jest używany w scenariuszach nieobjętych przez MediaStore: praca z dowolnymi dokumentami (PDF, ZIP, APK), dostęp do katalogów na karcie SD, import i eksport plików z innych aplikacji. Użytkownik jawnie wybiera plik przez systemowy selektor plików — gwarantuje to jego zgodę na dostęp.
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
// Uzyskaj trwałe uprawnienie URI
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF daje aplikacji dostęp do wybranego przez użytkownika katalogu aż do ponownego uruchomienia urządzenia (przy użyciu takePersistableUriPermission). To jedyny sposób uzyskania dostępu do dowolnych plików we współdzielonej pamięci na Android 11+.
Migracja istniejącej aplikacji Androida na Scoped Storage wymaga zmian w kilku kluczowych obszarach. Google zaleca podejście etapowe z testowaniem na Android 11+.
Zamień wszystkie bezpośrednie operacje na File we współdzielonej pamięci na ContentResolver.openInputStream / openOutputStream. Dla plików w piaskownicy aplikacji (getExternalFilesDir) ścieżki plików nadal działają.
Usuń READ_EXTERNAL_STORAGE i WRITE_EXTERNAL_STORAGE, jeśli nie są potrzebne do konkretnych scenariuszy (np. zarządzanie plikami — do tego jest specjalne uprawnienie MANAGE_EXTERNAL_STORAGE).
| Scenariusz | Stare podejście | Podejście Scoped Storage |
|---|---|---|
| Zapisywanie zdjęcia | File(path).writeBytes() | MediaStore + ContentResolver |
| Czytanie PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| Własne pliki | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Pamięć podręczna | File(cacheDir).writeBytes() | context.cacheDir (bez zmian) |
Po migracji przetestuj aplikację na Android 11+ (API 30), ustawiając w build.gradle targetSdk = 30 lub wyższe. Upewnij się, że wszystkie operacje na plikach we współdzielonej pamięci działają przez MediaStore lub SAF, a nie przez bezpośrednie ścieżki plików.
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()
}
}
Według Google Play Console (2024), ponad 78% aktywnych urządzeń z Androidem działa na Android 10+ z Scoped Storage. Migracja jest obowiązkowym krokiem do publikacji aktualizacji w Google Play: nowe aplikacje muszą targetować API 31+, aktualizacje — API 30+.
Często zadawane pytania
Nie, począwszy od Android 11 (API 30) Scoped Storage jest obowiązkowy dla wszystkich aplikacji. Flaga requestLegacyExternalStorage, dostępna w Android 10, nie działa na API 30+. Jedynym sposobem uzyskania szerokiego dostępu do systemu plików jest uprawnienie MANAGE_EXTERNAL_STORAGE, ale jest ono przeznaczone tylko dla menedżerów plików i antywirusów.
MANAGE_EXTERNAL_STORAGE — specjalne uprawnienie dla aplikacji, które potrzebują pełnego dostępu do systemu plików (menedżery plików, narzędzia do backupu). Żądanie uprawnienia otwiera ekran systemowy z ostrzeżeniem dla użytkownika. Przy publikacji w Google Play wymagane jest wypełnienie formularza Declaration o konieczności tego uprawnienia.
W Scoped Storage na Android 11+ nie można uzyskać bezpośredniej ścieżki pliku z content URI. Zamiast tego używaj ContentResolver.openInputStream() do odczytu i openOutputStream() do zapisu. Jeśli potrzebujesz ścieżki pliku dla zgodności z biblioteką zewnętrzną — utwórz kopię pliku w getCacheDir() i pracuj na kopii.
FileProvider działa bez zmian — jest używany do udostępniania plików z piaskownicy aplikacji innym aplikacjom przez content URIs. Scoped Storage nie wpływa na FileProvider, ponieważ FileProvider działa na poziomie content URIs, a nie bezpośrednich ścieżek plików.
Sprawdź Build.VERSION.SDK_INT: jeśli >= Build.VERSION_CODES.Q (29), urządzenie obsługuje Scoped Storage. Jednak na Android 10 Scoped Storage może być wyłączony przez requestLegacyExternalStorage. Na Android 11+ sprawdź, czy flaga w manifeście requestLegacyExternalStorage jest ustawiona — jeśli nie, Scoped Storage jest aktywny.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również