Menyimpan File

Android menggunakan sistem file yang mirip dengan sistem file berbasis disk pada platform lainnya. Latihan ini akan menjelaskan bagaimana bekerja dengan sistem file Android untuk membaca dan menulis file dengan menggunakan API-API 'File'.

Object 'File' cocok untuk membaca atau menulis data yang besar secara urut dari awal hingga akhir tanpa melompat-lompat. Misalnya, sangat cocok untuk file-file image atau file apapun yang saling dipertukarkan lewat jaringan.

Latihan ini akan menunjukkan bagaimana melakukan pekerjaan yang terkait dengan file di app kita. Latihan ini menganggap bahwa kita familiar dengan dasar-dasar sistem file Linux dan API-API input/output file standar di 'java.io'.

Memilih Storage 'Internal' atau 'External'

Semua perangkat Android memiliki dua tempat penyimpanan file: storage 'internal' dan 'external'. Istilah-istilah tersebut berasal dari masa-masa awal Android ketika sebagian besar perangkat menyediakan memory non-volatile yang sudah 'built-in' (storage 'internal'), plus suatu storage yang 'removable' misalnya 'micro SD card' (storage 'external'). Beberapa perangkat membagi ruang storage permanen menjadi partisi 'internal' dan 'external', sehingga bahkan tanpa storage 'removable', akan selalu ada dua ruang storage dan perilaku API akan sama apakah storage 'external' itu 'removable' atau bukan. Daftar berikut ini akan merangkum fakta-fakta tentang masing-masing ruang storage.

Storage 'internal':
  • Pasti selalu ada
  • File-file yang disimpan disini hanya bisa diakses oleh app kita
  • Ketika user men-uninstall app kita, sistem akan menghapus semua file-file app kita dari storage 'internal'
  • Storage 'internal' saat terbaiknya adalah ketika kita ingin memastikan bahwa tidak ada user atau app lain yang bisa mengakses file-file kita.
Storage 'external':
  • Tidak selalu ada, karena user bisa me-'mount' storage 'external' seperti storage 'USB' dan dalam beberapa kasus mengeluarkannya dari perangkat.
  • Bisa dibaca semua orang, jadi file-file yang disimpan disini mungkin bisa dibaca diluar kendali kita.
  • Ketika user men-uninstall app kita, sistem akan menghapus file-file dari sini hanya jika kita menyimpannya di direktori yang di dapat dari 'getExternalFilesDir()'.
  • Storage 'external' adalah tempat terbaik untuk file-file yang tidak memerlukan batasan akses dan untuk file-file yang ingin kita 'share' dengan app lain atau mengijinkan user untuk mengakses melalui komputer.
Catatan:
Sebelum Android N, file-file internal bisa di-set untuk diakses app lain dengan cara melonggarkan 'permissions' sistem file. Ini tidak ada lagi sekarang. Bila kita ingin membuat konten file 'private' yang bisa diakses app lain, app kita bisa menggunakan 'FileProvider'. Lihat tentang 'Sharing Files'.
Tip:
Meskipun app di-instal pada storage 'internal' secara default, tetapi kita bisa menentukan atribut 'android:installlocation' di manifest kita sehingga app kita bisa di-instal di storage 'external'. User lebih menyukai pilihan ini ketika ukuran APK sangat besar dan mereka memiliki ruang storage 'external' yang lebih besar daripada storage 'internal'. Untuk informasi lebih lanjut, lihat 'App Install Location'.

Mendapatkan 'Permissions' untuk Storage 'External'

Untuk menulis ke storage 'external', kita harus meminta permission: 'WRITE_EXTERNAL_STORAGE' di dalam file manifest kita.

 ...>
     android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
Harap diperhatikan:
Saat ini, semua app mempunyai kemampuan untuk membaca storage 'external' tanpa suatu 'permission' khusus. Tetapi, ini akan berubah pada rilis mendatang. Bila app kita perlu membaca storage 'external' (tetapi tidak menulis), maka kita perlu membuat permission: 'READ_EXTERNAL_STORAGE'. Untuk memastikan bahwa app kita bisa terus berjalan seperti yang diharapkan, kita seharusnya menyatakan 'permission' ini sekarang, sebelum perubahan nanti berlaku.
 ...>
     android:name="android.permission.READ_EXTERNAL_STORAGE" />
    ...
Tetapi, bila app kita menggunakan permission 'WRITE_EXTERNAL_STORAGE', maka secara implisit app tersebut memiliki permission untuk membaca storage 'external' juga.
Kita tidak perlu 'permissions' apapun untuk menyimpan file di storage 'internal'. Aplikasi kita selalu punya 'permission' untuk membaca dan menulis di direktori storage 'internal' nya.


Menyimpan File di Storage 'Internal'

Ketika menyimpan file di storage 'internal', kita bisa mendapatkan direktori yang sesuai seperti suatu 'File' dengan cara memanggil salah satu dari dua method berikut:
  • 'getFilesDir()' : akan mengembalikan suatu (class) 'File' yang menyajikan direktori 'internal' untuk app kita.
  • 'getCacheDir()': akan mengembalikan suatu (class) 'File' yang menyajikan direktori 'internal' untuk file-file 'temporary cache' app kita. Pastikan untuk menghapus masing-masing file setelah tidak lagi diperlukan dan implementasikan batasan ukuran yang masuk akal untuk jumlah memori yang kita gunakan kapanpun itu, misalnya 1MB. Bila sistem mulai berjalan lambat pada storage, sistem mungkin menghapus file-file 'cache' tanpa pemberitahuan.
Untuk membuat file baru di dalam salah satu direktori-direktori tersebut, kita bisa menggunakan konstruktor 'File()', dengan melewatkan 'File' dengan paramenter yang disediakan oleh salah satu dari dua method di atas yang menentukan direktori 'internal' kita. Contohnya:

File file = new File(context.getFilesDir(), filename);
Alternatifnya, kita bisa memanggil 'openFileOutput()' untuk mendapatkan 'FileOutputStream' yang akan menulis ke file di dalam direktori 'internal' kita. Contohnya, berikut ini adalah bagaimana menulis suatu teks ke file:

String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try {
  outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
  outputStream.write(string.getBytes());
  outputStream.close();
} catch (Exception e) {
  e.printStackTrace();
}
Atau, bila kita memerlukan untuk men-'cache' beberapa file, sebagai gantinya kita seharusnya menggunakan 'createTempFile()'. Contohnya, method berikut akan mengekstrak nama file dari suatu 'URL' dan membuat suatu file dengan nama tersebut di dalam direktori 'cache' internal app kita:

public File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    } catch (IOException e) {
        // Error while creating file
    }
    return file;
}
Catatan:
Direktori storage 'internal' app kita ditentukan oleh nama 'package' app kita di lokasi tertentu di sistem file Android. Secara teknis, app yang lain bisa membaca file-file internal kita bila kita men-set 'file mode' ke 'readable'. Tetapi, app yang lain tersebut juga perlu mengetahui nama 'package' dan nama file app kita. App lain tidak akan bisa menelusuri/melihat direktori-direktori 'internal' kita dan tidak punya akses untuk 'baca atau tulis' kecuali jika kita men-set file-file tersebut secara eksplisit ke 'readable' atau 'writable'. Jadi selama kita menggunakan 'MODE_PRIVATE' pada file-file kita di storage 'internal', file-file tersebut tidak pernah bisa diakses oleh app-app yang lain.

Menyimpan File di Storage 'External'

Karena storage 'external bisa saja tidak ada -- misalnya user sudah melakukan 'mount' storage tersebut ke PC atau sudah memindahkan 'SD card' yang menyediakan storage 'external' tersebut -- kita seharusnya selalu memverifikasi bahwa 'volume' tersebut ada/tersedia sebelum kita mengaksesnya. Kita bisa melakukan 'query' status storage 'external' dengan memanggil 'getExternalStorage()'. Bila status yang dikembalikan sama dengan 'MEDIA_MOUNTED', maka kita bisa membaca dan menulis ke file-file kita. Contohnya, method-method berikut ini sangat bermanfaat untuk menentukan ada/tidak-nya storage:

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}
/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

Meskipun storage 'external' bisa diakses dan dimodifikasi oleh user dan app yang lain, ada dua kategori file yang bisa kita simpan disini:
  • File-file 'Public': file-file yang seharusnya bisa diakses secara bebas oleh app yang lain dan user. Ketika user men-uninstall app kita, file-file ini seharusnya tetap ada/tersedia. 
    • Contohnya, photo yang diambil oleh app kita atau file-file yang di-download.
  • File-file 'Private': File-file yang menjadi hak penuh app yang memilikinya dan seharusnya terhapus ketika user men-uninstall app kita. Meskipun file-file ini secara teknis bisa diakses oleh user dan app yang lain karena file-file tersebut ada di storage 'external', tetapi file-file tersebut pada kenyataanya tidak memberi value apapun bagi user diluar app kita. Ketika user men-uninstall app kita, sistem akan menghapus semua file di direktori 'private' external app kita. 
    • Contohnya, 'resources' tambahan yang di-download oleh app kita atau file-file media 'temporary'.
Bila kita ingin menyimpan file-file 'public' di storage 'external', kita gunakan method 'getExternalStoragePublicDirectory()' untuk mendapatkan (class) 'File' yang memberikan direktori yang sesuai. Method tersebut akan mengambil satu argumen/parameter yang menentukan jenis file yang ingin kita simpan sehingga secara lojik bisa diatur bersama dengan file-file 'public' lainnya, misalnya 'DIRECTORY_MUSIC' atau 'DIRECTORY_PICTURES'. Contohnya:

public File getAlbumStorageDir(String albumName) {
    // Get the directory for the user's public pictures directory.
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

Bila kita ingin menyimpan file-file yang 'private' bagi app kita, kita bisa mendapatkan direktori yang sesuai dengan memanggil 'getExternalFilesDir()' dan melewatkannya dengan suatu nama yang menunjukkan jenis direktori yang kita inginkan. Masing-masing direktori yang dibuat dengan cara ini akan ditambahkan ke direktori induk yang men-encapsulasi semua file di storage 'external' app kita, yang akan dihapus sistem bila user men-uninstall app kita.

Contohnya, berikut ini adalah method yang bisa kita gunakan untuk membuat direktori untuk album photo sendiri:
public File getAlbumStorageDir(Context context, String albumName) {
    // Get the directory for the app's private pictures directory.
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

Bila tidak ada nama-nama sub-direktori yang cocok dengan file-file kita, sebagai gantinya kita bisa memanggil 'getExternalFilesDir()' dan melewatkan null. Ini akan mengembalikan direktori 'root' untuk direktori 'private' app kita di storage 'external'.

Harap diingat bahwa 'getExternalFilesDir()' akan membuat direktori di dalam direktori yang dihapus ketika user men-uninstall app kita. Bila file-file yang kita simpan seharusnya tetap kita inginkan ada/tersedia setelah user men-uninstall app kita -- misalnya ketika app kita adalah suatu kamera dan user ingin mempertahankan photo-photonya -- kita seharusnya menggunakan 'getExternalStoragePublicDirectory()' sebagai penggantinya.

Tanpa menghiraukan apakah kita menggunakan 'getExternalStoragePublicDirectory()' untuk file-file yang di-share atau menggunakan 'getExternalFileDir()' untuk file-file yang 'private' bagi app kita, sangatlah penting bahwa kita menggunakan nama-nama direktori yang disediakan oleh konstanta-konstanta API seperti misalnya 'DIRECTORY_PICTURES'. Nama-nama direktori ini memastikan bahwa file-file tersebut akan diperlakukan dengan benar oleh sistem. Contohnya, file-file yang disimpan di 'DIRECTORY_RINGTONES' dikategorikan oleh sistem sebagai 'ringtones' alih-alih sebagai 'music'.


Mencari Ruang Kosong

Bila kita sudah tahu terlebih dahulu banyaknya data yang kita simpan, kita bisa mengetahui apakah ruang yang ada cukup atau tidak tanpa menyebabkan 'IOException' dengan memanggil 'getFreeSpace()' atau 'getTotalSpace()'. Method-method ini memberitahukan ruang yang ada saat itu dan ruang totalnya di 'volume' storage. Informasi ini juga sangat bermanfaat untuk menghindari mengisi 'volume' storage melebihi batas ambangnya.

Tetapi sistem tidak akan menjamin bahwa kita bisa menulis bytes sebanyak ditunjukkan oleh 'getFreeSpace()'. Bila jumlah yang dikembalikan beberapa MB lebih dari ukuran data yang ingin kita simpan, atau bila sistem file masih dibawah 90% dari full, maka barangkali aman untuk jalan terus. Tetapi bila sebaliknya, sebaiknya kita tidak menulis ke storage.
Catatan:
Kita tidak diminta untuk men-cek jumlah ruang yang ada/tersedia sebelum kita menyimpan file kita. Sebaliknya kita bisa mencoba menulis ke file langsung, kemudian menagkapnya dengan 'IOException' bila terjadi error. Kita mungkin perlu melakukan ini bila kita tidak tahu pasti berapa ruang yang kita perlukan. Contohnya, bila kita mengubah 'encoding' file sebelum menyimpannya dengan meng-konversi gambar PNG ke JPEG, kita tidak akan tahu ukuran file sebelumnya.

Menghapus File

Kita seharusnya selalu menghapus file-file yang tidak lagi diperlukan. Cara yang paling langsung untuk menghapus file adalah dengan memanggil 'delete()' pada dirinya sendiri (file itu sendiri).

myFile.delete();
Bila file tersebut disimpan di storage 'internal', kita bisa juga meminta 'Context' untuk menemukan dan menghapus file dengan memanggil 'deleteFile()':

myContext.deleteFile(fileName);
Catatan: 
Ketika user men-uninstall app kita, sistem Android akan menghapus hal-hal berikut:
  • Semua file yang kita simpan di storage 'internal'
Tetapi, kita seharusnya menghapus semua 'cached files'  yang dibuat dengan 'getCacheDir()' secara manual dan regular dan juga menghapus file-file lain yang tidak lagi diperlukan secara regular juga.

license: cc by

No comments: