diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 276d08aa836..48de0ee9068 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.24+1 + +* Fix crash in `DeviceOrientationManager` caused by `UnsupportedOperationException` when `getDisplay()` is called on a null or destroyed Activity during rotation. + ## 0.6.24 * Change plugin to assume mp4 format for capture videos. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java index 52b811ac003..01296bbdd67 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.util.Log; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; @@ -48,8 +49,8 @@ Context getContext() { *

When orientation information is updated, the callback method of the {@link * DeviceOrientationManagerProxyApi} is called with the new orientation. */ - @SuppressLint( - "UnprotectedReceiver") // orientationIntentFilter only listens to protected broadcast + @SuppressLint("UnprotectedReceiver") + // orientationIntentFilter only listens to protected broadcast public void start() { stop(); @@ -182,7 +183,19 @@ PlatformChannel.DeviceOrientation getUiOrientation() { * Surface.ROTATION_270} */ int getDefaultRotation() { - return getDisplay().getRotation(); + Display display = getDisplay(); + + if (display == null) { + // The Activity is not available (null or destroyed), which can happen briefly + // during configuration changes or due to race conditions. Returning ROTATION_0 ensures safe + // fallback and prevents crashes until a valid Activity is attached again. + Log.w( + "DeviceOrientationManager", + "Cannot get display: Activity may be null (destroyed or not yet attached) due to a race condition."); + return Surface.ROTATION_0; + } + + return display.getRotation(); } /** @@ -194,6 +207,7 @@ int getDefaultRotation() { * @return An instance of the Android {@link android.view.Display}. */ @VisibleForTesting + @Nullable Display getDisplay() { return api.getPigeonRegistrar().getDisplay(); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java index b814b8bbbef..3941932385c 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java @@ -144,10 +144,15 @@ long getDefaultClearFinalizedWeakReferencesInterval() { "deprecation") // getSystemService was the way of getting the default display prior to API 30 @Nullable Display getDisplay() { + Activity activity = getActivity(); + if (activity == null || activity.isDestroyed()) { + return null; + } + if (sdkIsAtLeast(Build.VERSION_CODES.R)) { - return getContext().getDisplay(); + return activity.getDisplay(); } else { - return ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)) + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/android/util/FakeActivity.java b/packages/camera/camera_android_camerax/android/src/test/java/android/util/FakeActivity.java new file mode 100644 index 00000000000..70c719a7e46 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/android/util/FakeActivity.java @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package android.util; + +import android.app.Activity; +import android.view.WindowManager; + +/** + * Fake Activity class used only for JVM unit tests. It avoids dependency on the real Android + * runtime and allows manual control of lifecycle states (isDestroyed, isFinishing) and the + * WindowManager instance. + */ +public class FakeActivity extends Activity { + private boolean destroyed; + private boolean finishing; + private WindowManager windowManager; + + public void setDestroyed(boolean destroyed) { + this.destroyed = destroyed; + } + + public void setFinishing(boolean finishing) { + this.finishing = finishing; + } + + public void setWindowManager(WindowManager windowManager) { + this.windowManager = windowManager; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public boolean isFinishing() { + return finishing; + } + + @Override + public WindowManager getWindowManager() { + return windowManager; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java index 55ef813cfb9..79bc2d5d813 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java @@ -20,9 +20,11 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.util.FakeActivity; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; @@ -199,4 +201,40 @@ public void getDisplayTest() { assertEquals(mockDisplay, display); } + + @Test + public void getDisplay_shouldReturnNull_whenActivityDestroyed() { + final DeviceOrientationManager deviceOrientationManager = createManager(true, false); + assertNull(deviceOrientationManager.getDisplay()); + assertEquals(deviceOrientationManager.getDefaultRotation(), Surface.ROTATION_0); + } + + @SuppressWarnings("deprecation") + private DeviceOrientationManager createManager(boolean destroyed, boolean finishing) { + FakeActivity activity = new FakeActivity(); + activity.setDestroyed(destroyed); + activity.setFinishing(finishing); + + WindowManager windowManager = mock(WindowManager.class); + when(windowManager.getDefaultDisplay()).thenReturn(mock(Display.class)); + activity.setWindowManager(windowManager); + + TestProxyApiRegistrar proxy = + new TestProxyApiRegistrar() { + @NonNull + @Override + public Context getContext() { + return activity; + } + + @Nullable + @Override + public Activity getActivity() { + return activity; + } + }; + when(mockApi.getPigeonRegistrar()).thenReturn(proxy); + + return new DeviceOrientationManager(mockApi); + } } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 12d68972dc6..f5017b26693 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.24 +version: 0.6.24+1 environment: sdk: ^3.9.0