11

Looking at the storage access changes introduced in Android 10 here, location informations are now redacted by default.

Google asks us to call setRequireOriginal() on the "MediaStore" object with the media's uri as a parameter. This works when you fetch medias one by one, but what about when we query the ContentResolver for the whole gallery?

See this sample:

String[] projection = {
        MediaStore.Files.FileColumns._ID,
        MediaStore.Files.FileColumns.DATA,
        MediaStore.Files.FileColumns.MEDIA_TYPE,
        MediaStore.Images.Media.DATE_TAKEN,
        MediaStore.Images.Media.WIDTH,
        MediaStore.Images.Media.HEIGHT,
        MediaStore.Images.Media.LATITUDE, // <----- THIS
        MediaStore.Images.Media.LONGITUDE, // <----- THIS
        MediaStore.Images.Media.MIME_TYPE,
};

String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "="
        + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;

Uri queryUri = MediaStore.Files.getContentUri("external");

Cursor cursor = null;
MediaStore a ;

try {
    cursor = context.getContentResolver().query(queryUri, projection, selection,
            null, MediaStore.Images.Media.DATE_TAKEN + " DESC");


}
catch (NullPointerException ex){
}

Starting Q the latitude and longitude are always set to 0. Is there a way to get the location data for a batch of medias assuming the ACCESS_MEDIA_LOCATION permission is added in the Manifest?

Community
  • 1
  • 1
Gauthier
  • 4,798
  • 5
  • 34
  • 44

1 Answers1

10

Unfortunately, MediaStore.Images.Media.LATITUDE and MediaStore.Images.Media.LONGITUDE were deprecated in Android Q.

The work around for this is to use ExifInterface like this:

Cursor cursor = null;
try {
    String[] projection = {
            MediaStore.Files.FileColumns._ID,
            MediaStore.Images.Media.DATE_TAKEN,
            MediaStore.Images.Media.WIDTH,
            MediaStore.Images.Media.HEIGHT,
            MediaStore.MediaColumns.TITLE,
            MediaStore.Images.Media.MIME_TYPE,
            MediaStore.Images.Media.LATITUDE,
            MediaStore.Images.Media.LONGITUDE
    };
    cursor = getContentResolver().query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            projection,
            null,
            null,
            MediaStore.Images.Media.DATE_TAKEN + " DESC");


    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
    int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE);
    int latColumn = BuildCompat.isAtLeastQ() ? -1
            : cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LATITUDE);
    int longColumn = BuildCompat.isAtLeastQ() ? -1
            : cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LONGITUDE);
    while (cursor.moveToNext()) {

        Uri photoUri = Uri.withAppendedPath(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                cursor.getString(idColumn));
        String title = cursor.getString(titleColumn);

        final double[] latLong;
        if (BuildCompat.isAtLeastQ()) {
            photoUri = MediaStore.setRequireOriginal(photoUri);
            InputStream stream = getContentResolver().openInputStream(photoUri);
            if (stream == null) {
                Log.w(TAG, "Got a null input stream for " + photoUri);
                continue;
            }

            ExifInterface exifInterface = new ExifInterface(stream);
            double[] returnedLatLong = exifInterface.getLatLong();
            // If it returns null, fall back to {0.0, 0.0}.
            latLong = returnedLatLong != null ? returnedLatLong : new double[2];

            // After using ExifInterface, the stream should not be reused.
            stream.close();
        } else {
            latLong = new double[]{
                    cursor.getFloat(latColumn),
                    cursor.getFloat(longColumn)
            };
        }

        Log.i(TAG, title + " | lat: " + latLong[0] + " | lng: " + latLong[1]);
    }
} catch (NullPointerException | IOException ex) {
    Log.e(TAG, "Caught exception", ex);
} finally {
    if (cursor != null) {
        cursor.close();
    }
}

This is the only way to get latitude & longitude of a photo on Android Q at the moment.

This requires holding the ACCESS_MEDIA_LOCATION permission. Note, this permission is not "user visible in the settings UI" (source), which means the user won't see a popup asking for permission, even though it is a runtime permission. This means you have to ask for permission during runtime (in contrast to just the manifest file) but the user won't have to consent to it. Adding this here because you might be wondering why no extra UI popups are shown.

Ben Butterworth
  • 22,056
  • 10
  • 114
  • 167
Nikki Borrelli
  • 899
  • 9
  • 21
  • I agree with the answer, although I would add that to avoid any step back by the missing constructor or any other functionality is better to use the ExifInterface support library: androidx.exifinterface:exifinterface – PerracoLabs Mar 20 '19 at 00:49
  • @PerracoLabs Good point. Thank you. I updated the code to use the androidx library, like you suggested, and put in appropriate API level checks. It still assumes the caller is holding `ACCESS_MEDIA_LOCATION` but I feel that's fine. – Nikki Borrelli Mar 20 '19 at 21:41
  • 1
    I created an issue here https://issuetracker.google.com/issues/128675076 that explains the pitfall of querying medias one by one, basically meaning a 180x performance decrease for fetching the same gallery. I'll accept your answer if Google makes no changes in the future, but I still got hopes they'll create a way to fetch multiple medias with location informations. – Gauthier Mar 20 '19 at 23:31
  • @Gauthier I saw your issue (thank you for doing the benchmarking!). I updated the official docs based on your question so developers are more aware of the issue and how to work around it in the meantime. – Nikki Borrelli Mar 22 '19 at 00:55
  • @NicoleBorrelli my images are not showing in android q but showing on older versions can you tell me please is there some new permissions? – Mateen Chaudhry Apr 04 '19 at 13:19
  • 1
    @MateenChaudhry If the app is targeting Android Q, it needs to request `READ_MEDIA_IMAGES` for images, `READ_MEDIA_VIDEO` for videos, and `READ_MEDIA_AUDIO` for audio content. – Nikki Borrelli Apr 10 '19 at 18:51
  • @NicoleBorrelli do i have to define some roles or its just a runtime permission? – Mateen Chaudhry Apr 12 '19 at 06:31
  • @MateenChaudhry It's just a runtime permission. – Nikki Borrelli Apr 16 '19 at 06:55
  • @NicoleBorrelli Looking at https://android-developers.googleblog.com/2019/04/android-q-scoped-storage-best-practices.html it seems Beta 3 is around the corner and there are no updates on how to get a batch of medias with location data. Do you have any updates on this? – Gauthier Apr 26 '19 at 10:57
  • @Gauthier - No update. The LATITUDE and LONGITUDE columns are still marked as deprecated, so apps will need to use `MediaStore.setRequireOriginal(uri)` and then `ExifInterface` (while holding `ACCESS_MEDIA_LOCATION` permission) to get the location an image was taken. – Nikki Borrelli May 01 '19 at 21:46
  • @Gauthier I'm seeing the following exception being thrown when trying to pull location data from an image `java.lang.UnsupportedOperationException: Caller must hold ACCESS_MEDIA_LOCATION permission to access original` even though I added the access_media_location permission to my manifest. What else did you need to do to get this to work? – user1809913 Jun 17 '19 at 16:43
  • 1
    @user1809913 - ACCESS_MEDIA_LOCATION is considered a "dangerous" permission, and so the app has to request it at runtime (like one would request READ_EXTERNAL_STORAGE). – Nikki Borrelli Jul 16 '19 at 20:44
  • @NicoleBorrelli I should have updated this and added that requesting the permission at runtime fixed it for me. It's odd that the documentation doesn't make that clear or at least didn't at the time. – user1809913 Jul 26 '19 at 16:57
  • Looks like `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`, `READ_MEDIA_AUDIO` have been removed, as links on other websites referencing this are 'broken': [here](https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VIDEO), [here](https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_AUDIO) and [here](https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_IMAGES). – Ben Butterworth Sep 22 '20 at 20:55