Scoped Storage là một mô hình truy cập hệ thống tệp được giới thiệu trong Android 10 (API 29), hạn chế quyền truy cập tùy tiện của ứng dụng vào bộ nhớ dùng chung của thiết bị. Theo Tài liệu dành cho nhà phát triển Android của Google (2024), Scoped Storage thay thế mô hình quyền cũ READ_EXTERNAL_STORAGE và WRITE_EXTERNAL_STORAGE, chỉ cấp cho ứng dụng quyền truy cập vào các tệp riêng của chúng trong hộp cát cách ly và vào các bộ sưu tập dùng chung thông qua MediaStore. Mô hình này tăng cường quyền riêng tư dữ liệu người dùng và ngăn chặn việc thu thập thông tin trái phép bởi các ứng dụng.
Những điểm chính
Scoped Storage là một thay đổi kiến trúc trong Android 10 (API 29) thay đổi cơ bản cách ứng dụng truy cập hệ thống tệp của thiết bị. Thay vì truy cập đầy đủ vào bộ nhớ dùng chung, ứng dụng chỉ được truy cập vào hộp cát của riêng mình và vào các bộ sưu tập phương tiện cụ thể thông qua MediaStore.
Trước Scoped Storage, bất kỳ ứng dụng nào có quyền READ_EXTERNAL_STORAGE đều có thể đọc tất cả tệp trong bộ nhớ dùng chung — ảnh, tài liệu, tệp tải xuống, dữ liệu của ứng dụng khác. Điều này tạo ra rủi ro về quyền riêng tư: các ứng dụng không đáng tin cậy có thể thu thập thông tin người dùng mà họ không biết.
Theo Google I/O 2019, hơn 65% người dùng Android coi quyền riêng tư dữ liệu là yếu tố quan trọng khi chọn ứng dụng. Scoped Storage là phản ứng trực tiếp cho nhu cầu này: mỗi ứng dụng hoạt động trong một môi trường cách ly và chỉ truy cập dữ liệu của ứng dụng khác khi có sự đồng ý rõ ràng của người dùng.
Quan trọng: Scoped Storage trong Android 10 là tùy chọn (ứng dụng có thể tắt nó qua requestLegacyExternalStorage). Bắt đầu từ Android 11 (API 30), Scoped Storage trở thành bắt buộc đối với tất cả ứng dụng, bất kể target SDK.
Google đã giới thiệu Scoped Storage để giải quyết ba vấn đề cơ bản của mô hình truy cập tệp cũ: quyền riêng tư, quản lý quyền và dọn dẹp tệp còn sót lại.
Trong mô hình cũ, ứng dụng có READ_EXTERNAL_STORAGE có thể quét toàn bộ bộ nhớ dùng chung và thu thập siêu dữ liệu tệp của người dùng — tọa độ địa lý của ảnh, tên tài liệu, cấu trúc thư mục. Scoped Storage loại bỏ khả năng này: ngay cả khi có quyền, ứng dụng chỉ thấy các tệp do chính nó tạo ra và các tệp được người dùng chọn rõ ràng thông qua SAF.
Trước Scoped Storage, ứng dụng có thể để lại tệp trong bộ nhớ dùng chung sau khi gỡ cài đặt. Theo thời gian, các thư mục rác tích tụ lại. Scoped Storage giải quyết điều này: tất cả tệp trong hộp cát của ứng dụng bị xóa cùng với ứng dụng và các tệp qua MediaStore có chủ sở hữu và có thể được hệ thống dọn dẹp.
| Khía cạnh | Mô hình cũ (Legacy) | Scoped Storage |
|---|---|---|
| Truy cập bộ nhớ dùng chung | Đầy đủ (có quyền) | Chỉ hộp cát riêng |
| Tệp đa phương tiện | Đường dẫn tệp trực tiếp | Qua URI MediaStore |
| Tệp của ứng dụng khác | Luôn có thể truy cập | Chỉ qua SAF |
| Dọn dẹp khi gỡ cài đặt | Tệp vẫn còn | Hộp cát bị xóa |
| Kiểm soát của người dùng | Tối thiểu | Đồng ý rõ ràng cho từng tệp |
Scoped Storage là một phần trong chiến lược quyền riêng tư tổng thể của Google trên Android, cũng bao gồm quyền "chỉ khi đang sử dụng", chỉ báo truy cập camera và micrô, và Bảng điều khiển quyền riêng tư.
Mỗi ứng dụng Android nhận được thư mục riêng tư của riêng mình, có thể truy cập qua Context.getFilesDir() và context.getCacheDir(). Để truy cập thư mục riêng tư bên ngoài (dùng chung), sử dụng Context.getExternalFilesDir().
Trong Scoped Storage, ứng dụng có quyền truy cập đầy đủ vào thư mục riêng tư bên ngoài mà không cần bất kỳ quyền nào. Đây là nơi chính để lưu trữ các tệp không được phép truy cập bởi ứng dụng khác hoặc ứng dụng tạo ra cho mục đích sử dụng riêng.
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()
}
}
Đường dẫn đến thư mục riêng tư bên ngoài: /storage/emulated/0/Android/data/{packageName}/files/. Bắt đầu từ Android 11, đường dẫn tệp trực tiếp không khả dụng — chỉ qua API. Đây là một cải tiến bảo mật khác của Scoped Storage.
MediaStore là API chính để truy cập các tệp đa phương tiện dùng chung (hình ảnh, video, âm thanh) trong Scoped Storage. Ứng dụng không làm việc với đường dẫn tệp mà với content URI do nhà cung cấp MediaStore cung cấp.
MediaStore được chia thành ba bộ sưu tập chính: Images, Video và Audio. Mỗi bộ sưu tập hỗ trợ các thao tác CRUD thông qua ContentResolver. Để chèn tệp mới vào bộ sưu tập Images, sử dụng MediaStore.Images.Media; để truy vấn tệp hiện có — query với URI tương ứng.
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
)
}
}
Quan trọng: MediaStore không hỗ trợ đường dẫn tệp tùy ý — chỉ content URI. Cố gắng lấy đường dẫn tệp từ URI qua MediaStore.Files.getContentUri() sẽ không cung cấp đường dẫn trực tiếp đến tệp trên Android 11+. Thay vào đó, hãy sử dụng ContentResolver.openInputStream() và openOutputStream() để làm việc với nội dung.
Storage Access Framework (SAF) là API để truy cập các tệp và thư mục tùy ý bên ngoài hộp cát của ứng dụng. SAF cung cấp cho người dùng giao diện chọn tệp hoặc thư mục, sau đó ứng dụng nhận được content URI với quyền truy cập tạm thời.
SAF được sử dụng cho các tình huống không được MediaStore bao phủ: làm việc với tài liệu tùy ý (PDF, ZIP, APK), truy cập thư mục trên thẻ SD, nhập và xuất tệp từ ứng dụng khác. Người dùng chọn tệp một cách rõ ràng thông qua trình chọn tệp hệ thống — điều này đảm bảo sự đồng ý của họ cho việc truy cập.
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 cấp cho ứng dụng quyền truy cập vào thư mục do người dùng chọn cho đến khi khởi động lại thiết bị (khi sử dụng takePersistableUriPermission). Đây là cách duy nhất để có được quyền truy cập vào các tệp tùy ý trong bộ nhớ dùng chung trên Android 11+.
Di chuyển ứng dụng Android hiện có sang Scoped Storage yêu cầu thay đổi ở một số lĩnh vực chính. Google khuyến nghị cách tiếp cận theo giai đoạn với thử nghiệm trên Android 11+.
Thay thế tất cả thao tác File trực tiếp trong bộ nhớ dùng chung bằng ContentResolver.openInputStream / openOutputStream. Đối với tệp trong hộp cát của ứng dụng (getExternalFilesDir), đường dẫn tệp vẫn hoạt động.
Xóa READ_EXTERNAL_STORAGE và WRITE_EXTERNAL_STORAGE nếu chúng không cần thiết cho các tình huống cụ thể (ví dụ: quản lý tệp — có quyền đặc biệt MANAGE_EXTERNAL_STORAGE cho việc này).
| Tình huống | Cách tiếp cận cũ | Cách tiếp cận Scoped Storage |
|---|---|---|
| Lưu ảnh | File(path).writeBytes() | MediaStore + ContentResolver |
| Đọc PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| Tệp riêng | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Bộ nhớ đệm | File(cacheDir).writeBytes() | context.cacheDir (không thay đổi) |
Sau khi di chuyển, hãy thử nghiệm ứng dụng trên Android 11+ (API 30), đặt targetSdk = 30 hoặc cao hơn trong build.gradle. Đảm bảo rằng tất cả thao tác tệp trong bộ nhớ dùng chung hoạt động qua MediaStore hoặc SAF, không phải qua đường dẫn tệp trực tiếp.
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()
}
}
Theo Google Play Console (2024), hơn 78% thiết bị Android đang hoạt động chạy Android 10+ với Scoped Storage. Di chuyển là bước bắt buộc để xuất bản cập nhật trên Google Play: ứng dụng mới phải nhắm mục tiêu API 31+, bản cập nhật — API 30+.
Câu hỏi thường gặp
Không, bắt đầu từ Android 11 (API 30) Scoped Storage là bắt buộc đối với tất cả ứng dụng. Cờ requestLegacyExternalStorage, có sẵn trong Android 10, không hoạt động trên API 30+. Cách duy nhất để có quyền truy cập rộng rãi vào hệ thống tệp là quyền MANAGE_EXTERNAL_STORAGE, nhưng nó chỉ dành cho trình quản lý tệp và phần mềm chống vi-rút.
MANAGE_EXTERNAL_STORAGE là quyền đặc biệt dành cho ứng dụng cần quyền truy cập đầy đủ vào hệ thống tệp (trình quản lý tệp, công cụ sao lưu). Yêu cầu quyền sẽ mở màn hình hệ thống với cảnh báo cho người dùng. Khi xuất bản trên Google Play, cần điền biểu mẫu Tuyên bố về sự cần thiết của quyền này.
Trong Scoped Storage trên Android 11+, không thể lấy đường dẫn tệp trực tiếp từ content URI. Thay vào đó, hãy sử dụng ContentResolver.openInputStream() để đọc và openOutputStream() để ghi. Nếu bạn cần đường dẫn tệp để tương thích với thư viện bên thứ ba, hãy tạo bản sao của tệp trong getCacheDir() và làm việc với bản sao.
FileProvider tiếp tục hoạt động mà không thay đổi — nó được sử dụng để cung cấp quyền truy cập vào tệp từ hộp cát của ứng dụng cho các ứng dụng khác thông qua content URI. Scoped Storage không ảnh hưởng đến FileProvider, vì FileProvider hoạt động ở cấp độ content URI, không phải đường dẫn tệp trực tiếp.
Kiểm tra Build.VERSION.SDK_INT: nếu >= Build.VERSION_CODES.Q (29), thì thiết bị hỗ trợ Scoped Storage. Tuy nhiên, trên Android 10, Scoped Storage có thể bị tắt qua requestLegacyExternalStorage. Trên Android 11+, kiểm tra xem cờ kê khai requestLegacyExternalStorage có được đặt không — nếu không, Scoped Storage đang hoạt động.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm