Scoped Storage — مدل دسترسی به سیستم فایل است که در اندروید ۱۰ (API 29) معرفی شد و دسترسی دلخواه برنامهها به حافظه مشترک دستگاه را محدود میکند. طبق Google Android Developer Documentation (2024)، Scoped Storage مدل قدیمی مجوزهای READ_EXTERNAL_STORAGE و WRITE_EXTERNAL_STORAGE را جایگزین میکند و به برنامهها فقط به فایلهای خودشان در محیط ایزوله و به مجموعههای مشترک از طریق MediaStore دسترسی میدهد. این مدل حریم خصوصی دادههای کاربر را افزایش میدهد و از جمعآوری غیرمجاز اطلاعات توسط برنامهها جلوگیری میکند.
نکات اصلی
Scoped Storage — یک تغییر معماری در اندروید ۱۰ (API 29) است که نحوه دسترسی برنامهها به سیستم فایل دستگاه را به صورت اساسی تغییر میدهد. به جای دسترسی کامل به حافظه مشترک، برنامه فقط به محفظه خود و مجموعههای مشخصی از فایلهای رسانهای از طریق MediaStore دسترسی پیدا میکند.
پیش از Scoped Storage، هر برنامهای با مجوز READ_EXTERNAL_STORAGE میتوانست همه فایلهای حافظه مشترک — عکسها، اسناد، دانلودها، دادههای سایر برنامهها — را بخواند. این امر خطرات حریم خصوصی ایجاد میکرد: برنامههای غیرصادق میتوانستند بدون اطلاع کاربر اطلاعاتی درباره او جمعآوری کنند.
طبق Google I/O 2019، بیش از ۶۵٪ کاربران اندروید حریم خصوصی دادهها را عامل حیاتی در انتخاب برنامه میدانند. Scoped Storage پاسخ مستقیم به این نیاز است: هر برنامه در محیطی ایزوله کار میکند و فقط با رضایت صریح کاربر به دادههای دیگران دسترسی پیدا میکند.
مهم: Scoped Storage در اندروید ۱۰ اختیاری بود (برنامه میتوانست آن را از طریق requestLegacyExternalStorage غیرفعال کند). از اندروید ۱۱ (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 برای تقویت حریم خصوصی در اندروید است که شامل مجوزهای «فقط هنگام استفاده»، نشانگرهای دسترسی به دوربین و میکروفون و Privacy Dashboard نیز میشود.
هر برنامه در اندروید یک دایرکتوری خصوصی خود را از طریق 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/. از اندروید ۱۱ به بعد مسیر مستقیم فایل در دسترس نیست — فقط از طریق 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() در اندروید ۱۱+ مسیر مستقیمی به فایل نمیدهد. به جای آن از 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). این تنها راه برای دریافت دسترسی به فایلهای دلخواه در حافظه مشترک در اندروید ۱۱+ است.
مهاجرت برنامه اندرویدی موجود به Scoped Storage نیازمند تغییرات در چندین حوزه کلیدی است. Google رویکرد مرحلهای با آزمایش در اندروید ۱۱+ را توصیه میکند.
همه عملیات مستقیم 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 (بدون تغییر) |
پس از مهاجرت، برنامه را در اندروید ۱۱+ (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)، بیش از ۷۸٪ دستگاههای فعال اندرویدی با Scoped Storage روی اندروید ۱۰+ کار میکنند. مهاجرت یک مرحله اجباری برای انتشار بهروزرسانیها در Google Play است: برنامههای جدید باید API 31+ را هدف قرار دهند، بهروزرسانیها — API 30+.
سوالات متداول
خیر، از اندروید ۱۱ (API 30) به بعد Scoped Storage برای همه برنامهها اجباری است. پرچم requestLegacyExternalStorage که در اندروید ۱۰ در دسترس بود، در API 30+ کار نمیکند. تنها راه برای دریافت دسترسی گسترده به سیستم فایل، مجوز MANAGE_EXTERNAL_STORAGE است، اما فقط برای مدیران فایل و آنتیویروسها در نظر گرفته شده است.
MANAGE_EXTERNAL_STORAGE — مجوز ویژه برای برنامههایی که نیاز به دسترسی کامل به سیستم فایل دارند (مدیران فایل، ابزارهای پشتیبانگیری). درخواست مجوز یک صفحه سیستم با هشدار برای کاربر باز میکند. هنگام انتشار در Google Play لازم است فرم Declaration در مورد نیاز به این مجوز پر شود.
در Scoped Storage در اندروید ۱۱+ نمیتوان مسیر مستقیم فایل از 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 پشتیبانی میکند. اما در اندروید ۱۰، Scoped Storage میتواند از طریق requestLegacyExternalStorage غیرفعال شود. در اندروید ۱۱+ بررسی کنید که پرچم requestLegacyExternalStorage در مانیفست تنظیم شده است یا خیر — اگر نه، Scoped Storage فعال است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید