Langsung ke konten utama

Android Scoped Storage di Aplikasi Capacitor

Penjelasan scoped storage Android di aplikasi Capacitor: apa yang berubah dari Android 10 hingga 14, izin mana yang masih berfungsi, MediaStore, SAF, dan direktori aman.

Aturan scoped storage Android yang berlaku untuk file di aplikasi Capacitor

Scoped storage Android membatasi apa yang dapat dibaca dan ditulis oleh aplikasi Capacitor di luar folder miliknya. Sejak Android 10, dan sepenuhnya sejak Android 11, aplikasi bebas menggunakan direktori privatnya, dapat menambahkan file ke koleksi bersama melalui MediaStore, dan pengguna harus memilih file lain melalui Storage Access Framework. Panduan ini menjelaskan apa yang berubah per versi Android, nilai Capacitor mana yang Directory masih berfungsi, dan cara menyimpan, mengimpor, serta mengekspor file dengan cara yang didukung di Capacitor 8.

Apa yang berubah, versi per versi

Android API Perubahan penting untuk aplikasi Capacitor
10 29 Scoped storage diperkenalkan. Aplikasi bisa opt out dengan requestLegacyExternalStorage="true" saat menargetkan API 29
11 30 Scoped storage diberlakukan untuk aplikasi yang menargetkan 30+. requestLegacyExternalStorage diabaikan. WRITE_EXTERNAL_STORAGE tidak lagi memberikan akses apa pun. MANAGE_EXTERNAL_STORAGE diperkenalkan. Tree picker tidak bisa memilih root Download atau root volume
12 31-32 Tidak ada perubahan storage signifikan
13 33 READ_EXTERNAL_STORAGE digantikan oleh READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. Tidak ada izin yang memberi akses ke dokumen aplikasi lain
14 34 Akses foto parsial dengan READ_MEDIA_VISUAL_USER_SELECTED
15 dan 16 35-36 Tidak ada model penyimpanan baru, kebijakan Play tentang izin foto dan video diberlakukan

Capacitor 8 menargetkan API 36, jadi aplikasi Anda selalu berjalan dengan scoped storage penuh di perangkat modern. Tidak ada flag untuk mengembalikan perilaku lama.

Apa yang rusak di versi lama Capacitor code

Code yang ditulis untuk Capacitor 2 atau 3, atau di-port dari Cordova, sering gagal tanpa error di perangkat baru:

  • Filesystem.writeFile({ directory: Directory.Documents }) atau Directory.ExternalStorage ke “simpan ke ponsel”. Di Android 11+, path ini menunjuk ke penyimpanan bersama, dan penulisan gagal atau file tidak dapat dibaca kembali setelah instal ulang.
  • Membaca path arbitrer seperti /storage/emulated/0/Download/file.pdf dari path yang diketik di pengaturan atau diterima dari server.
  • Meminta READ_EXTERNAL_STORAGE untuk menampilkan daftar dokumen pengguna. Pada Android 13+, izin ini tidak berlaku untuk file non-media.
  • Menggunakan Filesystem.requestPermissions() dan dengan asumsi granted berarti aplikasi dapat menulis di mana saja.
  • Menyusun path file dari sebuah content:// URI yang dikembalikan oleh picker.

Apa yang tetap berfungsi tanpa izin apa pun

Direktori aplikasi Anda sendiri tidak terpengaruh oleh scoped storage dan tidak memerlukan izin:

Capacitor Directory Path Android Terlihat oleh pengguna Dihapus saat uninstall
Data, Library, LibraryNoCloud /data/data/<package>/files Tidak Ya
Cache /data/data/<package>/cache Tidak Ya
External /storage/emulated/0/Android/data/<package>/files Hanya melalui USB atau beberapa file manager Ya
ExternalCache /storage/emulated/0/Android/data/<package>/cache Sama Ya
Documents Dibagikan Documents folder Ya Tidak
ExternalStorage Root penyimpanan bersama Ya Tidak

Empat baris pertama aman. Documents dan ExternalStorage adalah baris legacy: sejak Android 11, aplikasi Anda hanya bisa melihat file yang dibuatnya sendiri, dan setelah instal ulang, akses ke file tersebut pun hilang. Anggap tidak didukung untuk code baru.

External berguna untuk file besar (video offline, peta) karena bisa berada di partisi yang lebih besar, dan file di sana tidak masuk Auto Backup. Sejak Android 11, aplikasi lain tidak bisa membaca Android/data, jadi secara praktis tetap privat.

Menyimpan file agar mudah ditemukan user: MediaStore

Saat user mengetuk “Download” pada invoice, mereka mengharapkan file ada di folder Downloads. Pada Android 10+, aplikasi bisa memasukkan file ke shared Downloads, Pictures, Movies, Music dan Documents collections melalui MediaStore tanpa perlu izin apa pun.

@capgo/capacitor-file-sharer membungkus ini dengan save():

bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });

const result = await FileSharer.save({
  filename: 'invoice-1042.pdf',
  path: uri,
  contentType: 'application/pdf',
  android: {
    saveDirectory: 'downloads',     // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
    relativePath: 'MyApp/Invoices', // subfolder, Android 10+
  },
});

console.log(result.uri); // content:// URI of the saved file

Di iOS, pemanggilan yang sama membuka share sheet, tempat pengguna memilih “Save to Files”, jadi satu path code berlaku untuk kedua platform.

Pada Android 9 ke bawah, menulis ke shared storage masih memerlukan WRITE_EXTERNAL_STORAGE. Deklarasikan hanya untuk versi tersebut:

<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

Capacitor 8 mendukung Android 7 (API 24) ke atas, jadi jika Anda masih merilis untuk perangkat Android 7 hingga 9, tetap gunakan baris tersebut.

Foto dan video

Untuk media, lebih baik gunakan alur platform daripada menulis file sendiri. @capacitor/camera menulis ke galeri dengan saveToGallery: true, dan hanya memerlukan izin legacy storage pada versi Android lama. Lihat panduan kami untuk mengambil dan mengedit foto di Capacitor.

Membaca file pengguna: picker alih-alih permission

Scoped storage mengharuskan pengguna untuk memilih file. Android picker akan mengembalikan sebuah content:// URI yang bisa dibaca aplikasi Anda meskipun tidak memiliki izin penyimpanan.

import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];

// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
  from: picked.path!,
  to: `imports/${picked.name}`,
  toDirectory: Directory.Data,
});

Untuk foto dan video, @capgo/capacitor-file-pickermilik pickMedia dan @capacitor/cameramilik chooseFromGallery gunakan Android Photo Picker di Android 13+, yang sama sekali tidak memerlukan izin.

Hindari izin READ_MEDIA_*

Kebijakan izin foto dan video Google Play hanya mengizinkan READ_MEDIA_IMAGES dan READ_MEDIA_VIDEO untuk aplikasi yang memang membutuhkan akses galeri yang luas, seperti aplikasi galeri atau editor foto. Semua aplikasi lain wajib menggunakan picker dan mendeklarasikannya di Play Console. Jika plugin yang Anda gunakan mendeklarasikan izin-izin ini, hapus dari merged manifest:

<uses-permission
    android:name="android.permission.READ_MEDIA_IMAGES"
    tools:node="remove" />
<uses-permission
    android:name="android.permission.READ_MEDIA_VIDEO"
    tools:node="remove" />

Tambah xmlns:tools="http://schemas.android.com/tools" ke dalam <manifest> tag dulu.

Akses folder dengan Storage Access Framework

Beberapa aplikasi butuh akses berkelanjutan ke folder: aplikasi catatan yang menyinkronkan ke folder pengguna, folder ekspor untuk aplikasi lapangan. Storage Access Framework (SAF) memungkinkan pengguna memberi akses ke folder dengan ACTION_OPEN_DOCUMENT_TREE:

const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI

Tiga hal yang perlu diketahui:

  1. Hasilnya berupa tree URI, bukan path. Anda tidak bisa meneruskannya ke Filesystem.writeFile. File di dalamnya dibuat dan dibaca melalui DocumentsContract atau DocumentFile di native code.
  2. Akses tidak bertahan setelah restart kecuali aplikasi Anda memanggil takePersistableUriPermission pada URI tersebut. Tambahkan metode native kecil jika Anda membutuhkannya:
@PluginMethod
public void persist(PluginCall call) {
    Uri uri = Uri.parse(call.getString("uri"));
    getContext().getContentResolver().takePersistableUriPermission(
        uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    call.resolve();
}
  1. Beberapa folder tidak bisa diberi izin. Pada Android 11+, picker menolak akses ke root penyimpanan internal, root dari Download folder, dan Android/data dan Android/obb. Minta pengguna untuk membuat atau memilih subfolder.

Jumlah izin yang tersimpan terbatas per aplikasi, dan pengguna bisa mencabutnya di pengaturan sistem atau dengan menghapus data aplikasi. Cek akses dengan getPersistedUriPermissions() saat startup dan minta lagi kalau aksesnya hilang.

Akses semua file: MANAGE_EXTERNAL_STORAGE

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

Izin ini memberi perilaku lama “baca dan tulis semuanya”, tapi ini bukan runtime prompt. Pengguna harus mengaktifkannya di layar pengaturan khusus, dan Google Play meninjaunya secara manual. Kasus penggunaan yang diterima adalah file manager, backup dan restore, antivirus, manajemen dokumen, pencarian file di perangkat, dan beberapa kasus lain di mana akses luas adalah fitur intinya. Aplikasi catatan, PDF viewer yang membuka file dari Downloads, atau aplikasi yang “hanya perlu menyimpan hasil ekspor” akan ditolak.

Jika Anda mendistribusikannya di luar Play (enterprise MDM, perangkat kiosk sideloaded), ini bisa menjadi opsi. Jika tidak, rancanglah menggunakan MediaStore dan SAF.

Bagaimana plugin lain terintegrasi

Plugin Dampak scoped storage
@capacitor/filesystem Aman untuk direktori aplikasi. Documents dan ExternalStorage sudah legacy di Android 11+. requestPermissions() hanya berlaku untuk direktori tersebut di Android versi lama
@capacitor/camera Menggunakan kamera sistem dan Photo Picker, tanpa izin penyimpanan kecuali saveToGallery di Android 9 ke bawah
@capacitor/share Berbagi melalui FileProvider. Hanya path yang tercantum di res/xml/file_paths.xml dapat dibagikan
@capgo/capacitor-file-sharer Menggunakan miliknya sendiri FileProvider dan MediaStore untuk save(), tanpa perlu konfigurasi
@capgo/capacitor-downloader Download ke storage eksternal app, lalu gunakan save() untuk ekspor
@capgo/capacitor-file-picker Berbasis Picker, tanpa izin di Android 13+
@capgo/capacitor-photo-library Manifest-nya menambahkan READ_MEDIA_* untuk baca seluruh library, sesuai kebijakan Play di atas. pickMedia() menggunakan system picker dan tidak butuh izin, jadi jika itu saja yang dipakai, hapus READ_MEDIA_* dengan tools:node="remove"

Direktori mana yang dipakai

  • Data app, settings, database: Directory.Data
  • File sementara dan ekspor untuk dibagikan: Directory.Cache
  • Media besar yang bisa di-download ulang: Directory.External di Android, Directory.LibraryNoCloud di iOS
  • File yang user inginkan ada di Downloads atau Pictures: MediaStore via FileSharer.save()
  • File yang dibawa user ke dalam app: picker, lalu salin ke Directory.Data
  • Folder yang ingin terus ditulis oleh pengguna ke dalam aplikasi: URI pohon SAF dengan izin yang persisten

Untuk API baca dan tulis selengkapnya, lihat panduan penanganan file Capacitor.

Migrasi aplikasi yang sudah ada

  1. Cari di code Anda untuk Directory.Documents, Directory.ExternalStorage, /storage/emulated, dan requestLegacyExternalStorage.
  2. Pindahkan file milik app ke Directory.Data atau Directory.External. Saat pertama kali menjalankan versi baru, salin file lama yang masih bisa dibaca, lalu hapus file tersebut.
  3. Ganti fitur “save to device” dengan FileSharer.save().
  4. Ganti input path dengan picker.
  5. Tambah android:maxSdkVersion ke izin storage legacy dan hapus READ_MEDIA_* kecuali jika Anda membutuhkannya.
  6. Uji pada perangkat atau emulator Android 10, 11, 13, dan 15 atau 16. Perilakunya berbeda tepat di batasan tersebut.

preserveLegacyExternalStorage="true" di dalam <application> tag mempertahankan akses legacy untuk aplikasi yang diperbarui dari versi yang menggunakan requestLegacyExternalStorage, sampai aplikasi diinstal ulang. Ini hanya membantu saat langkah migrasi satu kali, jadi jangan bergantung padanya.

Pemecahan Masalah

EACCES (Permission denied) menulis ke /storage/emulated/0/...: Anda menulis langsung ke shared storage. Gunakan MediaStore atau direktori aplikasi.

File disimpan dengan Directory.Documents hilang setelah instal ulang: aplikasi kehilangan kepemilikannya. Gunakan MediaStore untuk file pengguna.

FileNotFoundException membaca content:// URI nanti: izin sementara kedaluwarsa. Salin file saat dipilih, atau simpan izin untuk URI tree.

Peringatan Play Console tentang izin foto dan video: plugin menambahkan READ_MEDIA_IMAGES atau READ_MEDIA_VIDEO. Hapus dengan tools:node="remove" jika tidak membutuhkannya.

requestPermissions() izin diberikan tapi penulisan tetap gagal: pada Android 11+, izin storage lama diberikan tapi tidak berguna untuk shared storage.

Sinkronkan bagian native dan web

Aturan storage adalah konfigurasi native, jadi perubahan pada manifest atau plugin butuh rilis store, yang Build Capgo bisa dilakukan di cloud. Logika file di sekitarnya (path, nama folder, alur ekspor) adalah JavaScript yang bisa Anda update dengan Live updates Capgo.

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.