diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 2fe0e44fa6c2..14d2289d99b1 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.6 + +* Add support for the v2 Android embedding. This shouldn't affect existing + functionality. + ## 0.5.5+1 * Fix event type check diff --git a/packages/camera/android/build.gradle b/packages/camera/android/build.gradle index ab2fc8fd89f0..ebd355385c1c 100644 --- a/packages/camera/android/build.gradle +++ b/packages/camera/android/build.gradle @@ -60,3 +60,28 @@ android { dependencies { testImplementation 'junit:junit:4.12' } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "2.1.0" + api "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + api "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + } + } + } +} diff --git a/packages/camera/android/gradle.properties b/packages/camera/android/gradle.properties index 8bd86f680510..94adc3a3f97a 100644 --- a/packages/camera/android/gradle.properties +++ b/packages/camera/android/gradle.properties @@ -1 +1,3 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true 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..3c86ce0d9816 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 @@ -7,20 +7,30 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener; + +final class CameraPermissions { + interface PermissionsRegistry { + void addListener(RequestPermissionsResultListener handler); + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } -public class CameraPermissions { private static final int CAMERA_REQUEST_ID = 9796; private boolean ongoing = false; - public void requestPermissions( - Registrar registrar, boolean enableAudio, ResultCallback callback) { + void requestPermissions( + Activity activity, + PermissionsRegistry permissionsRegistry, + 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( + permissionsRegistry.addListener( new CameraRequestPermissionsListener( (String errorCode, String errorDescription) -> { ongoing = false; @@ -51,6 +61,7 @@ private boolean hasAudioPermission(Activity activity) { private static class CameraRequestPermissionsListener implements PluginRegistry.RequestPermissionsResultListener { + final ResultCallback callback; private CameraRequestPermissionsListener(ResultCallback callback) { @@ -73,8 +84,4 @@ public boolean onRequestPermissionsResult(int id, String[] permissions, int[] gr return false; } } - - 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 b504f039e326..9bd34e17aa02 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,172 +4,108 @@ package io.flutter.plugins.camera; -import android.hardware.camera2.CameraAccessException; +import android.app.Activity; 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 androidx.annotation.Nullable; +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.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterView; +import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; -public class CameraPlugin implements MethodCallHandler { +/** + * Platform implementation of the camera_plugin. + * + *
Instantiate this in an add to app scenario to gracefully handle activity and context changes. + * See {@code io.flutter.plugins.camera.MainActivity} for an example. + * + *
Call {@link #registerWith(Registrar)} to register an implementation of this that uses the + * stable {@code io.flutter.plugin.common} package. + */ +public final class CameraPlugin implements FlutterPlugin, ActivityAware { - private final CameraPermissions cameraPermissions = new CameraPermissions(); - private final FlutterView view; - private final Registrar registrar; - private final EventChannel imageStreamChannel; - private Camera camera; + private static final String TAG = "CameraPlugin"; + private @Nullable FlutterPluginBinding flutterPluginBinding; + private @Nullable MethodCallHandlerImpl methodCallHandler; - private CameraPlugin(Registrar registrar) { - this.registrar = registrar; - this.view = registrar.view(); - this.imageStreamChannel = - new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"); - } + /** + * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. + * + *
See {@code io.flutter.plugins.camera.MainActivity} for an example. + */ + public CameraPlugin() {} + /** + * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} + * package. + * + *
Calling this automatically initializes the plugin. However plugins initialized this way
+ * won't react to changes in activity or context, unlike {@link CameraPlugin}.
+ */
public static void registerWith(Registrar registrar) {
- 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;
- }
+ CameraPlugin plugin = new CameraPlugin();
+ plugin.maybeStartListening(
+ registrar.activity(),
+ registrar.messenger(),
+ registrar::addRequestPermissionsResultListener,
+ registrar.view());
+ }
- final MethodChannel channel =
- new MethodChannel(registrar.messenger(), "plugins.flutter.io/camera");
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
+ this.flutterPluginBinding = binding;
+ }
- channel.setMethodCallHandler(new CameraPlugin(registrar));
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ this.flutterPluginBinding = 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");
- TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = view.createSurfaceTexture();
- DartMessenger dartMessenger =
- new DartMessenger(registrar.messenger(), flutterSurfaceTexture.id());
- camera =
- new Camera(
- registrar.activity(),
- flutterSurfaceTexture,
- dartMessenger,
- cameraName,
- resolutionPreset,
- enableAudio);
+ @Override
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+ maybeStartListening(
+ binding.getActivity(),
+ flutterPluginBinding.getFlutterEngine().getDartExecutor(),
+ binding::addRequestPermissionsResultListener,
+ flutterPluginBinding.getFlutterEngine().getRenderer());
+ }
- camera.open(result);
+ @Override
+ public void onDetachedFromActivity() {
+ if (methodCallHandler == null) {
+ // Could be on too low of an SDK to have started listening originally.
+ return;
+ }
+
+ methodCallHandler.stopListening();
+ methodCallHandler = null;
}
@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);
- }
- });
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
+ onAttachedToActivity(binding);
+ }
- 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;
- }
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
}
- // 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);
+ private void maybeStartListening(
+ Activity activity,
+ BinaryMessenger messenger,
+ PermissionsRegistry permissionsRegistry,
+ TextureRegistry textureRegistry) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
+ return;
}
- throw (RuntimeException) exception;
+ methodCallHandler =
+ new MethodCallHandlerImpl(
+ activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
}
}
diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
new file mode 100644
index 000000000000..cb58d19a9a02
--- /dev/null
+++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
@@ -0,0 +1,174 @@
+package io.flutter.plugins.camera;
+
+import android.app.Activity;
+import android.hardware.camera2.CameraAccessException;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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.plugin.common.MethodChannel.Result;
+import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
+import io.flutter.view.TextureRegistry;
+
+final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
+ private final Activity activity;
+ private final BinaryMessenger messenger;
+ private final CameraPermissions cameraPermissions;
+ private final PermissionsRegistry permissionsRegistry;
+ private final TextureRegistry textureRegistry;
+ private final MethodChannel methodChannel;
+ private final EventChannel imageStreamChannel;
+ private @Nullable Camera camera;
+
+ MethodCallHandlerImpl(
+ Activity activity,
+ BinaryMessenger messenger,
+ CameraPermissions cameraPermissions,
+ PermissionsRegistry permissionsAdder,
+ TextureRegistry textureRegistry) {
+ this.activity = activity;
+ this.messenger = messenger;
+ this.cameraPermissions = cameraPermissions;
+ this.permissionsRegistry = permissionsAdder;
+ this.textureRegistry = textureRegistry;
+
+ methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera");
+ imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream");
+ methodChannel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull final 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,
+ permissionsRegistry,
+ 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;
+ }
+ }
+
+ void stopListening() {
+ methodChannel.setMethodCallHandler(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");
+ TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
+ textureRegistry.createSurfaceTexture();
+ DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id());
+ camera =
+ new Camera(
+ activity,
+ flutterSurfaceTexture,
+ dartMessenger,
+ cameraName,
+ resolutionPreset,
+ enableAudio);
+
+ 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, Result result) {
+ if (exception instanceof CameraAccessException) {
+ result.error("CameraAccess", exception.getMessage(), null);
+ }
+
+ throw (RuntimeException) exception;
+ }
+}
diff --git a/packages/camera/example/android/app/build.gradle b/packages/camera/example/android/app/build.gradle
index 39003759e4a3..e47b6db5e21e 100644
--- a/packages/camera/example/android/app/build.gradle
+++ b/packages/camera/example/android/app/build.gradle
@@ -58,6 +58,7 @@ flutter {
dependencies {
testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test:runner:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test:rules:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
diff --git a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java b/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java
new file mode 100644
index 000000000000..95b5f4373b62
--- /dev/null
+++ b/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java
@@ -0,0 +1,13 @@
+package io.flutter.plugins.cameraexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class EmbeddingV1ActivityTest {
+ @Rule
+ public ActivityTestRule