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

feat(camera): adds multi-camera feature for Android (#1616) #2013

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions camera/android/build.gradle
Expand Up @@ -6,6 +6,7 @@ ext {
androidxExifInterfaceVersion = project.hasProperty('androidxExifInterfaceVersion') ? rootProject.ext.androidxExifInterfaceVersion : '1.3.6'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
androidxMaterialVersion = project.hasProperty('androidxMaterialVersion') ? rootProject.ext.androidxMaterialVersion : '1.10.0'
cameraxVersion = project.hasProperty('cameraxVersion') ? rootProject.ext.cameraxVersion : '1.3.1'
}

buildscript {
Expand Down Expand Up @@ -77,6 +78,9 @@ dependencies {
implementation "androidx.exifinterface:exifinterface:$androidxExifInterfaceVersion"
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "com.google.android.material:material:$androidxMaterialVersion"
implementation "androidx.camera:camera-camera2:${cameraxVersion}"
implementation "androidx.camera:camera-view:${cameraxVersion}"
implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
Expand Down
1 change: 1 addition & 0 deletions camera/android/src/main/AndroidManifest.xml
Expand Up @@ -4,4 +4,5 @@
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

Large diffs are not rendered by default.

Expand Up @@ -27,6 +27,8 @@
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.fragment.app.FragmentTransaction;

import com.getcapacitor.FileUtils;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
Expand All @@ -48,6 +50,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -104,6 +107,7 @@ public class CameraPlugin extends Plugin {
private static final String IMAGE_EDIT_ERROR = "Unable to edit image";
private static final String IMAGE_GALLERY_SAVE_ERROR = "Unable to save the image in the gallery";
private static final String USER_CANCELLED = "User cancelled photos app";
private static final String CAMERA_CAPTURE_CANCELED_ERROR = "User canceled the camera capture session";

private String imageFileSavePath;
private String imageEditedFileSavePath;
Expand Down Expand Up @@ -152,6 +156,9 @@ private void doShow(PluginCall call) {
case CAMERA:
showCamera(call);
break;
case CAMERA_MULTI:
showMultiCamera(call);
break;
case PHOTOS:
showPhotos(call);
break;
Expand All @@ -166,6 +173,7 @@ private void showPrompt(final PluginCall call) {
List<String> options = new ArrayList<>();
options.add(call.getString("promptLabelPhoto", "From Photos"));
options.add(call.getString("promptLabelPicture", "Take Picture"));
options.add(call.getString("promptLabelPicture", "Take Multiple Pictures"));

final CameraBottomSheetDialogFragment fragment = new CameraBottomSheetDialogFragment();
fragment.setTitle(call.getString("promptLabelHeader", "Photo"));
Expand All @@ -178,6 +186,9 @@ private void showPrompt(final PluginCall call) {
} else if (index == 1) {
settings.setSource(CameraSource.CAMERA);
openCamera(call);
} else if (index == 2) {
settings.setSource(CameraSource.CAMERA_MULTI);
openMultiCamera(call);
}
},
() -> call.reject(USER_CANCELLED)
Expand All @@ -193,6 +204,14 @@ private void showCamera(final PluginCall call) {
openCamera(call);
}

private void showMultiCamera(final PluginCall call) {
if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
call.reject(NO_CAMERA_ERROR);
return;
}
openMultiCamera(call);
}

private void showPhotos(final PluginCall call) {
openPhotos(call);
}
Expand Down Expand Up @@ -234,7 +253,7 @@ private void cameraPermissionsCallback(PluginCall call) {
if (call.getMethodName().equals("pickImages")) {
openPhotos(call, true);
} else {
if (settings.getSource() == CameraSource.CAMERA && getPermissionState(CAMERA) != PermissionState.GRANTED) {
if ((settings.getSource() == CameraSource.CAMERA || settings.getSource() == CameraSource.CAMERA_MULTI) && getPermissionState(CAMERA) != PermissionState.GRANTED) {
Logger.debug(getLogTag(), "User denied camera permission: " + getPermissionState(CAMERA).toString());
call.reject(PERMISSION_DENIED_ERROR_CAMERA);
return;
Expand Down Expand Up @@ -315,6 +334,27 @@ public void openCamera(final PluginCall call) {
}
}

public void openMultiCamera(final PluginCall call) {
if (checkCameraPermissions(call)) {
final CameraFragment fragment = new CameraFragment();
fragment.setImagesCapturedCallback(new CameraFragment.OnImagesCapturedCallback() {
@Override
public void onCaptureSuccess(HashMap<Uri, Bitmap> images) {
returnMultiCameraResult(call, images);
}

@Override
public void onCaptureCanceled() {
call.reject(CAMERA_CAPTURE_CANCELED_ERROR);
}
});

FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
transaction.add(android.R.id.content, fragment);
transaction.commit();
}
}

public void openPhotos(final PluginCall call) {
openPhotos(call, false);
}
Expand Down Expand Up @@ -588,28 +628,24 @@ private File getTempFile(Uri uri) {
return new File(cacheDir, filename);
}

/**
* After processing the image, return the final result back to the caller.
* @param call
* @param bitmap
* @param u
*/


@SuppressWarnings("deprecation")
private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
private JSObject createReturnFrom(PluginCall call, Bitmap bitmap, Uri u) {
ExifWrapper exif = ImageUtils.getExifData(getContext(), bitmap, u);
try {
bitmap = prepareBitmap(bitmap, u, exif);
} catch (IOException e) {
call.reject(UNABLE_TO_PROCESS_IMAGE);
return;
return null;
}
// Compress the final image and prepare for output to client
ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, settings.getQuality(), bitmapOutputStream);

if (settings.isAllowEditing() && !isEdited) {
editImage(call, u, bitmapOutputStream);
return;
return null;
}

boolean saveToGallery = call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY);
Expand Down Expand Up @@ -645,10 +681,10 @@ private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
}
} else {
String inserted = MediaStore.Images.Media.insertImage(
getContext().getContentResolver(),
fileToSavePath,
fileToSave.getName(),
""
getContext().getContentResolver(),
fileToSavePath,
fileToSave.getName(),
""
);

if (inserted == null) {
Expand All @@ -664,15 +700,35 @@ private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
}
}

JSObject ret = null;
if (settings.getResultType() == CameraResultType.BASE64) {
returnBase64(call, exif, bitmapOutputStream);
ret = returnBase64(call, exif, bitmapOutputStream);
} else if (settings.getResultType() == CameraResultType.URI) {
returnFileURI(call, exif, bitmap, u, bitmapOutputStream);
ret = returnFileURI(call, exif, bitmap, u, bitmapOutputStream);
if (ret == null) {
call.reject(UNABLE_TO_PROCESS_IMAGE);
}
} else if (settings.getResultType() == CameraResultType.DATAURL) {
returnDataUrl(call, exif, bitmapOutputStream);
ret = returnDataUrl(call, exif, bitmapOutputStream);
} else {
call.reject(INVALID_RESULT_TYPE_ERROR);
}

return ret;
}

/**
* After processing the image, return the final result back to the caller.
* @param call
* @param bitmap
* @param u
*/
private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
JSObject ret = createReturnFrom(call, bitmap, u);
if (ret != null) {
call.resolve(ret);
};

// Result returned, clear stored paths and images
if (settings.getResultType() != CameraResultType.URI) {
deleteImageFile();
Expand All @@ -683,6 +739,22 @@ private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
imageEditedFileSavePath = null;
}

private void returnMultiCameraResult(PluginCall call, HashMap<Uri, Bitmap> images) {
settings.setAllowEditing(false); // Editing multiple photos would be cumbersome

JSObject ret = new JSObject();
JSArray photos = new JSArray();
for (Map.Entry<Uri, Bitmap> image : images.entrySet()) {
JSObject single = createReturnFrom(call, image.getValue(), image.getKey());
if (single != null){
photos.put(single);
};
}
ret.put("photos", photos);

call.resolve(ret);
}

private void deleteImageFile() {
if (imageFileSavePath != null && !settings.isSaveToGallery()) {
File photoFile = new File(imageFileSavePath);
Expand All @@ -692,7 +764,7 @@ private void deleteImageFile() {
}
}

private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) {
private JSObject returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) {
Uri newUri = getTempImage(u, bitmapOutputStream);
exif.copyExif(newUri.getPath());
if (newUri != null) {
Expand All @@ -702,9 +774,9 @@ private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri
ret.put("path", newUri.toString());
ret.put("webPath", FileUtils.getPortablePath(getContext(), bridge.getLocalUrl(), newUri));
ret.put("saved", isSaved);
call.resolve(ret);
return ret;
} else {
call.reject(UNABLE_TO_PROCESS_IMAGE);
return null;
}
}

Expand Down Expand Up @@ -756,26 +828,26 @@ private Bitmap replaceBitmap(Bitmap bitmap, final Bitmap newBitmap) {
return bitmap;
}

private void returnDataUrl(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
private JSObject returnDataUrl(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
byte[] byteArray = bitmapOutputStream.toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP);

JSObject data = new JSObject();
data.put("format", "jpeg");
data.put("dataUrl", "data:image/jpeg;base64," + encoded);
data.put("exif", exif.toJson());
call.resolve(data);
return data;
}

private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
private JSObject returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
byte[] byteArray = bitmapOutputStream.toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP);

JSObject data = new JSObject();
data.put("format", "jpeg");
data.put("base64String", encoded);
data.put("exif", exif.toJson());
call.resolve(data);
return data;
}

@Override
Expand Down
Expand Up @@ -3,6 +3,7 @@
public enum CameraSource {
PROMPT("PROMPT"),
CAMERA("CAMERA"),
CAMERA_MULTI("CAMERA_MULTI"),
PHOTOS("PHOTOS");

private String source;
Expand Down
@@ -0,0 +1,11 @@
package com.capacitorjs.plugins.camera;

import android.content.Context;
import android.util.DisplayMetrics;

public class DeviceUtils {
public static int dpToPx(Context context, int dp) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (dp * displayMetrics.density + 0.5f);
}
}