Preparing for Scoped Storage | raywenderlich.com

Android apps targeting Android 11 will be required to use scoped storage to read and write files. In this tutorial, you’ll learn how to migrate your application and also why scoped storage is such a big improvement for the end user.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/10217168-preparing-for-scoped-storage

I’m trying to upgrade an existing app to Android 11 standards. (Yes I know I should have begun this over a year ago.) I’m having trouble with saving under scoped storage. I thought your LeMemify would point me to the right way to update my app, but it doesn’t work for me.

I want to create an app that runs on older versions of Android. Your build.gradle specifies a minSdkVersion of 19, but apparently something’s missing.

On a device running Android 11 LeMemify works fine, but on a Samsung Galaxy S5 running Android 6.0.1 (Android level 23) it crashes on line 73 of FileOperations.kt, where queryImagesOnDevice is calling ContentResolver.query.

I can fix the first crash by removing RELATIVE_PATH from projection when Build.VERSION.SDK_INT is less than 29, and using a path of “”.

Another crash occurs when I try to save a copy of the image. It crashes on line 132 at contentResolver.insert. To fix it, when Build.VERSION.SDK_INT is less than 29, I set collection to MediaStore.Video.Media.EXTERNAL_CONTENT_URI (as suggested in 공유 저장소의 미디어 파일에 액세스  |  Android 개발자  |  Android Developers), and I do not put a relative path in ContentValues. ContentResolver.insert no longer crashes, but it returns a null uri.

How can a adapt leMemify to an older Android device?

Hello Jerry,

Thank you for the feedback!

I’m going to enumerate all your questions and give my best to answer them all:

  1. MediaStore.MediaColumns.RELATIVE_PATH requires API 29

I’m looking at the documentation and indeed this is only available in API 29. I would follow a different approach, instead of using an empty string I would something similar to:

@SuppressLint("InlinedApi")
private val RELATIVE_PATH = if (hasAndroid11()) {
  MediaStore.MediaColumns.RELATIVE_PATH
} else {
  "relative_path"
}

And then use this RELATIVE_PATH instead. The issue is that this constant was made available only on API 29, so the system won’t find it, this should solve this issue.

  1. Unable to save a copy of the image
    I’ve noticed that IS_PENDING has the same issue as RELATIVE_PATH, perhaps following the previous approach also solves your issue?
@SuppressLint("InlinedApi")
private val IS_PENDING = if (hasAndroid11()) {
  MediaStore.Images.Media.IS_PENDING
} else {
  "is_pending"
}

It’s also worth mentioning that you’re using MediaStore.Video.Media.EXTERNAL_CONTENT_URI which is used for Video instead of Image.

After the IS_PENDING change, everything seems to be working on my devices (I don’t have one with API 23 though). If possible, could you send me the stack trace of the issue? Thank you.

Best,
Carlos

Thank you for your response.

I made the modifications you suggested, and it didn’t change anything. LeMemify runs fine on Android 11, but it crashes on API level 23.

I’m trying to update an existing app with thousands of downloads. Google estimates that 31% of current Android systems worldwide are running API level 28 or earlier. I can’t afford to abandon one third of my users. Someone needs to run and debug this on either a device or an emulator with an API level of 28 or earlier.

Here’s the stack trace:

2022-04-13 15:44:00.897 679-679/com.raywenderlich.android.lememeify E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.raywenderlich.android.lememeify, PID: 679
android.database.sqlite.SQLiteException: no such column: relative_path (code 1): , while compiling: SELECT _id, relative_path, _display_name, _size, mime_type, width, height, date_modified FROM images ORDER BY date_modified DESC
#################################################################
Error Code : 1 (SQLITE_ERROR)
Caused By : SQL(query) error or missing database.
(no such column: relative_path (code 1): , while compiling: SELECT _id, relative_path, _display_name, _size, mime_type, width, height, date_modified FROM images ORDER BY date_modified DESC)
#################################################################
#################################################################
Error Code : 1 (SQLITE_ERROR)
Caused By : SQL(query) error or missing database.
(no such column: relative_path (code 1): , while compiling: SELECT _id, relative_path, _display_name, _size, mime_type, width, height, date_modified FROM images ORDER BY date_modified DESC
#################################################################
Error Code : 1 (SQLITE_ERROR)
Caused By : SQL(query) error or missing database.
(no such column: relative_path (code 1): , while compiling: SELECT _id, relative_path, _display_name, _size, mime_type, width, height, date_modified FROM images ORDER BY date_modified DESC)
#################################################################)
#################################################################
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:179)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:135)
at android.content.ContentProviderProxy.query(ContentProviderNative.java:421)
at android.content.ContentResolver.query(ContentResolver.java:502)
at android.content.ContentResolver.query(ContentResolver.java:445)
at com.raywenderlich.android.lememeify.FileOperations$queryImagesOnDevice$2.invokeSuspend(FileOperations.kt:76)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2022-04-13 15:44:03.587 679-679/com.raywenderlich.android.lememeify I/Process: Sending signal. PID: 679 SIG: 9

Hello Jerry,

I initially thought that the issue was related to the constant not being available, but it’s indeed because there’s no column on the database with that value. That’s why you got the crash that you’ve sent before.

Typically, when talking about storage I’ve seen apps following two different approaches:
1 - Use the SAF (Storage Access Framework), where you open the native file explorer, and part of the logic is handled directly by the system.
2 - Implement this logic to show all images/videos/etc. like we do on Le Memify.

If you want to follow this second approach, it seems you might need to have both implementations available, since some of the parameters that scoped storage uses (like the RELATIVE_PATH and IS_PENDING are only available on more recent APIs).

My suggestion is that if you want to keep the same behavior your app has is to create a factory that will load one implementation or the other, depending on the device version.

Something similar to (this is pseudo-code, so it might change a bit):

//Utils.kt
fun hasAndroid11OrNewer(): Boolean {
  return Build.VERSION.CODENAME >= ANDROID_R
}
//FileOperationsAPI29.kt
fun queryImagesOnDeviceAPI29(context: Context): List<Image> {
  // code from FileOperations.kt
}
//FileOperationsPreAPI29.kt
fun queryImagesOnDevicePreAPI29(context: Context): List<Image> {
  // your current code on your app to fetch images
}
//FileOperations.kt
fun queryImagesOnDevice(context: Context): List<Image> {
  if(hasAndroid11OrNewer()) {
    return queryImagesOnDeviceAPI29(context)
  } else {
    return queryImagesOnDevicePreAPI29(context)
  }
}

You can call FileOperations.queryImagesOnDevice(context) as you’re calling now, and the system will automatically call one function or the other, depending on the current version of the SDK.

Hope this solves your issue.

Best,
Carlos