Scoped Storage هو نموذج للوصول إلى نظام الملفات تم تقديمه في Android 10 (API 29) ويقيد الوصول العشوائي للتطبيقات إلى التخزين المشترك للجهاز. وفقًا لوثائق مطوري Android من Google (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 |
|---|---|---|
| الوصول إلى التخزين المشترك | كامل (بإذن) | فقط البيئة المعزولة الخاصة |
| ملفات الوسائط | مسار ملف مباشر | عبر URI MediaStore |
| ملفات التطبيقات الأخرى | متاحة دائمًا | فقط من خلال SAF |
| التنظيف عند إلغاء التثبيت | تبقى الملفات | يتم حذف البيئة المعزولة |
| التحكم للمستخدم | أدنى | موافقة صريحة لكل ملف |
Scoped Storage هو جزء من استراتيجية الخصوصية الشاملة من Google في Android، والتي تشمل أيضًا أذونات "أثناء الاستخدام فقط" ومؤشرات الوصول إلى الكاميرا والميكروفون ولوحة تحكم الخصوصية.
كل تطبيق في 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 للوصول إلى الملفات والدلائل العشوائية خارج البيئة المعزولة للتطبيق. يوفر 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)، تستمر مسارات الملفات في العمل.
قم بإزالة 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، ولكنه مخصص فقط لمديري الملفات ومضادات الفيروسات.
MANAGE_EXTERNAL_STORAGE هو إذن خاص للتطبيقات التي تحتاج إلى وصول كامل لنظام الملفات (مديري الملفات، أدوات النسخ الاحتياطي). يفتح طلب الإذن شاشة نظام مع تحذير للمستخدم. عند النشر على Google Play، يجب ملء نموذج إعلان حول ضرورة هذا الإذن.
في 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. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا