Một số lưu ý thay đổi liên quan đến External Storage trên Android 10(API 29)

1. Thay đổi

  • Quyền truy cập vào bộ nhớ ngoài của ứng dụng: ứng dụng được cấp quyền truy cập vào bộ nhớ ngoài theo phạm vi của ứng dụng, phạm vi này gọi là scoped storage (Khác với trước đây là ứng dụng khá là thoải mái trong việc truy cập vào những file lưu ở bộ nhớ ngoài )
    – Các file dành riêng cho ứng dụng thì truy cập thông qua getExternalFilesDir()
    – Các file dạng Photo, Video hoặc Audio thì nên sử dụng media store.

2. Hành động

2.1 Tạm thời

  • Nếu không muốn áp dụng những thay đổi này thì trong file AndroidManifest.xml thêm thuộc tính cho <application>

    android:requestLegacyExternalStorage=”true”

  • Tuy nhiên cách này chỉ là tạm thời áp dụng với các ứng dụng target Android 10 hoặc thấp hơn. Nếu target từ Android 11 và cao hơn thì thuộc tính này không có tác dụng. Tức là bạn thực sự cần chuẩn bị để migrate những phần data bị ảnh hưởng bởi thay đổi này.

2.2 Lâu dài

2.2.1 External storage

Android 10 và cao hơn:

/storage/emulated/0/Android/data/package_app/files/Pictures/IMG_20201118_113434.jpg

Android thấp hơn 10:

storage/emulated/0/Pictures/IMG_20201118_113434.jpg

2.2.1 Media Store

  • Thực chất là 1 cơ sở dữ liệu dạng SQL, do vậy có thể dễ dàng thực hiện một số thao tác insert, update, delete hoặc query.

    Ví dụ 1 ảnh được lưu trong Media Store tại đường dẫn và uri tương ứng:

path = storage/emulated/0/Pictures/IMG_20201118_113434.jpg

uri = content://media/external/images/media/47

  • Lưu ý:

– Không thể sử dụng các thao tác vào ra file thông thường như FileInputStream(), FileOutputStream() để thao tác trên nhưng file này. Nếu tiếp tục sẽ sinh ra ngoại lệ FileNotFoundException (open failed: EACCES permission denied)
– Hãy sử dụng ContentResolver để thao tác trên những file này. Tham khảo openInputStream, openOutputStream

  • Sử dụng với Glide
    Với những ảnh lưu trong Media Store, Glide.load(path) sẽ không hoạt động, thay vào đó hãy sử dụng Glide.load(uri).

  • Một số đoạn code thông dụng

– Lấy danh sách Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

data class Image(
val id: Long,
val name: String,
val uri: Uri,
val path: String
)

fun getImages(Context context): List<Image> {
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATA}
val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC")

val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)

val images = arrayListOf<Image>()

if (cursor.moveToFirst()) {
do {

val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val path = cursor.getString(dataColumn)
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
val image = Image(id, name, uri, path)
images.add(image)

} while (cursor.moveToNext())
}
}

– Kiểm tra 1 file đã tồn tại trong Media Store

1
2
3
4
5
6
7
fun exist(Context context, String fileName): Boolean {
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val selection = MediaStore.Images.Media.DISPLAY_NAME + " = ?"
val selectionArgs = arrayOf(fileName)
val cursor = context.contentResolver.query(uri, null, selection, selectionArgs, null)
return cursor.moveToFirst()
}

– Get uri của 1 file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun getUriByFileName(Context context, String fileName): Uri {

long id;
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Images.ImageColumns._ID)
val selection = MediaStore.Images.ImageColumns.DISPLAY_NAME + " LIKE ?"
val selectionArgs = arrayOf(fileName)
val cursor = context.contentResolver().query(uri, projection, selection, selectionArgs, null);

cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(projection[0]);
val id = cursor.getLong(columnIndex);
cursor.close();

return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
}