Scoped Storage — είναι ένα μοντέλο πρόσβασης στο σύστημα αρχείων που εισήχθη στο Android 10 (API 29) και περιορίζει την αυθαίρετη πρόσβαση των εφαρμογών στον κοινόχρηστο αποθηκευτικό χώρο της συσκευής. Σύμφωνα με το Google Android Developer Documentation (2024), το Scoped Storage αντικαθιστά το παλιό μοντέλο αδειών READ_EXTERNAL_STORAGE και WRITE_EXTERNAL_STORAGE, παρέχοντας στις εφαρμογές πρόσβαση μόνο στα δικά τους αρχεία σε απομονωμένο sandbox και σε κοινόχρηστες συλλογές μέσω του MediaStore. Αυτό το μοντέλο αυξάνει το απόρρητο των δεδομένων των χρηστών και αποτρέπει τη μη εξουσιοδοτημένη συλλογή πληροφοριών από εφαρμογές.
Κύρια σημεία
Scoped Storage — είναι μια αρχιτεκτονική αλλαγή στο Android 10 (API 29) που αλλάζει θεμελιωδώς τον τρόπο πρόσβασης των εφαρμογών στο σύστημα αρχείων της συσκευής. Αντί για πλήρη πρόσβαση στον κοινόχρηστο αποθηκευτικό χώρο, η εφαρμογή αποκτά πρόσβαση μόνο στο δικό της sandbox και σε ορισμένες συλλογές αρχείων πολυμέσων μέσω του 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 το λύνει: όλα τα αρχεία στο sandbox της εφαρμογής διαγράφονται μαζί με την εφαρμογή, ενώ τα αρχεία μέσω MediaStore έχουν ιδιοκτήτη και μπορούν να καθαριστούν από το σύστημα.
| Πτυχή | Παλιό μοντέλο (Legacy) | Scoped Storage |
|---|---|---|
| Πρόσβαση σε κοινόχρηστο χώρο | Πλήρης (με άδεια) | Μόνο δικό του sandbox |
| Αρχεία πολυμέσων | Άμεση διαδρομή αρχείου | Μέσω URI MediaStore |
| Ξένα αρχεία | Πάντα διαθέσιμα | Μόνο μέσω SAF |
| Καθαρισμός κατά αφαίρεση | Τα αρχεία παραμένουν | Το sandbox διαγράφεται |
| Έλεγχος χρήστη | Ελάχιστος | Ρητή συγκατάθεση ανά αρχείο |
Το 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.
Το 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 για πρόσβαση σε αυθαίρετα αρχεία και καταλόγους εκτός του sandbox της εφαρμογής. Το 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. Για αρχεία στο sandbox της εφαρμογής (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) με targetSdk = 30 ή υψηλότερο στο build.gradle. Βεβαιωθείτε ότι όλες οι λειτουργίες αρχείων στον κοινόχρηστο αποθηκευτικό χώρο λειτουργούν μέσω 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, αλλά προορίζεται μόνο για διαχειριστές αρχείων και antivirus.
MANAGE_EXTERNAL_STORAGE — μια ειδική άδεια για εφαρμογές που χρειάζονται πλήρη πρόσβαση στο σύστημα αρχείων (διαχειριστές αρχείων, εργαλεία δημιουργίας αντιγράφων ασφαλείας). Το αίτημα άδειας ανοίγει μια οθόνη συστήματος με προειδοποίηση για τον χρήστη. Κατά τη δημοσίευση στο Google Play, απαιτείται η συμπλήρωση της φόρμας Declaration σχετικά με την αναγκαιότητα αυτής της άδειας.
Στο Scoped Storage σε Android 11+, δεν μπορεί να ληφθεί άμεση διαδρομή αρχείου από content URI. Αντ’ αυτού, χρησιμοποιήστε το ContentResolver.openInputStream() για ανάγνωση και το openOutputStream() για εγγραφή. Εάν χρειάζεστε διαδρομή αρχείου για συμβατότητα με βιβλιοθήκη τρίτου μέρους — δημιουργήστε ένα αντίγραφο του αρχείου στο getCacheDir() και εργαστείτε με το αντίγραφο.
Το FileProvider συνεχίζει να λειτουργεί χωρίς αλλαγές — χρησιμοποιείται για την παροχή πρόσβασης σε αρχεία από το sandbox της εφαρμογής σε άλλες εφαρμογές μέσω 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 έχει οριστεί στο manifest — εάν όχι, το Scoped Storage είναι ενεργό.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης