Truy cập vào tệp dành riêng cho ứng dụng

Trong nhiều trường hợp, ứng dụng của bạn sẽ tạo các tệp mà những ứng dụng khác không cần truy cập hoặc không được truy cập. Hệ thống cung cấp những vị trí sau để lưu trữ các tệp dành riêng cho ứng dụng như vậy:

  • Thư mục bộ nhớ trong: Các thư mục này có cả một vị trí chuyên để lưu trữ các tệp bền vững và một vị trí khác để lưu trữ dữ liệu bộ nhớ đệm. Hệ thống sẽ ngăn các ứng dụng khác truy cập vào những vị trí này, và trên Android 10 (API cấp 29) trở lên, những vị trí này sẽ được mã hoá. Các đặc điểm trên khiến những vị trí này trở thành nơi phù hợp để lưu trữ dữ liệu nhạy cảm mà chỉ ứng dụng của bạn mới có thể truy cập.

  • Thư mục bộ nhớ ngoài: Các thư mục này có cả một vị trí chuyên để lưu trữ các tệp bền vững và một vị trí khác để lưu trữ dữ liệu bộ nhớ đệm. Mặc dù ứng dụng khác có thể truy cập vào các thư mục này nếu ứng dụng đó có quyền thích hợp, nhưng tệp lưu trữ trong các thư mục này chỉ dành cho ứng dụng của bạn. Nếu bạn định tạo các tệp mà ứng dụng khác có thể truy cập, thì ứng dụng của bạn nên lưu trữ các tệp này trong bộ nhớ dùng chung thuộc bộ nhớ ngoài.

Khi người dùng gỡ cài đặt ứng dụng của bạn, các tệp lưu trong bộ nhớ dành riêng cho ứng dụng sẽ bị xoá. Do đó, bạn không nên dùng bộ nhớ này để lưu bất cứ dữ liệu nào mà người dùng muốn lưu trữ độc lập với ứng dụng của bạn. Ví dụ: nếu ứng dụng của bạn cho phép người dùng chụp ảnh, thì người dùng vẫn muốn truy cập được những bức ảnh đó ngay cả sau khi gỡ cài đặt ứng dụng. Vì vậy, bạn nên sử dụng bộ nhớ dùng chung để lưu các loại tệp đó vào bộ sưu tập nội dung nghe nhìn thích hợp.

Những phần sau đây mô tả cách lưu trữ và truy cập tệp trong các thư mục dành riêng cho ứng dụng.

Truy cập từ bộ nhớ trong

Đối với mỗi ứng dụng, hệ thống sẽ cung cấp các thư mục thuộc bộ nhớ trong, nơi ứng dụng có thể sắp xếp các tệp riêng. Một thư mục được thiết kế để lưu các tệp bền vững của ứng dụng và một thư mục khác chứa các tệp đã lưu vào bộ nhớ đệm của ứng dụng. Ứng dụng của bạn không yêu cầu quyền hệ thống nào để đọc và ghi vào các tệp trong những thư mục này.

Các ứng dụng khác không thể truy cập tệp được lưu vào bộ nhớ trong. Đặc điểm trên khiến bộ nhớ trong trở thành nơi phù hợp để lưu dữ liệu ứng dụng mà ứng dụng khác không được truy cập.

Tuy nhiên, hãy lưu ý rằng các thư mục này thường có xu hướng nhỏ. Trước khi ghi các tệp dành riêng cho ứng dụng vào bộ nhớ trong, ứng dụng của bạn nên truy vấn dung lượng trống trên thiết bị.

Truy cập vào các tệp bền vững

Các tệp thông thường, bền vững của ứng dụng nằm trong một thư mục mà bạn có thể truy cập thông qua thuộc tính filesDir của đối tượng ngữ cảnh. Khung này cung cấp một số phương thức nhằm giúp bạn truy cập và lưu trữ các tệp trong thư mục này.

Truy cập và lưu trữ tệp

Bạn có thể dùng API File để truy cập và lưu trữ tệp.

Để giúp duy trì hiệu suất của ứng dụng, đừng mở và đóng cùng một tệp nhiều lần.

Đoạn mã sau đây minh hoạ cách sử dụng API File:

Kotlin

val file = File(context.filesDir, filename)

Java

File file = new File(context.getFilesDir(), filename);

Lưu trữ tệp bằng một luồng

Thay vì sử dụng API File, bạn có thể gọi openFileOutput() để lấy FileOutputStream ghi vào một tệp trong thư mục filesDir.

Đoạn mã sau đây cho biết cách ghi một số văn bản vào tệp:

Kotlin

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

Java

String filename = "myfile";
String fileContents = "Hello world!";
try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) {
    fos.write(fileContents.toByteArray());
}

Để cho phép ứng dụng khác truy cập vào các tệp lưu trữ trong thư mục này thuộc bộ nhớ trong, hãy dùng FileProvider kèm theo thuộc tính FLAG_GRANT_READ_URI_PERMISSION.

Truy cập vào tệp bằng một luồng

Để đọc tệp ở dạng luồng, hãy dùng openFileInput():

Kotlin

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

Java

FileInputStream fis = context.openFileInput(filename);
InputStreamReader inputStreamReader =
        new InputStreamReader(fis, StandardCharsets.UTF_8);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
    String line = reader.readLine();
    while (line != null) {
        stringBuilder.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Error occurred when opening raw file for reading.
} finally {
    String contents = stringBuilder.toString();
}

Xem danh sách tệp

Bạn có thể lấy một mảng chứa tên của mọi tệp trong thư mục filesDir bằng cách gọi fileList(), như minh hoạ trong đoạn mã sau:

Kotlin

var files: Array<String> = context.fileList()

Java

Array<String> files = context.fileList();

Tạo các thư mục lồng nhau

Bạn cũng có thể tạo các thư mục lồng nhau, hoặc mở một thư mục bên trong, bằng cách gọi getDir() trong mã dựa trên Kotlin hoặc bằng cách chuyển thư mục gốc và tên thư mục mới vào hàm khởi tạo File trong mã dựa trên Java:

Kotlin

context.getDir(dirName, Context.MODE_PRIVATE)

Java

File directory = context.getFilesDir();
File file = new File(directory, filename);

Tạo các tệp bộ nhớ đệm

Nếu chỉ cần lưu trữ tạm thời dữ liệu nhạy cảm, bạn nên dùng thư mục bộ nhớ đệm được chỉ định của ứng dụng thuộc bộ nhớ trong để lưu dữ liệu. Tương tự như với trường hợp tất cả bộ nhớ dành riêng cho ứng dụng, các tệp lưu trữ trong thư mục này sẽ bị xoá khi người dùng gỡ cài đặt ứng dụng của bạn, mặc dù các tệp trong thư mục này có thể bị xoá sớm hơn.

Để tạo một tệp được lưu vào bộ nhớ đệm, hãy gọi File.createTempFile():

Kotlin

File.createTempFile(filename, null, context.cacheDir)

Java

File.createTempFile(filename, null, context.getCacheDir());

Ứng dụng của bạn truy cập vào một tệp trong thư mục này bằng thuộc tính cacheDir của đối tượng ngữ cảnh và API File:

Kotlin

val cacheFile = File(context.cacheDir, filename)

Java

File cacheFile = new File(context.getCacheDir(), filename);

Xoá các tệp bộ nhớ đệm

Mặc dù đôi khi, Android sẽ tự xoá các tệp bộ nhớ đệm, nhưng bạn không nên dựa vào hệ thống để dọn dẹp các tệp này cho mình. Bạn nên thường xuyên lưu tệp bộ nhớ đệm của ứng dụng vào bộ nhớ trong.

Để xoá một tệp khỏi thư mục bộ nhớ đệm thuộc bộ nhớ trong, hãy dùng một trong các phương thức sau:

  • Phương thức delete() trên đối tượng File biểu thị tệp đó:

    Kotlin

    cacheFile.delete()

    Java

    cacheFile.delete();
  • Phương thức deleteFile() trong ngữ cảnh của ứng dụng, chuyển tên tệp:

    Kotlin

    context.deleteFile(cacheFileName)

    Java

    context.deleteFile(cacheFileName);

Truy cập từ bộ nhớ ngoài

Nếu bộ nhớ trong không có đủ dung lượng để lưu trữ tệp dành riêng cho ứng dụng, hãy cân nhắc dùng bộ nhớ ngoài. Hệ thống cung cấp các thư mục trong bộ nhớ ngoài nơi ứng dụng có thể sắp xếp những tệp mang lại giá trị cho người dùng chỉ trong ứng dụng của bạn. Một thư mục được thiết kế để lưu các tệp bền vững của ứng dụng và một thư mục khác chứa các tệp được lưu vào bộ nhớ đệm của ứng dụng.

Trên Android 4.4 (API cấp 19) trở lên, ứng dụng của bạn không cần yêu cầu quyền liên quan đến bộ nhớ để truy cập vào các thư mục dành riêng cho ứng dụng trong bộ nhớ ngoài. Các tệp lưu trữ trong những thư mục này sẽ bị xoá khi ứng dụng đó bị gỡ cài đặt.

Trên các thiết bị chạy Android 9 (API cấp 28) trở xuống, ứng dụng của bạn có thể truy cập vào các tệp dành riêng cho ứng dụng thuộc ứng dụng khác, miễn là ứng dụng của bạn có quyền thích hợp liên quan đến bộ nhớ. Để mang lại cho người dùng nhiều quyền kiểm soát hơn đối với tệp của họ và giảm thiểu tình trạng tệp lộn xộn, các ứng dụng nhắm đến Android 10 (API cấp 29) trở lên được cấp quyền truy cập có giới hạn vào bộ nhớ ngoài, hoặc bộ nhớ có giới hạn, theo mặc định. Khi bạn bật bộ nhớ có giới hạn, các ứng dụng không thể truy cập vào thư mục dành riêng cho ứng dụng thuộc ứng dụng khác.

Xác minh rằng bộ nhớ còn trống

Vì bộ nhớ ngoài nằm trên một ổ đĩa thực mà người dùng có thể tháo, hãy xác minh rằng bạn truy cập được ổ đĩa trước khi tìm cách đọc/ghi dữ liệu dành riêng cho ứng dụng từ/vào bộ nhớ ngoài.

Bạn có thể truy vấn trạng thái của ổ đĩa bằng cách gọi Environment.getExternalStorageState(). Nếu trạng thái trả về là MEDIA_MOUNTED, thì bạn có thể đọc và ghi các tệp dành riêng cho ứng dụng trong bộ nhớ ngoài. Nếu trạng thái là MEDIA_MOUNTED_READ_ONLY, bạn chỉ có thể đọc các tệp này.

Ví dụ: các phương thức sau đây hữu ích để xác định phạm vi cung cấp bộ nhớ:

Kotlin

// Checks if a volume containing external storage is available
// for read and write.
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

// Checks if a volume containing external storage is available to at least read.
fun isExternalStorageReadable(): Boolean {
     return Environment.getExternalStorageState() in
        setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

Java

// Checks if a volume containing external storage is available
// for read and write.
private boolean isExternalStorageWritable() {
    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}

// Checks if a volume containing external storage is available to at least read.
private boolean isExternalStorageReadable() {
     return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ||
            Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
}

Trên các thiết bị không có bộ nhớ ngoài có thể tháo rời, hãy dùng lệnh sau để bật ổ đĩa ảo nhằm kiểm thử logic phạm vi cung cấp bộ nhớ ngoài:

adb shell sm set-virtual-disk true

Chọn vị trí bộ nhớ thực

Đôi khi, thiết bị phân bổ một phân vùng của bộ nhớ trong làm bộ nhớ ngoài cũng cung cấp khe cắm thẻ SD. Nghĩa là thiết bị có nhiều ổ đĩa thực có thể chứa bộ nhớ ngoài, do đó, bạn cần chọn ổ đĩa sẽ dùng cho bộ nhớ dành riêng cho ứng dụng.

Để truy cập vào nhiều vị trí, hãy gọi ContextCompat.getExternalFilesDirs(). Như minh hoạ trong đoạn mã, phần tử đầu tiên trong mảng trả về sẽ được coi là ổ bộ nhớ ngoài chính. Hãy sử dụng ổ đĩa này trừ khi đã đầy hoặc không dùng được.

Kotlin

val externalStorageVolumes: Array<out File> =
        ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]

Java

File[] externalStorageVolumes =
        ContextCompat.getExternalFilesDirs(getApplicationContext(), null);
File primaryExternalStorage = externalStorageVolumes[0];

Truy cập vào các tệp bền vững

Để truy cập vào các tệp dành riêng cho ứng dụng từ bộ nhớ ngoài, hãy gọi getExternalFilesDir().

Để giúp duy trì hiệu suất của ứng dụng, đừng mở và đóng cùng một tệp nhiều lần.

Đoạn mã sau đây minh hoạ cách gọi getExternalFilesDir():

Kotlin

val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)

Java

File appSpecificExternalDir = new File(context.getExternalFilesDir(null), filename);

Tạo các tệp bộ nhớ đệm

Để thêm một tệp dành riêng cho ứng dụng vào bộ nhớ đệm trong bộ nhớ ngoài, hãy lấy thông tin tham chiếu đến externalCacheDir:

Kotlin

val externalCacheFile = File(context.externalCacheDir, filename)

Java

File externalCacheFile = new File(context.getExternalCacheDir(), filename);

Xoá các tệp bộ nhớ đệm

Để xoá một tệp khỏi thư mục bộ nhớ đệm bên ngoài, hãy dùng phương thức delete() trên đối tượng File biểu thị tệp đó:

Kotlin

externalCacheFile.delete()

Java

externalCacheFile.delete();

Nội dung đa phương tiện

Nếu ứng dụng của bạn xử lý các tệp nội dung đa phương tiện mang lại giá trị cho người dùng chỉ trong ứng dụng của bạn, thì bạn nên lưu trữ các tệp đó trong thư mục dành riêng cho ứng dụng trong bộ nhớ ngoài, như minh hoạ trong đoạn mã sau:

Kotlin

fun getAppSpecificAlbumStorageDir(context: Context, albumName: String): File? {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    val file = File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName)
    if (!file?.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

Java

@Nullable
File getAppSpecificAlbumStorageDir(Context context, String albumName) {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (file == null || !file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

Bạn cần phải sử dụng tên thư mục do các hằng số API cung cấp như DIRECTORY_PICTURES. Những tên thư mục này đảm bảo rằng các tệp được hệ thống xử lý đúng cách. Nếu không có tên thư mục con định sẵn nào phù hợp với tệp của bạn, thì bạn có thể chuyển null vào getExternalFilesDir(). Thao tác này sẽ trả về thư mục gốc dành riêng cho ứng dụng trong bộ nhớ ngoài.

Truy vấn dung lượng trống

Nhiều người dùng không có nhiều không gian lưu trữ trên thiết bị, vì vậy, ứng dụng của bạn nên sử dụng dung lượng một cách hợp lý.

Nếu biết trước lượng dữ liệu bạn sắp lưu trữ, thì bạn có thể nắm được mức dung lượng mà thiết bị có thể cung cấp cho ứng dụng của mình bằng cách gọi getAllocatableBytes(). Giá trị trả về của getAllocatableBytes() có thể lớn hơn lượng dung lượng trống hiện tại trên thiết bị. Nguyên nhân là vì hệ thống đã xác định được các tệp có thể xoá khỏi thư mục bộ nhớ đệm của ứng dụng khác.

Nếu có đủ dung lượng để lưu dữ liệu của ứng dụng, hãy gọi allocateBytes(). Nếu không, ứng dụng của bạn có thể yêu cầu người dùng xoá một số tệp khỏi thiết bị hoặc xoá tất cả các tệp bộ nhớ đệm khỏi thiết bị.

Đoạn mã sau đây cho thấy một ví dụ về cách ứng dụng của bạn có thể truy vấn dung lượng trống trên thiết bị:

Kotlin

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        // To request that the user remove all app cache files instead, set
        // "action" to ACTION_CLEAR_APP_CACHE.
        action = ACTION_MANAGE_STORAGE
    }
}

Java

// App needs 10 MB within internal storage.
private static final long NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

StorageManager storageManager =
        getApplicationContext().getSystemService(StorageManager.class);
UUID appSpecificInternalDirUuid = storageManager.getUuidForPath(getFilesDir());
long availableBytes =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid);
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
            appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP);
} else {
    // To request that the user remove all app cache files instead, set
    // "action" to ACTION_CLEAR_APP_CACHE.
    Intent storageIntent = new Intent();
    storageIntent.setAction(ACTION_MANAGE_STORAGE);
}

Tạo hoạt động quản lý bộ nhớ

Ứng dụng của bạn có thể khai báo và tạo một hoạt động tuỳ chỉnh (khi được chạy) sẽ cho phép người dùng quản lý dữ liệu mà ứng dụng đó đã lưu trữ trên thiết bị của người dùng. Bạn khai báo hoạt động "quản lý dung lượng" tuỳ chỉnh này bằng thuộc tính android:manageSpaceActivity trong tệp kê khai. Các ứng dụng quản lý tệp có thể gọi hoạt động này ngay cả khi ứng dụng của bạn không xuất hoạt động này; tức là khi hoạt động của bạn đặt android:exported thành false.

Yêu cầu người dùng xoá một số tệp trên thiết bị

Để yêu cầu người dùng xoá các tệp trên thiết bị, hãy gọi một ý định bao gồm hành động ACTION_MANAGE_STORAGE. Ý định này sẽ hiển thị một lời nhắc cho người dùng. Nếu muốn, lời nhắc này có thể cho biết lượng dung lượng trống trên thiết bị. Để hiển thị thông tin thân thiện với người dùng này, hãy sử dụng kết quả của phép tính sau:

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

Yêu cầu người dùng xoá mọi tệp bộ nhớ đệm

Ngoài ra, bạn có thể yêu cầu người dùng xoá tệp bộ nhớ đệm khỏi tất cả các ứng dụng trên thiết bị. Để làm vậy, hãy gọi một ý định bao gồm thao tác theo ý định ACTION_CLEAR_APP_CACHE.

Tài nguyên khác

Để biết thêm thông tin về cách lưu tệp vào bộ nhớ của thiết bị, hãy tham khảo các tài nguyên sau.

Video