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..fe64c77e73f8 --- /dev/null +++ b/packages/camera/android/src/main/java/dev/flutter/plugins/camera/CameraPlugin.java @@ -0,0 +1,103 @@ +// 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; +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 { + + private FlutterPluginBinding pluginBinding; + private ActivityPluginBinding activityBinding; + + private final CameraPermissions cameraPermissions = new CameraPermissions(); + private EventChannel imageStreamChannel; + private Camera camera; + + @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(new CameraChannelHandler( + activityPluginBinding.getActivity(), + pluginBinding.getFlutterEngine().getRenderer(), + pluginBinding.getFlutterEngine().getDartExecutor(), + new EventChannel(pluginBinding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/camera/imageStream"), + new NewEmbeddingPermissions(activityPluginBinding) + )); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + // Ignore + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + // Ignore + } + + @Override + public void onDetachedFromActivity() { + // Teardown + this.activityBinding = null; + } + + 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/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; } 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