Skip to content

Commit

Permalink
Fix (android): memory leaks (#2141)
Browse files Browse the repository at this point in the history
* fix(android): fix a lot of leaks

Streams and file descriptors needs to be closed when no longer used.

I converted those to try-with-resources syntax, so they are
automatically closed after the block ends.

* fix(android) Ensure MediaMetadataRetriever is closed

* MediaMetadataRetriever does not inherits AutoCloseable until API 29, so let's use a custom wrapper around it
  • Loading branch information
renchap committed Jun 15, 2023
1 parent dc67109 commit 8e4b205
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 73 deletions.
5 changes: 2 additions & 3 deletions android/src/main/java/com/imagepicker/ImageMetadata.java
Expand Up @@ -8,8 +8,7 @@

public class ImageMetadata extends Metadata {
public ImageMetadata(Uri uri, Context context) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
try(InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
ExifInterface exif = new ExifInterface(inputStream);
String datetimeTag = exif.getAttribute(ExifInterface.TAG_DATETIME);

Expand All @@ -23,7 +22,7 @@ public ImageMetadata(Uri uri, Context context) {

@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
Expand Down
73 changes: 33 additions & 40 deletions android/src/main/java/com/imagepicker/Utils.java
Expand Up @@ -95,17 +95,15 @@ public static void saveToPublicDirectory(Uri uri, Context context, String mediaT
}

public static void copyUri(Uri fromUri, Uri toUri, ContentResolver resolver) {
try {
OutputStream os = resolver.openOutputStream(toUri);
InputStream is = resolver.openInputStream(fromUri);
try(OutputStream os = resolver.openOutputStream(toUri);
InputStream is = resolver.openInputStream(fromUri)) {

byte[] buffer = new byte[8192];
int bytesRead;

while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}

} catch (IOException e) {
e.printStackTrace();
}
Expand Down Expand Up @@ -141,18 +139,15 @@ public static void setFrontCamera(Intent intent) {
}

public static int[] getImageDimensions(Uri uri, Context reactContext) {
InputStream inputStream;
try {
inputStream = reactContext.getContentResolver().openInputStream(uri);
} catch (FileNotFoundException e) {
try(InputStream inputStream = reactContext.getContentResolver().openInputStream(uri)) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream,null, options);
return new int[]{options.outWidth, options.outHeight};
} catch (IOException e) {
e.printStackTrace();
return new int[]{0, 0};
}

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream,null, options);
return new int[]{options.outWidth, options.outHeight};
}

static boolean hasPermission(final Activity activity) {
Expand All @@ -161,27 +156,21 @@ static boolean hasPermission(final Activity activity) {
}

static String getBase64String(Uri uri, Context reactContext) {
InputStream inputStream;
try {
inputStream = reactContext.getContentResolver().openInputStream(uri);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
try(InputStream inputStream = reactContext.getContentResolver().openInputStream(uri);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
byte[] bytes;
byte[] buffer = new byte[8192];
int bytesRead;

byte[] bytes;
byte[] buffer = new byte[8192];
int bytesRead;
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
bytes = output.toByteArray();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
} catch (IOException e) {
e.printStackTrace();
return null;
}
bytes = output.toByteArray();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}

// Resize image
Expand All @@ -196,20 +185,25 @@ public static Uri resizeImage(Uri uri, Context context, Options options) {

int[] newDimens = getImageDimensBasedOnConstraints(origDimens[0], origDimens[1], options);

InputStream imageStream = context.getContentResolver().openInputStream(uri);
String mimeType = getMimeTypeFromFileUri(uri);
Bitmap b = BitmapFactory.decodeStream(imageStream);
b = Bitmap.createScaledBitmap(b, newDimens[0], newDimens[1], true);
String originalOrientation = getOrientation(uri, context);
try(InputStream imageStream = context.getContentResolver().openInputStream(uri)) {
String mimeType = getMimeTypeFromFileUri(uri);
Bitmap b = BitmapFactory.decodeStream(imageStream);

File file = createFile(context, getFileTypeFromMime(mimeType));
OutputStream os = context.getContentResolver().openOutputStream(Uri.fromFile(file));
b.compress(getBitmapCompressFormat(mimeType), options.quality, os);
setOrientation(file, originalOrientation, context);
b = Bitmap.createScaledBitmap(b, newDimens[0], newDimens[1], true);
String originalOrientation = getOrientation(uri, context);

deleteFile(uri);
File file = createFile(context, getFileTypeFromMime(mimeType));

try(OutputStream os = context.getContentResolver().openOutputStream(Uri.fromFile(file))) {
b.compress(getBitmapCompressFormat(mimeType), options.quality, os);
}

return Uri.fromFile(file);
setOrientation(file, originalOrientation, context);

deleteFile(uri);

return Uri.fromFile(file);
}

} catch (Exception e) {
e.printStackTrace();
Expand Down Expand Up @@ -254,8 +248,7 @@ static int[] getImageDimensBasedOnConstraints(int origWidth, int origHeight, Opt
}

static double getFileSize(Uri uri, Context context) {
try {
ParcelFileDescriptor f = context.getContentResolver().openFileDescriptor(uri, "r");
try(ParcelFileDescriptor f = context.getContentResolver().openFileDescriptor(uri, "r")) {
return f.getStatSize();
} catch (Exception e) {
e.printStackTrace();
Expand Down
71 changes: 41 additions & 30 deletions android/src/main/java/com/imagepicker/VideoMetadata.java
Expand Up @@ -9,49 +9,60 @@

import java.io.IOException;

// MetadataRetriever only implements AutoCloseable starting with Android API 29
// So let's use our own wrapper for it
// See https://stackoverflow.com/a/74808462/1377358
class CustomMediaMetadataRetriever extends MediaMetadataRetriever implements AutoCloseable {
public CustomMediaMetadataRetriever() {
super();
}

@Override
public void close() throws IOException {
release();
}
}

public class VideoMetadata extends Metadata {
private int duration;
private int bitrate;

public VideoMetadata(Uri uri, Context context) {
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(context, uri);
try(CustomMediaMetadataRetriever metadataRetriever = new CustomMediaMetadataRetriever()) {
metadataRetriever.setDataSource(context, uri);

String duration = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
String bitrate = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
String datetime = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
String duration = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
String bitrate = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
String datetime = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);

// Extract anymore metadata here...
if(duration != null) this.duration = Math.round(Float.parseFloat(duration)) / 1000;
if(bitrate != null) this.bitrate = parseInt(bitrate);
// Extract anymore metadata here...
if(duration != null) this.duration = Math.round(Float.parseFloat(duration)) / 1000;
if(bitrate != null) this.bitrate = parseInt(bitrate);

if(datetime != null) {
// METADATA_KEY_DATE gives us the following format: "20211214T102646.000Z"
// This format is very hard to parse, so we convert it to "20211214 102646" ("yyyyMMdd HHmmss")
String datetimeToFormat = datetime.substring(0, datetime.indexOf(".")).replace("T", " ");
this.datetime = getDateTimeInUTC(datetimeToFormat, "yyyyMMdd HHmmss");
}
if(datetime != null) {
// METADATA_KEY_DATE gives us the following format: "20211214T102646.000Z"
// This format is very hard to parse, so we convert it to "20211214 102646" ("yyyyMMdd HHmmss")
String datetimeToFormat = datetime.substring(0, datetime.indexOf(".")).replace("T", " ");
this.datetime = getDateTimeInUTC(datetimeToFormat, "yyyyMMdd HHmmss");
}

String width = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String height = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
String width = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String height = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);

if(height != null && width != null) {
String rotation = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
int rotationI = rotation == null ? 0 : Integer.parseInt(rotation);
if(height != null && width != null) {
String rotation = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
int rotationI = rotation == null ? 0 : Integer.parseInt(rotation);

if(rotationI == 90 || rotationI == 270) {
this.width = Integer.parseInt(height);
this.height = Integer.parseInt(width);
} else {
this.width = Integer.parseInt(width);
this.height = Integer.parseInt(height);
if(rotationI == 90 || rotationI == 270) {
this.width = Integer.parseInt(height);
this.height = Integer.parseInt(width);
} else {
this.width = Integer.parseInt(width);
this.height = Integer.parseInt(height);
}
}
}

try {
metadataRetriever.release();
} catch (IOException e) {
Log.e("VideoMetadata", "IO error releasing metadataRetriever", e);
e.printStackTrace();
}
}

Expand Down

0 comments on commit 8e4b205

Please sign in to comment.