Mark M. | has entered the room |
Mark M. | turned on guest access |
Aug 16 | 4:00 PM |
Tad F. | has entered the room |
Tad F. |
Hi Mark!
|
Mark M. |
hello, Tad!
|
Mark M. |
how can I help you today?
|
Tad F. |
Hey I've got some questions for you around a topic you've posted some on back a couple years - it is how to properly interface with the external storage APIs to save media taken with the camera (image, video) such that it is available to other apps, not just mine.
|
Tad F. |
So I've read all those posts
|
Tad F. |
And I've gone over the doc.
|
Tad F. |
I still have a couple of what are probably fundamental questions.
|
Tad F. |
According to my reading, it seems that if my app (21+) wants to access the area returned from the API getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) for either reading or writing, the permissions for read/write storage must be declared and user must allow. However - I notice when I turn this permission off, then attempt to open an image file the Android picker still lets me get into these directories - so clearly I am "reading" them. How is this possible?
|
Mark M. |
"so clearly I am "reading" them" -- not really
|
Mark M. |
ACTION_PICK, ACTION_GET_CONTENT, and ACTION_OPEN_DOCUMENT bring up system activities or activities from other apps
|
Tad F. |
ok why isn't being able to see them in the picker and choose one "reading" it?
|
Tad F. |
Maybe I don't understand "reading"
|
Mark M. |
reading means "getting the bytes"
|
Mark M. |
and in terms of external storage, it means "getting the bytes via filesystem APIs"
|
Tad F. |
OK - so if I tried to do anything with the Uri returned from the picker I'd get a security exceptions
|
Mark M. |
no
|
Mark M. |
you are being granted access to that content by the picker
|
Mark M. |
that other app -- typically a system one -- has rights to the content
|
Mark M. |
it grants you temporary rights to the user-chosen content
|
Mark M. |
but not to arbitrary other content
|
Tad F. |
So I could open the file up and put it in imageview for the life of the app that ALSO had the picker active.
|
Tad F. |
i.e. I start my app, use the picker, find a file, open and view it.
|
Mark M. |
stop!
|
Tad F. |
But I can't store that URI and use it tomorrow once the app has shut down today?
|
Mark M. |
stop!!
|
Aug 16 | 4:05 PM |
Mark M. |
let's make this a bit more concrete: what Intent are you using to bring up what you refer to as the "picker"?
|
Tad F. |
Hang on
|
Tad F. |
ACTION_GET_CONTENT
|
Mark M. |
OK
|
Mark M. |
note that this is ACTION_GET_CONTENT -- it is not called ACTION_GET_FILE
|
Mark M. |
that is important
|
Mark M. |
the user does not have to choose a file on the filesystem that you can access
|
Mark M. |
and so you really need to be *very* judicious about where you use the word "file"
|
Tad F. |
Ok point taken.
|
Mark M. |
by saying things like "I start my app, use the picker, find a file, open and view it", you are putting yourself in a mental model that isn't accurate
|
Mark M. |
so, you use ACTION_GET_CONTENT, and the user picks a piece of content and you get a Uri back
|
Mark M. |
you have temporary rights to access the content identified by that Uri
|
Mark M. |
see https://commonsware.com/blog/2016/08/10/uri-acc... for more
|
Mark M. |
"But I can't store that URI and use it tomorrow once the app has shut down today?" -- correct
|
Mark M. |
think of the Uri as being akin to a URL from a Web app, where the app requires authentication
|
Mark M. |
so long as the user's session has not timed out, that URL is perfectly fine
|
Mark M. |
once the user's session times out, the URL is ineffective
|
Mark M. |
same thing here
|
Mark M. |
if you use ACTION_OPEN_DOCUMENT, there is an option to get longer-term access rights
|
Tad F. |
OK - I am also using the takePersistableUriPermission API on the resulting Uri to be able to store it and open it tomorrow.
|
Mark M. |
that only works with ACTION_OPEN_DOCUMENT
|
Mark M. |
it *might* work for *some* Uri values you get from ACTION_GET_CONTENT, but don't assume that it will
|
Aug 16 | 4:10 PM |
Tad F. |
OK - right. Hadn't gotten that far yet in this implementation to bump up against that error :)
|
Tad F. |
Let me start over.
|
Tad F. |
I am using ACTION_GET_CONTENT in the part of my app that allows the user to choose existing content
|
Tad F. |
That all has been working fine, using the takePersistableUriPermission approach
|
Tad F. |
Sorry - ACTION_OPEN_DOCUMENT
|
Mark M. |
ah, OK, that feels better
|
Tad F. |
Then I just started implementing a feature whereby the user gets a dialog that allows them to either take a picture/video and "add it to the app", or choose from existing media they already have on their device.
|
Mark M. |
hmmm... "add" may be too generic of a verb
|
Tad F. |
So what I do is allow them to choose media from whatever the SAF presents to them.
|
Mark M. |
if you are holding onto Uri values, "link" might reflect what you are doing better
|
Tad F. |
Then I use the takePersistableUriPermission API on it, and store that Uri in my own DB
|
Tad F. |
Then at runtime I use that Uri with Glide to show the image in an ImageVie
|
Tad F. |
ImageView
|
Tad F. |
All this works fine.
|
Tad F. |
Besides the risk that the Uri might not be valid sometime in the future, which I understand.
|
Tad F. |
And deal with in the app.
|
Tad F. |
Now - the second part is what I'm tripping up on.
|
Aug 16 | 4:15 PM |
Tad F. |
That is where I want to give the option for them to use the Camera to take a photo or video, then I will pass in the intent a Uri to the File that the Camera API will use to store the resulting media. That same Uri is what I would want to do the same thing on - takePersistableUriPermission.
|
Tad F. |
But I hadn't gotten that far, because when I started to write the code to store the image, I started down the path about learning which permissions are needed for which directories, etc.
|
Mark M. |
OK
|
Tad F. |
And I got confused when it appeared I could select files from the public PICTURES directory even though I had turned off my storage permission for the app.
|
Tad F. |
select content
|
Mark M. |
"select" meaning your existing ACTION_OPEN_DOCUMENT code?
|
Tad F. |
Yes
|
Mark M. |
you can use the Storage Access Framework without having external storage permissions
|
Mark M. |
that's because the user is always involved in the selection
|
Mark M. |
external storage permissions let you access the filesystem without user involvement (after the initial permission grant)
|
Mark M. |
so, what you are seeing is perfectly normal
|
Aug 16 | 4:20 PM |
Tad F. |
OK - so for example, my app allows user A to send media to user B via WebRTC. Service running on both sides manage this transfer. Since a service is doing the work, I would need storage permissions granted on both sides, if I read bytes from the public Pictures directory on A and save to the public Pictures directory on B. Correct?
|
Mark M. |
if you are doing it using filesystem APIs (e.g., java.io.File), yes
|
Mark M. |
and note that it won't work on Android Q (by default) and Android R+ (for all apps)
|
Tad F. |
vs. what?
|
Mark M. |
versus using the Storage Access Framework
|
Mark M. |
so if user A picks the content via ACTION_OPEN_DOCUMENT, you do not need external storage permission
|
Mark M. |
and if user B picks the "directory" via ACTION_OPEN_DOCUMENT_TREE, you do not need external storage permissiosn
|
Mark M. |
er, permissions
|
Tad F. |
so is it the SAF or the fact that the user instigated SAF?
|
Tad F. |
Sounds like it is user driven
|
Mark M. |
Android doesn't know that the user "instigated" SAF -- it has no way to know that because the user clicked something-or-another that you brought up ACTION_OPEN_DOCUMENT
|
Mark M. |
it is the Storage Access Framework itself -- allowing direct user involvement -- that eliminates the need for the storage permissions
|
Tad F. |
"allowing direct user involvement" - I haven't looked at doing this, but is it possible to call the SAF API's directly from a service with the info they need such that no UI involvement is done at all?
|
Mark M. |
no
|
Tad F. |
ok I think I understand then.
|
Mark M. |
there are no SAF APIs other than ACTION_OPEN_DOCUMENT, ACTION_CREATE_DOCUMENT, and ACTION_OPEN_DOCUMENT_TREE
|
Tad F. |
I will need to be sure permission has been granted for read/write to access this storage via my service approach.
|
Tad F. |
Which is fine.
|
Tad F. |
Next question :)
|
Aug 16 | 4:25 PM |
Tad F. |
My strategy has been to check and see whether those permissions have been granted, and if so utilize the public storage area from getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), and DIRECTORY_MOVIES.
|
Tad F. |
If the permissions have not been granted, I will fall back to directories under getFilesDir()
|
Mark M. |
you could use getExternalFilesDir() if you wanted
|
Mark M. |
it's external storage (so user-accessible) but no permissions are required
|
Tad F. |
right
|
Tad F. |
I saw that also.
|
Tad F. |
And it gets deleted when the app is uninstalled, apparently - so user accessible, but not persistent across apps.
|
Tad F. |
Anyway -
|
Mark M. |
I would describe it as "not persistent across installs"
|
Mark M. |
same as getFilesDir()
|
Tad F. |
Right
|
Tad F. |
When I was reading about how Android wants me to set this up using a FileProvider, I got a bit confused.
|
Tad F. |
What is confusing to me is that the API description for FileProvider states that you must explicitly tell it what paths it should be using.
|
Mark M. |
relative to select root locations, yes
|
Tad F. |
But I don't see a good generic way to indicate access to that same directory root structure returned by getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) (for example)
|
Tad F. |
Because the return from that call will be different on different devices I presume.
|
Mark M. |
your root location would be <external-path>
|
Tad F. |
Yes I have that:
|
Aug 16 | 4:30 PM |
Tad F. |
View paste
|
Tad F. |
But I was nervous about hard-coding "Pictures" and "Movies"
|
Tad F. |
What if those change?
|
Tad F. |
in external-path
|
Mark M. |
agreed, at least for <external-path>
|
Mark M. |
so, use <external-path name="tad-stuff" path="/" />
|
Mark M. |
(substituting in a more appropriate value for the "name" attribute :-)
|
Tad F. |
Is the ramification of that then that all directories from the root are available to be browsed?
|
Mark M. |
no
|
Tad F. |
I'm confused then about what path is doing
|
Mark M. |
no other app has access to your FileProvider content *except* what you grant manually in code
|
Mark M. |
path="/" says "I am willing to allow my FileProvider to serve from any location on external storage"
|
Tad F. |
But if path is just "/", then I would (on my Samsung S7 phone) be allowing FileProvider to browse: /storage/emulated/0/
|
Mark M. |
whereas path="Pictures? says "I am willing to allow my fileProvider to serve from any location off of Pictures/ on external storage"
|
Mark M. |
FileProvider doesn't really "browse", so I don't know how to interpret that verb
|
Tad F. |
ok - "serve from any location"
|
Mark M. |
right
|
Mark M. |
however, it only serves what you tell it to serve at runtime
|
Tad F. |
point is - it serve up content from the root "directory"
|
Tad F. |
Yeah, ok
|
Tad F. |
by mimetype, yes
|
Tad F. |
?
|
Mark M. |
so, don't screw up your FileProvider.getUriForFile() calls
|
Mark M. |
no
|
Mark M. |
by Uri
|
Aug 16 | 4:35 PM |
Mark M. |
with FileProvider, at runtime, you do three key things:
|
Mark M. |
1. you use FileProvider.getUriForFile() to get a Uri that points to the desired file
|
Mark M. |
2. you put that in an Intent somewhere (e.g., EXTRA_OUTPUT for ACTION_IMAGE_CAPTURE)
|
Mark M. |
3. you call addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
Mark M. |
(and/or FLAG_GRANT_READ_URI_PERMISSION)
|
Mark M. |
that Intent, in turn, winds up in the hands of another app, such as a camera app
|
Mark M. |
that app has rights to work with the content identified by that Uri, for whichever rights you put in the addFlags() call
|
Mark M. |
however, that app has no rights to any other content that the FileProvider might be capable of serving
|
Mark M. |
and no other app has rights to that Uri's content, just because this one other specific app has rights
|
Mark M. |
and, the granted rights are temporary -- you saw the reverse of this before you started using takePersistableUriPermissions() (and, since this isn't ACTION_OPEN_DOCUMENT, that's not an option for the camera app in this scenario)
|
Tad F. |
OK - yes that all makes sense, which is why the Camera app can write bytes to that one file.
|
Mark M. |
right
|
Tad F. |
In terms of the appropriate API to use to get to the public directory, the docs here: https://developer.android.com/training/data-sto... suggest normal best practice is to use getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) to save a photo taken with the camera as the user will expect it to be available to other apps, not just mine. That is fine, however I notice now that this API is deprecated as of API level 29
|
Mark M. |
yup
|
Tad F. |
The blurb there says "To improve user privacy, direct access to shared/external storage devices is deprecated. When an app targets Build.VERSION_CODES.Q, the path returned from this method is no longer directly accessible to apps. Apps can continue to access content stored on shared/external storage by migrating to alternatives such as Context#getExternalFilesDir(String), MediaStore, or Intent#ACTION_OPEN_DOCUMENT."
|
Mark M. |
yup
|
Mark M. | |
Aug 16 | 4:40 PM |
Mark M. |
and see the first few chapters of *Elements of Android Q*, available in a Warescription near you :-)
|
Mark M. |
in short, I wrote a *lot* about these changes earlier this year
|
Tad F. |
I guess I don't understand this, or am implying too much by it. It seems that this implies that a central area where photos would be stored that would be available to all apps is going away?
|
Mark M. |
filesystem-level access to it is going away
|
Mark M. |
so, for example, in *Elements of Android Q*, I have a sample app that downloads videos from my Web site to the device
|
Mark M. |
I have code in there for an Android Q approach and a pre-Q approach
|
Mark M. | |
Tad F. |
So I feel like I'm going around in circles a little bit. The API docs for how to handle letting the Camera know that you want it to save the media to the file system require that you create an empty file, then use the FileProvider API to generate a Uri which you pass in the intent to the camera.
|
Mark M. |
(warning: contains Kotlin)
|
Tad F. |
But at the root of all this - my code has to create an empty FILE.
|
Tad F. |
So that means I need file access.
|
Mark M. |
technically, you need a Uri that the other app can write to
|
Tad F. |
Where am I supposed to create this File if the API is now deprecated?
|
Mark M. |
well, again, technically, you need a Uri that the other app can write to
|
Mark M. |
that could still be FileProvider, but you will need to use filesystem locations that you can write to yourself, such as getFilesDir() or getExternalFilesDir()
|
Aug 16 | 4:45 PM |
Tad F. |
OK - but if the API is deprecated that knows where this public area is, what do I do to tell the Camera app to store the image into that public PICTURES directory?
|
Mark M. |
or, you could use MediaStore, get a Uri that you want to use from there, and hand that to the camera app, using FLAG_GRANT_WRITE_URI_PERMISSION so you allow that app to write to that Uri as well
|
Tad F. |
I think I'm missing something fundamental - does MediaStore have an API that allows me to define a new Uri that doesn't yet exist?
|
Mark M. |
yes
|
Tad F. |
My understanding was that I need to hand Camera the Uri for where to place the new image.
|
Mark M. |
correct
|
Tad F. |
And one way to do this was create an empty file, then a Uri from that and hand it to Camera
|
Mark M. |
correct
|
Tad F. |
But you are saying file creation is verbotin now in the public area?
|
Mark M. |
correct
|
Tad F. |
So.....
|
Mark M. |
you have three options, as is outlined (opaquely) by that documentation that you quoted:
|
Mark M. |
1. continue to use FileProvider, for a location that you still have write access to
|
Mark M. |
2. use MediaStore
|
Mark M. |
3. use ACTION_CREATE_DOCUMENT
|
Mark M. |
option #1 is easy, but the image doesn't necessarily wind up in a useful spot for the user
|
Mark M. |
option #2 is a bit odd but not that bad, but you only have general control over the location (e.g., "it's an image")
|
Mark M. |
option #3 lets the user choose the actual location
|
Tad F. |
How do you use MediaStore to create a new Uri that doesn't yet exist? Is there an API for this?
|
Mark M. |
you call insert() on a ContentResolver
|
Aug 16 | 4:50 PM |
Mark M. | |
Tad F. |
Is this in your code examples?
|
Tad F. |
ok I'll check those out.
|
Tad F. |
Btw - how was your transition from Java to Kotlin?
|
Mark M. |
in my case, I am downloading a video, but the process for an image will be the same, except for the base Uri -- use MediaStore.Image where I have MediaStore.Video
|
Mark M. |
well, I'm old, so I've seen lots of programming languages :-)
|
Tad F. |
I just bought a course from coursera to learn it.
|
Mark M. |
picking up Kotlin wasn't too bad, which is why I have a book on it (and a follow-up coming out on Monday)
|
Mark M. |
see also *Elements of Kotlin*, also available in a Warescription near you
|
Tad F. |
Well, I'm old too but still having fun developing apps
|
Tad F. |
OK
|
Mark M. |
actually, going back to my earlier list, there is a fourth option:
|
Mark M. |
4. use ACTION_OPEN_DOCUMENT_TREE, then use DocumentFile to get a Uri for some new piece of content inside of that tree
|
Tad F. |
Do you have a code example for this?
|
Mark M. |
that's getting more complicated, but it allows the user to give you access and a target "directory" once, whereas the ACTION_CREATE_DOCUMENT approach requires user interaction each time
|
Mark M. |
of that option, I don't think so, but hold on...
|
Mark M. |
yeah, as I thought, I don't
|
Aug 16 | 4:55 PM |
Tad F. |
ok
|
Tad F. |
Well I will take a look at the various links you posted, thanks very much.
|
Tad F. |
Sure seems complicated just to allow my app to launch the camera and get to the resulting image!
|
Mark M. |
it's only complicated because of where you want the image to reside in the end
|
Mark M. |
if only your app needed the image, FileProvider and getFilesDir() handles your case neatly
|
Mark M. |
in your case, you not only want the image, but you want to allow the user to keep the image somewhere beyond your app
|
Tad F. |
You mean in that public area? I guess I just want my app "to behave" properly - i.e. it launches the Camera and user takes a picture, the docs seem to suggest (and I would agree) that the user would expect this picture to be available in the Gallery, etc.
|
Mark M. |
I disagree
|
Tad F. |
That's interesting.
|
Tad F. |
Why?
|
Mark M. |
as a user, if I personally launch the camera app, I would expect the image to wind up where the camera app normally stores pictures
|
Mark M. |
as a user, if I launch a barcode scanner, I would not expect the barcode to wind up where the camera app normally stores pictures
|
Aug 16 | 5:00 PM |
Mark M. |
as a user, if I launch Instagram or something, I would not necessarily assume pictures that I take in there to wind up where the camera app normally stores pictures
|
Tad F. |
Let me put it to you this way - I had previously implemented support for when the user is using the camera (not my app yet), takes a picture, then decides they want to share that picture, for my app to be one of the choices they can make. If they do, the app launches, I use the Uri to get at the picture, my app does its thing, and then we are back to the Camera.
|
Mark M. |
(note: I am not an Instagram user)
|
Tad F. |
"Does its thing" in my case means adds the picture to a playlist or to an internal DB that can be used to create playlists.
|
Tad F. |
So now the scenario is the user launches my app, and decides they want to create a new playlist.
|
Tad F. |
They can opt to choose from those that my app knows about (the DB)
|
Tad F. |
or the Picker
|
Tad F. |
or the Gallery
|
Tad F. |
or whatever.
|
Tad F. |
Or
|
Tad F. |
they can choose to take a new picture
|
Mark M. |
and that's fine, but that last option is a bit unusual and is not well-covered in the Android APIs
|
Tad F. |
So if they are taking a new picture, particularly if they just finished a session whereby they shared a pic with my app, I can see how it might confuse them if in the first case the picture was available to them but the second was not.
|
Mark M. |
and in your case, that's understandable
|
Mark M. |
however, you are swimming against the Android security tide
|
Mark M. |
and you will need to decide whether that particular feature is worth the headache
|
Mark M. |
and that's all the time that I have for today
|
Tad F. |
Oops - holy smoke you are right.
|
Mark M. |
as usual, the transcript will appear on https://commonsware.com/office-hours/ shortly
|
Tad F. |
Thanks for your time!
|
Mark M. |
yeah, that hour blew by
|
Mark M. |
next chat is Monday at 9am US Eastern
|
Tad F. |
Bye now
|
Mark M. |
have a pleasant day!
|
Mark M. |
whoops, sorry -- next chat is tomorrow at 7:30pm US Eastern
|
Aug 16 | 5:05 PM |
Tad F. | has left the room |
Mark M. | turned off guest access |