Skip to content

Commit 2f95ecd

Browse files
authored
[camerax] Add LifecycleOwner Proxy (#3837)
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.
1 parent 815cfca commit 2f95ecd

File tree

6 files changed

+321
-20
lines changed

6 files changed

+321
-20
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
* Fixes cast of CameraInfo to fix integration test failure.
2020
* Updates internal Java InstanceManager to only stop finalization callbacks when stopped.
2121
* Implements image streaming.
22+
* Provides LifecycleOwner implementation for Activities that use the plugin that do not implement it themselves.

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
package io.flutter.plugins.camerax;
66

7+
import android.app.Activity;
78
import android.content.Context;
89
import androidx.annotation.NonNull;
10+
import androidx.annotation.VisibleForTesting;
911
import androidx.lifecycle.LifecycleOwner;
1012
import io.flutter.embedding.engine.plugins.FlutterPlugin;
1113
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
@@ -17,10 +19,11 @@
1719
public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware {
1820
private InstanceManager instanceManager;
1921
private FlutterPluginBinding pluginBinding;
20-
private ProcessCameraProviderHostApiImpl processCameraProviderHostApi;
2122
private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl;
22-
private ImageCaptureHostApiImpl imageCaptureHostApi;
23-
public SystemServicesHostApiImpl systemServicesHostApi;
23+
private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
24+
public SystemServicesHostApiImpl systemServicesHostApiImpl;
25+
26+
@VisibleForTesting ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl;
2427

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

32-
void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) {
35+
@VisibleForTesting
36+
public void setUp(
37+
@NonNull BinaryMessenger binaryMessenger,
38+
@NonNull Context context,
39+
@NonNull TextureRegistry textureRegistry) {
3340
// Set up instance manager.
3441
instanceManager =
3542
InstanceManager.create(
@@ -47,16 +54,17 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex
4754
binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager));
4855
GeneratedCameraXLibrary.JavaObjectHostApi.setup(
4956
binaryMessenger, new JavaObjectHostApiImpl(instanceManager));
50-
processCameraProviderHostApi =
57+
processCameraProviderHostApiImpl =
5158
new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context);
5259
GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup(
53-
binaryMessenger, processCameraProviderHostApi);
54-
systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
55-
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
60+
binaryMessenger, processCameraProviderHostApiImpl);
61+
systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
62+
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl);
5663
GeneratedCameraXLibrary.PreviewHostApi.setup(
5764
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
58-
imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
59-
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi);
65+
imageCaptureHostApiImpl =
66+
new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
67+
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApiImpl);
6068
imageAnalysisHostApiImpl = new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager);
6169
GeneratedCameraXLibrary.ImageAnalysisHostApi.setup(binaryMessenger, imageAnalysisHostApiImpl);
6270
GeneratedCameraXLibrary.AnalyzerHostApi.setup(
@@ -86,10 +94,18 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi
8694
pluginBinding.getApplicationContext(),
8795
pluginBinding.getTextureRegistry());
8896
updateContext(pluginBinding.getApplicationContext());
89-
processCameraProviderHostApi.setLifecycleOwner(
90-
(LifecycleOwner) activityPluginBinding.getActivity());
91-
systemServicesHostApi.setActivity(activityPluginBinding.getActivity());
92-
systemServicesHostApi.setPermissionsRegistry(
97+
98+
Activity activity = activityPluginBinding.getActivity();
99+
100+
if (activity instanceof LifecycleOwner) {
101+
processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity);
102+
} else {
103+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
104+
processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider);
105+
}
106+
107+
systemServicesHostApiImpl.setActivity(activity);
108+
systemServicesHostApiImpl.setPermissionsRegistry(
93109
activityPluginBinding::addRequestPermissionsResultListener);
94110
}
95111

@@ -113,12 +129,12 @@ public void onDetachedFromActivity() {
113129
* Updates context that is used to fetch the corresponding instance of a {@code
114130
* ProcessCameraProvider}.
115131
*/
116-
public void updateContext(Context context) {
117-
if (processCameraProviderHostApi != null) {
118-
processCameraProviderHostApi.setContext(context);
132+
public void updateContext(@NonNull Context context) {
133+
if (processCameraProviderHostApiImpl != null) {
134+
processCameraProviderHostApiImpl.setContext(context);
119135
}
120-
if (imageCaptureHostApi != null) {
121-
processCameraProviderHostApi.setContext(context);
136+
if (imageCaptureHostApiImpl != null) {
137+
imageCaptureHostApiImpl.setContext(context);
122138
}
123139
if (imageAnalysisHostApiImpl != null) {
124140
imageAnalysisHostApiImpl.setContext(context);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import android.app.Activity;
8+
import android.app.Application.ActivityLifecycleCallbacks;
9+
import android.os.Bundle;
10+
import androidx.annotation.NonNull;
11+
import androidx.annotation.VisibleForTesting;
12+
import androidx.lifecycle.Lifecycle;
13+
import androidx.lifecycle.Lifecycle.Event;
14+
import androidx.lifecycle.LifecycleOwner;
15+
import androidx.lifecycle.LifecycleRegistry;
16+
17+
/**
18+
* This class provides a custom {@link LifecycleOwner} for the activity driven by {@link
19+
* ActivityLifecycleCallbacks}.
20+
*
21+
* <p>This is used in the case where a direct {@link LifecycleOwner} is not available.
22+
*/
23+
public class ProxyLifecycleProvider implements ActivityLifecycleCallbacks, LifecycleOwner {
24+
25+
@VisibleForTesting @NonNull public LifecycleRegistry lifecycle = new LifecycleRegistry(this);
26+
private final int registrarActivityHashCode;
27+
28+
ProxyLifecycleProvider(@NonNull Activity activity) {
29+
this.registrarActivityHashCode = activity.hashCode();
30+
activity.getApplication().registerActivityLifecycleCallbacks(this);
31+
}
32+
33+
@Override
34+
public void onActivityCreated(@NonNull Activity activity, @NonNull Bundle savedInstanceState) {
35+
if (activity.hashCode() != registrarActivityHashCode) {
36+
return;
37+
}
38+
lifecycle.handleLifecycleEvent(Event.ON_CREATE);
39+
}
40+
41+
@Override
42+
public void onActivityStarted(@NonNull Activity activity) {
43+
if (activity.hashCode() != registrarActivityHashCode) {
44+
return;
45+
}
46+
lifecycle.handleLifecycleEvent(Event.ON_START);
47+
}
48+
49+
@Override
50+
public void onActivityResumed(@NonNull Activity activity) {
51+
if (activity.hashCode() != registrarActivityHashCode) {
52+
return;
53+
}
54+
lifecycle.handleLifecycleEvent(Event.ON_RESUME);
55+
}
56+
57+
@Override
58+
public void onActivityPaused(@NonNull Activity activity) {
59+
if (activity.hashCode() != registrarActivityHashCode) {
60+
return;
61+
}
62+
lifecycle.handleLifecycleEvent(Event.ON_PAUSE);
63+
}
64+
65+
@Override
66+
public void onActivityStopped(@NonNull Activity activity) {
67+
if (activity.hashCode() != registrarActivityHashCode) {
68+
return;
69+
}
70+
lifecycle.handleLifecycleEvent(Event.ON_STOP);
71+
}
72+
73+
@Override
74+
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
75+
76+
@Override
77+
public void onActivityDestroyed(@NonNull Activity activity) {
78+
if (activity.hashCode() != registrarActivityHashCode) {
79+
return;
80+
}
81+
activity.getApplication().unregisterActivityLifecycleCallbacks(this);
82+
lifecycle.handleLifecycleEvent(Event.ON_DESTROY);
83+
}
84+
85+
@NonNull
86+
@Override
87+
public Lifecycle getLifecycle() {
88+
return lifecycle;
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import static org.mockito.Mockito.any;
8+
import static org.mockito.Mockito.doNothing;
9+
import static org.mockito.Mockito.mock;
10+
import static org.mockito.Mockito.spy;
11+
import static org.mockito.Mockito.verify;
12+
import static org.mockito.Mockito.when;
13+
import static org.mockito.Mockito.withSettings;
14+
15+
import android.app.Activity;
16+
import android.app.Application;
17+
import androidx.lifecycle.LifecycleOwner;
18+
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
19+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
20+
import org.junit.Rule;
21+
import org.junit.Test;
22+
import org.mockito.Mock;
23+
import org.mockito.junit.MockitoJUnit;
24+
import org.mockito.junit.MockitoRule;
25+
26+
public class CameraAndroidCameraxPluginTest {
27+
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
28+
29+
@Mock ActivityPluginBinding activityPluginBinding;
30+
@Mock FlutterPluginBinding flutterPluginBinding;
31+
32+
@Test
33+
public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() {
34+
CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
35+
Activity mockActivity =
36+
mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class));
37+
ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
38+
mock(ProcessCameraProviderHostApiImpl.class);
39+
40+
doNothing().when(plugin).setUp(any(), any(), any());
41+
when(activityPluginBinding.getActivity()).thenReturn(mockActivity);
42+
43+
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
44+
plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);
45+
46+
plugin.onAttachedToEngine(flutterPluginBinding);
47+
plugin.onAttachedToActivity(activityPluginBinding);
48+
49+
verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class));
50+
}
51+
52+
@Test
53+
public void
54+
onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwner() {
55+
CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
56+
Activity mockActivity = mock(Activity.class);
57+
ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
58+
mock(ProcessCameraProviderHostApiImpl.class);
59+
60+
doNothing().when(plugin).setUp(any(), any(), any());
61+
when(activityPluginBinding.getActivity()).thenReturn(mockActivity);
62+
when(mockActivity.getApplication()).thenReturn(mock(Application.class));
63+
64+
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
65+
plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);
66+
67+
plugin.onAttachedToEngine(flutterPluginBinding);
68+
plugin.onAttachedToActivity(activityPluginBinding);
69+
70+
verify(mockProcessCameraProviderHostApiImpl)
71+
.setLifecycleOwner(any(ProxyLifecycleProvider.class));
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import static org.junit.Assert.assertEquals;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.verifyNoInteractions;
11+
import static org.mockito.Mockito.when;
12+
13+
import android.app.Activity;
14+
import android.app.Application;
15+
import android.os.Bundle;
16+
import androidx.lifecycle.Lifecycle.Event;
17+
import androidx.lifecycle.LifecycleRegistry;
18+
import org.junit.Before;
19+
import org.junit.Rule;
20+
import org.junit.Test;
21+
import org.mockito.Mock;
22+
import org.mockito.junit.MockitoJUnit;
23+
import org.mockito.junit.MockitoRule;
24+
25+
public class ProxyLifecycleProviderTest {
26+
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
27+
28+
@Mock Activity activity;
29+
@Mock Application application;
30+
@Mock LifecycleRegistry mockLifecycleRegistry;
31+
32+
private final int testHashCode = 27;
33+
34+
@Before
35+
public void setUp() {
36+
when(activity.getApplication()).thenReturn(application);
37+
}
38+
39+
@Test
40+
public void onActivityCreated_handlesOnCreateEvent() {
41+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
42+
Bundle mockBundle = mock(Bundle.class);
43+
44+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
45+
46+
proxyLifecycleProvider.onActivityCreated(activity, mockBundle);
47+
48+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_CREATE);
49+
}
50+
51+
@Test
52+
public void onActivityStarted_handlesOnActivityStartedEvent() {
53+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
54+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
55+
56+
proxyLifecycleProvider.onActivityStarted(activity);
57+
58+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_START);
59+
}
60+
61+
@Test
62+
public void onActivityResumed_handlesOnActivityResumedEvent() {
63+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
64+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
65+
66+
proxyLifecycleProvider.onActivityResumed(activity);
67+
68+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_RESUME);
69+
}
70+
71+
@Test
72+
public void onActivityPaused_handlesOnActivityPausedEvent() {
73+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
74+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
75+
76+
proxyLifecycleProvider.onActivityPaused(activity);
77+
78+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_PAUSE);
79+
}
80+
81+
@Test
82+
public void onActivityStopped_handlesOnActivityStoppedEvent() {
83+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
84+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
85+
86+
proxyLifecycleProvider.onActivityStopped(activity);
87+
88+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_STOP);
89+
}
90+
91+
@Test
92+
public void onActivityDestroyed_handlesOnActivityDestroyed() {
93+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
94+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
95+
96+
proxyLifecycleProvider.onActivityDestroyed(activity);
97+
98+
verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_DESTROY);
99+
}
100+
101+
@Test
102+
public void onActivitySaveInstanceState_doesNotHandleLifecycleEvvent() {
103+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
104+
Bundle mockBundle = mock(Bundle.class);
105+
106+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
107+
108+
proxyLifecycleProvider.onActivitySaveInstanceState(activity, mockBundle);
109+
110+
verifyNoInteractions(mockLifecycleRegistry);
111+
}
112+
113+
@Test
114+
public void getLifecycle_returnsExpectedLifecycle() {
115+
ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
116+
117+
proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
118+
119+
assertEquals(proxyLifecycleProvider.getLifecycle(), mockLifecycleRegistry);
120+
}
121+
}

0 commit comments

Comments
 (0)