Skip to content
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

[expo-media-library] [Android] Flip dimensions based on media rotation data #7980

Merged
merged 3 commits into from May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/expo-media-library/CHANGELOG.md
Expand Up @@ -9,3 +9,4 @@
### 🐛 Bug fixes

- Fixed `MediaLibrary` not compiling with the `use_frameworks!` option in the bare React Native application. ([#7861](https://github.com/expo/expo/pull/7861) by [@Ashoat](https://github.com/Ashoat))
- Flip dimensions based on media rotation data on Android to match `<Image>` and `<Video>` as well as iOS behavior. ([#7980](https://github.com/expo/expo/pull/7980) by [@Ashoat](https://github.com/Ashoat))
@@ -1,6 +1,7 @@
package expo.modules.medialibrary;

import android.content.Context;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
Expand Down Expand Up @@ -37,7 +38,8 @@ protected Void doInBackground(Void... params) {
final String order = getQueryInfo.getOrder();
final int limit = getQueryInfo.getLimit();
final int offset = getQueryInfo.getOffset();
try (Cursor assets = mContext.getContentResolver().query(
ContentResolver contentResolver = mContext.getContentResolver();
try (Cursor assets = contentResolver.query(
EXTERNAL_CONTENT,
ASSET_PROJECTION,
selection,
Expand All @@ -47,7 +49,7 @@ protected Void doInBackground(Void... params) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get assets. Query returns null.");
} else {
ArrayList<Bundle> assetsInfo = new ArrayList<>();
putAssetsInfo(assets, assetsInfo, limit, offset, false);
putAssetsInfo(contentResolver, assets, assetsInfo, limit, offset, false);
response.putParcelableArrayList("assets", assetsInfo);
response.putBoolean("hasNextPage", !assets.isAfterLast());
response.putString("endCursor", Integer.toString(assets.getPosition()));
Expand Down
Expand Up @@ -56,8 +56,8 @@ final class MediaLibraryConstants {
put(SORT_BY_CREATION_TIME, MediaStore.Images.Media.DATE_TAKEN);
put(SORT_BY_MODIFICATION_TIME, MediaStore.Images.Media.DATE_MODIFIED);
put(SORT_BY_MEDIA_TYPE, MediaStore.Files.FileColumns.MEDIA_TYPE);
put(SORT_BY_WIDTH, MediaStore.Images.Media.WIDTH);
put(SORT_BY_HEIGHT, MediaStore.Images.Media.HEIGHT);
put(SORT_BY_WIDTH, MediaStore.MediaColumns.WIDTH);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't exactly recall if this change was necessary. I think my intention was to use a media-type-agnostic identifier here, with the hope that it would help for videos

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this a bit more. These two constants are exactly the same.

I think I prefer MediaStore.MediaColumns.WIDTH since the name doesn't appear to be image-specific, but I can revert these changes in the interest of preserving git blame. Let me know!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if there are the same and we also use them when we deal with videos, MediaColumns is better ;)

Thank for investigating this 🥇

put(SORT_BY_HEIGHT, MediaStore.MediaColumns.HEIGHT);
put(SORT_BY_DURATION, MediaStore.Video.VideoColumns.DURATION);
}
};
Expand All @@ -70,8 +70,8 @@ final class MediaLibraryConstants {
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.LATITUDE,
Expand Down
@@ -1,17 +1,23 @@
package expo.modules.medialibrary;

import android.content.Context;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.media.MediaMetadataRetriever;
import android.provider.MediaStore;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.Images.Media;
import androidx.exifinterface.media.ExifInterface;
import android.text.TextUtils;
import android.net.Uri;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
Expand Down Expand Up @@ -96,7 +102,8 @@ static File safeCopyFile(final File src, final File dir) throws IOException {
}

static void queryAssetInfo(Context context, final String selection, final String[] selectionArgs, boolean fullInfo, Promise promise) {
try (Cursor asset = context.getContentResolver().query(
ContentResolver contentResolver = context.getContentResolver();
try (Cursor asset = contentResolver.query(
EXTERNAL_CONTENT,
ASSET_PROJECTION,
selection,
Expand All @@ -109,7 +116,7 @@ static void queryAssetInfo(Context context, final String selection, final String
if (asset.getCount() == 1) {
asset.moveToFirst();
ArrayList<Bundle> array = new ArrayList<>();
putAssetsInfo(asset, array, 1, 0, fullInfo);
putAssetsInfo(contentResolver, asset, array, 1, 0, fullInfo);
// actually we want to return just the first item, but array.getMap returns ReadableMap
// which is not compatible with promise.resolve and there is no simple solution to convert
// ReadableMap to WritableMap so it's easier to return an array and pick the first item on JS side
Expand All @@ -126,7 +133,7 @@ static void queryAssetInfo(Context context, final String selection, final String
}
}

static void putAssetsInfo(Cursor cursor, ArrayList<Bundle> response, int limit, int offset, boolean fullInfo) throws IOException {
static void putAssetsInfo(ContentResolver contentResolver, Cursor cursor, ArrayList<Bundle> response, int limit, int offset, boolean fullInfo) throws IOException {
final int idIndex = cursor.getColumnIndex(Media._ID);
final int filenameIndex = cursor.getColumnIndex(Media.DISPLAY_NAME);
final int mediaTypeIndex = cursor.getColumnIndex(Files.FileColumns.MEDIA_TYPE);
Expand All @@ -142,9 +149,16 @@ static void putAssetsInfo(Cursor cursor, ArrayList<Bundle> response, int limit,
return;
}
for (int i = 0; i < limit && !cursor.isAfterLast(); i++) {
String localUri = "file://" + cursor.getString(localUriIndex);
String path = cursor.getString(localUriIndex);
String localUri = "file://" + path;
int mediaType = cursor.getInt(mediaTypeIndex);
int[] size = getSizeFromCursor(cursor, mediaType, localUriIndex);

ExifInterface exifInterface = null;
if (mediaType == Files.FileColumns.MEDIA_TYPE_IMAGE) {
exifInterface = new ExifInterface(path);
}

int[] size = getSizeFromCursor(contentResolver, exifInterface, cursor, mediaType, localUriIndex);

Bundle asset = new Bundle();
asset.putString("id", cursor.getString(idIndex));
Expand All @@ -159,8 +173,8 @@ static void putAssetsInfo(Cursor cursor, ArrayList<Bundle> response, int limit,
asset.putString("albumId", cursor.getString(albumIdIndex));

if (fullInfo) {
if (mediaType == Files.FileColumns.MEDIA_TYPE_IMAGE) {
getExifFullInfo(cursor, asset);
if (exifInterface != null) {
getExifFullInfo(exifInterface, asset);
}

asset.putString("localUri", localUri);
Expand Down Expand Up @@ -213,25 +227,68 @@ static Integer convertMediaType(String mediaType) throws IllegalArgumentExceptio
return MEDIA_TYPES.get(mediaType);
}

static int[] getSizeFromCursor(Cursor cursor, int mediaType, int localUriIndex){
static int[] getSizeFromCursor(ContentResolver contentResolver, ExifInterface exifInterface, Cursor cursor, int mediaType, int localUriIndex) throws IOException {
final String uri = cursor.getString(localUriIndex);

if (mediaType == Files.FileColumns.MEDIA_TYPE_VIDEO) {
Uri videoUri = Uri.parse("file://" + uri);
MediaMetadataRetriever retriever = null;
try (AssetFileDescriptor photoDescriptor = contentResolver.openAssetFileDescriptor(videoUri, "r")) {
retriever = new MediaMetadataRetriever();
retriever.setDataSource(photoDescriptor.getFileDescriptor());
int videoWidth = Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
);
int videoHeight = Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
);
int videoOrientation = Integer.parseInt(
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
);
return maybeRotateAssetSize(videoWidth, videoHeight, videoOrientation);
} catch (NumberFormatException e) {
Log.e("expo-media-library", "MediaMetadataRetriever unexpectedly returned non-integer: " + e.getMessage());
} catch (FileNotFoundException e) {
Log.e("expo-media-library", String.format("ContentResolver failed to read %s: %s", uri, e.getMessage()));
} finally {
if (retriever != null) {
retriever.release();
}
}
}

final int widthIndex = cursor.getColumnIndex(MediaStore.MediaColumns.WIDTH);
final int heightIndex = cursor.getColumnIndex(MediaStore.MediaColumns.HEIGHT);
final int orientationIndex = cursor.getColumnIndex(Media.ORIENTATION);
final int widthIndex = cursor.getColumnIndex(Media.WIDTH);
final int heightIndex = cursor.getColumnIndex(Media.HEIGHT);

int[] size;
// If image doesn't have the required information, we can get them from Bitmap.Options
if ((cursor.getType(widthIndex) == Cursor.FIELD_TYPE_NULL ||
cursor.getType(heightIndex) == Cursor.FIELD_TYPE_NULL) &&
mediaType == Files.FileColumns.MEDIA_TYPE_IMAGE) {
Comment on lines -223 to -225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to omit these conditions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, good catch. The getType checks shouldn't be necessary, but we should keep the mediaType check, if only to avoid the perf hit of a useless check

int width = cursor.getInt(widthIndex);
int height = cursor.getInt(heightIndex);
int orientation = cursor.getInt(orientationIndex);

// If the image doesn't have the required information, we can get them from Bitmap.Options
if (mediaType == Files.FileColumns.MEDIA_TYPE_IMAGE && (width <= 0 || height <= 0)) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(uri, options);
width = options.outWidth;
height = options.outHeight;
}

BitmapFactory.decodeFile(cursor.getString(localUriIndex), options);
size = maybeRotateAssetSize(options.outWidth, options.outHeight, cursor.getInt(orientationIndex));
} else {
size = maybeRotateAssetSize(cursor.getInt(widthIndex), cursor.getInt(heightIndex), cursor.getInt(orientationIndex));
if (exifInterface != null) {
int exifOrientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
);
if (
exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 ||
exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 ||
exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE ||
exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE
) {
orientation = 90;
}
}
return size;

return maybeRotateAssetSize(width, height, orientation);
}

static int[] maybeRotateAssetSize(int width, int height, int orientation) {
Expand Down Expand Up @@ -265,9 +322,7 @@ static String mapOrderDescriptor(List orderDescriptor) throws IllegalArgumentExc
return TextUtils.join(",", result);
}

static void getExifFullInfo(Cursor cursor, Bundle response) throws IOException {
File input = new File(cursor.getString(cursor.getColumnIndex(Media.DATA)));
ExifInterface exifInterface = new ExifInterface(input.getPath());
static void getExifFullInfo(ExifInterface exifInterface, Bundle response) {
Bundle exifMap = new Bundle();
for (String[] tagInfo : exifTags) {
String name = tagInfo[1];
Expand Down