Scoped Storage سیستم ذخیرهسازی جدید است که توسط اندروید معرفی شده است.
سناریوی فعلی
- همه برنامهها پوشه مخصوص خود را در حافظه داخلی دارند که برای سایر برنامهها قابل مشاهده نیست، مانند Android/data/your package name.
- بیشتر برنامههای فعلی برای انجام یک کار ساده نیاز به مجوزهای گستردهای دارند. مانند بارگیری یک تصویر و یا یک انتخاب کننده تصویر و غیره. بیشتر اوقات در زمان حذف برنامهها فایلها حذف نمیشوند. در نتیجه باعث میشود فضای ذخیرهسازی کم شود.
بنابراین اندروید 10 با یک راهحل به میان آمد: Scoped Storage.
فضای ذخیرهسازی چیست؟
- این یک مفهوم برای ذخیره فایلها، تصاویر و غیره به صورت جداگانه، به نام مجموعهها است، که دسترسی معمولی کل فضای ذخیرهسازی را محدود میکند.
- تخصیص بهتر: این یعنی که سیستم میداند کدام برنامه کدام فایل را ایجاد کرده است. این زمانی مفید است که برنامه حذف شود، بنابراین تمام دادههای مربوط به برنامه نیز حذف میشود.
- محافظت از دادههای برنامه: پوشههای داخلی و خارجی برنامه خصوصی هستند.
- حفاظت از دادههای کاربر: تصویر بارگیری شده توسط برنامه دیگر قابل استفاده نیست.
ویژگیهای اصلی
- دسترسی بدون محدودیت به حافظه شخصی برنامه(داخلی/خارجی) بدون نیاز به مجوز
- دسترسی نامحدود به فایلهای رسانهای و مجموعههای بارگیری شده: مانند فایل تصویر را بدون مجوز ذخیره کنید
- فقط مجموعههای رسانهای با مجوز ذخیرهسازی قابل خواندن هستند
- ACCESS_MEDIA_LOCATION برای دسترسی به محل تصاویر
- برای فایلهایی مانند pdf, text و غیره از System Picker استفاده کنید
- خواندن و نوشتن در خارج از مجموعه نیاز به "System Picker" دارد
چگونه آن را پیادهسازی کنیم؟
- از ACTION_OPEN_DOCUMENT برای انتخاب فایل استفاده کنید
fun newFile(view: View) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "textFileDemo.txt")
startActivityForResult(intent, CREATE_REQUEST_CODE)
}
- برای انتخاب پوشه از ACTION_OPEN_DOCUMENT_TREE استفاده کنید: این در اندروید 10 برای دسترسی کامل به پوشه درخواست مجوز میدهد.
fun openDocTree(view: View) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
}
startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Log.i("TAG","${data.data}"}
}
}
- دسترسی به محتوا با استفاده از مسیر خام پروندهها
- فایل تصویری را با استفاده از MediaStore Api ذخیره کنید
هدف استفاده از IS_PENDING در MediaStore
fun downloadImageWithMediaStore(){
try{
val bitmap: Bitmap = (ivImageDownload.drawable as BitmapDrawable).bitmap
val values = ContentValues().apply{
put(MediaStore.Images.Media.DISPLAY_NAME,"imgMS.jpeg")
put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg")
put(MediaStore.Images.Media.IS_PENDING,1) }
val resolver = contentResolver
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = resolver.insert(collection,values)
if (item != null) {
resolver.openOutputStream(item).use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING,0)
resolver.update(item,values,null,null)
Toast.makeText(
applicationContext,
"Download successfully to ${item.path}",
Toast.LENGTH_LONG
).show()
}
}
catch (e:Exception){
// handle exception here
}
}
هنگامی که یک آیتم را با pending intent مقدار 1 درج میکنید، به صورت پیشفرض، از برنامههای دیگر پنهان خواهد شد.
این میتواند هنگام بارگیریهای طولانی مدت مانند بارگیری فیلم از URL استفاده شود. پس از اتمام بارگیری، pending intent را برروی 0 تنظیم کنید تا آن را در سایر برنامههای دستگاه نشان دهد.
- در مثال بالا، مسیر ذخیرهسازی را تعیین/مشخص نکردهایم، بنابراین سیستمعامل به طور خودکار مسیر را بر اساس نوع فایل انتخاب میکند. در اینجا ما نوع فایل را image/JPEG قرار دادهایم، بنابراین به صورت پیشفرض تصویر را در پوشه تصاویر ذخیره میکند.
- همچنین شما میتوانید مسیر فایل را با Media.RELATIVE.PATH انتخاب کنید
- VOLUME_EXTERNAL_PRIMARY برای ذخیر در حافظه ذخیرهسازی اصلی است. و برای بدست آوردن لیست ذخیرهسازیهای موجود در دستگاه از (MediaStore.getExternalVolumeNames(Context استفاده میکنیم.
val values = ContentValues().apply{
put(MediaStore.Images.Media.RELATIVE_PATH,"MyPics")
put(MediaStore.Images.Media.DISPLAY_NAME,"imgMS.jpeg")
put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg")
put(MediaStore.Images.Media.IS_PENDING,1)
}
- هنگام گرفتن URI یک سند میتوانیم از ContentResolver.takePersistableUriPermission استفاده کنیم، تا هنگام restart همچنان مجوز را داشته باشیم.
- اگر برنامه شما از فضای ذخیرهسازی استفاده میکند، دسترسی به مسیر خام فایلها به پوشههای مخصوص برنامه در حافظه خارجی محدود میشود، حتی اگر به برنامه شما مجوز READ_EXTERNAL_STORAGE داده شود. اگ برنامه شما سعی دارد از یک مسیر خام برای باز کردن فایلی در حافظه خاجی که در پوشه مخصوص برنامه نیست استفاده کند، FileNotFoundException رخ میدهد. برای مثال مسیر یک فایل در خارج از پوشه مخصوص برنامه /sdcard/DCMI/ABC.JPG است. برنامه شما باید از روشهای موجود در MediaStore API استفاده کند.
- در اندروید Q و بالاتر، امکان تغییر یا حذف فایلها به صورت مستقیم در پوشه MediaStore نیست، و معمولا برای انجام این کار باید مجوز صریح بدست آید. روش کار به این صورت است که سیستمعامل یک RecoverableSecurityException پرتاب میکند که میتوانیم آن را catch کنیم. در داخل آن یک IntentSender وجود دارد که اکتیویتی میتواند از آن برای گرفتن مجوز از کاربر استفاده کند تا آن را تغییر دهد یا حذف کند.
private suspend fun performDeleteImage(image: MediaStoreModel) {
withContext(Dispatchers.IO) {
try {
getApplication<Application>().contentResolver.delete(
image.contentUri,
"${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString())
)
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
startIntentSenderForResult(
recoverableSecurityException.userAction.actionIntent.intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
} else {
throw securityException
}
}
}
}
تگ RequestLagecyAccess
در فایل manifest، ما هنوز میتوانیم به اندروید بگوییم که میخواهیم از دسترسی به مجوزها مانند نسخههای پایینتر از اندروید 10 استفاده کنیم. اما این تنها توسط 2% از برنامههای اندرویدی استفاده میشود، و قرار است در نسخههای بعدی اندروید منسوخ شود.
مقدار این تگ به صورت پیشفرض در اندرویدهای 9 (و پایینتر از آن) true است.
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme">
مجوز ACCESS_MEDIA_LOCATION
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
- این یک مجوز زمان اجرا است(در تنظیمات قابل مشاهده نیست)
- هیچ تضمینی مبنی بر اینکه همیشه این مجوز را خواهید داشت نیست، حتی اگر مجوز READ_EXTERNAL_STORAGE را داشته باشید.
fun openIntentChooser() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
intent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(Intent.createChooser(intent, "Select Picture"), CHOOSE_FILE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == CHOOSE_FILE) {
if (data != null) {
var inputStream: InputStream? = null
try {
inputStream = contentResolver.openInputStream(data.data!!)
val exifInterface = ExifInterface(inputStream!!)
Log.i(
"TAG",
"===${exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE)}"
)
} catch (e: IOException) {
// Handle any errors here
} finally {
if (inputStream != null) {
try {
inputStream.close()
} catch (ignored: IOException) {
}
}
}
}
}
}
}
- برای بدست آوردن تعداد دقیق بایت فایلها، از MediaStore.SetReqiuedOriginal() استفاده کنید، اگر موفقیتآمیز نباشد یک exception رخ میدهد.
اصلاح و حذف رسانهها
- رضایت کاربر برای اصلاح یا حذف منابع رسانه نیاز است
- رضایت کاربر حتی برای دسترسی به مسیر فایل نیاز است
- حذف یا ویرایش در همان دیالوگ(در نسخه بعدی اندروید)
بایدها و نبایدها
- از یک مسیر ثابت استفاده نکنید. دسترسی به مسیر فایل را قفل کنید
- از MediaStore استفاده کنید
- MediaStore باید بدرستی استفاده شود، برای مثال فایلهای موسیقی خود را در پوشه مربوط به تصاویر قرار ندهید
- توصیه میشود فایلهای غیر رسانهای را در پوشه download قرار دهید
دسترسی ویژه به برنامه
- فقط برنامههایی که توسط گوگل تایید شدهاند دسترسی کامل به فضای ذخیرهسازی را دارند
- فرم اظهارنامه را به گوگل ارسال کنید
- لیست سفید برنامهها توسط گوگل
تغییرات در نسخه بعدی اندروید
- بروزرسانی مجوز برای UI: کاربر یک UI مجوز متفاوت را براساس بروزرسانی و اینکه آیا از فضای ذخیرهسازی استفاده میکنند یا نه میبینند. برای مثال پیش از این، ده برنامه دسترسی به فضای ذخیرهسازی را داشتند، و پس از آن ده مجموعه به فضای ذخیرهسازی دسترسی دارند
- فعال کردن مسیر فایل و کتابخانههای بومی برای خواندن رسانهها
- بروزرسانی فایل رسانه و اصلاح APIها
- محافظت از پوشههای برنامه خارجی
- خواندن فایلهایی که توسط برنامه شما ایجاد نشده به مجوز READ_EXTERNAL_STORAGE احتیاج دارد
- برای ویرایش یا حذف فایلهایی که برنامه شما در آن مشارکت ندارد، به رضایت صریح کاربر نیاز دارید
- WRITE_EXTERNAL_STORAGE در نسخه بعدی اندروید منسوخ میشود و مجوز خواندن فقط در زمان استفاده داده میشود
دسترسی به فایلهای غیر رسانهای
برای دسترسی به فایلهای غیر رسانهای توسط برنامههای دیگر از System Picker با (SAF(Storage Access FrameWork استفاده کنید. یک درخواست مجوز در زمان اجرا برای دسترسی کامل داده میشود.
یادآوری
اگر برنامه شما در محدوده اندروید 10 است، از MediaStore و System Picker برای دسترسی به فایلها و اسناد استفاده کنید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید