Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Perubahan privasi Android Q: Penyimpanan terbatas

Mulai Android Q Beta 5, aplikasi yang menargetkan Android 9 (API level 28) atau yang lebih rendah secara default tidak akan mengalami perubahan pada cara kerja penyimpanan dibandingkan Android versi sebelumnya. Saat mengupdate aplikasi yang ada agar berfungsi dengan penyimpanan terbatas, Anda dapat menggunakan atribut manifes requestLegacyExternalStorage baru agar dapat mengaktifkan perilaku baru untuk aplikasi di perangkat Android Q, meskipun aplikasi Anda menargetkan Android 9 atau versi lebih rendah.

Untuk memberi pengguna kontrol yang lebih besar atas file mereka dan untuk membatasi ketidakrapian file, Android Q mengubah cara aplikasi dapat mengakses file di penyimpanan eksternal perangkat, seperti file yang disimpan di lokasi /sdcard. Android Q tetap menggunakan izin READ_EXTERNAL_STORAGE dan WRITE_EXTERNAL_STORAGE, yang sesuai dengan izin runtime Storage pada sistem pengguna. Namun, aplikasi yang menargetkan Android Q secara default (serta aplikasi yang memilih ikut perubahan) diberi tampilan terfilter ke penyimpanan eksternal. Aplikasi tersebut hanya dapat melihat direktori khusus aplikasi dan jenis media tertentu, sehingga aplikasi tidak perlu meminta izin pengguna tambahan apa pun.

Panduan ini menjelaskan file yang disertakan dalam tampilan terfilter, serta cara mengupdate aplikasi agar dapat terus membagikan, mengakses, dan memodifikasi file yang disimpan di perangkat penyimpanan eksternal. Panduan ini juga menjelaskan beberapa pertimbangan terkait informasi lokasi dalam foto, akses media dari kode native, dan penggunaan nama kolom dalam kueri konten.

Untuk mempelajari lebih lanjut perubahan pada penyimpanan eksternal di Android Q, lihat bagian yang membahas Penyempurnaan dalam membuat file di penyimpanan eksternal.

Tampilan terfilter ke penyimpanan eksternal

Secara default, jika menargetkan Android Q, aplikasi Anda akan memiliki tampilan terfilter untuk file yang ada di perangkat penyimpanan eksternal. Aplikasi dapat menyimpan file yang ditujukan untuk aplikasi itu sendiri dalam direktori khusus aplikasi menggunakan Context.getExternalFilesDir().

Aplikasi yang memiliki tampilan terfilter selalu memiliki akses baca/tulis ke file yang dibuatnya, baik di dalam maupun di luar direktori khusus aplikasinya. Aplikasi Anda tidak perlu mendeklarasikan izin penyimpanan apa pun untuk mengakses file ini.

Aplikasi Anda dapat mengakses file yang dibuat oleh aplikasi lain hanya jika kedua kondisi berikut terpenuhi:

  1. Aplikasi Anda telah diberi izin READ_EXTERNAL_STORAGE.
  2. File berada di salah satu koleksi media yang terdefinisi dengan baik berikut:

Agar dapat mengakses file lain yang dibuat aplikasi lain, termasuk file dalam direktori "download", aplikasi Anda harus menggunakan Storage Access Framework, yang memungkinkan pengguna untuk memilih file tertentu.

Tampilan terfilter juga menerapkan batasan data terkait media berikut:

  • Metadata Exif dalam file gambar disunting, kecuali jika aplikasi Anda telah diberi izin ACCESS_MEDIA_LOCATION. Pelajari lebih lanjut di bagian cara mengakses informasi lokasi dalam gambar.
  • Kolom DATA disunting untuk setiap file di media store.
  • Tabel MediaStore.Files otomatis difilter sehingga hanya menampilkan file foto, video, dan audio. Misalnya, tabel ini tidak lagi menampilkan file PDF.

Untuk mengakses file media dalam kode native, ambil file tersebut menggunakan MediaStore dalam kode berbasis Java atau Kotlin, lalu teruskan deskriptor file yang terkait ke kode native Anda. Untuk mengetahui informasi lebih lanjut, lihat bagian tentang cara mengakses file media dari kode native.

Menyimpan file aplikasi setelah di-uninstal

Jika aplikasi yang memiliki tampilan terfilter ke penyimpanan eksternal di-uninstal, semua file dalam direktori khusus aplikasi tersebut akan dihapus. Untuk mempertahankan file ini setelah aplikasi di-uninstal, simpan ke direktori dalam MediaStore.

Berhenti menggunakan tampilan terfilter

Sebagian besar aplikasi yang telah mengikuti praktik terbaik penyimpanan akan berfungsi dengan penyimpanan tercakup setelah membuat sedikit perubahan. Sebelum aplikasi Anda kompatibel atau teruji sepenuhnya, Anda dapat menghentikan perilaku penyimpanan terbatas untuk sementara berdasarkan level SDK target aplikasi atau atribut manifes baru yang disebut requestLegacyExternalStorage:

  • Targetkan Android 9 (API level 28) atau yang lebih rendah.

  • Jika Anda menargetkan Android Q, tetapkan nilai requestLegacyExternalStorage ke true dalam file manifes aplikasi Anda:

        <manifest ... >
          <!-- This attribute is "false" by default on apps targeting Android Q. -->
          <application android:requestLegacyExternalStorage="true" ... >
            ...
          </application>
        </manifest>
        

Jika aplikasi diinstal sementara penyimpanan eksternal lama berstatus aktif, aplikasi akan tetap berada dalam mode ini hingga di-uninstal. Perilaku kompatibilitas ini berlaku terlepas dari apakah perangkat nanti diupgrade untuk menjalankan Android Q, atau apakah aplikasi nanti diupdate untuk menargetkan Android Q.

Menyiapkan perangkat penyimpanan eksternal virtual

Pada perangkat tanpa penyimpanan eksternal yang dapat dilepas, gunakan perintah berikut untuk mengaktifkan disk virtual untuk tujuan pengujian:

    adb shell sm set-virtual-disk true
    

Ringkasan akses file tampilan terfilter

Tabel berikut merangkum bagaimana aplikasi yang memiliki tampilan terfilter ke penyimpanan eksternal dapat mengakses file:

Lokasi file Izin diperlukan Metode akses (*) File dihapus setelah aplikasi di-uninstal?
Direktori khusus aplikasi Tidak ada getExternalFilesDir() Ya
Koleksi media
(foto, video, audio)
READ_EXTERNAL_STORAGE
hanya jika
mengakses file aplikasi lain
MediaStore Tidak
Hasil download
(dokumen dan
e-book)
Tidak ada Storage Access Framework
(memuat pemilih file sistem)
Tidak

*Anda dapat menggunakan Storage Access Framework untuk mengakses setiap lokasi yang ditunjukkan pada tabel sebelumnya tanpa meminta izin apa pun.

Menyesuaikan jenis pola penggunaan tertentu dengan perubahan

Bagian ini menyajikan saran untuk beberapa jenis aplikasi berbasis media agar beradaptasi dengan perubahan perilaku penyimpanan yang terjadi di aplikasi yang menargetkan Android Q.

Sebagai praktik terbaik, gunakan tampilan terfilter kecuali jika aplikasi Anda memerlukan akses ke file yang tidak berada di direktori khusus aplikasi atau MediaStore.

Berbagi file media

Beberapa aplikasi memungkinkan pengguna saling berbagi file media. Misalnya, aplikasi media sosial memberikan kemampuan kepada pengguna untuk berbagi foto dan video dengan teman.

Untuk mengakses file media yang ingin dibagikan pengguna, gunakan API MediaStore. Anda juga dapat menggunakan API yang sama untuk menyimpan file apa pun yang diterima pengguna melalui aplikasi, dengan memanfaatkan penyempurnaan yang diperkenalkan dalam Android Q.

Apabila Anda menyediakan paket aplikasi pendamping, seperti aplikasi pesan dan aplikasi profil, siapkan berbagi file menggunakan URI content://. Kami telah merekomendasikan alur kerja ini sebagai praktik terbaik keamanan.

Bekerja dalam dokumen

Beberapa aplikasi menggunakan dokumen sebagai unit penyimpanan tempat pengguna memasukkan data yang mungkin ingin mereka bagikan dengan rekan atau impor ke dokumen lain. Contohnya antara lain pengguna yang membuka dokumen produktivitas bisnis atau membuka buku yang disimpan sebagai file EPUB.

Dalam kasus ini, izinkan pengguna memilih file yang akan dibuka dengan memanggil intent ACTION_OPEN_DOCUMENT, yang membuka aplikasi alat pilih file pada sistem. Untuk hanya menampilkan jenis file yang didukung aplikasi Anda, sertakan Intent.EXTRA_MIME_TYPES tambahan dalam intent Anda.

Sampel ActionOpenDocument di GitHub menunjukkan cara menggunakan ACTION_OPEN_DOCUMENT untuk membuka file setelah mendapatkan persetujuan pengguna.

Mengelola grup file

Aplikasi pengelolaan file dan pembuatan media biasanya mengelola grup file dalam sebuah hierarki direktori. Aplikasi ini dapat memanggil intent ACTION_OPEN_DOCUMENT_TREE untuk mengizinkan pengguna memberikan akses ke seluruh hierarki direktori. Aplikasi semacam itu akan dapat mengedit file apa pun pada direktori yang dipilih, serta sub-direktorinya.

Dengan antarmuka ini, pengguna dapat mengakses file dari semua instance DocumentsProvider yang terinstal, yang dapat didukung oleh solusi yang di-backup secara lokal atau berbasis cloud.

Sampel ActionOpenDocumentTree di GitHub menunjukkan cara menggunakan ACTION_OPEN_DOCUMENT_TREE untuk membuka hierarki direktori setelah mendapatkan persetujuan pengguna.

Mengakses dan mengedit konten media

Bagian ini menyajikan praktik terbaik untuk memuat dan menyimpan file media di penyimpanan eksternal sehingga aplikasi Anda terus memberikan pengalaman pengguna yang baik di Android Q.

Catatan: Jika memiliki tampilan terfilter ke penyimpanan eksternal dan meminta izin runtime Storage, aplikasi dapat menampilkan file tertentu hanya jika file tersebut berada di direktori khusus aplikasi atau salah satu koleksi media berikut:

Bahkan dengan izin Storage sekalipun, aplikasi semacam itu yang mengakses tampilan sistem file mentah dari perangkat penyimpanan eksternal hanya memiliki akses ke jalur file mentah khusus paket untuk aplikasi tersebut. Jika aplikasi mencoba membuka file di luar jalur khusus paketnya menggunakan tampilan sistem file mentah, error akan terjadi:

Mengakses file

Jangan memuat file media menggunakan kolom DATA yang tidak digunakan lagi. Sebagai gantinya, panggil salah satu metode berikut dari ContentResolver:

  • Untuk thumbnail file media tunggal, gunakan loadThumbnail(), dengan memasukkan ukuran thumbnail yang ingin Anda muat.
  • Untuk file media tunggal, gunakan openFileDescriptor().
  • Untuk kumpulan file media, gunakan query().

Cuplikan kode berikut menunjukkan cara mengakses file media:

    // Load thumbnail of a specific media item.
    val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)

    // Open a specific media item.
    resolver.openFileDescriptor(item, mode).use { pfd ->
        // ...
    }

    // Find all videos on a given storage device, including pending files.
    val collection = MediaStore.Video.Media.getContentUri(volumeName)
    val collectionWithPending = MediaStore.setIncludePending(collection)
    resolver.query(collectionWithPending, null, null, null).use { c ->
        // ...
    }
    

Mengakses dari kode native

Anda mungkin menemukan situasi ketika aplikasi Anda perlu menangani file media tertentu dalam kode native, seperti file yang telah dibagikan aplikasi lain kepada aplikasi Anda, atau file media dari koleksi media pengguna. Dalam kasus semacam ini, mulailah proses penemuan file media Anda dalam kode berbasis Java atau Koltin, lalu teruskan deskriptor file yang terkait dengan file tersebut ke kode native Anda.

Cuplikan kode berikut ini menunjukkan cara meneruskan deskriptor file objek media ke kode native aplikasi Anda:

Kotlin

    val contentUri: Uri =
            ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(BaseColumns._ID))
    val fileOpenMode = "r"
    val parcelFd = resolver.openFileDescriptor(uri, fileOpenMode)
    val fd = parcelFd?.detachFd()
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
    

Java

    Uri contentUri = ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(Integer.parseInt(BaseColumns._ID)));
    String fileOpenMode = "r";
    ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
    if (parcelFd != null) {
        int fd = parcelFd.detachFd();
        // Pass the integer value "fd" into your native code. Remember to call
        // close(2) on the file descriptor when you're done using it.
    }
    

Untuk mempelajari akses file dalam kode native lebih lanjut, lihat presentasi Files for Miles dari Android Dev Summit '18, mulai 15:20.

Memperbarui file media aplikasi lain

Untuk mengubah file media tertentu yang sebelumnya disimpan aplikasi lain ke perangkat penyimpanan eksternal, ambil RecoverableSecurityException yang ditampilkan platform. Selanjutnya, Anda dapat meminta agar pengguna memberi aplikasi Anda akses tulis ke item tertentu, seperti yang ditunjukkan dalam cuplikan kode berikut:

Kotlin

    try {
        // ...
    } catch (rse: RecoverableSecurityException) {
        val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null)
    }
    

Java

    try {
        // ...
    } catch (RecoverableSecurityException rse) {
        IntentSender requestAccessIntentSender = rse.getUserAction()
                .getActionIntent().getIntentSender();

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null);
    }
    

Informasi lokasi dalam gambar

Beberapa foto berisi informasi lokasi di metadata Exif, yang memungkinkan pengguna melihat tempat diambilnya foto. Karena informasi lokasi ini sensitif, Android Q secara default menyembunyikan informasi ini dari aplikasi Anda jika aplikasi tersebut memiliki tampilan terfilter ke penyimpanan eksternal. Batasan informasi lokasi ini berbeda dengan batasan yang berlaku untuk karakteristik kamera.

Jika aplikasi Anda memerlukan akses ke informasi lokasi foto, selesaikan langkah-langkah berikut:

  1. Tambahkan izin ACCESS_MEDIA_LOCATION baru ke manifes aplikasi Anda.
  2. Dari objek MediaStore, panggil setRequireOriginal(), dengan memasukkan URI foto.

Contoh proses ini terlihat dalam cuplikan kode berikut:

Kotlin

    // Get location data from the ExifInterface class.
    val photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri).use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            val latLong = ?: doubleArrayOf(0.0, 0.0)
        }
    }
    

Java

    Uri photoUri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex));

    final double[] latLong;

    // Get location data from the ExifInterface class.
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if (stream != null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();

        // If lat/long is null, fall back to the coordinates (0, 0).
        latLong = returnedLatLong != null ? returnedLatLong : new double[2];

        // Don't reuse the stream associated with the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
    

Nama kolom dalam kueri konten

Jika kode aplikasi Anda menggunakan proyeksi nama kolom, seperti mime_type AS MimeType, harap diingat bahwa Android Q mengharuskan nama kolom yang ditetapkan di MediaStore API.

Jika kode Anda bergantung pada library yang mengharapkan nama kolom yang tidak ditetapkan di API Android, seperti MimeType, gunakan CursorWrapper untuk menerjemahkan nama kolom secara dinamis dalam proses aplikasi Anda.