-
Notifications
You must be signed in to change notification settings - Fork 27
Update AndroidPicturesService to reduce memory usage and prevent leaks #441
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||
| * Copyright (c) 2020, 2024, Gluon | ||||||||||||||||||||||||||||||||
| * Copyright (c) 2020, 2026, Gluon | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * This program is free software: you can redistribute it and/or modify | ||||||||||||||||||||||||||||||||
| * it under the terms of the GNU General Public License as published by | ||||||||||||||||||||||||||||||||
|
|
@@ -31,6 +31,9 @@ | |||||||||||||||||||||||||||||||
| import android.app.Activity; | ||||||||||||||||||||||||||||||||
| import android.content.Intent; | ||||||||||||||||||||||||||||||||
| import android.database.Cursor; | ||||||||||||||||||||||||||||||||
| import android.graphics.Bitmap; | ||||||||||||||||||||||||||||||||
| import android.graphics.BitmapFactory; | ||||||||||||||||||||||||||||||||
| import android.graphics.Matrix; | ||||||||||||||||||||||||||||||||
| import android.media.ExifInterface; | ||||||||||||||||||||||||||||||||
| import android.media.MediaScannerConnection; | ||||||||||||||||||||||||||||||||
| import android.net.Uri; | ||||||||||||||||||||||||||||||||
|
|
@@ -41,6 +44,7 @@ | |||||||||||||||||||||||||||||||
| import android.util.Log; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import java.io.File; | ||||||||||||||||||||||||||||||||
| import java.io.FileInputStream; | ||||||||||||||||||||||||||||||||
| import java.io.FileNotFoundException; | ||||||||||||||||||||||||||||||||
| import java.io.FileOutputStream; | ||||||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||||||
|
|
@@ -64,6 +68,10 @@ public class DalvikPicturesService { | |||||||||||||||||||||||||||||||
| private final String authority; | ||||||||||||||||||||||||||||||||
| private String photoPath; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private static final int TARGET_SIZE = 1280; | ||||||||||||||||||||||||||||||||
| private static final int JPEG_QUALITY = 85; | ||||||||||||||||||||||||||||||||
| private static final byte[] DECODE_BUFFER = new byte[16 * 1024]; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public DalvikPicturesService(Activity activity) { | ||||||||||||||||||||||||||||||||
| this.activity = activity; | ||||||||||||||||||||||||||||||||
| this.debug = Util.isDebug(); | ||||||||||||||||||||||||||||||||
|
|
@@ -92,6 +100,7 @@ public void takePhoto(final boolean savePhoto) { | |||||||||||||||||||||||||||||||
| Log.v(TAG, "Permission verification failed: Camera disabled"); | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| clearCache(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -132,12 +141,18 @@ public void gotActivityResult (int requestCode, int resultCode, Intent intent) { | |||||||||||||||||||||||||||||||
| if (debug) { | ||||||||||||||||||||||||||||||||
| Log.v(TAG, "Image file located at " + photoFile.getAbsolutePath() + " with rotation: " + imageRotation); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| sendPhotoFile(photoFile.getAbsolutePath(), imageRotation); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| String originalPath = photoFile.getAbsolutePath(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (savePhoto) { | ||||||||||||||||||||||||||||||||
| // media scanner to rescan DIRECTORY_PICTURES after an image is saved/deleted | ||||||||||||||||||||||||||||||||
| // Saved photos: keep the original file untouched in | ||||||||||||||||||||||||||||||||
| // DIRECTORY_PICTURES, scan it into the gallery, and | ||||||||||||||||||||||||||||||||
| // send a preprocessed cache copy for display. | ||||||||||||||||||||||||||||||||
| MediaScannerConnection.scanFile(activity, new String[]{photoFile.toString()}, null, null); | ||||||||||||||||||||||||||||||||
| photoFile = copyToCache(photoFile); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| preprocessImage(photoFile, imageRotation); | ||||||||||||||||||||||||||||||||
| sendPhotoFile(originalPath, photoFile.getAbsolutePath()); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| Log.e(TAG, "Picture file doesn't exist for: " + photoFile.getAbsolutePath()); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -162,6 +177,8 @@ public void gotActivityResult (int requestCode, int resultCode, Intent intent) { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private void selectPicture() { | ||||||||||||||||||||||||||||||||
| clearCache(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
179
to
+181
|
||||||||||||||||||||||||||||||||
| Intent intent = new Intent(Intent.ACTION_GET_CONTENT); | ||||||||||||||||||||||||||||||||
| intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||||||||||||||||||||||||||||||
| intent.setType("image/*"); | ||||||||||||||||||||||||||||||||
|
|
@@ -193,7 +210,9 @@ public void gotActivityResult(int requestCode, int resultCode, Intent intent) { | |||||||||||||||||||||||||||||||
| if (debug) { | ||||||||||||||||||||||||||||||||
| Log.v(TAG, "Image file located at " + cachePhotoFile.getAbsolutePath() + " with rotation: " + imageRotation); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| sendPhotoFile(cachePhotoFile.getAbsolutePath(), imageRotation); | ||||||||||||||||||||||||||||||||
| String originalPath = cachePhotoFile.getAbsolutePath(); | ||||||||||||||||||||||||||||||||
| preprocessImage(cachePhotoFile, imageRotation); | ||||||||||||||||||||||||||||||||
| sendPhotoFile(originalPath, cachePhotoFile.getAbsolutePath()); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -232,7 +251,9 @@ private int getImageRotation(Uri uri) { | |||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| ExifInterface ei; | ||||||||||||||||||||||||||||||||
| if (Build.VERSION.SDK_INT > 23) { | ||||||||||||||||||||||||||||||||
| ei = new ExifInterface(activity.getContentResolver().openInputStream(uri)); | ||||||||||||||||||||||||||||||||
| try (InputStream is = activity.getContentResolver().openInputStream(uri)) { | ||||||||||||||||||||||||||||||||
| ei = new ExifInterface(is); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| ei = new ExifInterface(uri.getPath()); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -258,7 +279,7 @@ private File copyFile(Uri uri) { | |||||||||||||||||||||||||||||||
| File selectedFile = new File(activity.getCacheDir(), getImageName(uri)); | ||||||||||||||||||||||||||||||||
| try (InputStream is = activity.getContentResolver().openInputStream(uri); | ||||||||||||||||||||||||||||||||
| OutputStream os = new FileOutputStream(selectedFile)) { | ||||||||||||||||||||||||||||||||
| byte[] buffer = new byte[8 * 1024]; | ||||||||||||||||||||||||||||||||
| byte[] buffer = new byte[32 * 1024]; | ||||||||||||||||||||||||||||||||
| int bytesRead; | ||||||||||||||||||||||||||||||||
| while ((bytesRead = is.read(buffer)) != -1) { | ||||||||||||||||||||||||||||||||
| os.write(buffer, 0, bytesRead); | ||||||||||||||||||||||||||||||||
|
|
@@ -271,7 +292,105 @@ private File copyFile(Uri uri) { | |||||||||||||||||||||||||||||||
| return selectedFile; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private File copyToCache(File source) { | ||||||||||||||||||||||||||||||||
| File dest = new File(activity.getCacheDir(), "display_" + source.getName()); | ||||||||||||||||||||||||||||||||
| try (InputStream is = new FileInputStream(source); OutputStream os = new FileOutputStream(dest)) { | ||||||||||||||||||||||||||||||||
| byte[] buffer = new byte[32 * 1024]; | ||||||||||||||||||||||||||||||||
| int bytesRead; | ||||||||||||||||||||||||||||||||
| while ((bytesRead = is.read(buffer)) != -1) { | ||||||||||||||||||||||||||||||||
| os.write(buffer, 0, bytesRead); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } catch (IOException ex) { | ||||||||||||||||||||||||||||||||
| Log.e(TAG, "copyToCache failed: " + ex.getMessage()); | ||||||||||||||||||||||||||||||||
| return source; // fall back to original | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return dest; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Scales and rotates the image file in place, with sub sampling and lower jpg quality, | ||||||||||||||||||||||||||||||||
| * to reduce memory footprint | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| private void preprocessImage(File imageFile, int rotation) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| // 1. Read dimensions only | ||||||||||||||||||||||||||||||||
| BitmapFactory.Options opts = new BitmapFactory.Options(); | ||||||||||||||||||||||||||||||||
| opts.inJustDecodeBounds = true; | ||||||||||||||||||||||||||||||||
| opts.inTempStorage = DECODE_BUFFER; | ||||||||||||||||||||||||||||||||
| BitmapFactory.decodeFile(imageFile.getAbsolutePath(), opts); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 2. Calculate inSampleSize | ||||||||||||||||||||||||||||||||
| int srcW = opts.outWidth; | ||||||||||||||||||||||||||||||||
| int srcH = opts.outHeight; | ||||||||||||||||||||||||||||||||
| if (rotation == 90 || rotation == 270) { | ||||||||||||||||||||||||||||||||
| srcW = opts.outHeight; | ||||||||||||||||||||||||||||||||
| srcH = opts.outWidth; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| opts.inSampleSize = calculateInSampleSize(srcW, srcH, TARGET_SIZE); | ||||||||||||||||||||||||||||||||
| opts.inJustDecodeBounds = false; | ||||||||||||||||||||||||||||||||
| opts.inPreferredConfig = Bitmap.Config.RGB_565; | ||||||||||||||||||||||||||||||||
| opts.inTempStorage = DECODE_BUFFER; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 3. Load sub-sampled bitmap | ||||||||||||||||||||||||||||||||
| Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), opts); | ||||||||||||||||||||||||||||||||
| if (bitmap == null) { | ||||||||||||||||||||||||||||||||
| Log.e(TAG, "preprocessImage: failed to decode bitmap"); | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| // 4. Build a single Matrix for scale + rotate combined | ||||||||||||||||||||||||||||||||
| Matrix matrix = new Matrix(); | ||||||||||||||||||||||||||||||||
| float scale = Math.min( | ||||||||||||||||||||||||||||||||
| (float) TARGET_SIZE / bitmap.getWidth(), | ||||||||||||||||||||||||||||||||
| (float) TARGET_SIZE / bitmap.getHeight()); | ||||||||||||||||||||||||||||||||
| if (scale < 1.0f) { | ||||||||||||||||||||||||||||||||
| matrix.postScale(scale, scale); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (rotation != 0) { | ||||||||||||||||||||||||||||||||
| // Rotate around the center of the image | ||||||||||||||||||||||||||||||||
| float cx = bitmap.getWidth() * Math.max(scale, 1.0f) / 2f; | ||||||||||||||||||||||||||||||||
| float cy = bitmap.getHeight() * Math.max(scale, 1.0f) / 2f; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+347
to
+353
|
||||||||||||||||||||||||||||||||
| if (scale < 1.0f) { | |
| matrix.postScale(scale, scale); | |
| } | |
| if (rotation != 0) { | |
| // Rotate around the center of the image | |
| float cx = bitmap.getWidth() * Math.max(scale, 1.0f) / 2f; | |
| float cy = bitmap.getHeight() * Math.max(scale, 1.0f) / 2f; | |
| float appliedScale = scale < 1.0f ? scale : 1.0f; | |
| if (appliedScale < 1.0f) { | |
| matrix.postScale(appliedScale, appliedScale); | |
| } | |
| if (rotation != 0) { | |
| // Rotate around the center of the image after any applied scaling | |
| float cx = bitmap.getWidth() * appliedScale / 2f; | |
| float cy = bitmap.getHeight() * appliedScale / 2f; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| [ | ||
| { | ||
| "name" : "com.gluonhq.attach.pictures.impl.AndroidPicturesService", | ||
| "methods":[{"name":"setResult","parameterTypes":["java.lang.String", "int"] }] | ||
| "methods":[{"name":"setResult","parameterTypes":["java.lang.String","java.lang.String"] }] | ||
| } | ||
| ] |
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.
Calling
System.gc()here is a forced full GC that is not guaranteed to reclaim the native/Prism resources you care about, and can introduce noticeable UI pauses (this callback can happen frequently when taking photos). It’s usually better to rely on dropping strong references (which you already do by clearing the property) and let the runtime decide when to collect; if explicit cleanup is needed, prefer an API-specific disposal mechanism rather than forcing GC.