Skip to content

Commit e22c523

Browse files
authored
[camera_android_camerax] Fixes crash with unsupported operation exception (#10342)
Fixes crash in `DeviceOrientationManager` caused by `UnsupportedOperationException` when `getDisplay()` is called on a null or destroyed Activity during rotation. Fixes flutter/flutter#176613 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 9c67aab commit e22c523

File tree

6 files changed

+113
-6
lines changed

6 files changed

+113
-6
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.24+1
2+
3+
* Fixes crash in `DeviceOrientationManager` caused by `UnsupportedOperationException` when `getDisplay()` is called on a null or destroyed Activity during rotation.
4+
15
## 0.6.24
26

37
* Change plugin to assume mp4 format for capture videos.

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import android.content.Intent;
1111
import android.content.IntentFilter;
1212
import android.content.res.Configuration;
13+
import android.util.Log;
1314
import android.view.Display;
1415
import android.view.OrientationEventListener;
1516
import android.view.Surface;
@@ -48,8 +49,8 @@ Context getContext() {
4849
* <p>When orientation information is updated, the callback method of the {@link
4950
* DeviceOrientationManagerProxyApi} is called with the new orientation.
5051
*/
51-
@SuppressLint(
52-
"UnprotectedReceiver") // orientationIntentFilter only listens to protected broadcast
52+
@SuppressLint("UnprotectedReceiver")
53+
// orientationIntentFilter only listens to protected broadcast
5354
public void start() {
5455
stop();
5556

@@ -182,7 +183,19 @@ PlatformChannel.DeviceOrientation getUiOrientation() {
182183
* Surface.ROTATION_270}
183184
*/
184185
int getDefaultRotation() {
185-
return getDisplay().getRotation();
186+
Display display = getDisplay();
187+
188+
if (display == null) {
189+
// The Activity is not available (null or destroyed), which can happen briefly
190+
// during configuration changes or due to race conditions. Returning ROTATION_0 ensures safe
191+
// fallback and prevents crashes until a valid Activity is attached again.
192+
Log.w(
193+
"DeviceOrientationManager",
194+
"Cannot get display: Activity may be null (destroyed or not yet attached) due to a race condition.");
195+
return Surface.ROTATION_0;
196+
}
197+
198+
return display.getRotation();
186199
}
187200

188201
/**
@@ -194,6 +207,7 @@ int getDefaultRotation() {
194207
* @return An instance of the Android {@link android.view.Display}.
195208
*/
196209
@VisibleForTesting
210+
@Nullable
197211
Display getDisplay() {
198212
return api.getPigeonRegistrar().getDisplay();
199213
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,15 @@ long getDefaultClearFinalizedWeakReferencesInterval() {
144144
"deprecation") // getSystemService was the way of getting the default display prior to API 30
145145
@Nullable
146146
Display getDisplay() {
147+
Activity activity = getActivity();
148+
if (activity == null || activity.isDestroyed()) {
149+
return null;
150+
}
151+
147152
if (sdkIsAtLeast(Build.VERSION_CODES.R)) {
148-
return getContext().getDisplay();
153+
return activity.getDisplay();
149154
} else {
150-
return ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
155+
return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE))
151156
.getDefaultDisplay();
152157
}
153158
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2013 The Flutter Authors
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 android.util;
6+
7+
import android.app.Activity;
8+
import android.view.WindowManager;
9+
10+
/**
11+
* Fake Activity class used only for JVM unit tests. It avoids dependency on the real Android
12+
* runtime and allows manual control of lifecycle states (isDestroyed, isFinishing) and the
13+
* WindowManager instance.
14+
*/
15+
public class FakeActivity extends Activity {
16+
private boolean destroyed;
17+
private boolean finishing;
18+
private WindowManager windowManager;
19+
20+
public void setDestroyed(boolean destroyed) {
21+
this.destroyed = destroyed;
22+
}
23+
24+
public void setFinishing(boolean finishing) {
25+
this.finishing = finishing;
26+
}
27+
28+
public void setWindowManager(WindowManager windowManager) {
29+
this.windowManager = windowManager;
30+
}
31+
32+
@Override
33+
public boolean isDestroyed() {
34+
return destroyed;
35+
}
36+
37+
@Override
38+
public boolean isFinishing() {
39+
return finishing;
40+
}
41+
42+
@Override
43+
public WindowManager getWindowManager() {
44+
return windowManager;
45+
}
46+
}

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import android.content.Context;
2121
import android.content.res.Configuration;
2222
import android.content.res.Resources;
23+
import android.util.FakeActivity;
2324
import android.view.Display;
2425
import android.view.OrientationEventListener;
2526
import android.view.Surface;
27+
import android.view.WindowManager;
2628
import androidx.annotation.NonNull;
2729
import androidx.annotation.Nullable;
2830
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
@@ -199,4 +201,40 @@ public void getDisplayTest() {
199201

200202
assertEquals(mockDisplay, display);
201203
}
204+
205+
@Test
206+
public void getDisplay_shouldReturnNull_whenActivityDestroyed() {
207+
final DeviceOrientationManager deviceOrientationManager = createManager(true, false);
208+
assertNull(deviceOrientationManager.getDisplay());
209+
assertEquals(deviceOrientationManager.getDefaultRotation(), Surface.ROTATION_0);
210+
}
211+
212+
@SuppressWarnings("deprecation")
213+
private DeviceOrientationManager createManager(boolean destroyed, boolean finishing) {
214+
FakeActivity activity = new FakeActivity();
215+
activity.setDestroyed(destroyed);
216+
activity.setFinishing(finishing);
217+
218+
WindowManager windowManager = mock(WindowManager.class);
219+
when(windowManager.getDefaultDisplay()).thenReturn(mock(Display.class));
220+
activity.setWindowManager(windowManager);
221+
222+
TestProxyApiRegistrar proxy =
223+
new TestProxyApiRegistrar() {
224+
@NonNull
225+
@Override
226+
public Context getContext() {
227+
return activity;
228+
}
229+
230+
@Nullable
231+
@Override
232+
public Activity getActivity() {
233+
return activity;
234+
}
235+
};
236+
when(mockApi.getPigeonRegistrar()).thenReturn(proxy);
237+
238+
return new DeviceOrientationManager(mockApi);
239+
}
202240
}

packages/camera/camera_android_camerax/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android_camerax
22
description: Android implementation of the camera plugin using the CameraX library.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.6.24
5+
version: 0.6.24+1
66

77
environment:
88
sdk: ^3.9.0

0 commit comments

Comments
 (0)