Mark L. Murphy's Blog, page 19
December 11, 2019
���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.
December 10, 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.
December 8, 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.
December 1, 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.
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:
Due to the lack of CTS testing, not only do devices ship with intentionally-broken
SAF UIs, but some devices ship with bugs in their SAF support
ACTION_CREATE_DOCUMENT does not allow the user to overwrite existing content
Google���s own Google Drive supports a subset of SAF functionality,
limiting options for users and setting a poor example for other document provider
implementations
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:
The basics of using the Storage Access Framework
Getting durable access to the selected content
Working with DocumentFile for individual documents
Working with document trees
Working with DocumentsContract
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.
November 23, 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.
Previously, I covered:
The basics of using the Storage Access Framework
Getting durable access to the selected content
Working with DocumentFile for individual documents
Working with document trees
In my next post in the series, I will point out other problems with DocumentFile
and the Storage Access Framework, beyond the performance limitations of DocumentFile.
November 11, 2019
Final ���Elements of Android Q��� Released
Subscribers now have
access to the final version 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!
Since the shipping version of Android 10 did not change much from the last few
Q beta releases, there were few changes in this update. Mostly, it involved bug
fixes.
This is the final update to this particular book, given that Android 10 has now
shipped. If all goes well, I will have ���Elements of Android R��� available after
Google releases the first beta of the 2020 flagship Android version.
November 9, 2019
Scoped Storage Stories: Trees
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 fourth post in a series where we will explore how to work with those
alternatives, starting with the Storage Access Framework (SAF).
Working with individual pieces of content via ACTION_OPEN_DOCUMENT or
ACTION_CREATE_DOCUMENT is not that difficult and is not that different than
working with files.
However, suppose you need to work with several related pieces of content. For
example, you want to offer a manual backup mechanism, and rather than backing
up several files to a single ZIP, you want to offer a backup to a user-chosen
directory.
In principle, you could still use ACTION_CREATE_DOCUMENT for this, asking
the user for the location of each piece of content that you wish to create as
part of the backup. This is annoying for the user, as they have to go through
the ACTION_CREATE_DOCUMENT UI N times for your N pieces of content. And if
the user screws something up ��� such as choosing different directories instead
of the same directory for all of the content ��� your app may run into problems
in consuming that content later on.
What would be better is if the user could create a directory for you, then grant
you access to that entire directory. You could then put your content into that
directory as you see fit.
Unfortunately, that is not completely supported. What is supported is for
the user to choose some ���directory���, via ACTION_OPEN_DOCUMENT_TREE, on Android 5.1+.
Your app can then create its own ���sub-directory��� in the user-chosen location, then
put your content there.
Here, I have ���directory��� and ���sub-directory��� in quotes, because technically that
is not what you are working with. You are working with document trees, reflecting
the name ACTION_OPEN_DOCUMENT_TREE. Whether or not a given document tree
reflects some directory on some filesystem on some machine is up to the implementers
of the user-selected document provider. In practice, it is likely to be a filesystem
directory on the device, as few cloud storage providers seem to support ACTION_OPEN_DOCUMENT_TREE.
At the outset, you use ACTION_OPEN_DOCUMENT_TREE similarly to how you use
ACTION_OPEN_DOCUMENT. You create an Intent with ACTION_OPEN_DOCUMENT_TREE
as the action, then pass that Intent to startActivityForResult():
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_TREE)
Then, in onActivityResult(), you can get a Uri that represents the tree:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
data?.data?.let { useTheTree(it) }
}
}
private fun useTheTree(root: Uri) {
TODO("something useful, but on a background thread please")
}
From there, what you do will vary based on scenario.
Suppose you want to save a backup of several files, as suggested earlier. You
could:
Wrap that Uri in a DocumentFile, via DocumentFile.fromTreeUri()
Call createDirectory() on that DocumentFile to create some sort of sub-tree
and give you a DocumentFile pointing to it
Call createFile() on the sub-tree DocumentFile for each file that you want
to back up, to get a DocumentFile representing the backup of that file
Call getUri() on the backup DocumentFile and use that with ContentResolver
and openOutputStream() to get an OutputStream that you can use to create the backup itself
Suppose instead that you want to restore from the backup. If your instructions
are for the user to choose the sub-tree that you created above, you could then
do this with the Uri from ACTION_OPEN_DOCUMENT_TREE:
Wrap that Uri in a DocumentFile, via DocumentFile.fromTreeUri()
Call listFiles() to get an array of DocumentFile objects, representing
the contents of that tree
Examine those to see if they look like your backed-up content, showing some
error to the user if it looks like they chose the wrong place
Use getUri() for each of those DocumentFile objects and use that with
ContentResolver and openInputStream() to get an InputStream that you can
use to restore the content from the backup
There are many other patterns that you could follow ��� these are just
for illustration purposes. The key is that you use DocumentFile much like you
would use File for creating or iterating over the contents of a directory.
While the details are different, the general approach is the same as with classic
Java file I/O.
Previously, I covered:
The basics of using the Storage Access Framework
Getting durable access to the selected content
Working with DocumentFile for individual documents
In my next post in the series, I will point out the limitations of DocumentFile for
batch operations, and will point out alternative approaches that will improve
performance.
November 4, 2019
���Exploring Android��� Version 0.9 Released
Subscribers now have
access to an update to Exploring Android,
known as Version 0.9, in PDF, EPUB, and MOBI/Kindle formats, in addition to the
online reader. Just log into
your Warescription page and download
away, or set up an account and subscribe!
After an entirely-too-long delay (sorry!), I am finally getting back into updating
my various books. First up is Exploring Android. Mostly, this update contains bug
fixes, along with changes to support Android Studio 3.5.1. It also adopts Flow
for getting query results from Room, which in turn affects how the ToDoRepository
gets used and tested.
If you are wondering ���what happened to 0.8?���, 0.9 is a version number that I use
for a ���release candidate���. I plan to publish 1.0 in early January, with no
changes other than bug fixes and perhaps some cross-references with the other
books. That might be followed fairly quickly by Version 1.1 for Android Studio 3.6, depending
on Google���s IDE release schedule. From there, I will update the book a few
times per year, based on Android Studio releases (and possibly Android releases,
if some change breaks backwards compatibility).
Next, now that my Pixel 4 has arrived, I will wrap up the final edition of
Elements of Android Q.
November 2, 2019
Scoped Storage Stories: DocumentFile
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 third post in a series where we will explore how to work with those
alternatives, starting with the Storage Access Framework (SAF).
Whether you use ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT, you wind up
with a Uri that points to the ���document���. Your primary use of that Uri is
with ContentResolver and its openInputStream() and openOutputStream() methods,
so you can read and/or write the document. This is not that different from having
a File and using the FileInputStream and FileOutputStream constructors to
be able to read and/or write the document.
However, File has a lot of other useful methods, such as:
getName() to get the filename
exists() to confirm that an actual file exists at the path wrapped by the File
delete() to delete the file
and so on
The way to get similar functionality for your document Uri is
via DocumentFile.
DocumentFile can wrap a File or a document Uri and give you an API that
resembles a subset of File functionality, including the three methods that I listed
above.
If you have a document Uri, you can wrap it in a DocumentFile via
DocumentFile.fromSingleUri(). You can then call getName(), exists(), delete(),
and other methods, such as:
getType() to get the MIME type associated with the content
length() to get the length of the content (i.e., how many bytes you could read in
from the InputStream)
renameTo() to rename the content
and so on
However, there are some limitations:
The ���name��� for a piece of content is not necessarily a filename with an extension.
It is a ���display name���, and it should be something that the user might recognize.
However, if you are expecting SomethingLike-This.png, you may be disappointed. The
display name might be a classic filename, but it does not have to be.
canWrite() is somewhat broken, in that
it may return true for a Uri that you cannot write to
Note that you can also get a DocumentFile for a File via DocumentFile.fromFile().
This allows you to treat document Uri values and files the same, so that portions
of your app can be written independently of where the data comes from.
Previously, I covered:
The basics of using the Storage Access Framework
Getting durable access to the selected content
In my next post in this series��� while I am not
the Lorax, I will speak for the
(document) trees.
October 27, 2019
Scoped Storage Stories: Durable Access
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 second post in a series where we will explore how to work with those
alternatives, starting with the Storage Access Framework (SAF).
So, you used ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT to get a Uri
that you can use to read and/or write some content. Great!
You got that Uri in the scope of some Activity. Perhaps you called startActivityForResult()
directly on the Activity. Or, perhaps you called startActivityForResult()
on some Fragment which, in turn, is managed by some Activity.
The good news is that your Activity should be able to work with that Uri,
even across configuration changes.
The bad news is that your rights to that Uri content end there, by default.
Using the Uri in Other Components
You might try using that Uri from other components in your app. Perhaps you
stick that Uri in some shared repository and have another Activity get that
Uri and try using it. Or, perhaps you try passing the Uri to some Service,
or try using it via WorkManager.
You will find out fairly quickly that your Uri rights vanish. You have access
to the content identified by the Uri from the Activity that receives the
Uri, and that���s it.
You can pass along those rights to other components of your app, or even
components of other apps. To do that, though, you need to:
Pass the Uri in the ���data��� facet of an Intent
Add Intent.FLAG_GRANT_READ_URI_PERMISSION and/or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
to that Intent
Use that Intent to start the other component (e.g., startActivity(),
startService())
val intent = Intent(this, TheOtherActivity::class.java)
.setData(theUri)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
The Intent flags indicate that you want to pass along the read or write
rights that your component has to the component that you are starting.
Note that this works for any content Uri, not just those from the Storage
Access Framework. ACTION_GET_CONTENT, for example, works the same as does
ACTION_OPEN_DOCUMENT in terms of these component-level access rights.
Getting Long-Term Access
Using the Intent flags has clear limitations:
Not everything offers you a place to put those flags (e.g., WorkManager)
Eventually, your process gets terminated, at which point all of your rights
lapse
For Storage Access Framework Uri values, you can request long-term access
to the content via takePersistableUriPermission() on a ContentResolver.
You just pass in the Uri along with
Intent.FLAG_GRANT_READ_URI_PERMISSION and/or Intent.FLAG_GRANT_WRITE_URI_PERMISSION.
This will give you the ability to work with that Uri not only between components,
but between process invocations. You can save the String representation of
the Uri somewhere (e.g., SharedPreferences) and use it again in the future.
However, bear in mind that there is no guarantee that the underlying content
itself is durable. The user might use some other app to move or delete that
content, at which point your persisted Uri becomes pointless. You will need to
be able to deal with the FileNotFoundException and related forms of IOException
that you will get when this sort of thing occurs.
In addition to takePersistableUriPermission(), there is:
releasePersistableUriPermission(), to say that you no longer need durable
access; and
getPersistedUriPermissions(), to retrieve information about all outstanding
persisted permissions that you took using takePersistableUriPermission() and
have not released using releasePersistableUriPermission()
What Else Is There?
Previously, I covered
the basics of using the Storage Access Framework.
Upcoming posts in this series will include:
The role of DocumentFile
How to work with document trees from ACTION_OPEN_DOCUMENT_TREE
And more!


