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 |
|---|---|---|
| Приступ заједничком складишту | Потпун (са дозволом) | Само сопствени пешчаник |
| Медијске датотеке | Директна путања датотеке | Преко 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, директна путања датотеке није доступна — само преко API-ја. Ово је још једно јачање безбедности Scoped Storage-а.
MediaStore — је основни API за приступ заједничким медијским датотекама (слике, видео, аудио) у Scoped Storage-у. Апликација ради не са путањама датотека, већ са 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 не подржава произвољне путање датотека — само content URIs. Покушај добијања путање датотеке из 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
// Узми трајну дозволу URI
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) путање датотека и даље раде.
Уклоните 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-а, а не преко директних путања датотека.
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-у: нове апликације морају да циљају 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+ директна путања датотеке из content URI-ја не може се добити. Уместо тога, користите ContentResolver.openInputStream() за читање и openOutputStream() за писање. Ако вам је потребна путања датотеке за компатибилност са библиотеком треће стране — направите копију датотеке у getCacheDir() и радите са копијом.
FileProvider наставља да ради без промена — користи се за пружање приступа датотекама из пешчаника апликације другим апликацијама преко content URIs. Scoped Storage не утиче на FileProvider, јер FileProvider ради на нивоу content URIs, а не директних путања датотека.
Проверите 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође