The CommonsBlog

Android Q, uiMode, and Configuration Changes

If you are a regular follower of my blog posts, I have complete confidence that you are a fine Android app developer and that all of your apps support configuration changes.

However, there seem to be a lot of apps out there that don’t.

In many cases, that is because the apps do not support orientation changes. The UIs are locked to portrait or landscape. Orientation changes are the most common source of configuration changes on phones. The other configuration change triggers are less common, with the second-most common one probably being changing locale. So, some developers think they can skip dealing with configuration changes.

Android Q puts far greater emphasis on the uiMode configuration change, though. That gets invoked whenever the device switches between normal and dark mode. The user might do that manually, and it might kick in when the device battery gets low. It would not shock me if some devices offer other options for automatically toggling into dark mode, just as stock Android 9.0 offers time-based “night light” mode (a blue light filter).

And, if your app does not handle configuration changes properly, your UI will lose context when the device switches between normal and dark mode, as your visible activities and their fragments get destroyed and recreated. Symptoms include:

  • Losing information in your widgets, because that information is not part of automatic saved instance state logic (e.g., enabled status)

  • Forgetting about a dialog, because you are showing it directly instead of using a DialogFragment, so the dialog vanishes after the configuration change

  • Reloading data from disk or the network, because you are not using a ViewModel, retained fragment, or other way to keep that data around across the configuration change

So, even if your UI is locked to a single orientation, please add configuration change support. Android Q gives us another easy way to test configuration changes: add the “Dark Theme” tile to the notification shade, then toggle between normal and dark modes using the tile.

Also, if you are manually managing configuration changes via android:configChanges, there is a good chance you will want to manage uiMode manually as well. For example, if your app involves continuous playback (e.g., a video player), you might not want to interrupt playback just because the device switched between normal and dark mode.

Jun 19, 2019

"Elements of Android Q" Version 0.4 Released

Subscribers now have access to Version 0.4 of Elements of Android Q, in PDF, EPUB, and MOBI/Kindle formats. Just log into your Warescription page to download it, or set up an account and subscribe!

The chapter on scoped storage was overhauled again, owing to the substantial changes that showed up in Q Beta 4.

Two short new chapters were added on dark mode and gesture navigation, replacing previous sections in the “Other Changes” chapter.

That “Other Changes” chapter, and bits of other chapters, were updated for Q Beta 4. Material was added on the audio capture APIs as well.

At this point, I am expecting two more updates to this book:

  • One when Android Q ships as Android 10, to make any tweaks based on the final shipping versions plus perhaps add a bit more of new material

  • One when the next generation of Pixel devices ships, to cover any Android Q things that are at least initially unique to those devices

It is possible that there will be another update sooner than those, if, say, Q Beta 5 makes unexpected changes to Android Q. That is unlikely.

Jun 18, 2019

DevFest DC Update!

On Friday, I will be speaking at DevFest DC on the changes to storage in Android Q.

That’s not new.

What is new is that my presentation has been moved to 10am from its original ~4pm slot.

So, if you were aiming to attend my talk, make sure you get there for the morning sessions!

Jun 12, 2019

"Elements of Android Jetpack" Version 0.4 Released

Subscribers now have access to Version 0.4 of Elements of Android Jetpack, in PDF, EPUB, and MOBI/Kindle formats. Just log into your Warescription page to download it or read it online, or set up an account and subscribe!

I apologize for the long time between updates of this title — I was focusing on Exploring Android, Elements of Android Q, and helping out customers.

Besides many bug fixes, this update adds several new chapters:

It also updates everything for Android Studio 3.4.1.

The next update for this book most likely will be in August. Between now and then, I will be working on Elements of Kotlin and other good stuff for you!

Jun 11, 2019

The Death of External Storage: The End of the Saga(?)

If Q Beta 4 really does have the final APIs, then we may now have the final implementation of scoped storage. While external storage as we know it is still going away, it will not be for a while, and the user experience should be reasonable.

So, let’s review where we are now, with another set of fictionally-asked questions (FAQs):

What Are the Options?

Apps can either have normal or legacy storage.

With legacy storage, everything behaves as it did in Android 4.4 through 9.0:

  • You can use getExternalFilesDir() and similar directories without permissions

  • You can work with the rest of external storage if you hold READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE

Without legacy storage, apps still can use getExternalFilesDir() and similar directories without permissions. However, the rest of external storage appears to be inaccessible via filesystem APIs. You can neither read nor write. This includes both files created by other apps and files put on the device by the user (e.g., via a USB cable).

It is conceivable that there are some types of content that are still visible via the filesystem, as the documentation has:

An app that has a filtered view always has read/write access to the files that it creates, both inside and outside its app-specific directory

So, I cannot rule out scenarios where the app can work outside of getExternalFilesDir() and similar directories via the filesystem APIs. I just have not found one yet.

What Changed From Q Beta 3?

Q Beta 3 also had two modes: legacy and sandboxed. Apps with sandboxed external storage could read and write everywhere on external storage… because they were not working with the real external storage. Instead, they would read and write from a sandbox. While this allowed existing code to keep working, it was costly from a user experience standpoint, as many users would not know to wander into the Android/sandboxes/ directory to find an app’s sandboxed edition of external storage.

Now, instead of apps having a “sandboxed” separate bit of external storage, they have a “filtered” view of the real external storage.

What Changed From Q Beta 1 and 2?

Too much changed to list here. Can’t we focus on more pleasant topics?

What is the User Experience?

Whether apps have normal or legacy external storage does not matter to the user, to a large degree. Files show up wherever they would have shown up originally. This is particularly important for apps with a lower targetSdkVersion that may never get updated — users can use those apps the same way they have for years.

What Am I Supposed to Use, Then?

You are welcome to continue using getExternalFilesDir(), getExternalCacheDir(), getExternalMediaDirs(), getExternalCacheDirs(), and getExternalFilesDirs(), if you were using those before.

However, the Storage Access Framework (e.g., ACTION_OPEN_DOCUMENT) is the primary way that apps should work with user-supplied content.

Apps that have a focus on media — audio, video, and images — can use the MediaStore. Note, though, that you need READ_EXTERNAL_STORAGE to be able to see other apps’ content in the MediaStore.

One thing that you are not supposed to use is an <intent-filter> supporting the file scheme. You will not be able to read files written by other apps, so if you get a Uri like that, probably it is useless to you. The technique that I wrote about previously, to use <activity-alias> to support file only on older devices, should still work.

I Don’t Like Change — How Do I Stick With What Worked Before?

For Android Q, you can add android:requestLegacyExternalStorage="true" to your <application> element in the manifest. This opts you into the legacy storage model, and your existing external storage code will work.

Technically, you only need this once you update your targetSdkVersion to 29. Apps with lower targetSdkVersion values default to opting into legacy storage and would need android:requestLegacyExternalStorage="false" to opt out.

What Happens Next Year?

The documentation still has:

Scoped storage will be required in next year’s major platform release for all apps, independent of target SDK level.

IMHO, this is unwise. Saying that it is required for targetSdkVersion 29 and higher is reasonable. Saying that it is required for all targetSdkVersion values means that lots of legacy apps will crash, as while they hold WRITE_EXTERNAL_STORAGE, they would be ineligible to write to previously-valid locations.

My hope is that Android R will “only” deprecate and ignore developer-supplied android:requestLegacyExternalStorage values, while setting the defaults to be:

  • true for targetSdkVersion 28 and older

  • false for 29 and newer

That ensures the maximum compatibility with legacy apps while still enforcing the new rules for actively-maintained apps.

It is unlikely that Android Q itself will change, though I cannot rule out an Android 10.1 (Q MR1) that messes with this stuff.

So, by August 2020 or thereabouts — whenever Android R ships — you will need to adapt to the new normal.

The idea is that you start adapting now. For some apps, switching to the Storage Access Framework will be easy. For some apps, it will be painful. Do not wait until 2020. Start migrating your apps now to using the alternative approaches. The Storage Access Framework (mostly) works back to Android 4.4, for example, and so many apps will have access to that set of Intent actions.

You can create a dedicated build type or product flavor where you set android:requestLegacyExternalStorage="false" and opt out of the legacy storage support. There, you can see what breaks and start creating plans to fix it.

Hey, Why Am I Seeing Deprecation Warnings?

If your code refers to Environment.getExternalStorageDirectory() or Environment.getExternalStoragePublicDirectory(), you will see that they are deprecated. They still work, but the deprecation warning is yet another nudge to remind you that you need to stop using those.

Once you no longer have the legacy storage model, those directories are unusable, which (presumably) is why they are deprecated.

How Can My Library Know What To Do?

In a library, you do not control whether the app is in normal or legacy mode. If you need different code for those two cases, you can call Environment.isExternalStorageLegacy(), which will return true if the app is in legacy mode, false otherwise.

What Happens When the App is Uninstalled?

Any files that you wrote to external storage in getExternalFilesDir() get removed, as normal.

If you were thinking of switching your Environment.getExternalStorageDirectory() and Environment.getExternalStoragePublicDirectory() code to use getExternalFilesDir(), that will work, but the cost is that your files go away when the app is uninstalled.

For files that are owned by the user and should remain after your app is removed, use the Storage Access Framework or MediaStore.

So, There Is Nothing I Need to Worry About Today?

Well, there may be. Some script-kiddie workarounds to avoid existing limits may cause your app to break on Android Q. Here are two examples:

Inhibiting FileUriExposedException

Back in Android 7.0, Google added logic to StrictMode to see if you have a Uri in your Intent that has the file scheme. If it does, and you use that Intent for something like startActivity(), your app would crash with a FileUriExposedException. The right solution is to use FileProvider or otherwise get a content Uri. However, some developers elected to reconfigure StrictMode to block that check and prevent the exception.

Technically, that hack still works, in that you should not crash with the exception. However, apps that opt out of the legacy filesystem support cannot access your file. So they crash when they try to use your Uri, and your users lose whatever functionality you were trying to offer by starting that third-party activity.

Reading DATA

Some developers, particularly for ACTION_PICK from the MediaStore, would query the MediaStore and read the DATA column to try to get a filesystem path corresponding to the picked content. That has not been reliable in quite some time, but I am sure that some developers are still using it.

Well, in Android Q, the DATA column is blocked, and you will not be able to get the values.

You will need to use the Uri that you get as intended, with a mix of ContentResolver (e.g., openInputStream()) and DocumentFile.fromSingleUri() (e.g., getName()).

Is There Anything Else? This Is Getting Rather Long.

Keep an eye out for future Q beta releases, as while the API is supposed to be stable, there still might be functionality changes.

If I stumble upon any new problems, I’ll write about them. If you encounter scoped storage bugs new to Q Beta 4, ping me on Twitter or reach out via email.

Jun 07, 2019

Older Posts