Skip to content

Commit

Permalink
[camerax] Add LifecycleOwner Proxy (flutter#3837)
Browse files Browse the repository at this point in the history
Adds proxy implementation of `LifecycleOwner` for `Activity`s this plugin may be bound to.

Heavily inspired by [`google_maps_flutter_android`](https://github.com/flutter/packages/blob/main/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java#L121).

Fixes flutter/flutter#125695.
  • Loading branch information
camsim99 authored and nploi committed Jul 16, 2023
1 parent a4739ed commit 6954af4
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
* Fixes cast of CameraInfo to fix integration test failure.
* Updates internal Java InstanceManager to only stop finalization callbacks when stopped.
* Implements image streaming.
* Provides LifecycleOwner implementation for Activities that use the plugin that do not implement it themselves.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

package io.flutter.plugins.camerax;

import android.app.Activity;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleOwner;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
Expand All @@ -17,10 +19,11 @@
public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware {
private InstanceManager instanceManager;
private FlutterPluginBinding pluginBinding;
private ProcessCameraProviderHostApiImpl processCameraProviderHostApi;
private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl;
private ImageCaptureHostApiImpl imageCaptureHostApi;
public SystemServicesHostApiImpl systemServicesHostApi;
private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
public SystemServicesHostApiImpl systemServicesHostApiImpl;

@VisibleForTesting ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl;

/**
* Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment.
Expand All @@ -29,7 +32,11 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
*/
public CameraAndroidCameraxPlugin() {}

void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) {
@VisibleForTesting
public void setUp(
@NonNull BinaryMessenger binaryMessenger,
@NonNull Context context,
@NonNull TextureRegistry textureRegistry) {
// Set up instance manager.
instanceManager =
InstanceManager.create(
Expand All @@ -47,16 +54,17 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex
binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager));
GeneratedCameraXLibrary.JavaObjectHostApi.setup(
binaryMessenger, new JavaObjectHostApiImpl(instanceManager));
processCameraProviderHostApi =
processCameraProviderHostApiImpl =
new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup(
binaryMessenger, processCameraProviderHostApi);
systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
binaryMessenger, processCameraProviderHostApiImpl);
systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl);
GeneratedCameraXLibrary.PreviewHostApi.setup(
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi);
imageCaptureHostApiImpl =
new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApiImpl);
imageAnalysisHostApiImpl = new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.ImageAnalysisHostApi.setup(binaryMessenger, imageAnalysisHostApiImpl);
GeneratedCameraXLibrary.AnalyzerHostApi.setup(
Expand Down Expand Up @@ -86,10 +94,18 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi
pluginBinding.getApplicationContext(),
pluginBinding.getTextureRegistry());
updateContext(pluginBinding.getApplicationContext());
processCameraProviderHostApi.setLifecycleOwner(
(LifecycleOwner) activityPluginBinding.getActivity());
systemServicesHostApi.setActivity(activityPluginBinding.getActivity());
systemServicesHostApi.setPermissionsRegistry(

Activity activity = activityPluginBinding.getActivity();

if (activity instanceof LifecycleOwner) {
processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity);
} else {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider);
}

systemServicesHostApiImpl.setActivity(activity);
systemServicesHostApiImpl.setPermissionsRegistry(
activityPluginBinding::addRequestPermissionsResultListener);
}

Expand All @@ -113,12 +129,12 @@ public void onDetachedFromActivity() {
* Updates context that is used to fetch the corresponding instance of a {@code
* ProcessCameraProvider}.
*/
public void updateContext(Context context) {
if (processCameraProviderHostApi != null) {
processCameraProviderHostApi.setContext(context);
public void updateContext(@NonNull Context context) {
if (processCameraProviderHostApiImpl != null) {
processCameraProviderHostApiImpl.setContext(context);
}
if (imageCaptureHostApi != null) {
processCameraProviderHostApi.setContext(context);
if (imageCaptureHostApiImpl != null) {
imageCaptureHostApiImpl.setContext(context);
}
if (imageAnalysisHostApiImpl != null) {
imageAnalysisHostApiImpl.setContext(context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2013 The Flutter 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 io.flutter.plugins.camerax;

import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;

/**
* This class provides a custom {@link LifecycleOwner} for the activity driven by {@link
* ActivityLifecycleCallbacks}.
*
* <p>This is used in the case where a direct {@link LifecycleOwner} is not available.
*/
public class ProxyLifecycleProvider implements ActivityLifecycleCallbacks, LifecycleOwner {

@VisibleForTesting @NonNull public LifecycleRegistry lifecycle = new LifecycleRegistry(this);
private final int registrarActivityHashCode;

ProxyLifecycleProvider(@NonNull Activity activity) {
this.registrarActivityHashCode = activity.hashCode();
activity.getApplication().registerActivityLifecycleCallbacks(this);
}

@Override
public void onActivityCreated(@NonNull Activity activity, @NonNull Bundle savedInstanceState) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
lifecycle.handleLifecycleEvent(Event.ON_CREATE);
}

@Override
public void onActivityStarted(@NonNull Activity activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
lifecycle.handleLifecycleEvent(Event.ON_START);
}

@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
lifecycle.handleLifecycleEvent(Event.ON_RESUME);
}

@Override
public void onActivityPaused(@NonNull Activity activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
lifecycle.handleLifecycleEvent(Event.ON_PAUSE);
}

@Override
public void onActivityStopped(@NonNull Activity activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
lifecycle.handleLifecycleEvent(Event.ON_STOP);
}

@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return;
}
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
lifecycle.handleLifecycleEvent(Event.ON_DESTROY);
}

@NonNull
@Override
public Lifecycle getLifecycle() {
return lifecycle;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2013 The Flutter 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 io.flutter.plugins.camerax;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import android.app.Activity;
import android.app.Application;
import androidx.lifecycle.LifecycleOwner;
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class CameraAndroidCameraxPluginTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

@Mock ActivityPluginBinding activityPluginBinding;
@Mock FlutterPluginBinding flutterPluginBinding;

@Test
public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() {
CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
Activity mockActivity =
mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class));
ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
mock(ProcessCameraProviderHostApiImpl.class);

doNothing().when(plugin).setUp(any(), any(), any());
when(activityPluginBinding.getActivity()).thenReturn(mockActivity);

plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onAttachedToActivity(activityPluginBinding);

verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class));
}

@Test
public void
onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwner() {
CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
Activity mockActivity = mock(Activity.class);
ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
mock(ProcessCameraProviderHostApiImpl.class);

doNothing().when(plugin).setUp(any(), any(), any());
when(activityPluginBinding.getActivity()).thenReturn(mockActivity);
when(mockActivity.getApplication()).thenReturn(mock(Application.class));

plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onAttachedToActivity(activityPluginBinding);

verify(mockProcessCameraProviderHostApiImpl)
.setLifecycleOwner(any(ProxyLifecycleProvider.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2013 The Flutter 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 io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleRegistry;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class ProxyLifecycleProviderTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

@Mock Activity activity;
@Mock Application application;
@Mock LifecycleRegistry mockLifecycleRegistry;

private final int testHashCode = 27;

@Before
public void setUp() {
when(activity.getApplication()).thenReturn(application);
}

@Test
public void onActivityCreated_handlesOnCreateEvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
Bundle mockBundle = mock(Bundle.class);

proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityCreated(activity, mockBundle);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_CREATE);
}

@Test
public void onActivityStarted_handlesOnActivityStartedEvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityStarted(activity);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_START);
}

@Test
public void onActivityResumed_handlesOnActivityResumedEvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityResumed(activity);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_RESUME);
}

@Test
public void onActivityPaused_handlesOnActivityPausedEvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityPaused(activity);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_PAUSE);
}

@Test
public void onActivityStopped_handlesOnActivityStoppedEvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityStopped(activity);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_STOP);
}

@Test
public void onActivityDestroyed_handlesOnActivityDestroyed() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivityDestroyed(activity);

verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_DESTROY);
}

@Test
public void onActivitySaveInstanceState_doesNotHandleLifecycleEvvent() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
Bundle mockBundle = mock(Bundle.class);

proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

proxyLifecycleProvider.onActivitySaveInstanceState(activity, mockBundle);

verifyNoInteractions(mockLifecycleRegistry);
}

@Test
public void getLifecycle_returnsExpectedLifecycle() {
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);

proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;

assertEquals(proxyLifecycleProvider.getLifecycle(), mockLifecycleRegistry);
}
}
Loading

0 comments on commit 6954af4

Please sign in to comment.