Saving to photos in Android Q+ (Android 10+)

Saving to photos in Android Q+ (Android 10+)

·

10 min read

As of Android 10, file access permissions have changed. In a previous blog post Migrating from getExternalStoragePublicDirectory in Android 10 & 11, I moved from the legacy storage to a simpler intent-based approach that opened the native share sheet. While this solved the problem in a different way than the original implementation, it changed the functionality—instead of writing directly to disk, I created an intent with the file data and the user chose where to put it. While this was a better solution for that app that dealt with JSON files, it's not a great solution for apps that deal with images.

I came across this problem again in a different app, and it was time to solve it the right way.

Saving to photos before Android 10

Before Android 10, you could save to photos by writing to the following directory:

Environment
  .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)

As of Android 10, this approach has been deprecated. You no longer have access to folders directly in this way. You could still use it for a little while though with the requestLegacyExternalStorage flag in your manifest, but this is a bandaid solution, and as soon as you upgrade your app to target Android 11, this will no longer work, which means you'll need to eventually overhaul your storage implementation. Targeting Android 11 will become mandatory in August 2021, giving developers about 1 year to migrate from the legacy storage implementation.

Share sheet for images

Sharing via the share sheet on Android is the easiest approach, and it's the one I outline in this blog post, but it's a cumbersome experience for some users.

On Pixel devices, Google Photos is the default photos program. When you want to see the photos on your device, you open Google Photos. There is no difference between Google Photos and your device photos anymore.

On other (non-Google) Android devices, this isn't the case, and many other device manufacturers have their own version of a gallery app. Unfortunately, some of these apps are not available as an option in the native share sheet when creating an image share intent. The OnePlus Gallery app, for example—which ships with One Plus devices—doesn't show up as an option to share to. This creates a cumbersome experience for these Android users who just want to save an image to their phone.

Problem: the go-to React Native plugin doesn't work on Android 10+

The React Native CameraRoll plugin, which works well enough for iOS, unfortunately doesn't work for recent Android versions. There are a few issues and/or pull requests open for this topic: #244, #219, #268, #248, #271 to name a few.

The problem I was solving was quite unique and had multiple layers: React Native app with an embedded web view, and the image is an export of an HTML5 canvas, which provides the image data as a base64 encoded string. To solve this problem on Android, I needed 2 separate plugins: the above-mentioned react-native-cameraroll plugin for iOS, as well as the React Native Share plugin to share with the native share sheet on Android since saving to photos didn't work. While this allowed me to get the image to the user to some degree, it wasn't the ideal solution for my needs.

Solution: Implement scoped storage for Android Q+ with legacy storage fallback

Because the React Native community didn't have a solution for my needs, I decided to develop one. While the react-native-cameraroll plugin sort of worked on iOS, it wasn't perfect—it would crash the app if the user had declined to share access to their photos.

I ended up developing the plugin I called React Native Save Base Sixty Four. The naming is unfortunately quite verbose but numbers in package names get marked as spam by NPM and prevent publishing.

The plugin I developed solves the following problems, which are limited in scope to an image that is in the format of an encoded base64 string.

On iOS:

  • Enable iOS users to save a base64 image to their photos
  • Allow the React Native app to handle the case that the iOS users decline to grant permissions to photos

On Android:

  • Enable users on Android 10 and higher to save a base64 image to their photos on their device
  • Enable users on Android 9 and older to save a base64 image to their photos on their device
  • Allow the React Native app to handle the case that the user declines to grant permissions to photos

The plugin also allows user to share a base64 image via the native share sheet on both platforms, but if that's all you need to do, I would recommend you use the React Native Share plugin as it's more mature and is supported by the community and handles base64 strings perfectly. I added this functionality to my plugin because it was simple enough, and I could use the same plugin for both platforms and uninstall the other two from my project.

How to use this plugin

The full instructions can be found in the project readme, including instructions for how to modify your Android manifest and iOS Info.plist file, but here's a snippet. It uses the promises API, which can be implemented with a set of .then().catch() blocks or async await.

import SaveBase64Image from 'react-native-save-base-sixty-four';

// Save to device
//    with async await
try {
  const success = await SaveBase64Image.save(base64ImageString, options);
  if (!success) {
    // 😭 user did not grant permission
  }
} catch (error) {
  // 💥 there was a crash
}

//    with promises
SaveBase64Image
  .save(base64ImageString, options)
  .then((success) => {
    if (!success) {
      // 😭 user did not grant permission
    }
  })
  .catch((error) => {
    // 💥 there was a crash
  });

The linked readme also includes further instructions on how to implement permission checking on Android. Legacy versions of Android require you to prompt the user when you run the app, while Android 10+ does not because access is scoped to files you create. Older versions of Android will crash if you try to access something you don't have permission to access. This snippet is a handy utility function for React Native apps to check Android permissions. It depends on the react-native-device-info plugin. The example app in the plugin I developed has an example of how to implement the plugin with permission checking.

Implementing scoped storage for Android 10+

I have provided some links below in the Further reading section to specifics on implementing scoped storage. The TL;DR is as follows:

  • Convert your base64 encoded string into a Bitmap. For this, you can use Android SDK classes Base64 and BitmapFactory to accomplish this task. Here is the source.
  • Implement the ContentResolver API to create an output stream and pass the bitmap to that stream.

The full source code can be viewed on the Github page but most of it is in this file. The proof of concept app is available for download on AppCenter.

Gratitude 🙏

I must give a huge thank you to the team that built the bob tool for scaffolding React Native modules as it was a pleasure to work with and saved me a huge amount of time.

Further reading