Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

scoped storage #android

Slide 4

Slide 4 text

10:00 android this is typically the file explorer of a brand new phone

Slide 5

Slide 5 text

this is my file explorer, that I periodically clean android 10:00 10:00

Slide 6

Slide 6 text

10:00 10:00 #influencer android

Slide 7

Slide 7 text

10:00 10:00 what? android

Slide 8

Slide 8 text

10:00 10:00 another cache? android

Slide 9

Slide 9 text

10:00 10:00 no longer exists android

Slide 10

Slide 10 text

10:00 10:00 no idea android

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

10:00 10:00 challenge sounds fishy (especially with lower case) share your storage with the hashtag #nomorescatteredfiles

Slide 13

Slide 13 text

android

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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 …

Slide 16

Slide 16 text

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 …

Slide 17

Slide 17 text

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 …

Slide 18

Slide 18 text

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)

Slide 19

Slide 19 text

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 (

Slide 20

Slide 20 text

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 (

Slide 21

Slide 21 text

building a gallery #android

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

ANDROID GALLERY APP adapted from IKEA instructions manual

Slide 24

Slide 24 text

getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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

Slide 25

Slide 25 text

getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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

Slide 26

Slide 26 text

getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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

Slide 27

Slide 27 text

getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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

Slide 28

Slide 28 text

dark ages getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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 }

Slide 29

Slide 29 text

getMedia(Environment.getExternalStorageDirectory().listFiles()) private fun getMedia(files: Array?): List { if (files #== null) { return emptyList() } val media = mutableListOf() 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

Slide 30

Slide 30 text

suspend fun queryImages(): List { 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().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

Slide 31

Slide 31 text

suspend fun queryImages(): List { 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().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

Slide 32

Slide 32 text

suspend fun queryImages(): List { 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().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

Slide 33

Slide 33 text

suspend fun queryImages(): List { 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().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”

Slide 34

Slide 34 text

suspend fun queryImages(): List { 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().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

Slide 35

Slide 35 text

deprecated suspend fun queryImages(): List { 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().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

Slide 36

Slide 36 text

MediaStore API suspend fun queryImages(): List { 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().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)) … }

Slide 37

Slide 37 text

MediaStore API suspend fun queryImages(): List { 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().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

Slide 38

Slide 38 text

building a gallery ready for scoped storage #scopedstorage

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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 val permissions = arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) ActivityCompat.requestPermissions(this, permissions, PERMISSION_STORAGE) Activity.kt android 6.0+

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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 … AndroidManifest.xml android 11

Slide 45

Slide 45 text

permissions Allow Playground to access Allow Playground to access

Slide 46

Slide 46 text

Using MediaStore API Filtering for media types Selecting which attributes we’re looking for listing files

Slide 47

Slide 47 text

deprecated suspend fun queryImages(): List { 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().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

Slide 48

Slide 48 text

suspend fun queryImages(): List { 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().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

Slide 49

Slide 49 text

scoped storage listing files suspend fun queryImages(): List { 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().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)) … }

Slide 50

Slide 50 text

suspend fun queryImages(): List { 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().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

Slide 51

Slide 51 text

suspend fun queryImages(): List { 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().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

Slide 52

Slide 52 text

suspend fun queryImages(): List { 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().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?

Slide 53

Slide 53 text

suspend fun queryImages(): List { 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().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

Slide 54

Slide 54 text

suspend fun queryImages(): List { 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().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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

scoped storage no scoped storage val exifInterface = ExifInterface(image.path) val latLong = exifInterface.latLong Well, remember? No paths.

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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]})") } } scoped storage media location

Slide 62

Slide 62 text

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]})") } } Coordinates = (51.9159379, 17.6344794) logcat scoped storage media location

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

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]})") } } Coordinates = (51.9159379, 17.6344794) logcat What if we run this code on an Android 11 device?

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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 scoped storage media location

Slide 67

Slide 67 text

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 For third-party files you need to request user permission Security above everything We’re accessing sensitive information

Slide 68

Slide 68 text

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) } scoped storage media location

Slide 69

Slide 69 text

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) } scoped approved storage scoped storage media location

Slide 70

Slide 70 text

scoped storage creating a new file Created through the MediaStore API Can be stored either in PICTURES or DCIM folders

Slide 71

Slide 71 text

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) } }

Slide 72

Slide 72 text

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) } }

Slide 73

Slide 73 text

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) }

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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) } }

Slide 76

Slide 76 text

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) } }

Slide 77

Slide 77 text

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) } }

Slide 78

Slide 78 text

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) } }

Slide 79

Slide 79 text

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) } }

Slide 80

Slide 80 text

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) } }

Slide 81

Slide 81 text

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) } }

Slide 82

Slide 82 text

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?

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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) }

Slide 87

Slide 87 text

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) }

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

scoped storage updating media The file was created by our app Request permission to modify third-party files

Slide 90

Slide 90 text

scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch { saveImageBW(getApplication().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 } } }

Slide 91

Slide 91 text

scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch { saveImageBW(getApplication().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 } } }

Slide 92

Slide 92 text

scoped storage updating media fun convertToBW(media: Media) { viewModelScope.launch { saveImageBW(getApplication().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) })

Slide 93

Slide 93 text

scoped storage deleting media Erase media from disk Data removed through the MediaStore API No need to additionally send notification broadcasts

Slide 94

Slide 94 text

suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.path).delete() sendScanFileBroadcast(context, target) } } scoped storage no scoped storage

Slide 95

Slide 95 text

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) } }

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

scoped storage no scoped storage suspend fun delete(media: Media) { withContext(Dispatchers.IO) { File(media.uri.path).delete() sendScanFileBroadcast(context, target) } }

Slide 98

Slide 98 text

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) } }

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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?

Slide 105

Slide 105 text

scoped storage scoped storage deleting media

Slide 106

Slide 106 text

new in Android 11 scoped storage #scopedstorage

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

scoped storage bulk operations Better UX One screen shows the media that’s going to be accessed

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

scoped storage bulk operations private fun deleteMediaBulk(resolver: ContentResolver, items: List) { 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

Slide 111

Slide 111 text

Allows to define specific media as favorite Which can be used to give them higher importance Showing them first, for instance scoped storage starred

Slide 112

Slide 112 text

fun addToFavorites(items: List, state: Boolean): PendingIntent { val resolver = getApplication().contentResolver val uris = items.map { it.uri } return MediaStore.createFavoriteRequest(resolver, uris, state) } scoped storage starred

Slide 113

Slide 113 text

val intent = viewModel.addToFavorites(media, true) startIntentSenderForResult(intent.intentSender, PERMISSION_FAVORITES, null, 0, 0, 0) scoped storage starred fun addToFavorites(items: List, state: Boolean): PendingIntent { val resolver = getApplication().contentResolver val uris = items.map { it.uri } return MediaStore.createFavoriteRequest(resolver, uris, state) } available on android 11+ only

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

fun addToTrash(items: List, state: Boolean): PendingIntent { val resolver = getApplication().contentResolver val uris = items.map { it.uri } return MediaStore.createTrashRequest(resolver, uris, state) } scoped storage trash

Slide 116

Slide 116 text

val intent = viewModel.addToTrash(media, true) startIntentSenderForResult(intent.intentSender, PERMISSION_TRASH, null, 0, 0, 0) scoped storage trash fun addToTrash(items: List, state: Boolean): PendingIntent { val resolver = getApplication().contentResolver val uris = items.map { it.uri } return MediaStore.createTrashRequest(resolver, uris, state) } available on android 11+ only

Slide 117

Slide 117 text

app exceptions scoped storage #scopedstorage

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

scoped storage exceptions fun openSettingsAllFilesAccess(activity: AppCompatActivity) { val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) activity.startActivity(intent) } scoped approved storage

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

migrating files scoped storage #scopedstorage

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

final remarks scoped storage #scopedstorage

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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