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

Hello,
Let me start by saying that i know very little about how phones work and how to properly use online forums so i apologize in advance if I confuse you, as I’m very confused myself.

My problem is that the screen of my android z flip 5g got damaged as its a faulty product and is currently under warranty. The phone repair shop made my phone completely unable to turn on so we’ve been waiting for over a month to get Samsung’s approval for a brand new phone. They said that once they recieve this new phone, they will be able to transfer all of my files by putting my old phone’s internal memory into my new phone. I can’t trust them 100% anymore.

The app i was using is called jotterpad. It uses a username and password so i thought that i’ll be able to find my files on my husband’s phone by logging in. Well, this is how i learned about scoped storage and private app directories:

And this is how i learned about local storage:

For this whole month that my phone has stayed with them, I’ve been calling around and trying to get a definate answer to this burning question that nobody seems confident enough to fully answer: will i be able to restore these scoped storage/private app directory/local storage files on my brand new phone by transferring the internal memory from my old phone or should i tragically cancel my warranty and take back my phone so i can plug it into my laptop and pray I find these files in a more sure way (that may still fail)? If it helps, the galaxy z flip 5g does not use an SD card so I’m not sure if there’s a way to access my broken phone’s external memory. Also, after i installed jotterpad on my husband’s phone and couldn’t find my files under my account, i uninstalled it. Will that affect anything on my phone?

Hello flipowner,

This article refers to how to implement scoped storage in Android programmatically, on your Android apps, which is different from what you’re requesting. In any case, I’ll do my best to help you.

If you’ve enabled the synchronization of your Google account, when you sign in on another device you’ll be able to restore your files, preferences, and apps on that new device. Which I believe is what the repair team is hoping for.

JotterPad allows you to create and edit files, and the scoped storage article that you’ve linked is where they explain how they address this new behavior, which in your case it doesn’t help much since you want to recover files.

If the files still exist on your device, you should be able to access them via a file manager, for instance, Google’s official version:

You can use it to browse your device and look/search for them.

1 Like

Thank you for trying to help and for your advice.

1 Like