From 6a1f86d85b20428fb9ed4d09101144e95c7756bf Mon Sep 17 00:00:00 2001 From: gouravfairwani <123166578+gouravfairwani@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:14:42 +0530 Subject: [PATCH 1/7] Update ImageMetadata.java --- .../java/com/imagepicker/ImageMetadata.java | 119 ++++++++++++++---- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/android/src/main/java/com/imagepicker/ImageMetadata.java b/android/src/main/java/com/imagepicker/ImageMetadata.java index dcc4806d9..18376a9cf 100644 --- a/android/src/main/java/com/imagepicker/ImageMetadata.java +++ b/android/src/main/java/com/imagepicker/ImageMetadata.java @@ -1,33 +1,98 @@ package com.imagepicker; -import android.content.Context; -import android.net.Uri; -import android.util.Log; -import androidx.exifinterface.media.ExifInterface; -import java.io.InputStream; - -public class ImageMetadata extends Metadata { - public ImageMetadata(Uri uri, Context context) { - try { - InputStream inputStream = context.getContentResolver().openInputStream(uri); - ExifInterface exif = new ExifInterface(inputStream); - String datetimeTag = exif.getAttribute(ExifInterface.TAG_DATETIME); - - // Extract anymore metadata here... - if(datetimeTag != null) this.datetime = getDateTimeInUTC(datetimeTag, "yyyy:MM:dd HH:mm:ss"); - } catch (Exception e) { - // This error does not bubble up to RN as we don't want failed datetime retrieval to prevent selection - Log.e("RNIP", "Could not load image metadata: " + e.getMessage()); +import static android.media.ExifInterface.TAG_APERTURE; +import static android.media.ExifInterface.TAG_DATETIME; +import static android.media.ExifInterface.TAG_DATETIME_DIGITIZED; +import static android.media.ExifInterface.TAG_EXPOSURE_TIME; +import static android.media.ExifInterface.TAG_FLASH; +import static android.media.ExifInterface.TAG_FOCAL_LENGTH; +import static android.media.ExifInterface.TAG_GPS_ALTITUDE; +import static android.media.ExifInterface.TAG_GPS_ALTITUDE_REF; +import static android.media.ExifInterface.TAG_GPS_DATESTAMP; +import static android.media.ExifInterface.TAG_GPS_LATITUDE; +import static android.media.ExifInterface.TAG_GPS_LATITUDE_REF; +import static android.media.ExifInterface.TAG_GPS_LONGITUDE; +import static android.media.ExifInterface.TAG_GPS_LONGITUDE_REF; +import static android.media.ExifInterface.TAG_GPS_PROCESSING_METHOD; +import static android.media.ExifInterface.TAG_GPS_TIMESTAMP; +import static android.media.ExifInterface.TAG_IMAGE_LENGTH; +import static android.media.ExifInterface.TAG_IMAGE_WIDTH; +import static android.media.ExifInterface.TAG_ISO; +import static android.media.ExifInterface.TAG_MAKE; +import static android.media.ExifInterface.TAG_MODEL; +import static android.media.ExifInterface.TAG_ORIENTATION; +import static android.media.ExifInterface.TAG_SUBSEC_TIME; +import static android.media.ExifInterface.TAG_SUBSEC_TIME_DIG; +import static android.media.ExifInterface.TAG_SUBSEC_TIME_ORIG; +import static android.media.ExifInterface.TAG_WHITE_BALANCE; + +import android.media.ExifInterface; +import android.os.Build; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class ImageMetadata { + + static WritableMap extract(String path) throws IOException { + WritableMap exifData = new WritableNativeMap(); + + List attributes = getBasicAttributes(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + attributes.addAll(getLevel23Attributes()); } + + ExifInterface exif = new ExifInterface(path); + + for (String attribute : attributes) { + String value = exif.getAttribute(attribute); + exifData.putString(attribute, value); + } + + return exifData; + } + + private static List getBasicAttributes() { + return new ArrayList<>(Arrays.asList( + TAG_APERTURE, + TAG_DATETIME, + TAG_EXPOSURE_TIME, + TAG_FLASH, + TAG_FOCAL_LENGTH, + TAG_GPS_ALTITUDE, + TAG_GPS_ALTITUDE_REF, + TAG_GPS_DATESTAMP, + TAG_GPS_LATITUDE, + TAG_GPS_LATITUDE_REF, + TAG_GPS_LONGITUDE, + TAG_GPS_LONGITUDE_REF, + TAG_GPS_PROCESSING_METHOD, + TAG_GPS_TIMESTAMP, + TAG_IMAGE_LENGTH, + TAG_IMAGE_WIDTH, + TAG_ISO, + TAG_MAKE, + TAG_MODEL, + TAG_ORIENTATION, + TAG_WHITE_BALANCE + )); } - @Override - public String getDateTime() { return datetime; } - - // At the moment we are not using the ImageMetadata class to get width/height - // TODO: to use this class for extracting image width and height in the future - @Override - public int getWidth() { return 0; } - @Override - public int getHeight() { return 0; } + private static List getLevel23Attributes() { + return new ArrayList<>(Arrays.asList( + TAG_DATETIME_DIGITIZED, + TAG_SUBSEC_TIME, + TAG_SUBSEC_TIME_DIG, + TAG_SUBSEC_TIME_ORIG + )); + + + } + } From 55de2f8bd2401d1ef88d4dead0012719615c78cc Mon Sep 17 00:00:00 2001 From: gouravfairwani <123166578+gouravfairwani@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:16:37 +0530 Subject: [PATCH 2/7] Update ImagePickerModuleImpl.java --- .../imagepicker/ImagePickerModuleImpl.java | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java b/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java index 89b69a87d..d38f6c390 100644 --- a/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java +++ b/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java @@ -1,5 +1,25 @@ package com.imagepicker; +import static com.imagepicker.Utils.cameraPermissionDescription; +import static com.imagepicker.Utils.collectUrisFromData; +import static com.imagepicker.Utils.createFile; +import static com.imagepicker.Utils.createUri; +import static com.imagepicker.Utils.deleteFile; +import static com.imagepicker.Utils.errCameraUnavailable; +import static com.imagepicker.Utils.errOthers; +import static com.imagepicker.Utils.errPermission; +import static com.imagepicker.Utils.getCancelMap; +import static com.imagepicker.Utils.getErrorMap; +import static com.imagepicker.Utils.getResponseMap; +import static com.imagepicker.Utils.hasPermission; +import static com.imagepicker.Utils.isCameraAvailable; +import static com.imagepicker.Utils.isCameraPermissionFulfilled; +import static com.imagepicker.Utils.isValidRequestCode; +import static com.imagepicker.Utils.mediaTypePhoto; +import static com.imagepicker.Utils.mediaTypeVideo; +import static com.imagepicker.Utils.saveToPublicDirectory; +import static com.imagepicker.Utils.setFrontCamera; + import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; @@ -10,17 +30,12 @@ import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.module.annotations.ReactModule; import java.io.File; import java.util.Collections; import java.util.List; -import static com.imagepicker.Utils.*; - public class ImagePickerModuleImpl implements ActivityEventListener { static final String NAME = "ImagePicker"; @@ -118,39 +133,26 @@ public void launchImageLibrary(final ReadableMap options, final Callback callbac Intent libraryIntent; requestCode = REQUEST_LAUNCH_LIBRARY; - int selectionLimit = this.options.selectionLimit; - boolean isSingleSelect = selectionLimit == 1; + boolean isSingleSelect = this.options.selectionLimit == 1; boolean isPhoto = this.options.mediaType.equals(mediaTypePhoto); boolean isVideo = this.options.mediaType.equals(mediaTypeVideo); + if(isSingleSelect && (isPhoto || isVideo)) { + libraryIntent = new Intent(Intent.ACTION_PICK); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - if (isSingleSelect && (isPhoto || isVideo)) { - libraryIntent = new Intent(Intent.ACTION_PICK); - } else { - libraryIntent = new Intent(Intent.ACTION_GET_CONTENT); - libraryIntent.addCategory(Intent.CATEGORY_OPENABLE); - } } else { - libraryIntent = new Intent(MediaStore.ACTION_PICK_IMAGES); + libraryIntent = new Intent(Intent.ACTION_GET_CONTENT); + libraryIntent.addCategory(Intent.CATEGORY_OPENABLE); } - if (!isSingleSelect) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - libraryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - } else { - if (selectionLimit != 1) { - int maxNum = selectionLimit; - if (selectionLimit == 0) maxNum = MediaStore.getPickImagesMaxLimit(); - libraryIntent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxNum); - } - } + if(!isSingleSelect) { + libraryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } - if (isPhoto) { + if(isPhoto) { libraryIntent.setType("image/*"); } else if (isVideo) { libraryIntent.setType("video/*"); - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + } else { libraryIntent.setType("*/*"); libraryIntent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"}); } From a2739571eed66321483002d4e0e36b32edda48d0 Mon Sep 17 00:00:00 2001 From: gouravfairwani <123166578+gouravfairwani@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:17:48 +0530 Subject: [PATCH 3/7] Create RealPathUtil.java --- .../java/com/imagepicker/RealPathUtil.java | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 android/src/main/java/com/imagepicker/RealPathUtil.java diff --git a/android/src/main/java/com/imagepicker/RealPathUtil.java b/android/src/main/java/com/imagepicker/RealPathUtil.java new file mode 100644 index 000000000..e3d3730a1 --- /dev/null +++ b/android/src/main/java/com/imagepicker/RealPathUtil.java @@ -0,0 +1,221 @@ +package com.imagepicker; +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +class RealPathUtil { + @TargetApi(Build.VERSION_CODES.KITKAT) + static String getRealPathFromURI(final Context context, final Uri uri) throws IOException { + + final boolean isKitKat = Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + final int splitIndex = docId.indexOf(':', 1); + final String tag = docId.substring(0, splitIndex); + final String path = docId.substring(splitIndex + 1); + + String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag); + if (nonPrimaryVolume != null) { + String result = nonPrimaryVolume + "/" + path; + File file = new File(result); + if (file.exists() && file.canRead()) { + return result; + } + return null; + } + } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * If an image/video has been selected from a cloud storage, this method + * should be call to download the file in the cache folder. + * + * @param context The context + * @param fileName donwloaded file's name + * @param uri file's URI + * @return file that has been written + */ + private static File writeToFile(Context context, String fileName, Uri uri) { + String tmpDir = context.getCacheDir() + "/react-native-image-picker"; + Boolean created = new File(tmpDir).mkdir(); + fileName = fileName.substring(fileName.lastIndexOf('/') + 1); + File path = new File(tmpDir); + File file = new File(path, fileName); + try { + FileOutputStream oos = new FileOutputStream(file); + byte[] buf = new byte[8192]; + InputStream is = context.getContentResolver().openInputStream(uri); + int c = 0; + while ((c = is.read(buf, 0, buf.length)) > 0) { + oos.write(buf, 0, c); + oos.flush(); + } + oos.close(); + is.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return file; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String[] projection = { + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + // Fall back to writing to file if _data column does not exist + final int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); + String path = index > -1 ? cursor.getString(index) : null; + if (path != null) { + return cursor.getString(index); + } else { + final int indexDisplayName = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); + String fileName = cursor.getString(indexDisplayName); + File fileWritten = writeToFile(context, fileName, uri); + return fileWritten.getAbsolutePath(); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String getPathToNonPrimaryVolume(Context context, String tag) { + File[] volumes = context.getExternalCacheDirs(); + if (volumes != null) { + for (File volume : volumes) { + if (volume != null) { + String path = volume.getAbsolutePath(); + if (path != null) { + int index = path.indexOf(tag); + if (index != -1) { + return path.substring(0, index) + tag; + } + } + } + } + } + return null; + } + +} From 4328e326b3c76de6f2cd7c7072b9be9c35aaff42 Mon Sep 17 00:00:00 2001 From: gouravfairwani <123166578+gouravfairwani@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:18:45 +0530 Subject: [PATCH 4/7] Update Utils.java --- .../src/main/java/com/imagepicker/Utils.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/imagepicker/Utils.java b/android/src/main/java/com/imagepicker/Utils.java index 6151f74b3..5f8eeb3c2 100644 --- a/android/src/main/java/com/imagepicker/Utils.java +++ b/android/src/main/java/com/imagepicker/Utils.java @@ -1,5 +1,9 @@ package com.imagepicker; +import static com.imagepicker.ImagePickerModuleImpl.REQUEST_LAUNCH_IMAGE_CAPTURE; +import static com.imagepicker.ImagePickerModuleImpl.REQUEST_LAUNCH_LIBRARY; +import static com.imagepicker.ImagePickerModuleImpl.REQUEST_LAUNCH_VIDEO_CAPTURE; + import android.Manifest; import android.app.Activity; import android.content.ClipData; @@ -40,8 +44,6 @@ import java.util.List; import java.util.UUID; -import static com.imagepicker.ImagePickerModuleImpl.*; - public class Utils { public static String fileNamePrefix = "rn_image_picker_lib_temp_"; @@ -391,9 +393,9 @@ static List collectUrisFromData(Intent data) { return fileUris; } - static ReadableMap getImageResponseMap(Uri uri, Options options, Context context) { + static ReadableMap getImageResponseMap(Uri uri, Options options, Context context, String origUri) { String fileName = uri.getLastPathSegment(); - ImageMetadata imageMetadata = new ImageMetadata(uri, context); + ImageMetadata imageMetadata = new ImageMetadata(); int[] dimensions = getImageDimensions(uri, context); WritableMap map = Arguments.createMap(); @@ -411,7 +413,16 @@ static ReadableMap getImageResponseMap(Uri uri, Options options, Context context if(options.includeExtra) { // Add more extra data here ... - map.putString("timestamp", imageMetadata.getDateTime()); + try { + if(origUri != "") { + WritableMap exifData = ImageMetadata.extract(origUri); + map.putString("exifData", exifData.toString()); + map.putString("timestamp", exifData.getString("DateTime")); + map.putString("physicalPath", origUri); + } + } catch (Exception e) { + e.printStackTrace(); + } map.putString("id", fileName); } @@ -451,9 +462,18 @@ static ReadableMap getResponseMap(List fileUris, Options options, Context c if (isImageType(uri, context)) { if (uri.getScheme().contains("content")) { uri = getAppSpecificStorageUri(uri, context); + + } + String origUri = ""; + try { + origUri = RealPathUtil.getRealPathFromURI(context, fileUris.get(i)); + } catch (Exception e) { + origUri = null; + e.printStackTrace(); } + uri = resizeImage(uri, context, options); - assets.pushMap(getImageResponseMap(uri, options, context)); + assets.pushMap(getImageResponseMap(uri, options, context,origUri)); } else if (isVideoType(uri, context)) { if (uri.getScheme().contains("content")) { uri = getAppSpecificStorageUri(uri, context); From 3e7f812e20eba5e5179dbd9933e94ff9d5e3b687 Mon Sep 17 00:00:00 2001 From: gouravfairwani <123166578+gouravfairwani@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:19:39 +0530 Subject: [PATCH 5/7] Update VideoMetadata.java --- android/src/main/java/com/imagepicker/VideoMetadata.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/imagepicker/VideoMetadata.java b/android/src/main/java/com/imagepicker/VideoMetadata.java index 8179cc6ca..ba37fd26e 100644 --- a/android/src/main/java/com/imagepicker/VideoMetadata.java +++ b/android/src/main/java/com/imagepicker/VideoMetadata.java @@ -43,11 +43,9 @@ public VideoMetadata(Uri uri, Context context) { this.height = bitmap.getHeight(); } - try { + metadataRetriever.release(); - } catch (IOException e) { - Log.e("VideoMetadata", "IO error releasing metadataRetriever", e); - } + } public int getBitrate() { From 30555fea4b1fa29ad54bc2db9249d87553299666 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 7 Sep 2023 19:37:00 -0500 Subject: [PATCH 6/7] Update VideoMetadata.java --- android/src/main/java/com/imagepicker/VideoMetadata.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/imagepicker/VideoMetadata.java b/android/src/main/java/com/imagepicker/VideoMetadata.java index ba37fd26e..e3fe79732 100644 --- a/android/src/main/java/com/imagepicker/VideoMetadata.java +++ b/android/src/main/java/com/imagepicker/VideoMetadata.java @@ -42,9 +42,12 @@ public VideoMetadata(Uri uri, Context context) { this.width = bitmap.getWidth(); this.height = bitmap.getHeight(); } - - + + try { metadataRetriever.release(); + } catch (IOException e) { + Log.e("VideoMetadata", "IO error releasing metadataRetriever", e); + } } From d43b360fb6e58df8d82ba8802e0c60054b19f0a3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 20 Sep 2023 09:31:50 +0800 Subject: [PATCH 7/7] Update VideoMetadata.java --- android/src/main/java/com/imagepicker/VideoMetadata.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/src/main/java/com/imagepicker/VideoMetadata.java b/android/src/main/java/com/imagepicker/VideoMetadata.java index e3fe79732..8fbd5f2a5 100644 --- a/android/src/main/java/com/imagepicker/VideoMetadata.java +++ b/android/src/main/java/com/imagepicker/VideoMetadata.java @@ -43,11 +43,7 @@ public VideoMetadata(Uri uri, Context context) { this.height = bitmap.getHeight(); } - try { - metadataRetriever.release(); - } catch (IOException e) { - Log.e("VideoMetadata", "IO error releasing metadataRetriever", e); - } + metadataRetriever.release(); }