TP-86947 | Optimised PhotoModule (#962)

This commit is contained in:
Aman Chaturvedi
2024-10-07 16:39:28 +05:30
committed by GitHub
4 changed files with 199 additions and 233 deletions

View File

@@ -109,16 +109,15 @@ public class MainApplication extends Application implements ReactApplication {
alfredConfig.enablePixelCopy(false);
AlfredManager.INSTANCE.init(alfredConfig, this, 27, "Critical Journey is active");
AlfredApiLogsManager.INSTANCE.init(new AlfredApiLogsProviderImpl());
PulseSDKConfig.Configuration pulseConfig =
new PulseSDKConfig.Configuration.Builder()
.setAppName("collections-agent-app")
.setAppVersion(String.valueOf(BuildConfig.VERSION_CODE))
.setAppVersionName(BuildConfig.VERSION_NAME)
.setDestinationUrl(getDestinationUrl(BuildConfig.BUILD_FLAVOR))
.setEventBatchSize(10)
.setEventInterval(2000)
.build();
PulseManager.INSTANCE.init(pulseConfig, this, null, false);
PulseSDKConfig.Configuration pulseConfig = new PulseSDKConfig.Configuration.Builder()
.setAppName("collections-agent-app")
.setAppVersion(String.valueOf(BuildConfig.VERSION_CODE))
.setAppVersionName(BuildConfig.VERSION_NAME)
.setDestinationUrl(getDestinationUrl(BuildConfig.BUILD_FLAVOR))
.setEventBatchSize(10)
.setEventInterval(2000)
.build();
PulseManager.INSTANCE.init(pulseConfig, this, null, false);
setupAlfredANRWatchDog(alfredConfig);
setupAlfredCrashReporting(alfredConfig);
@@ -126,7 +125,7 @@ public class MainApplication extends Application implements ReactApplication {
try {
Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
field.setAccessible(true);
field.set(null, 40 * 1024 * 1024); // 40MB
field.set(null, 10 * 1024 * 1024); // 10MB
} catch (Exception e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
@@ -180,7 +179,7 @@ public class MainApplication extends Application implements ReactApplication {
if (isAlfredEnabledFromFirebase && AlfredManager.INSTANCE.isAlfredRecordingEnabled()
&& alfredConfig.getAnrEnableStatus() && MainActivity.isAppInForeground()) {
anrEventProperties.put(STACK_TRACE, error.getCause().getStackTrace()[0].toString());
if("release".equals(BuildConfig.BUILD_TYPE)) {
if ("release".equals(BuildConfig.BUILD_TYPE)) {
PulseManager.INSTANCE.trackEvent("COSMOS_ANR", anrEventProperties);
}
AlfredManager.INSTANCE.handleAnrEvent(anrEventProperties);
@@ -210,7 +209,7 @@ public class MainApplication extends Application implements ReactApplication {
if (stackTraceElement != null) {
crashEventProperties.put(STACK_TRACE, stackTraceElement.toString());
}
if("release".equals(BuildConfig.BUILD_TYPE)) {
if ("release".equals(BuildConfig.BUILD_TYPE)) {
PulseManager.INSTANCE.trackEvent("COSMOS_CRASH", crashEventProperties);
}
AlfredManager.INSTANCE.handleCrashEvent(crashEventProperties);

View File

@@ -68,6 +68,11 @@ public class PhotoModule extends ReactContextBaseJavaModule {
private String oldGeolocationText;
private WritableMap attributes;
@Override
public String getName() {
return "PhotoModule";
}
private void handleError(PhotoModuleError errorCode, Promise promise) {
handleError(errorCode, promise, null);
}
@@ -86,15 +91,33 @@ public class PhotoModule extends ReactContextBaseJavaModule {
return image;
}
private int getExifOrientation(String filePath) {
int orientation = ExifInterface.ORIENTATION_NORMAL;
try {
ExifInterface exif = new ExifInterface(filePath);
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
} catch (IOException e) {
e.printStackTrace();
// Optimize memory by downscaling the image before processing
private Bitmap downscaleImage(String imagePath) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, 1024, 1024);
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565; // Use RGB_565 to save memory
return BitmapFactory.decodeFile(imagePath, options);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return orientation;
return inSampleSize;
}
private Bitmap rotateImageIfRequired(Bitmap img, String imagePath) throws IOException {
@@ -133,41 +156,34 @@ public class PhotoModule extends ReactContextBaseJavaModule {
DateFormatSymbols symbols = new DateFormatSymbols(Locale.getDefault());
symbols.setAmPmStrings(new String[] { "AM", "PM" });
SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy, h:mm a", symbols);
String formattedDate = sdf.format(new Date());
return formattedDate;
return sdf.format(new Date());
}
private Bitmap addTextToBottom(Bitmap img, Context context) {
// Calculate scale factor based on the base resolution and current image
// resolution
final float baseWidth = 3060f; // Base width you mentioned looks good
final float baseHeight = 4080f; // Base height you mentioned looks good
final float baseWidth = 3060f;
final float baseHeight = 4080f;
float scaleFactor = Math.min(img.getWidth() / baseWidth, img.getHeight() / baseHeight);
// Adjust text size and padding based on scale factor
int baseTextSize = 100; // Base text size for the base resolution
int basePadding = 100; // Base padding for the base resolution
int textSize = (int) (baseTextSize * scaleFactor); // Scale text size
int paddingPx = (int) (basePadding * scaleFactor); // Scale padding
int baseTextSize = 100;
int basePadding = 100;
int textSize = (int) (baseTextSize * scaleFactor);
int paddingPx = (int) (basePadding * scaleFactor);
TextPaint textPaint = new TextPaint();
try {
// Adjust the path according to where you place the font file
Typeface customTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/NaviBody.otf");
textPaint.setTypeface(customTypeface);
} catch (Exception e) {
e.printStackTrace();
// Handle error here if font is not found or failed to load
}
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(textSize); // Use scaled text size
textPaint.setTextSize(textSize);
textPaint.setAntiAlias(true);
float maxWidth = img.getWidth() - (2 * paddingPx);
photoText += "\n" + getTime();
StaticLayout textLayout = new StaticLayout(photoText, textPaint, (int) maxWidth, Layout.Alignment.ALIGN_NORMAL,
1.2f,
0.0f, false);
1.2f, 0.0f, false);
int backgroundHeight = textLayout.getHeight() + (2 * paddingPx);
Bitmap mutableBitmap = Bitmap.createBitmap(img.getWidth(), img.getHeight(), Bitmap.Config.ARGB_8888);
@@ -185,9 +201,8 @@ public class PhotoModule extends ReactContextBaseJavaModule {
textLayout.draw(canvas);
canvas.restore();
// Adjust launcher icon size based on scale factor
int baseIconSize = 300; // Base icon size for the base resolution
int iconSize = (int) (baseIconSize * scaleFactor); // Scale icon size
int baseIconSize = 300;
int iconSize = (int) (baseIconSize * scaleFactor);
Bitmap originalIconBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
Bitmap iconBitmap = Bitmap.createScaledBitmap(originalIconBitmap, iconSize, iconSize, true);
int iconX = img.getWidth() - iconBitmap.getWidth() - paddingPx;
@@ -198,72 +213,6 @@ public class PhotoModule extends ReactContextBaseJavaModule {
return mutableBitmap;
}
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == IMAGE_CAPTURE_REQUEST) {
if (promise != null) {
if (resultCode != Activity.RESULT_OK) {
// If the image capture was not successful, delete the temporary file
if (mCurrentPhotoPath != null) {
File imgFile = new File(mCurrentPhotoPath);
if (imgFile.exists()) {
imgFile.delete();
}
}
Log.d("PhotoModule", "onActivityResult NOT OK");
handleError(PhotoModuleError.IMAGE_CAPTURE_CANCELLED, promise);
}
if (resultCode == Activity.RESULT_OK) {
Log.d("PhotoModule", "onActivityResult OK");
executorService.execute(() -> fetchLocationAndManipulatePhoto(activity));
}
}
}
}
};
private Uri saveImageToGallery(Context context, Bitmap image, String fileName, ReadableMap attributes)
throws IOException {
final ContentValues contentValues = new ContentValues();
String fileNameString = fileName + "_" + System.currentTimeMillis() + ".jpg";
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileNameString);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
}
Uri imageUri = null;
try {
imageUri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues);
if (imageUri == null) {
Log.d("PhotoModule", "Failed to create new MediaStore record.");
handleError(PhotoModuleError.FAILED_TO_CREATE_MEDIASTORE_RECORD, promise);
throw new IOException("Failed to create new MediaStore record.");
}
try (OutputStream os = context.getContentResolver().openOutputStream(imageUri)) {
if (!image.compress(Bitmap.CompressFormat.JPEG, 100, os)) {
Log.d("PhotoModule", "Failed to save bitmap.");
handleError(PhotoModuleError.FAILED_TO_SAVE_BITMAP, promise);
throw new IOException("Failed to save bitmap.");
}
}
// Pass the URI and attributes to the method
writeExifAttributes(context, imageUri);
} catch (Exception e) {
if (imageUri != null) {
context.getContentResolver().delete(imageUri, null, null);
imageUri = null;
}
throw e;
}
return imageUri;
}
private String convertToDMS(double coord) {
coord = Math.abs(coord);
int degree = (int) coord;
@@ -290,7 +239,6 @@ public class PhotoModule extends ReactContextBaseJavaModule {
if (is == null)
throw new IOException("Unable to open InputStream for URI: " + imageUri);
// Create a temporary file to write the InputStream content
tempFile = File.createTempFile("exif_", ".jpg", context.getExternalCacheDir());
try (OutputStream os = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[1024];
@@ -299,7 +247,7 @@ public class PhotoModule extends ReactContextBaseJavaModule {
os.write(buffer, 0, length);
}
} finally {
is.close(); // Ensure the InputStream is closed
is.close();
}
// Modify the EXIF data on the temporary file
@@ -314,12 +262,10 @@ public class PhotoModule extends ReactContextBaseJavaModule {
String latitude = attributes.getString("latitude");
String longitude = attributes.getString("longitude");
// Store the JSON string in the UserComment tag or another suitable tag
exif.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, json.toString());
exif.setAttribute(ExifInterface.TAG_ARTIST, "Aman Chaturvedi (1932)");
String currentDateTime = getCurrentFormattedDateTime();
// Set date and time tags
exif.setAttribute(ExifInterface.TAG_DATETIME, currentDateTime);
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, currentDateTime);
@@ -349,8 +295,6 @@ public class PhotoModule extends ReactContextBaseJavaModule {
Log.e("PhotoModule", "Failed to write EXIF attributes: ", e);
handleError(PhotoModuleError.FAILED_TO_WRITE_EXIF, promise);
} finally {
// Clean up: Delete the temporary file in the finally block to ensure it's
// always executed
if (tempFile != null && tempFile.exists()) {
if (!tempFile.delete()) {
Log.e("PhotoModule", "Failed to delete temporary file: " + tempFile.getAbsolutePath());
@@ -359,69 +303,48 @@ public class PhotoModule extends ReactContextBaseJavaModule {
}
}
public PhotoModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(mActivityEventListener);
}
@Override
public String getName() {
return "PhotoModule";
}
private void continueEditingPhoto(Activity activity) {
try {
File imgFile = new File(mCurrentPhotoPath);
if (imgFile.exists()) {
Bitmap imageBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath());
Bitmap rotatedBitmap = rotateImageIfRequired(imageBitmap, imgFile.getAbsolutePath());
Bitmap imageWithText = addTextToBottom(rotatedBitmap,
getReactApplicationContext());
// Rotate bitmap is no longer needed after adding text
if (!rotatedBitmap.isRecycled() && rotatedBitmap != imageWithText) {
rotatedBitmap.recycle();
}
Uri modifiedImageUri = saveImageToGallery(activity, imageWithText, "modified_image",
attributes);
String base64Thumbnail = bitmapToBase64(imageWithText);
WritableMap resultMap = Arguments.createMap();
resultMap.putString("base64", base64Thumbnail);
resultMap.putString("uri", modifiedImageUri.toString());
resultMap.putString("imageWidth", String.valueOf(imageWithText.getWidth()));
resultMap.putString("imageHeight", String.valueOf(imageWithText.getHeight()));
activity.runOnUiThread(() -> promise.resolve(resultMap));
// delete imgFile
imgFile.delete();
// Ensure the original bitmap is recycled if it's no longer needed
if (!imageBitmap.isRecycled()) {
imageBitmap.recycle();
}
// Recycle the final bitmap as it's no longer needed
if (!imageWithText.isRecycled()) {
imageWithText.recycle();
}
} else {
activity.runOnUiThread(() -> handleError(PhotoModuleError.FAILED_TO_PROCESS_IMAGE, promise));
}
} catch (Exception e) {
activity.runOnUiThread(() -> handleError(PhotoModuleError.FAILED_TO_PROCESS_IMAGE, promise));
private Uri saveImageToGallery(Context context, Bitmap image, String fileName)
throws IOException {
final ContentValues contentValues = new ContentValues();
String fileNameString = fileName + "_" + System.currentTimeMillis() + ".jpg";
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileNameString);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
}
Uri imageUri = null;
try {
imageUri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues);
if (imageUri == null) {
handleError(PhotoModuleError.FAILED_TO_CREATE_MEDIASTORE_RECORD, promise);
throw new IOException("Failed to create new MediaStore record.");
}
try (OutputStream os = context.getContentResolver().openOutputStream(imageUri)) {
if (!image.compress(Bitmap.CompressFormat.JPEG, 100, os)) {
handleError(PhotoModuleError.FAILED_TO_SAVE_BITMAP, promise);
throw new IOException("Failed to save bitmap.");
}
}
writeExifAttributes(context, imageUri);
} catch (Exception e) {
if (imageUri != null) {
context.getContentResolver().delete(imageUri, null, null);
imageUri = null;
}
throw e;
}
return imageUri;
}
// Add a method to fetch location
private void fetchLocationAndManipulatePhoto(Activity currentActivity) {
Log.d("PhotoModule", "fetchLocationAndManipulatePhoto");
// If geolocation passed down by the app is not old, then proceed with that
if (!photoText.isEmpty()) {
currentActivity.runOnUiThread(() -> {
continueEditingPhoto(currentActivity);
});
continueEditingPhoto(currentActivity);
} else {
// If geolocation is old, then capture a newer one and use that
HandlerThread handlerThread = new HandlerThread("LocationThread");
@@ -435,74 +358,118 @@ public class PhotoModule extends ReactContextBaseJavaModule {
public void onSuccess(Double latitude, Double longitude, String timestamp) {
// Since we're updating UI elements or resolving promises, switch back to the
// main thread
currentActivity.runOnUiThread(() -> {
photoText += "Lat: " + latitude + ", Long: " + longitude;
attributes.putString("geolocationTimestamp", timestamp);
attributes.putString("latitude", latitude.toString());
attributes.putString("longitude", longitude.toString());
continueEditingPhoto(currentActivity);
// Consider stopping the HandlerThread when done
handlerThread.quitSafely();
});
photoText += "Lat: " + latitude + ", Long: " + longitude;
attributes.putString("geolocationTimestamp", timestamp);
attributes.putString("latitude", latitude.toString());
attributes.putString("longitude", longitude.toString());
continueEditingPhoto(currentActivity);
handlerThread.quitSafely();
}
@Override
public void onError(String error) {
// If problem fetching geolocation, proceed with the older geolocation from the
// app
currentActivity.runOnUiThread(() -> {
photoText += oldGeolocationText;
continueEditingPhoto(currentActivity);
// Consider stopping the HandlerThread when done
handlerThread.quitSafely();
});
photoText += oldGeolocationText;
continueEditingPhoto(currentActivity);
handlerThread.quitSafely();
}
});
});
}
}
@ReactMethod
public void capturePhoto(String geolocationText, String oldGeolocationText, ReadableMap attributes,
Promise promise) {
Activity currentActivity = getCurrentActivity();
private void continueEditingPhoto(Activity activity) {
try {
Bitmap originalBitmap = downscaleImage(mCurrentPhotoPath);
originalBitmap = rotateImageIfRequired(originalBitmap, mCurrentPhotoPath);
originalBitmap = addTextToBottom(originalBitmap, getReactApplicationContext());
String encodedBitmap = bitmapToBase64(originalBitmap);
Uri modifiedImageUri = saveImageToGallery(activity, originalBitmap, "modified_image");
WritableMap resultMap = Arguments.createMap();
resultMap.putString("base64", encodedBitmap);
resultMap.putString("uri", modifiedImageUri.toString());
resultMap.putString("imageWidth", String.valueOf(originalBitmap.getWidth()));
resultMap.putString("imageHeight", String.valueOf(originalBitmap.getHeight()));
promise.resolve(resultMap);
originalBitmap.recycle();
} catch (Exception e) {
handleError(PhotoModuleError.FAILED_TO_PROCESS_IMAGE, promise);
}
}
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == IMAGE_CAPTURE_REQUEST) {
if (promise != null) {
if (resultCode != Activity.RESULT_OK) {
if (mCurrentPhotoPath != null) {
File imgFile = new File(mCurrentPhotoPath);
if (imgFile.exists()) {
imgFile.delete();
}
}
handleError(PhotoModuleError.IMAGE_CAPTURE_CANCELLED, promise);
return;
}
if (mCurrentPhotoPath == null) {
handleError(PhotoModuleError.FAILED_TO_PROCESS_IMAGE, promise);
return;
}
executorService.execute(() -> {
fetchLocationAndManipulatePhoto(activity);
});
}
}
}
};
public PhotoModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(mActivityEventListener);
}
@ReactMethod
public void capturePhoto(ReadableMap options, final Promise promise) {
this.promise = promise;
this.photoText = options.getString("geolocationText");
this.oldGeolocationText = options.getString("oldGeolocationText");
this.attributes = Arguments.createMap();
if (options.hasKey("attributes")) {
ReadableMap attributesMap = options.getMap("attributes");
ReadableMapKeySetIterator iterator = attributesMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
attributes.putString(key, attributesMap.getString(key));
}
}
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
Log.d("PhotoModule", "Activity doesn't exist");
handleError(PhotoModuleError.ACTIVITY_DOES_NOT_EXIST, promise);
handleError(PhotoModuleError.FAILED_TO_SHOW_PICKER, promise);
return;
}
this.promise = promise;
this.photoText = geolocationText;
WritableMap newAttributes = Arguments.createMap();
newAttributes.merge(attributes);
this.attributes = newAttributes;
this.oldGeolocationText = oldGeolocationText;
try {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(currentActivity.getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile(currentActivity);
mCurrentPhotoPath = photoFile.getAbsolutePath();
} catch (IOException ex) {
handleError(PhotoModuleError.FILE_CREATION_FAILED, promise);
return;
}
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(currentActivity, BuildConfig.APPLICATION_ID + ".provider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
currentActivity.startActivityForResult(takePictureIntent, IMAGE_CAPTURE_REQUEST);
}
} else {
handleError(PhotoModuleError.FAILED_TO_SHOW_PICKER, promise);
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(currentActivity.getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile(currentActivity);
} catch (IOException ex) {
handleError(PhotoModuleError.FAILED_TO_PROCESS_IMAGE, promise);
return;
}
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(currentActivity, BuildConfig.APPLICATION_ID + ".provider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
currentActivity.startActivityForResult(takePictureIntent, IMAGE_CAPTURE_REQUEST);
}
} catch (Exception e) {
handleError(PhotoModuleError.FAILED_TO_SHOW_PICKER, promise);
}
}
}
}