The CommonsBlog


"Elements of Android Jetpack" Version 0.6 Released

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

This update adds two new chapters:

It also updates everything for Android Studio 3.5.3, adds some more Android 10 notes, updates many of the dependencies used in the book, and fixes lots of bugs.

Dec 11, 2019


Securing Jetpack Compose

Jetpack Compose is nothing short of a complete overhaul of how we create Android UIs. This sort of major change is unusual, to say the least. This gives us a great opportunity to fix all sorts of limitations of the preceding system… including how it impacts user privacy and security. Two examples are FLAG_SECURE and password text entry.

Compose and FLAG_SECURE

You can add FLAG_SECURE to an Android window to prevent that window from being included in screenshots and screencasts. However, you need to have access to the window in order to be able to do that. Adding FLAG_SECURE to an activity is easy. Adding FLAG_SECURE to a dialog is not that bad. However, lots of widgets and other classic UI elements create windows: Spinner, AutoCompleteTextView, ShareActionProvider, etc. We do not have access to their windows, so we cannot add FLAG_SECURE to them. And the implementations of those widgets fail to set FLAG_SECURE themselves, if they are being used from a window that itself has FLAG_SECURE. As a result, the screenshot blocking is leaky, and resulted in security flaws in password managers among other apps.

Ideally, Jetpack Compose allows us to specify a policy that results in FLAG_SECURE being used by any windows that it creates. Even better would be if the default policy were to propagate FLAG_SECURE: if FLAG_SECURE is set on an activity or fragment that is displaying a composable, then those composables should also set FLAG_SECURE on any windows that they create.

At least as of a month ago, none of that was available.

So, I filed issues to get FLAG_SECURE be honored in popup windows and dialogs.

Compose and Passwords

A general recommendation with passwords is to wipe them out of memory as soon as you no longer need them. That way, security flaws in the app or certain other types of attacks can no longer obtain (“exfiltrate”) those passwords.

In Java, though, String is immutable, and the same is true in Kotlin/JVM. This is why in Java it is generally recommended to use char[], as you can replace the array elements.

EditText uses an Editable, which is a form of CharSequence. We can get at the char[] and clear it as needed.

Unfortunately, at least as of a month ago, PasswordTextField uses String instead of char[]. Ideally, PasswordTextField switches to char[] (perhaps wrapped in a CharSequence or something). So, I filed an issue for that too. However, yesterday that issue’s priority was dropped, which is not reassuring.

How You Can Help!

If you are a security expert or a seasoned developer, and you can think of other privacy/security aspects of a UI toolkit that might not be handled by Jetpack Compose, your input would be very valuable. If you are in position to work with Compose and can determine whether your concerns are addressed, that would be wonderful, and please file issues for any gaps that you find. If you know of areas of concern but are not in position to see how Compose fares, feel free to contact me, and I will see what I can find.

Also, keep tabs on the issues that I filed already (linked above), if they interest you.

Since Jetpack Compose is shipping in library form, there is always the possibility of security-related fixes being added after a 1.0 edition of Compose ships. However, I suspect that it will be much easier to get these problems solved now while APIs are fluid.

I am hoping that Jetpack Compose can be world-class, not just in terms of the programming APIs and end-user experience, but also in terms of privacy and security.

Dec 10, 2019


More on the Missing SAF

Last week, I mentioned:

The biggest is that device manufacturers may unilaterally eliminate the Storage Access Framework, by removing or replacing the activities that handle ACTION_OPEN_DOCUMENT and kin. Hopefully, manufacturers will stop doing this, as Android and its apps become more reliant on SAF. However, since Google does not seem to test whether devices support SAF, there is no real pressure for device manufacturers to allow SAF to function.

The link in that quoted section points to an issue that I filed two years ago, around the time that I wrote my original post about the missing SAF. I had not received any significant response to that issue… until this past Wednesday, when I found out that Google does indeed test SAF.

Sometimes.

So, first, I would like to apologize for mis-interpreting the original lack of response to my issue and for mis-interpreting the problem.

However, the response to the issue serves as a useful illustration of why we have compatibility problems despite the Compatibility Test Suite (CTS).


To the best of my knowledge, there is no iCanHazSAF() method, or the equivalent, anywhere in the Android SDK, to tell us if the device supports the Storage Access Framework. Similarly, there is no <uses-feature> value for this, the way there is for app widgets or WebView.

The closest thing that we had is to see if the device supports whatever SAF Intent action(s) we want to use, such as using resolveActivity() on Intent. If that returns null, we know that we cannot successfully start that activity. In principle, some users might not have access to SAF activities due to restricted profile configuration (e.g., work profiles). So, we should be checking for the availability of these activities before starting them, just in case.

(in reality, not everyone does this, and I’m as bad as anyone at forgetting to check these particular Intent actions)

So, from a compatibility standpoint, historically developers were stuck with:

  • If the SAF Intent actions resolve, assume that the activities work correctly

  • Hope that devices that do not support the SAF do not export activities that match the SAF Intent structure

Unfortunately, that second bullet did not hold true in practice, as my SAF issue points out.


Earlier, I said that Google tests the SAF in the CTS “sometimes”. Wednesday’s issue update states:

These intents have been very thoroughly CTS tested since they were first introduced in Android KitKat, so you can rely on them on all handheld devices. However, certain product teams have decided that these intents are not needed on their products (such as watches and TVs), which is why the tests are skipped on those devices.

It was great to hear that CTS does test the SAF, at least on most devices. However, the described testing does not quite match what developers need.

Ideally, testing would look like this (using the Google engineer’s terminology for device types):

  • On a handheld device, the SAF Intent actions MUST resolve and their activities MUST work

  • On a watch or TV, the SAF Intent actions MAY resolve, but if they do resolve their activities MUST work

The comment suggests that the testing is more akin to:

  • On a handheld device, the SAF Intent actions MUST resolve and their activities MUST work

  • On a watch or TV, ¯\_(ツ)_/¯

This is what burns developers on TVs. Some device manufacturers (e.g., Xiaomi) exported activities matching the SAF Intent structure but without actually allowing the user to perform SAF actions. This passes the CTS, because the CTS skips these tests on TV devices. And so we try starting the SAF activities on these broken devices and get poor results.

I can understand why Google does not want to enforce SAF requirements on watches. I’m less convinced about the TV argument, and as another issue comment points out, without SAF on TVs, our ability to access public portions of external storage is really limited. But I could live with TVs not supporting SAF. The bigger problem is the fact that there is no clear-cut way to determine if the SAF is supported on a device and no clear-cut testing to ensure that devices that claim to support the SAF really do.

If we assume that the statement from the Google engineer is literally true, then we “know” whether SAF is available or not based on hardware type:

  • android.hardware.type.television = no (even though some devices might actually support it)

  • android.hardware.type.watch = no

  • android.hardware.type.automotive = technically ¯\_(ツ)_/¯, but you hopefully are not trying to collect significant user input in this mode anyway

  • others = technically ¯\_(ツ)_/¯, but since there is no explicit hardware type for handheld devices, we have to treat this as “yes”

Unfortunately, AFAIK, this is not documented, and we do not know if this is really what Google wants and what device manufacturers are honoring. Plus, having the vast majority of devices covered by ¯\_(ツ)_/¯ does not inspire confidence. But, it is probably our best solution right now.


There is no nefarious intent here (not even a nefarious Intent). This is just another example of edge and corner cases not being covered in specifications, testing, or both. This sort of thing abounds in software development; this SAF scenario is far from unique. It’s just that at “Android scale”, even edge and corner cases might represent a million users or more. This is why “Android scale” compatibility is hard, and why we developers run into problems despite Google’s best and earnest efforts.

Dec 08, 2019


Scoped Storage Stories: Problems with SAF

Android 10 is greatly restricting access to external storage via filesystem APIs. Instead, we need to use other APIs to work with content. This is the sixth post in a series where we will explore how to work with those alternatives, starting with the Storage Access Framework (SAF).


The Storage Access Framework is “the go-to solution” for getting access to arbitrary content on Android 10 and higher. And, since it works back to Android 4.4 (for documents) and 5.1 (for trees), it is likely to be a solution that you can apply for all versions of Android that your app supports.

However, it has a bugs and undocumented limitations.

I have filed issues about these. Unfortunately, my track record of getting anyone to pay attention to issues is not good, so all of these problems still exist.

The biggest is that device manufacturers may unilaterally eliminate the Storage Access Framework, by removing or replacing the activities that handle ACTION_OPEN_DOCUMENT and kin. Hopefully, manufacturers will stop doing this, as Android and its apps become more reliant on SAF. However, since Google does not seem to test whether devices support SAF, there is no real pressure for device manufacturers to allow SAF to function. UPDATE 2019-12-08: the situation is complicated.

ACTION_CREATE_DOCUMENT implies that we have write access to the location identified by the returned Uri. We tend to assume that ACTION_OPEN_DOCUMENT also grants us write access to that location. That is not a safe assumption, as ACTION_OPEN_DOCUMENT may return a Uri that represents read-only content. We have no way to request that the ACTION_OPEN_DOCUMENT UI limit the user to read-write locations. And while a document provider might include some hints that the Uri it returns points to read-only content, DocumentFile does not support read-only content correctly.

The Storage Access Framework UI has this annoying habit of starting up in a mode that hides most of external storage. The user has to tap an item in an overflow menu to get that to show up. It turns out that there is an undocumented extra to make external storage visible in the SAF UI. However, since it is undocumented, it may be unreliable.

Among the issues filed by others, we have:

With luck, some of these will get addressed eventually, though I do not have a lot of hope.

Of course, some of these problems are less with the Storage Access Framework itself and more with particular implementations of the SAF UI or of document providers. I hold out even less hope that these will ever get fixed. In many respects, this is the single biggest problem with the death of external storage: the replacement solutions involve multiple parties and multiple sources of bugs. Google needed to ensure that the SAF ecosystem was implemented properly first, and that did not happen.


Previously, I covered:

In my next post in the series, I will switch gears and start covering MediaStore as an alternative to the SAF for accessing and creating content.

Dec 01, 2019


Scoped Storage Stories: DocumentsContract

Android 10 is greatly restricting access to external storage via filesystem APIs. Instead, we need to use other APIs to work with content. This is the fifth post in a series where we will explore how to work with those alternatives, starting with the Storage Access Framework (SAF).


DocumentFile is easy to use and gives us an API that resembles java.io.File for basic operations. Couple that with ContentResolver to be able to get streams on Uri-identified content, and we seem to have a reasonable API to use.

However, that API does not always perform that well.

An oft-cited complaint about the Storage Access Framework — particularly as the long-term alternative to filesystem operations — is speed. Figures of 100x slowdowns are cited. Some of that is hyperbole, but there are definite performance considerations when using DocumentFile.

DocumentFile and its support classes use DocumentsContract to work with SAF-supplied Uri values. DocumentsContract provides a ContentResolver-centric API for obtaining information about a document. That is because the implementation of SAF is based on ContentProvider, specifically the DocumentsProvider subclass. As a result, the underlying protocol is based on Cursor objects, which is a workable but clunky approach. However, it is the only option available to us.

Worse, multiple IPC hops seem to be made as part of using DocumentsContract:

  • You use DocumentsContract and ContentResolver, such as using DocumentsContract.buildChildDocumentsUriUsingTree() and query() to find the child documents for a tree Uri

  • The Uri returned by DocumentsContract.buildChildDocumentsUriUsingTree() is one pointing to a system-supplied ContentProvider, and we make one IPC hop to query() that provider

  • However, that provider serves as a proxy, and it in turn makes a similar query() on the DocumentsProvider supporting this document, resulting in a second IPC hop

That adds overhead, overhead that is unavoidable.

However, DocumentFile does not always use the DocumentsContract API very efficiently. For example, here is its findFiles() implementation (from version 1.0.1):

@Nullable
public DocumentFile findFile(@NonNull String displayName) {
    for (DocumentFile doc : listFiles()) {
        if (displayName.equals(doc.getName())) {
            return doc;
        }
    }
    return null;
}

This seems innocuous. However, listFiles() is an abstract method, as DocumentFile supports both regular files and SAF-backed document trees. listFiles(), for a SAF Uri, retrieves all child documents from the provider (from version 1.0.1):

@Override
public DocumentFile[] listFiles() {
    final ContentResolver resolver = mContext.getContentResolver();
    final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
            DocumentsContract.getDocumentId(mUri));
    final ArrayList<Uri> results = new ArrayList<>();

    Cursor c = null;
    try {
        c = resolver.query(childrenUri, new String[] {
                DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
        while (c.moveToNext()) {
            final String documentId = c.getString(0);
            final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
                    documentId);
            results.add(documentUri);
        }
    } catch (Exception e) {
        Log.w(TAG, "Failed query: " + e);
    } finally {
        closeQuietly(c);
    }

    final Uri[] result = results.toArray(new Uri[results.size()]);
    final DocumentFile[] resultFiles = new DocumentFile[result.length];
    for (int i = 0; i < result.length; i++) {
        resultFiles[i] = new TreeDocumentFile(this, mContext, result[i]);
    }
    return resultFiles;
}

For cases where we need all of the children, that is not an unreasonable implementation. However, findFiles() needs only one child, not all of them. It would be more efficient to query() for children whose DISPLAY_NAME matches the desired value… but that is not how DocumentFile is implemented at the present time. Perhaps it will be improved in the future, but for now, we have an inefficient implementation.

So, use DocumentFile, but if you run into performance issues, consider working directly with DocumentsContract, using the implementation of DocumentFile as a guide. DocumentsContract is not a particularly developer-friendly API, but it is “bare metal” in terms of SAF, so you will not get better performance.


Other posts in this series include:

Nov 23, 2019


Older Posts