From 475f4ab96422166ab90612035c7df93c5bb9e5ba Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Mon, 23 Sep 2019 22:29:32 -0700 Subject: [PATCH 1/4] Replicated existing code and migrated references to new embedding, added new example Activity that uses new embedding - still needs lots of refactoring and tests. --- .../dev/flutter/plugins/camera/Camera.java | 557 ++++++++++++++++++ .../plugins/camera/CameraPermissions.java | 84 +++ .../flutter/plugins/camera/CameraPlugin.java | 207 +++++++ .../flutter/plugins/camera/CameraUtils.java | 122 ++++ .../camera/example/android/app/build.gradle | 4 + .../android/app/src/main/AndroidManifest.xml | 11 + .../plugins/cameraexample/MainActivity.java | 14 + .../camera/example/android/gradle.properties | 2 + 8 files changed, 1001 insertions(+) create mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java create mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java create mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java create mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java create mode 100644 packages/camera/example/android/app/src/main/java/dev/flutter/plugins/cameraexample/MainActivity.java diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java new file mode 100644 index 000000000000..8d205400ba7c --- /dev/null +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java @@ -0,0 +1,557 @@ +package dev.flutter.plugins.camera; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaRecorder; +import android.os.Build; +import android.util.Size; +import android.view.OrientationEventListener; +import android.view.Surface; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + +import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; +import static dev.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + +public class Camera { + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final OrientationEventListener orientationEventListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + + private CameraDevice cameraDevice; + private CameraCaptureSession cameraCaptureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + private EventChannel.EventSink eventSink; + private CaptureRequest.Builder captureRequestBuilder; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private CamcorderProfile recordingProfile; + private int currentOrientation = ORIENTATION_UNKNOWN; + + // Mirrors camera.dart + public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, + } + + public Camera( + final Activity activity, + final TextureRegistry textureRegistry, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = textureRegistry.createSurfaceTexture(); + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + orientationEventListener = + new OrientationEventListener(activity.getApplicationContext()) { + @Override + public void onOrientationChanged(int i) { + if (i == ORIENTATION_UNKNOWN) { + return; + } + // Convert the raw deg angle to the nearest multiple of 90. + currentOrientation = (int) Math.round(i / 90.0) * 90; + } + }; + orientationEventListener.enable(); + + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + StreamConfigurationMap streamConfigurationMap = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + //noinspection ConstantConditions + sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + //noinspection ConstantConditions + isFrontFacing = + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraName, preset); + } + + public void setupCameraEventChannel(EventChannel cameraEventChannel) { + cameraEventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink sink) { + eventSink = sink; + } + + @Override + public void onCancel(Object arguments) { + eventSink = null; + } + }); + } + + private void prepareMediaRecorder(String outputFilePath) throws IOException { + if (mediaRecorder != null) { + mediaRecorder.release(); + } + mediaRecorder = new MediaRecorder(); + + // There's a specific order that mediaRecorder expects. Do not change the order + // of these function calls. + if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + mediaRecorder.setOutputFormat(recordingProfile.fileFormat); + if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); + mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); + mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); + if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); + mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); + mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + mediaRecorder.setOutputFile(outputFilePath); + mediaRecorder.setOrientationHint(getMediaOrientation()); + + mediaRecorder.prepare(); + } + + @SuppressLint("MissingPermission") + public void open(@NonNull final Result result) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + } catch (CameraAccessException e) { + result.error("CameraAccess", e.getMessage(), null); + close(); + return; + } + Map reply = new HashMap<>(); + reply.put("textureId", flutterTexture.id()); + reply.put("previewWidth", previewSize.getWidth()); + reply.put("previewHeight", previewSize.getHeight()); + result.success(reply); + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + sendEvent(EventType.CAMERA_CLOSING); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + sendEvent(EventType.ERROR, "The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + sendEvent(EventType.ERROR, errorDescription); + } + }, + null); + } + + private void writeToFile(ByteBuffer buffer, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + while (0 < buffer.remaining()) { + outputStream.getChannel().write(buffer); + } + } + } + + SurfaceTextureEntry getFlutterTexture() { + return flutterTexture; + } + + public void takePicture(String filePath, @NonNull final Result result) { + final File file = new File(filePath); + + if (file.exists()) { + result.error( + "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null); + return; + } + + pictureImageReader.setOnImageAvailableListener( + reader -> { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + writeToFile(buffer, file); + result.success(null); + } catch (IOException e) { + result.error("IOError", "Failed saving image", null); + } + }, + null); + + try { + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + + cameraCaptureSession.capture( + captureBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + String reason; + switch (failure.getReason()) { + case CaptureFailure.REASON_ERROR: + reason = "An error happened in the framework"; + break; + case CaptureFailure.REASON_FLUSHED: + reason = "The capture has failed due to an abortCaptures() call"; + break; + default: + reason = "Unknown reason"; + } + result.error("captureFailure", reason, null); + } + }, + null); + } catch (CameraAccessException e) { + result.error("cameraAccess", e.getMessage(), null); + } + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + captureRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + captureRequestBuilder.addTarget(surface); + } + } + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + try { + if (cameraDevice == null) { + sendEvent(EventType.ERROR, "The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + captureRequestBuilder.set( + CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + sendEvent(EventType.ERROR, e.getMessage()); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + sendEvent(EventType.ERROR, "Failed to configure camera session."); + } + }; + + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + // Start the session + cameraDevice.createCaptureSession(surfaceList, callback, null); + } + + public void startVideoRecording(String filePath, Result result) { + if (new File(filePath).exists()) { + result.error("fileExists", "File at path '" + filePath + "' already exists.", null); + return; + } + try { + prepareMediaRecorder(filePath); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + mediaRecorder.stop(); + mediaRecorder.reset(); + startPreview(); + result.success(null); + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void startPreview() throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_STILL_CAPTURE, imageStreamReader.getSurface()); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + + private void sendEvent(EventType eventType) { + sendEvent(eventType, null); + } + + private void sendEvent(EventType eventType, String description) { + if (eventSink != null) { + Map event = new HashMap<>(); + event.put("eventType", eventType.toString().toLowerCase()); + // Only errors have description + if (eventType != EventType.ERROR) { + event.put("errorDescription", description); + } + eventSink.success(event); + } + } + + private void closeCaptureSession() { + if (cameraCaptureSession != null) { + cameraCaptureSession.close(); + cameraCaptureSession = null; + } + } + + public void close() { + closeCaptureSession(); + + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } + } + + public void dispose() { + close(); + flutterTexture.release(); + orientationEventListener.disable(); + } + + private int getMediaOrientation() { + final int sensorOrientationOffset = + (currentOrientation == ORIENTATION_UNKNOWN) + ? 0 + : (isFrontFacing) ? -currentOrientation : currentOrientation; + return (sensorOrientationOffset + sensorOrientation + 360) % 360; + } + + private enum EventType { + ERROR, + CAMERA_CLOSING, + } +} diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java new file mode 100644 index 000000000000..2d70bd15b2a9 --- /dev/null +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java @@ -0,0 +1,84 @@ +package dev.flutter.plugins.camera; + +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.PluginRegistry; + +public class CameraPermissions { + private static final int CAMERA_REQUEST_ID = 9796; + private boolean ongoing = false; + + public void requestPermissions( + ActivityPluginBinding activityPluginBinding, + boolean enableAudio, + ResultCallback callback + ) { + if (ongoing) { + callback.onResult("cameraPermission", "Camera permission request ongoing"); + } + Activity activity = activityPluginBinding.getActivity(); + if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { + activityPluginBinding.addRequestPermissionsResultListener( + new CameraRequestPermissionsListener( + (String errorCode, String errorDescription) -> { + ongoing = false; + callback.onResult(errorCode, errorDescription); + })); + ongoing = true; + ActivityCompat.requestPermissions( + activity, + enableAudio + ? new String[] {permission.CAMERA, permission.RECORD_AUDIO} + : new String[] {permission.CAMERA}, + CAMERA_REQUEST_ID); + } else { + // Permissions already exist. Call the callback with success. + callback.onResult(null, null); + } + } + + private boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasAudioPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + private static class CameraRequestPermissionsListener + implements PluginRegistry.RequestPermissionsResultListener { + final ResultCallback callback; + + private CameraRequestPermissionsListener(ResultCallback callback) { + this.callback = callback; + } + + @Override + public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + if (id == CAMERA_REQUEST_ID) { + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + callback.onResult("cameraPermission", "MediaRecorderCamera permission not granted"); + } else if (grantResults.length > 1 + && grantResults[1] != PackageManager.PERMISSION_GRANTED) { + callback.onResult("cameraPermission", "MediaRecorderAudio permission not granted"); + } else { + callback.onResult(null, null); + } + return true; + } + return false; + } + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } +} diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java new file mode 100644 index 000000000000..c8dfb73d9c32 --- /dev/null +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java @@ -0,0 +1,207 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.plugins.camera; + +import android.hardware.camera2.CameraAccessException; +import android.os.Build; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +public class CameraPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler { + + private final CameraPermissions cameraPermissions = new CameraPermissions(); + private EventChannel imageStreamChannel; + private Camera camera; + + private FlutterPluginBinding pluginBinding; + private ActivityPluginBinding activityBinding; + + @Override + public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { + this.pluginBinding = flutterPluginBinding; + this.imageStreamChannel = new EventChannel( + this.pluginBinding.getFlutterEngine().getDartExecutor(), + "plugins.flutter.io/camera/imageStream" + ); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) { + this.pluginBinding = null; + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { + // Setup + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // When a background flutter view tries to register the plugin, the registrar has no activity. + // We stop the registration process as this plugin is foreground only. Also, if the sdk is + // less than 21 (min sdk for Camera2) we don't register the plugin. + return; + } + + this.activityBinding = activityPluginBinding; + + final MethodChannel channel = + new MethodChannel(pluginBinding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/camera"); + + channel.setMethodCallHandler(this); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + // Ignore + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + // Ignore + } + + @Override + public void onDetachedFromActivity() { + // Teardown + this.activityBinding = null; + } + + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String resolutionPreset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + camera = new Camera( + activityBinding.getActivity(), + pluginBinding.getFlutterEngine().getRenderer(), + cameraName, + resolutionPreset, + enableAudio + ); + + EventChannel cameraEventChannel = + new EventChannel( + pluginBinding.getFlutterEngine().getDartExecutor(), + "flutter.io/cameraPlugin/cameraEvents" + camera.getFlutterTexture().id()); + camera.setupCameraEventChannel(cameraEventChannel); + + camera.open(result); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activityBinding.getActivity())); + } catch (Exception e) { + handleException(e, result); + } + break; + case "initialize": + { + if (camera != null) { + camera.close(); + } + cameraPermissions.requestPermissions( + activityBinding, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + + break; + } + case "takePicture": + { + camera.takePicture(call.argument("path"), result); + break; + } + case "prepareForVideoRecording": + { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": + { + camera.startVideoRecording(call.argument("filePath"), result); + break; + } + case "stopVideoRecording": + { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "dispose": + { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } + + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + } + + throw (RuntimeException) exception; + } +} diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java new file mode 100644 index 000000000000..65e4476c8e21 --- /dev/null +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java @@ -0,0 +1,122 @@ +package dev.flutter.plugins.camera; + +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; +import android.util.Size; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.flutter.plugins.camera.Camera.ResolutionPreset; + +/** Provides various utilities for camera. */ +public final class CameraUtils { + + private CameraUtils() {} + + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { + // For still image captures, we use the largest available size. + return Collections.max( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + } + + public static List> getAvailableCameras(Activity activity) + throws CameraAccessException { + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + String[] cameraNames = cameraManager.getCameraIdList(); + List> cameras = new ArrayList<>(); + for (String cameraName : cameraNames) { + HashMap details = new HashMap<>(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + details.put("name", cameraName); + int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + details.put("sensorOrientation", sensorOrientation); + + int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + switch (lensFacing) { + case CameraMetadata.LENS_FACING_FRONT: + details.put("lensFacing", "front"); + break; + case CameraMetadata.LENS_FACING_BACK: + details.put("lensFacing", "back"); + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + details.put("lensFacing", "external"); + break; + } + cameras.add(details); + } + return cameras; + } + + static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + String cameraName, ResolutionPreset preset) { + int cameraId = Integer.parseInt(cameraName); + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + + private static class CompareSizesByArea implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + // We cast here to ensure the multiplications won't overflow. + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } +} diff --git a/packages/camera/example/android/app/build.gradle b/packages/camera/example/android/app/build.gradle index 39003759e4a3..f4eb5498a77d 100644 --- a/packages/camera/example/android/app/build.gradle +++ b/packages/camera/example/android/app/build.gradle @@ -57,6 +57,10 @@ flutter { } dependencies { + // TODO: remove these dependencies when engine POM is working correctly. + implementation 'androidx.lifecycle:lifecycle-runtime:2.1.0' + implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/example/android/app/src/main/AndroidManifest.xml index 15f6087e4ebe..51b898f1b4d6 100644 --- a/packages/camera/example/android/app/src/main/AndroidManifest.xml +++ b/packages/camera/example/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,17 @@ + + + + + + diff --git a/packages/camera/example/android/app/src/main/java/dev/flutter/plugins/cameraexample/MainActivity.java b/packages/camera/example/android/app/src/main/java/dev/flutter/plugins/cameraexample/MainActivity.java new file mode 100644 index 000000000000..cdc794a3d495 --- /dev/null +++ b/packages/camera/example/android/app/src/main/java/dev/flutter/plugins/cameraexample/MainActivity.java @@ -0,0 +1,14 @@ +package dev.flutter.plugins.cameraexample; + +import android.os.Bundle; + +import dev.flutter.plugins.camera.CameraPlugin; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new CameraPlugin()); + } +} diff --git a/packages/camera/example/android/gradle.properties b/packages/camera/example/android/gradle.properties index 8bd86f680510..4d3226abc21b 100644 --- a/packages/camera/example/android/gradle.properties +++ b/packages/camera/example/android/gradle.properties @@ -1 +1,3 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file From 0d53fb0542cb0b6df6d9c2df84a4e399f56ab323 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 27 Sep 2019 16:11:22 -0700 Subject: [PATCH 2/4] Simplest migration of camera plugin with maximum shared code - but otherwise a really terrible quality of code that's untestable. --- .../dev/flutter/plugins/camera/Camera.java | 557 ------------------ .../plugins/camera/CameraPermissions.java | 84 --- .../flutter/plugins/camera/CameraPlugin.java | 28 +- .../flutter/plugins/camera/CameraUtils.java | 122 ---- .../io/flutter/plugins/camera/Camera.java | 7 +- .../plugins/camera/CameraChannelHandler.java | 163 +++++ .../plugins/camera/CameraPermissions.java | 15 +- .../flutter/plugins/camera/CameraPlugin.java | 158 +---- .../flutter/plugins/camera/CameraUtils.java | 2 +- 9 files changed, 223 insertions(+), 913 deletions(-) delete mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java delete mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java delete mode 100644 packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java create mode 100644 packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraChannelHandler.java diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java deleted file mode 100644 index 8d205400ba7c..000000000000 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/Camera.java +++ /dev/null @@ -1,557 +0,0 @@ -package dev.flutter.plugins.camera; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.graphics.ImageFormat; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaRecorder; -import android.os.Build; -import android.util.Size; -import android.view.OrientationEventListener; -import android.view.Surface; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.view.FlutterView; -import io.flutter.view.TextureRegistry; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; - -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; -import static dev.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - -public class Camera { - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - - private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - private EventChannel.EventSink eventSink; - private CaptureRequest.Builder captureRequestBuilder; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private CamcorderProfile recordingProfile; - private int currentOrientation = ORIENTATION_UNKNOWN; - - // Mirrors camera.dart - public enum ResolutionPreset { - low, - medium, - high, - veryHigh, - ultraHigh, - max, - } - - public Camera( - final Activity activity, - final TextureRegistry textureRegistry, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = textureRegistry.createSurfaceTexture(); - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; - } - }; - orientationEventListener.enable(); - - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - StreamConfigurationMap streamConfigurationMap = - characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - //noinspection ConstantConditions - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - //noinspection ConstantConditions - isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - } - - public void setupCameraEventChannel(EventChannel cameraEventChannel) { - cameraEventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink sink) { - eventSink = sink; - } - - @Override - public void onCancel(Object arguments) { - eventSink = null; - } - }); - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - if (mediaRecorder != null) { - mediaRecorder.release(); - } - mediaRecorder = new MediaRecorder(); - - // There's a specific order that mediaRecorder expects. Do not change the order - // of these function calls. - if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - mediaRecorder.setOutputFile(outputFilePath); - mediaRecorder.setOrientationHint(getMediaOrientation()); - - mediaRecorder.prepare(); - } - - @SuppressLint("MissingPermission") - public void open(@NonNull final Result result) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - startPreview(); - } catch (CameraAccessException e) { - result.error("CameraAccess", e.getMessage(), null); - close(); - return; - } - Map reply = new HashMap<>(); - reply.put("textureId", flutterTexture.id()); - reply.put("previewWidth", previewSize.getWidth()); - reply.put("previewHeight", previewSize.getHeight()); - result.success(reply); - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - sendEvent(EventType.CAMERA_CLOSING); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - close(); - sendEvent(EventType.ERROR, "The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - sendEvent(EventType.ERROR, errorDescription); - } - }, - null); - } - - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - - SurfaceTextureEntry getFlutterTexture() { - return flutterTexture; - } - - public void takePicture(String filePath, @NonNull final Result result) { - final File file = new File(filePath); - - if (file.exists()) { - result.error( - "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null); - return; - } - - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - result.success(null); - } catch (IOException e) { - result.error("IOError", "Failed saving image", null); - } - }, - null); - - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - String reason; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - break; - default: - reason = "Unknown reason"; - } - result.error("captureFailure", reason, null); - } - }, - null); - } catch (CameraAccessException e) { - result.error("cameraAccess", e.getMessage(), null); - } - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); - } - } - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - try { - if (cameraDevice == null) { - sendEvent(EventType.ERROR, "The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - sendEvent(EventType.ERROR, e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - sendEvent(EventType.ERROR, "Failed to configure camera session."); - } - }; - - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - // Start the session - cameraDevice.createCaptureSession(surfaceList, callback, null); - } - - public void startVideoRecording(String filePath, Result result) { - if (new File(filePath).exists()) { - result.error("fileExists", "File at path '" + filePath + "' already exists.", null); - return; - } - try { - prepareMediaRecorder(filePath); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - mediaRecorder.stop(); - mediaRecorder.reset(); - startPreview(); - result.success(null); - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void startPreview() throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_STILL_CAPTURE, imageStreamReader.getSurface()); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); - } - - private void sendEvent(EventType eventType) { - sendEvent(eventType, null); - } - - private void sendEvent(EventType eventType, String description) { - if (eventSink != null) { - Map event = new HashMap<>(); - event.put("eventType", eventType.toString().toLowerCase()); - // Only errors have description - if (eventType != EventType.ERROR) { - event.put("errorDescription", description); - } - eventSink.success(event); - } - } - - private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; - } - } - - public void close() { - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } - } - - public void dispose() { - close(); - flutterTexture.release(); - orientationEventListener.disable(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; - } - - private enum EventType { - ERROR, - CAMERA_CLOSING, - } -} diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java deleted file mode 100644 index 2d70bd15b2a9..000000000000 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPermissions.java +++ /dev/null @@ -1,84 +0,0 @@ -package dev.flutter.plugins.camera; - -import android.Manifest.permission; -import android.app.Activity; -import android.content.pm.PackageManager; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.PluginRegistry; - -public class CameraPermissions { - private static final int CAMERA_REQUEST_ID = 9796; - private boolean ongoing = false; - - public void requestPermissions( - ActivityPluginBinding activityPluginBinding, - boolean enableAudio, - ResultCallback callback - ) { - if (ongoing) { - callback.onResult("cameraPermission", "Camera permission request ongoing"); - } - Activity activity = activityPluginBinding.getActivity(); - if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { - activityPluginBinding.addRequestPermissionsResultListener( - new CameraRequestPermissionsListener( - (String errorCode, String errorDescription) -> { - ongoing = false; - callback.onResult(errorCode, errorDescription); - })); - ongoing = true; - ActivityCompat.requestPermissions( - activity, - enableAudio - ? new String[] {permission.CAMERA, permission.RECORD_AUDIO} - : new String[] {permission.CAMERA}, - CAMERA_REQUEST_ID); - } else { - // Permissions already exist. Call the callback with success. - callback.onResult(null, null); - } - } - - private boolean hasCameraPermission(Activity activity) { - return ContextCompat.checkSelfPermission(activity, permission.CAMERA) - == PackageManager.PERMISSION_GRANTED; - } - - private boolean hasAudioPermission(Activity activity) { - return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) - == PackageManager.PERMISSION_GRANTED; - } - - private static class CameraRequestPermissionsListener - implements PluginRegistry.RequestPermissionsResultListener { - final ResultCallback callback; - - private CameraRequestPermissionsListener(ResultCallback callback) { - this.callback = callback; - } - - @Override - public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { - if (id == CAMERA_REQUEST_ID) { - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderCamera permission not granted"); - } else if (grantResults.length > 1 - && grantResults[1] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderAudio permission not granted"); - } else { - callback.onResult(null, null); - } - return true; - } - return false; - } - } - - interface ResultCallback { - void onResult(String errorCode, String errorDescription); - } -} diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java index c8dfb73d9c32..d01f041bab84 100644 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java @@ -17,16 +17,20 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.camera.Camera; +import io.flutter.plugins.camera.CameraPermissions; +import io.flutter.plugins.camera.CameraUtils; public class CameraPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler { + private FlutterPluginBinding pluginBinding; + private ActivityPluginBinding activityBinding; + private final CameraPermissions cameraPermissions = new CameraPermissions(); private EventChannel imageStreamChannel; private Camera camera; - private FlutterPluginBinding pluginBinding; - private ActivityPluginBinding activityBinding; - @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { this.pluginBinding = flutterPluginBinding; @@ -112,7 +116,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) camera.close(); } cameraPermissions.requestPermissions( - activityBinding, + activityBinding.getActivity(), + new NewEmbeddingPermissions(activityBinding), call.argument("enableAudio"), (String errCode, String errDesc) -> { if (errCode == null) { @@ -204,4 +209,19 @@ private void handleException(Exception exception, Result result) { throw (RuntimeException) exception; } + + private static class NewEmbeddingPermissions implements CameraPermissions.Permissions { + private ActivityPluginBinding activityPluginBinding; + + private NewEmbeddingPermissions(@NonNull ActivityPluginBinding activityPluginBinding) { + this.activityPluginBinding = activityPluginBinding; + } + + @Override + public PluginRegistry.Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener listener) { + activityPluginBinding.addRequestPermissionsResultListener(listener); + + return null; // <- this is a breaking change. + } + } } diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java deleted file mode 100644 index 65e4476c8e21..000000000000 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraUtils.java +++ /dev/null @@ -1,122 +0,0 @@ -package dev.flutter.plugins.camera; - -import android.app.Activity; -import android.content.Context; -import android.graphics.ImageFormat; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.util.Size; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import dev.flutter.plugins.camera.Camera.ResolutionPreset; - -/** Provides various utilities for camera. */ -public final class CameraUtils { - - private CameraUtils() {} - - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - - public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { - CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - String[] cameraNames = cameraManager.getCameraIdList(); - List> cameras = new ArrayList<>(); - for (String cameraName : cameraNames) { - HashMap details = new HashMap<>(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - details.put("name", cameraName); - int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - details.put("sensorOrientation", sensorOrientation); - - int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); - switch (lensFacing) { - case CameraMetadata.LENS_FACING_FRONT: - details.put("lensFacing", "front"); - break; - case CameraMetadata.LENS_FACING_BACK: - details.put("lensFacing", "back"); - break; - case CameraMetadata.LENS_FACING_EXTERNAL: - details.put("lensFacing", "external"); - break; - } - cameras.add(details); - } - return cameras; - } - - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); - } - } - } - - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } -} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 80da644a146a..2a86c30a134f 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -29,6 +29,7 @@ import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.FileOutputStream; @@ -74,7 +75,7 @@ public enum ResolutionPreset { public Camera( final Activity activity, - final FlutterView flutterView, + final TextureRegistry textureRegistry, final String cameraName, final String resolutionPreset, final boolean enableAudio) @@ -85,7 +86,7 @@ public Camera( this.cameraName = cameraName; this.enableAudio = enableAudio; - this.flutterTexture = flutterView.createSurfaceTexture(); + this.flutterTexture = textureRegistry.createSurfaceTexture(); this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); orientationEventListener = new OrientationEventListener(activity.getApplicationContext()) { @@ -233,7 +234,7 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException { } } - SurfaceTextureEntry getFlutterTexture() { + public SurfaceTextureEntry getFlutterTexture() { return flutterTexture; } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraChannelHandler.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraChannelHandler.java new file mode 100644 index 000000000000..739d66057c0c --- /dev/null +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraChannelHandler.java @@ -0,0 +1,163 @@ +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.view.TextureRegistry; + +public class CameraChannelHandler implements MethodChannel.MethodCallHandler { + + private final CameraPermissions cameraPermissions = new CameraPermissions(); + private final Activity activity; + private final TextureRegistry textureRegistry; + private final BinaryMessenger messenger; + private final EventChannel imageStreamChannel; + private final CameraPermissions.Permissions permissions; + private Camera camera; + + public CameraChannelHandler( + @NonNull Activity activity, + @NonNull TextureRegistry textureRegistry, + @NonNull BinaryMessenger binaryMessenger, + @NonNull EventChannel imageStreamChannel, + @NonNull CameraPermissions.Permissions permissions + ) { + this.activity = activity; + this.textureRegistry = textureRegistry; + this.messenger = binaryMessenger; + this.imageStreamChannel = imageStreamChannel; + this.permissions = permissions; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activity)); + } catch (Exception e) { + handleException(e, result); + } + break; + case "initialize": + { + if (camera != null) { + camera.close(); + } + cameraPermissions.requestPermissions( + activity, + permissions, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + + break; + } + case "takePicture": + { + camera.takePicture(call.argument("path"), result); + break; + } + case "prepareForVideoRecording": + { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": + { + camera.startVideoRecording(call.argument("filePath"), result); + break; + } + case "stopVideoRecording": + { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "dispose": + { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } + + private void instantiateCamera(MethodCall call, MethodChannel.Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String resolutionPreset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + camera = new Camera(activity, textureRegistry, cameraName, resolutionPreset, enableAudio); + + EventChannel cameraEventChannel = + new EventChannel( + messenger, + "flutter.io/cameraPlugin/cameraEvents" + camera.getFlutterTexture().id()); + camera.setupCameraEventChannel(cameraEventChannel); + + camera.open(result); + } + + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, MethodChannel.Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + } + + throw (RuntimeException) exception; + } +} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index e45fb1e5a594..ee07afaf83d5 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -6,6 +6,8 @@ import android.content.pm.PackageManager; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; + +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; @@ -14,13 +16,12 @@ public class CameraPermissions { private boolean ongoing = false; public void requestPermissions( - Registrar registrar, boolean enableAudio, ResultCallback callback) { + Activity activity, Permissions permissions, boolean enableAudio, ResultCallback callback) { if (ongoing) { callback.onResult("cameraPermission", "Camera permission request ongoing"); } - Activity activity = registrar.activity(); if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { - registrar.addRequestPermissionsResultListener( + permissions.addRequestPermissionsResultListener( new CameraRequestPermissionsListener( (String errorCode, String errorDescription) -> { ongoing = false; @@ -74,7 +75,13 @@ public boolean onRequestPermissionsResult(int id, String[] permissions, int[] gr } } - interface ResultCallback { + public interface Permissions { + Registrar addRequestPermissionsResultListener( + PluginRegistry.RequestPermissionsResultListener listener + ); + } + + public interface ResultCallback { void onResult(String errorCode, String errorDescription); } } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index b3a1da8b1b09..98e8d164343b 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -4,31 +4,16 @@ package io.flutter.plugins.camera; -import android.hardware.camera2.CameraAccessException; import android.os.Build; + import androidx.annotation.NonNull; + import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterView; - -public class CameraPlugin implements MethodCallHandler { - private final CameraPermissions cameraPermissions = new CameraPermissions(); - private final FlutterView view; - private final Registrar registrar; - private final EventChannel imageStreamChannel; - private Camera camera; - - private CameraPlugin(Registrar registrar) { - this.registrar = registrar; - this.view = registrar.view(); - this.imageStreamChannel = - new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"); - } +public class CameraPlugin { public static void registerWith(Registrar registrar) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { @@ -38,133 +23,30 @@ public static void registerWith(Registrar registrar) { return; } + CameraChannelHandler channelHandler = new CameraChannelHandler( + registrar.activity(), + registrar.view(), + registrar.messenger(), + new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"), + new OldEmbeddingPermissions(registrar) + ); + final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/camera"); - channel.setMethodCallHandler(new CameraPlugin(registrar)); + channel.setMethodCallHandler(channelHandler); } - private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { - String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); - camera = new Camera(registrar.activity(), view, cameraName, resolutionPreset, enableAudio); + private static class OldEmbeddingPermissions implements CameraPermissions.Permissions { + private Registrar registrar; - EventChannel cameraEventChannel = - new EventChannel( - registrar.messenger(), - "flutter.io/cameraPlugin/cameraEvents" + camera.getFlutterTexture().id()); - camera.setupCameraEventChannel(cameraEventChannel); - - camera.open(result); - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { - switch (call.method) { - case "availableCameras": - try { - result.success(CameraUtils.getAvailableCameras(registrar.activity())); - } catch (Exception e) { - handleException(e, result); - } - break; - case "initialize": - { - if (camera != null) { - camera.close(); - } - cameraPermissions.requestPermissions( - registrar, - call.argument("enableAudio"), - (String errCode, String errDesc) -> { - if (errCode == null) { - try { - instantiateCamera(call, result); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error(errCode, errDesc, null); - } - }); - - break; - } - case "takePicture": - { - camera.takePicture(call.argument("path"), result); - break; - } - case "prepareForVideoRecording": - { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": - { - camera.startVideoRecording(call.argument("filePath"), result); - break; - } - case "stopVideoRecording": - { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": - { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": - { - camera.resumeVideoRecording(result); - break; - } - case "startImageStream": - { - try { - camera.startPreviewWithImageStream(imageStreamChannel); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "stopImageStream": - { - try { - camera.startPreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "dispose": - { - if (camera != null) { - camera.dispose(); - } - result.success(null); - break; - } - default: - result.notImplemented(); - break; + private OldEmbeddingPermissions(@NonNull Registrar registrar) { + this.registrar = registrar; } - } - // We move catching CameraAccessException out of onMethodCall because it causes a crash - // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to - // to be able to compile with <21 sdks for apps that want the camera and support earlier version. - @SuppressWarnings("ConstantConditions") - private void handleException(Exception exception, Result result) { - if (exception instanceof CameraAccessException) { - result.error("CameraAccess", exception.getMessage(), null); + @Override + public Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener listener) { + return registrar.addRequestPermissionsResultListener(listener); } - - throw (RuntimeException) exception; } } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index a7bb3b7d4914..ec0399e04446 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -24,7 +24,7 @@ public final class CameraUtils { private CameraUtils() {} - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + public static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } From eb190f701c2deea751f7e48424cdaf69c508728f Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 27 Sep 2019 16:17:04 -0700 Subject: [PATCH 3/4] Removed an unnecessary duplication. --- .../flutter/plugins/camera/CameraPlugin.java | 121 ++---------------- 1 file changed, 9 insertions(+), 112 deletions(-) diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java index d01f041bab84..b4b0aaad0d31 100644 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java @@ -19,10 +19,11 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugins.camera.Camera; +import io.flutter.plugins.camera.CameraChannelHandler; import io.flutter.plugins.camera.CameraPermissions; import io.flutter.plugins.camera.CameraUtils; -public class CameraPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler { +public class CameraPlugin implements FlutterPlugin, ActivityAware { private FlutterPluginBinding pluginBinding; private ActivityPluginBinding activityBinding; @@ -60,7 +61,13 @@ public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { final MethodChannel channel = new MethodChannel(pluginBinding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/camera"); - channel.setMethodCallHandler(this); + channel.setMethodCallHandler(new CameraChannelHandler( + activityPluginBinding.getActivity(), + pluginBinding.getFlutterEngine().getRenderer(), + pluginBinding.getFlutterEngine().getDartExecutor(), + new EventChannel(pluginBinding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/camera/imageStream"), + new NewEmbeddingPermissions(activityPluginBinding) + )); } @Override @@ -100,116 +107,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce camera.open(result); } - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { - switch (call.method) { - case "availableCameras": - try { - result.success(CameraUtils.getAvailableCameras(activityBinding.getActivity())); - } catch (Exception e) { - handleException(e, result); - } - break; - case "initialize": - { - if (camera != null) { - camera.close(); - } - cameraPermissions.requestPermissions( - activityBinding.getActivity(), - new NewEmbeddingPermissions(activityBinding), - call.argument("enableAudio"), - (String errCode, String errDesc) -> { - if (errCode == null) { - try { - instantiateCamera(call, result); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error(errCode, errDesc, null); - } - }); - - break; - } - case "takePicture": - { - camera.takePicture(call.argument("path"), result); - break; - } - case "prepareForVideoRecording": - { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": - { - camera.startVideoRecording(call.argument("filePath"), result); - break; - } - case "stopVideoRecording": - { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": - { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": - { - camera.resumeVideoRecording(result); - break; - } - case "startImageStream": - { - try { - camera.startPreviewWithImageStream(imageStreamChannel); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "stopImageStream": - { - try { - camera.startPreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "dispose": - { - if (camera != null) { - camera.dispose(); - } - result.success(null); - break; - } - default: - result.notImplemented(); - break; - } - } - - // We move catching CameraAccessException out of onMethodCall because it causes a crash - // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to - // to be able to compile with <21 sdks for apps that want the camera and support earlier version. - @SuppressWarnings("ConstantConditions") - private void handleException(Exception exception, Result result) { - if (exception instanceof CameraAccessException) { - result.error("CameraAccess", exception.getMessage(), null); - } - - throw (RuntimeException) exception; - } - private static class NewEmbeddingPermissions implements CameraPermissions.Permissions { private ActivityPluginBinding activityPluginBinding; From d6ab1d68998e608d37f79884f1dcaa217cf5dd9f Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 27 Sep 2019 16:17:56 -0700 Subject: [PATCH 4/4] Removed another unnecessary duplication. --- .../flutter/plugins/camera/CameraPlugin.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java index b4b0aaad0d31..fe64c77e73f8 100644 --- a/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java @@ -86,27 +86,6 @@ public void onDetachedFromActivity() { this.activityBinding = null; } - private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { - String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); - camera = new Camera( - activityBinding.getActivity(), - pluginBinding.getFlutterEngine().getRenderer(), - cameraName, - resolutionPreset, - enableAudio - ); - - EventChannel cameraEventChannel = - new EventChannel( - pluginBinding.getFlutterEngine().getDartExecutor(), - "flutter.io/cameraPlugin/cameraEvents" + camera.getFlutterTexture().id()); - camera.setupCameraEventChannel(cameraEventChannel); - - camera.open(result); - } - private static class NewEmbeddingPermissions implements CameraPermissions.Permissions { private ActivityPluginBinding activityPluginBinding;