Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scoping Scoped Storage Extended

D8a3623b157508fecdae1f8e756f362f?s=47 cmota
September 26, 2020

Scoping Scoped Storage Extended

This is still a sensitive subject to most of us due to the impact it had on most apps, but a most needed one! Scoped storage is one of the most important security features that has been released that empowers the user to have the final word when an app is accessing external files.

In this talk, we're going to share our pains about these developments and how everything will get better at the end.

D8a3623b157508fecdae1f8e756f362f?s=128

cmota

September 26, 2020
Tweet

Transcript

  1. 11 10 P N O TBD L JB HC M

    KK ICS F G TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD Scoped Storage Scoping @cafonsomota
  2. Android Dev Lead @WITSoftware Founder @GDGCoimbra and co-founder @Kotlin_Knights ✍

    Author @rwenderlich and @opinioesonline Podcaster wannabe Loves travel, photography and running speakerdeck.com/cmota/scoping-scoped-storage-extended MATERIALS @cafonsomota github.com/cmota/ScopedStorage
  3. scoped storage #android

  4. 10:00 android this is typically the file explorer of a

    brand new phone
  5. this is my file explorer, that I periodically clean android

    10:00 10:00
  6. 10:00 10:00 #influencer android

  7. 10:00 10:00 what? android

  8. 10:00 10:00 another cache? android

  9. 10:00 10:00 no longer exists android

  10. 10:00 10:00 no idea android

  11. 10:00 10:00 sounds fishy (especially with lower case) android

  12. 10:00 10:00 challenge sounds fishy (especially with lower case) share

    your storage with the hashtag #nomorescatteredfiles
  13. android

  14. why Scattered files throughout your disk Files from uninstalled apps

    are kept in disk using up space An app can access any file on external storage after the permission is granted There’s no clear distinction where files should be stored Some times you use full path for files other Uri’s You end up doing “hacks” in some file operations
  15. you are here 10 11 scoped storage (can be opted

    out) scoped storage (mandatory) 6 runtime permissions dark ages 5 install permissions (free for all) 4.4 SAF released (Storage Access Framework) timeline: privacy 7 file provider …
  16. timeline: 2020 you are here Dec target min API level

    29+ for app updates Sep Aug Jul target min API level 29+ for new apps Oct Nov developers can start submitting their apps for review for background location access Jun May …
  17. timeline: 2021? you are going to be here Dec target

    min API level 30+ for app updates Sep Aug? Jul target min API level 30+ for new apps Oct Nov? Jun May …
  18. concepts No longer rely on file paths to access files,

    but instead Uri’s Files from uninstalled apps are removed (unless if saved through MediaStore/ SAF) MediaStore API is going to be your best friend Easy access to Images, Videos and Audio For other files use SAF Files are now secure and private (as they should be)
  19. directories Type Access method Permissions Access Uninstalled app-specific files (internal

    storage) getFilesDir() /data/user/… Not needed No other app can access Data removed app-specific files (external storage) getExternalFilesDir() /storage/emulated/… Not needed No other app can access* Data removed Media MediaStore API READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE (<API 28) Yes Nothing Other files SAF None Through the system file picker Nothing
  20. directories Type Access method Permissions Access Uninstalled app-specific files (internal

    storage) getFilesDir() /data/user/… Not needed No other app can access Data removed app-specific files (external storage) getExternalFilesDir() /storage/emulated/… Not needed Yes Data removed Media MediaStore API READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE (<API 28) Yes Nothing Other files SAF None Through the system file picker Nothing Select the appropriate folder for your app requirements Asking for permissions is always suspicious for the user
  21. building a gallery #android

  22. Get all media files Filter by images, videos, starred and

    trashed items Create a new file Update one or more existing files Delete one or more files features
  23. ANDROID GALLERY APP adapted from IKEA instructions manual

  24. getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if (files #==

    null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media } dark ages
  25. getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if (files #==

    null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media } dark ages getting all files from disk
  26. getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if (files #==

    null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media } dark ages
  27. getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if (files #==

    null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media } deprecated dark ages
  28. dark ages getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if

    (files #== null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media }
  29. getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array<File>?): List<Media> { if (files #==

    null) { return emptyList() } val media = mutableListOf<Media>() for (file in files) { if (file.isDirectory) { media += getMedia(file.listFiles()) } else { if (isAnImage(file.extension)) { media += Media(file.path, file.name, file.length(), file.lastModified()) } } } return media } Well… not the best approach out there Bad performance Doesn’t take into account that new files can be added/ modified during iteration Uses deprecated methods Individually check for files media type dark ages
  30. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } MediaStore API
  31. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } MediaStore API you can select which columns you want to access
  32. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } MediaStore API
  33. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } MediaStore API You can filter the results by adding an instruction, for instance: “${MediaStore.Images.Media.IS_FAVORITE} = 1”
  34. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } MediaStore API
  35. deprecated suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection

    = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } deprecated MediaStore API
  36. MediaStore API suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val

    projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … }
  37. MediaStore API suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val

    projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } Files should no longer be accessed via direct paths Uri’s increase data security
  38. building a gallery ready for scoped storage #scopedstorage

  39. scoped storage not possible to disable requires runtime permissions target:

    android 10 target: android 11 target: a all other android versions android 6.0+ scoped storage enabled by default accessing files
  40. requires runtime permissions target: android 10 target: android 11 target:

    a all other android versions android 6.0+ scoped storage enabled by default scoped storage not possible to disable accessing files
  41. scoped storage not possible to disable requires runtime permissions AndroidManifest.xml

    target: android 10 target: android 11 target: a all other android versions <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" #/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" #/> android 6.0+ scoped storage enabled by default val permissions = arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) ActivityCompat.requestPermissions(this, permissions, PERMISSION_STORAGE) Activity.kt android 6.0+
  42. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" #/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" #/> <application … android:requestLegacyExternalStorage="true"> scoped

    storage not possible to disable requires runtime permissions AndroidManifest.xml target: android 10 target: android 11 target: a all other android versions android 6.0+ scoped storage enabled by default to disable android 10
  43. scoped storage not possible to disable requires runtime permissions target:

    android 10 target: android 11 target: a all other android versions android 6.0+ scoped storage enabled by default build.gradle android { compileSdkVersion 30 defaultConfig { targetSdkVersion 30 … android 11
  44. scoped storage not possible to disable requires runtime permissions target:

    android 10 target: android 11 target: a all other android versions android 6.0+ scoped storage enabled by default although it can be preserved build.gradle android { compileSdkVersion 30 defaultConfig { targetSdkVersion 30 … <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"#/> <application … android:preserveLegacyExternalStorage="true"> AndroidManifest.xml android 11
  45. permissions Allow Playground to access Allow Playground to access

  46. Using MediaStore API Filtering for media types Selecting which attributes

    we’re looking for listing files
  47. deprecated suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection

    = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } deprecated no scoped storage
  48. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … } no scoped storage
  49. scoped storage listing files suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO)

    { val projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) … }
  50. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) … } scoped storage listing files
  51. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) … } scoped storage listing files scoped approved storage
  52. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) … } scoped storage listing files scoped approved storage What about listing videos?
  53. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.SIZE, MediaStore.Video.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Video.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) … } scoped storage listing files scoped approved storage
  54. suspend fun queryImages(): List<Media> { withContext(Dispatchers.IO) { val projection =

    arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.SIZE, MediaStore.Video.Media.DATE_MODIFIED) getApplication<Application>().contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null, "${MediaStore.Video.Media.DATE_MODIFIED} DESC" )#?.use { cursor #-> val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) … } scoped storage listing files scoped approved storage
  55. scoped storage media location Allows to retrieve image coordinates The

    information is stored in the image metadata Sensitive information It can be the user home location, for instance
  56. val exifInterface = ExifInterface(image.path) val latLong = exifInterface.latLong scoped storage

    no scoped storage
  57. scoped storage no scoped storage val exifInterface = ExifInterface(image.path) val

    latLong = exifInterface.latLong Well, remember? No paths.
  58. val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run {

    val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } scoped storage media location
  59. E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 21205 java.lang.UnsupportedOperationException: Caller

    must hold ACCESS_MEDIA_LOCATION permission to access original logcat val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } scoped storage media location
  60. scoped storage media location val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use {

    stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 21205 java.lang.UnsupportedOperationException: Caller must hold ACCESS_MEDIA_LOCATION permission to access original logcat Media location is really sensitive information Malware apps might get access to the user’s home location, work, etc. Additional permissions are required for this operation
  61. required on android 10+ val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use {

    stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> scoped storage media location
  62. required on android 10+ <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> val uri =

    MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } Coordinates = (51.9159379, 17.6344794) logcat scoped storage media location
  63. None
  64. scoped storage media location <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> val uri =

    MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } Coordinates = (51.9159379, 17.6344794) logcat What if we run this code on an Android 11 device?
  65. val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run {

    val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> scoped storage media location
  66. val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run {

    val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 21205 java.lang.UnsupportedOperationException: Caller must hold ACCESS_MEDIA_LOCATION permission to access original logcat scoped storage media location
  67. scoped storage media location val uri = MediaStore.setRequireOriginal(media[0].uri) contentResolver.openInputStream(uri).use {

    stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 21205 java.lang.UnsupportedOperationException: Caller must hold ACCESS_MEDIA_LOCATION permission to access original logcat For third-party files you need to request user permission Security above everything We’re accessing sensitive information
  68. if (ActivityCompat.checkSelfPermission(baseContext, Manifest.permission.ACCESS_MEDIA_LOCATION) #== PackageManager.PERMISSION_GRANTED) { val uri = MediaStore.setRequireOriginal(media[0].uri)

    contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } } else { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_MEDIA_LOCATION), PERMISSION_MEDIA_ACCESS) } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> scoped storage media location
  69. if (ActivityCompat.checkSelfPermission(baseContext, Manifest.permission.ACCESS_MEDIA_LOCATION) #== PackageManager.PERMISSION_GRANTED) { val uri = MediaStore.setRequireOriginal(media[0].uri)

    contentResolver.openInputStream(uri).use { stream #-> ExifInterface(stream#!!).run { val coordinates = latLong#!!.toList() Log.d("TAG", "Coordinates = (${coordinates[0]}, ${coordinates[0]})") } } } else { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_MEDIA_LOCATION), PERMISSION_MEDIA_ACCESS) } <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" #/> scoped approved storage scoped storage media location
  70. scoped storage creating a new file Created through the MediaStore

    API Can be stored either in PICTURES or DCIM folders
  71. scoped storage no scoped storage fun duplicate(media: Media) { viewModelScope.launch

    { val file = File(media.path) val target = File("${file.parent}/${file.nameWithoutExtension}-copy.${file.extension}") file.copyTo(target, true) sendScanFileBroadcast(this, target) } }
  72. scoped storage no scoped storage fun duplicate(media: Media) { viewModelScope.launch

    { val file = File(media.path) val target = File("${file.parent}/${file.nameWithoutExtension}-copy.${file.extension}") file.copyTo(target, true) sendScanFileBroadcast(this, target) } }
  73. scoped storage no scoped storage fun duplicate(media: Media) { viewModelScope.launch

    { val file = File(media.path) val target = File("${file.parent}/${file.nameWithoutExtension}-copy.${file.extension}") file.copyTo(target, true) sendScanFileBroadcast(this, target) } } fun sendScanFileBroadcast(context: Context, file: File) { val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) intent.data = Uri.fromFile(file) context.sendBroadcast(intent) }
  74. no scoped storage fun duplicate(media: Media) { viewModelScope.launch { val

    file = File(media.path) val target = File("${file.parent}/${file.nameWithoutExtension}-copy.${file.extension}") file.copyTo(target, true) sendScanFileBroadcast(this, target) } } fun sendScanFileBroadcast(context: Context, file: File) { val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) intent.data = Uri.fromFile(file) context.sendBroadcast(intent) } No update on MediaStore tables You could create a file almost anywhere on disk
  75. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  76. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  77. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  78. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  79. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  80. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  81. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } }
  82. suspend fun duplicateMedia(resolver: ContentResolver, media: Media, bitmap: Bitmap) { withContext(Dispatchers.IO)

    { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } newMedia.clear() newMedia.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newMediaUri, newMedia, null, null) } } scoped storage creating new media What if we want to save this file on a different location?
  83. suspend fun duplicateMedia(resolver: ContentResolver, media: Media, bitmap: Bitmap) { withContext(Dispatchers.IO)

    { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val dirDest = File(Environment.DIRECTORY_PICTURES, context.getString(R.string.app_name)) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${File.separator}") put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } … scoped storage creating new media
  84. scoped storage creating new media suspend fun duplicateMedia(resolver: ContentResolver, media:

    Media, bitmap: Bitmap) { withContext(Dispatchers.IO) { val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val dirDest = File(Environment.DIRECTORY_PICTURES, context.getString(R.string.app_name)) val newMedia = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “${media.name}-cp.${media.extension}") put(MediaStore.MediaColumns.MIME_TYPE, "image/png") put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis()) put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${File.separator}") put(MediaStore.Images.Media.IS_PENDING, 1) } val newMediaUri = resolver.insert(collection, newMedia) resolver.openOutputStream(newMediaUri#!!, "w").use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) } … scoped approved storage or DIRECTORY_DCIM
  85. scoped storage defining a location Location selected through system file

    picker The user can select where to save the file And change it’s name
  86. scoped storage specific location fun openLocationPicker(media: Media) { val intent

    = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_TITLE, media.name) type = "image#/*" } startActivityForResult(intent, PERMISSION_SAVE_ON_LOCATION) }
  87. scoped storage specific location fun openLocationPicker(media: Media) { val intent

    = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_TITLE, media.name) type = "image#/*" } startActivityForResult(intent, PERMISSION_SAVE_ON_LOCATION) }
  88. scoped storage specific location fun openLocationPicker(media: Media) { val intent

    = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_TITLE, media.name) type = "image#/*" } startActivityForResult(intent, PERMISSION_SAVE_ON_LOCATION) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode #== Activity.RESULT_OK) { when(requestCode) { PERMISSION_SAVE_ON_LOCATION #-> duplicateMedia(data#?.data#!!) … scoped approved storage
  89. scoped storage updating media The file was created by our

    app Request permission to modify third-party files
  90. scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch {

    saveImageBW(getApplication<Application>().contentResolver, media, paint) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.Q)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException _permissionNeededForUpdate.postValue( recoverableSecurityException.userAction.actionIntent.intentSender) } else throw securityException } } }
  91. scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch {

    saveImageBW(getApplication<Application>().contentResolver, media, paint) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.Q)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException _permissionNeededForUpdate.postValue( recoverableSecurityException.userAction.actionIntent.intentSender) } else throw securityException } } }
  92. scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch {

    saveImageBW(getApplication<Application>().contentResolver, media, paint) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.Q)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException _permissionNeededForUpdate.postValue( recoverableSecurityException.userAction.actionIntent.intentSender) } else throw securityException } } } scoped approved storage viewModel.permissionNeededForUpdate.observe(this, Observer { sender #-> startIntentSenderForResult(sender, PERMISSION_UPDATE, null, 0, 0, 0, null) })
  93. scoped storage deleting media Erase media from disk Data removed

    through the MediaStore API No need to additionally send notification broadcasts
  94. suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.path).delete() sendScanFileBroadcast(context, target)

    } } scoped storage no scoped storage
  95. fun sendScanFileBroadcast(context: Context, file: File) { val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)

    intent.data = Uri.fromFile(file) context.sendBroadcast(intent) } scoped storage no scoped storage suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.path).delete() sendScanFileBroadcast(context, target) } }
  96. fun sendScanFileBroadcast(context: Context, file: File) { val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)

    intent.data = Uri.fromFile(file) context.sendBroadcast(intent) } suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.path).delete() sendScanFileBroadcast(context, target) } } scoped storage no scoped storage Manually need to notify other apps that you’ve deleted a file Might end up with references on MediaStore table from files that no longer exist
  97. scoped storage no scoped storage suspend fun delete(media: Media) {

    withContext(Dispatchers.IO) { File(media.uri.path).delete() sendScanFileBroadcast(context, target) } }
  98. logcat E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 8407 kotlin.io.NoSuchFileException:

    /external/images/media/74: The source file doesn't exist. scoped storage no scoped storage suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.uri.path).delete() sendScanFileBroadcast(context, target) } }
  99. suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.uri.path).delete() sendScanFileBroadcast(context, target)

    } } scoped storage no scoped storage logcat E/AndroidRuntime: FATAL EXCEPTION: main Process: com.cmota.playground.storage, PID: 8407 kotlin.io.NoSuchFileException: /external/images/media/74: The source file doesn't exist. Uri is not a direct access to a file Even if you retrieve the path from Uri it won’t be a exact path on disk You can only delete files your app creates Other app files require additional user permission
  100. suspend fun delete(resolver: ContentResolver, item: Media): IntentSender? { withContext(Dispatchers.IO) {

    try { val uri = item.uri resolver.delete(uri, "${MediaStore.Images.Media._ID} = ?", arrayOf(item.id.toString())) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.P)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException return recoverableSecurityException.userAction.actionIntent.intentSender } else throw securityException } } return null } scoped storage scoped storage deleting media
  101. suspend fun delete(resolver: ContentResolver, item: Media): IntentSender? { withContext(Dispatchers.IO) {

    try { val uri = item.uri resolver.delete(uri, "${MediaStore.Images.Media._ID} = ?", arrayOf(item.id.toString())) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.P)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException return recoverableSecurityException.userAction.actionIntent.intentSender } else throw securityException } } return null } scoped storage scoped storage deleting media
  102. suspend fun delete(resolver: ContentResolver, item: Media): IntentSender? { withContext(Dispatchers.IO) {

    try { val uri = item.uri resolver.delete(uri, "${MediaStore.Images.Media._ID} = ?", arrayOf(item.id.toString())) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.P)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException return recoverableSecurityException.userAction.actionIntent.intentSender } else throw securityException } } return null } scoped storage scoped storage deleting media
  103. suspend fun delete(resolver: ContentResolver, item: Media): IntentSender? { withContext(Dispatchers.IO) {

    try { val uri = item.uri resolver.delete(uri, "${MediaStore.Images.Media._ID} = ?", arrayOf(item.id.toString())) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.P)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException return recoverableSecurityException.userAction.actionIntent.intentSender } else throw securityException } } return null } scoped storage scoped storage deleting media viewModel.permissionNeededForDelete.observe(this, Observer { sender #-> startIntentSenderForResult(sender, PERMISSION_DELETE, null, 0, 0, 0, null) }) scoped approved storage
  104. suspend fun delete(resolver: ContentResolver, item: Media): IntentSender? { withContext(Dispatchers.IO) {

    try { val uri = item.uri resolver.delete(uri, "${MediaStore.Images.Media._ID} = ?", arrayOf(item.id.toString())) } catch (securityException: SecurityException) { if (hasSdkHigherThan(Build.VERSION_CODES.P)) { val recoverableSecurityException = securityException as? RecoverableSecurityException #?: throw securityException return recoverableSecurityException.userAction.actionIntent.intentSender } else throw securityException } } return null } scoped storage scoped storage deleting media viewModel.permissionNeededForDelete.observe(this, Observer { sender #-> startIntentSenderForResult(sender, PERMISSION_DELETE, null, 0, 0, 0, null) }) What if we have multiple files to delete?
  105. scoped storage scoped storage deleting media

  106. new in Android 11 scoped storage #scopedstorage

  107. scoped storage android 11 Changes on ACTION_OPEN_DOCUMENT and ACTION_OPEN_DOCUMENT_TREE No

    longer access to root folders of internal storage/sd card Android/data and Android/obb are also blocked Downloads/ blocked for ACTION_OPEN_DOCUMENT_TREE File paths can be used on native libraries Bulk operations Starred and trash categories
  108. scoped storage bulk operations Better UX One screen shows the

    media that’s going to be accessed
  109. scoped storage bulk operations private fun deleteMediaBulk(resolver: ContentResolver, items: List<Media>)

    { val uris = items.map { it.uri } _permissionNeededForDelete.postValue( MediaStore.createDeleteRequest(resolver, uris).intentSender) }
  110. scoped storage bulk operations private fun deleteMediaBulk(resolver: ContentResolver, items: List<Media>)

    { val uris = items.map { it.uri } _permissionNeededForDelete.postValue( MediaStore.createDeleteRequest(resolver, uris).intentSender) } viewModel.permissionNeededForDelete.observe(this, Observer { sender #-> startIntentSenderForResult(sender, PERMISSION_DELETE, null, 0, 0, 0, null) }) available on android 11+ only
  111. Allows to define specific media as favorite Which can be

    used to give them higher importance Showing them first, for instance scoped storage starred
  112. fun addToFavorites(items: List<Media>, state: Boolean): PendingIntent { val resolver =

    getApplication<Application>().contentResolver val uris = items.map { it.uri } return MediaStore.createFavoriteRequest(resolver, uris, state) } scoped storage starred
  113. val intent = viewModel.addToFavorites(media, true) startIntentSenderForResult(intent.intentSender, PERMISSION_FAVORITES, null, 0, 0,

    0) scoped storage starred fun addToFavorites(items: List<Media>, state: Boolean): PendingIntent { val resolver = getApplication<Application>().contentResolver val uris = items.map { it.uri } return MediaStore.createFavoriteRequest(resolver, uris, state) } available on android 11+ only
  114. It’s different from delete Files can be restored during a

    certain period of time It will be automatically deleted after 30 days Similar behaviour of your computer recycler bin scoped storage trash
  115. fun addToTrash(items: List<Media>, state: Boolean): PendingIntent { val resolver =

    getApplication<Application>().contentResolver val uris = items.map { it.uri } return MediaStore.createTrashRequest(resolver, uris, state) } scoped storage trash
  116. val intent = viewModel.addToTrash(media, true) startIntentSenderForResult(intent.intentSender, PERMISSION_TRASH, null, 0, 0,

    0) scoped storage trash fun addToTrash(items: List<Media>, state: Boolean): PendingIntent { val resolver = getApplication<Application>().contentResolver val uris = items.map { it.uri } return MediaStore.createTrashRequest(resolver, uris, state) } available on android 11+ only
  117. app exceptions scoped storage #scopedstorage

  118. scoped storage exceptions File managers and backup apps Need to

    access the entire file system The user needs to manually grant this access Google Play needs to also approve this access Playground
  119. scoped storage exceptions <uses-permission android:name=“android.permission.MANAGE_EXTERNAL_STORAGE" #/> fun openSettingsAllFilesAccess(activity: AppCompatActivity) {

    val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) activity.startActivity(intent) } scoped approved storage
  120. scoped storage limitations App’s internal and external files are still

    unavailable No detailed information about disk usage in-app No longer possible to clear all apps cache
  121. fun openNativeFileExplorer(activity: AppCompatActivity) { val intent = Intent(Settings.ACTION_MANAGE_STORAGE) activity.startActivity(intent) }

    scoped storage disk usage scoped approved storage
  122. fun clearAppsCacheFiles(activity: AppCompatActivity) { val intent = Intent(Settings.ACTION_CLEAR_APP_CACHE) activity.startActivity(intent) }

    scoped storage clear cache scoped approved storage
  123. migrating files scoped storage #scopedstorage

  124. scoped storage migration How to migrate data? Remember: users cannot

    lose their apps data! Depends on your app and it’s use cases You can role out an app update targeting android 10 With requestLegacyStorage enable Migrate all files to the new directories before targeting android 11 You can use preserveLegacyStorage for app updates Rethink your app UX
  125. final remarks scoped storage #scopedstorage

  126. scoped storage TL;DR Scoped storage is mandatory when targeting Android

    11 It can be disabled on Android 10 via requestLegacyStorage Redesign your app to use the appropriate folders Don’t forget to migrating files You can still use preserveLegacyStorage if it’s an app update Use MediaStore API for accessing images, videos and audio Files are accessible via Uri’s and not file paths SAF (Storage Access Framework) should be used for other shareable content If your app didn’t created that file you’ll need to ask for user permission
  127. scoped storage useful links Storage updates in Android 11 developer.android.com/preview/privacy/storage

    Preparing for Scoped Storage raywenderlich.com/10217168-preparing-for-scoped-storage Storage access with Android 11 youtube.com/watch?v=RjyYCUW-9tY Android 11 Storage FAQ medium.com/androiddevelopers/android-11-storage-faq-78cefea52b7c Bringing modern storage to Viber’s users android-developers.googleblog.com/2020/07/bringing-modern-storage-to-vibers-users.html
  128. 10 P N O TBD L JB HC M KK

    ICS F G TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD TBD Scoped Storage 11 @cafonsomota github.com/cmota/ScopedStorage