New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[media-library][android] retrieve location data from photos on androi… #14413
Conversation
@@ -2,4 +2,5 @@ | |||
<manifest package="expo.modules.medialibrary" xmlns:android="http://schemas.android.com/apk/res/android"> | |||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | |||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | |||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to use a config plugin for this so it can be more visible, and easier to remove. Otherwise users will be getting a new permission for a library they've already implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay will do - I would like to understand better though -- doesn't a config plugin accomplish the same thing? e.g it will add this new permission regardless?
}; | ||
} | ||
return new String[]{ | ||
READ_EXTERNAL_STORAGE, | ||
WRITE_EXTERNAL_STORAGE | ||
WRITE_EXTERNAL_STORAGE, | ||
ACCESS_MEDIA_LOCATION, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format
#13123 probably related |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getString(idIndex)); | ||
getExifLocationForUri(contentResolver, photoUri, asset); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this block intentionally inside if (exifInterface != null)
? As getExifLocationForUri
does not depend on that original exifInterface
, maybe we can extract it outside?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it does require this check as it also requires the getExifFullInfo
- I could extract but then would have to duplicate the logic around adding extra info - I think its a matter of taste so either way let me know what you'd prefer!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, if most likely null
ified "global" ExifInterface
implies null exifInterface created by getExifLocationForUri
(so most likely there are cases when either none or both of them are null
), then it can stay as it is.
And I suspect in most cases it's true (unless permission is not granted, but this is caught in a separate exception).
@@ -358,8 +369,40 @@ static void getExifFullInfo(ExifInterface exifInterface, Bundle response) { | |||
response.putParcelable("exif", exifMap); | |||
} | |||
|
|||
// reference: https://developer.android.com/training/data-storage/shared/media#location-info-photos | |||
@RequiresApi(api = Build.VERSION_CODES.Q) | |||
static void getExifLocationForUri(ContentResolver contentResolver, Uri photoUri, Bundle asset) throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this throw general Exception
or maybe some specific one? It would be good to narrow down the exception range because error handling in MediaLib is already a bit confusing (regarding the real source of error).
Hint: when you hover and click on throws Exception
(the Exception word) - Android Studio highlights all calls which throw that exception (or its subclasses)
photoUri = MediaStore.setRequireOriginal(photoUri); | ||
InputStream stream = contentResolver.openInputStream(photoUri); | ||
if (stream != null) { | ||
ExifInterface exifInterface = new ExifInterface(stream); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should sync this somehow with #14408
7ee70f7
to
a64060c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a good idea would be to display a meaningful message on rejection, when ACCESS_MEDIA_LOCATION
permission is needed
@@ -60,7 +60,7 @@ protected Void doInBackground(Void... params) { | |||
mPromise.reject(ERROR_UNABLE_TO_LOAD_PERMISSION, | |||
"Could not get asset: need READ_EXTERNAL_STORAGE permission.", e); | |||
} catch (IOException e) { | |||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not read file", e); | |||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not read file or parse EXIF tags", e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just rebase mistake or intentional? (EXIF-specific exceptions are now handled elsewhere)
@@ -155,7 +158,7 @@ static void queryAssetInfo(Context context, final String selection, final String | |||
promise.reject(ERROR_UNABLE_TO_LOAD_PERMISSION, | |||
"Could not get asset: need READ_EXTERNAL_STORAGE permission.", e); | |||
} catch (IOException e) { | |||
promise.reject(ERROR_IO_EXCEPTION, "Could not read file", e); | |||
promise.reject(ERROR_IO_EXCEPTION, "Could not read file or parse EXIF tags", e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here 😉
} else { | ||
asset.putParcelable("location", null); | ||
} | ||
stream.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When ExifInterface
creation fails, the stream is never closed. It should be moved to the finally
block.
Unfortunately, this means the whole getExifLocationForUri
will have to throws IOException
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no opinion on the config plugin, besides that looks good to me 😁 thanks for taking my suggestions and ideas into account 😉
642587d
to
6d0f621
Compare
# Why [versioned qa][android] test-suite "MediaLibrary" show `Tests were skipped - not enough permissions to run them.` on android 8 device # How requesting `ACCESS_MEDIA_LOCATION` permission only on android 10+ # Test Plan bare-expo test-suite "MediaLibrary" on android 8 device for android 10 emulator, which is expected failed as #14413 mentioned.
# Why [versioned qa][android] test-suite "MediaLibrary" show `Tests were skipped - not enough permissions to run them.` on android 8 device # How requesting `ACCESS_MEDIA_LOCATION` permission only on android 10+ # Test Plan bare-expo test-suite "MediaLibrary" on android 8 device for android 10 emulator, which is expected failed as #14413 mentioned. (cherry picked from commit 35d639f)
Why
Closes #14361
Android 10+ requires an additional permission and getter implementation to get exif location data for photos.
From what I can tell, the old implementation still seems to work via the
requestLegacyExternalStorage
key in Android manifest, but as of 11 this no longer seems to workI was able to reproduce the location data being missing via a emulator running Android 11
How
The new implementation checks whether or not the device is running Android 10+ and if so, runs a different call to retrieve location info. This method throws an exception if the
ACCESS_MEDIA_LOCATION
permission is missing fromAndroidManifest
- so I also added this value as well as handled the exceptionTest Plan
From what I can tell, this implementation still doesn't seem to work on the emulator, which is unfortunate, but I was able to confirm that it works on a real device running Android 10
Also - the permissions prompt doesn't seem to change at all between this implementation and the previous, the only concern might be if adding this additional permission would cause issues with app submissions
Checklist
expo build
(eg: updated@expo/xdl
).expo prebuild
& EAS Build (eg: updated a module plugin).