Skip to content

[camera_android_camerax] Swap out BroadcastReceiver for OrientationEventListener #9261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.6.17

* Replaces `BroadcastReceiver` usage with an `OrientationEventListener` to detect changes in device
orientation to fix issue where some devices do not report changes in device configuration if it
is rotated between the same sort of orientation (landscape/portrait).

## 0.6.16

* Fixes incorrect camera preview rotation for landscape-oriented devices.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
Expand All @@ -28,6 +30,8 @@ public class DeviceOrientationManager {
private PlatformChannel.DeviceOrientation lastOrientation;
private BroadcastReceiver broadcastReceiver;

@VisibleForTesting @Nullable protected OrientationEventListener orientationEventListener;

DeviceOrientationManager(DeviceOrientationManagerProxyApi api) {
this.api = api;
}
Expand All @@ -38,39 +42,46 @@ Context getContext() {
}

/**
* Starts listening to the device's sensors or UI for orientation updates.
* Starts listening to the device's sensors for device orientation updates.
*
* <p>When orientation information is updated, the callback method of the {@link
* DeviceOrientationManagerProxyApi} is called with the new orientation.
*
* <p>If the device's ACCELEROMETER_ROTATION setting is enabled the {@link
* DeviceOrientationManager} will report orientation updates based on the sensor information. If
* the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to
* the deliver orientation updates based on the UI orientation.
*/
public void start() {
stop();

broadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleUIOrientationChange();
}
};
getContext().registerReceiver(broadcastReceiver, orientationIntentFilter);
broadcastReceiver.onReceive(getContext(), null);
// Listen for changes in device orientation at the default rate that is suitable for monitoring
// typical screen orientation changes.
orientationEventListener = createOrientationEventListener();
orientationEventListener.enable();
}

@VisibleForTesting
@NonNull
/**
* Creates an {@link OrientationEventListener} that will call the callback method of the {@link
* DeviceOrientationManagerProxyApi} whenever it is notified of a new device orientation and this
* {@code DeviceOrientationManager} instance determines that the orientation of the device {@link
* Configuration} has changed.
*/
protected OrientationEventListener createOrientationEventListener() {
return new OrientationEventListener(getContext()) {
@Override
public void onOrientationChanged(int orientation) {
handleUIOrientationChange();
}
};
}

/** Stops listening for orientation updates. */
public void stop() {
if (broadcastReceiver == null) {
if (orientationEventListener == null) {
return;
}
getContext().unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;

lastOrientation = null;

orientationEventListener.disable();
orientationEventListener = null;
}

/**
Expand All @@ -87,8 +98,7 @@ void handleUIOrientationChange() {
}

/**
* Handles orientation changes coming from either the device's sensors or the
* OrientationIntentFilter.
* Handles orientation changes coming from the device's sensors.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
package io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -18,15 +20,14 @@
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.provider.Settings;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;

public class DeviceOrientationManagerTest {
private Activity mockActivity;
Expand Down Expand Up @@ -63,21 +64,40 @@ Display getDisplay() {
}

@Test
public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() {
try (MockedStatic<Settings.System> mockedSystem = mockStatic(Settings.System.class)) {
mockedSystem
.when(
() ->
Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0)))
.thenReturn(0);
setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0);

deviceOrientationManager.handleUIOrientationChange();
}
public void start_createsExpectedOrientationEventListener() {
DeviceOrientationManager deviceOrientationManagerSpy = spy(deviceOrientationManager);

verify(mockApi, times(1))
.onDeviceOrientationChanged(
eq(deviceOrientationManager), eq(DeviceOrientation.LANDSCAPE_LEFT.toString()), any());
doNothing().when(deviceOrientationManagerSpy).handleUIOrientationChange();

deviceOrientationManagerSpy.start();
deviceOrientationManagerSpy.orientationEventListener.onOrientationChanged(
/* some device orientation */ 3);

verify(deviceOrientationManagerSpy).handleUIOrientationChange();
}

@Test
public void start_enablesOrientationEventListener() {
DeviceOrientationManager deviceOrientationManagerSpy = spy(deviceOrientationManager);
OrientationEventListener mockOrientationEventListener = mock(OrientationEventListener.class);

when(deviceOrientationManagerSpy.createOrientationEventListener())
.thenReturn(mockOrientationEventListener);

deviceOrientationManagerSpy.start();

verify(mockOrientationEventListener).enable();
}

@Test
public void stop_disablesOrientationListener() {
OrientationEventListener mockOrientationEventListener = mock(OrientationEventListener.class);
deviceOrientationManager.orientationEventListener = mockOrientationEventListener;

deviceOrientationManager.stop();

verify(mockOrientationEventListener).disable();
assertNull(deviceOrientationManager.orientationEventListener);
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.16
version: 0.6.17

environment:
sdk: ^3.6.0
Expand Down