blob: dd1e489e622554ac8e99bc465c75b9155e275974 [file] [log] [blame]
// 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.camera.features.sensororientation;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
import io.flutter.plugins.camera.DartMessenger;
/**
* Support class to help to determine the media orientation based on the orientation of the device.
*/
public class DeviceOrientationManager {
private static final IntentFilter orientationIntentFilter =
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
private final Activity activity;
private final DartMessenger messenger;
private final boolean isFrontFacing;
private final int sensorOrientation;
private PlatformChannel.DeviceOrientation lastOrientation;
private BroadcastReceiver broadcastReceiver;
/** Factory method to create a device orientation manager. */
public static DeviceOrientationManager create(
@NonNull Activity activity,
@NonNull DartMessenger messenger,
boolean isFrontFacing,
int sensorOrientation) {
return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation);
}
private DeviceOrientationManager(
@NonNull Activity activity,
@NonNull DartMessenger messenger,
boolean isFrontFacing,
int sensorOrientation) {
this.activity = activity;
this.messenger = messenger;
this.isFrontFacing = isFrontFacing;
this.sensorOrientation = sensorOrientation;
}
/**
* Starts listening to the device's sensors or UI for orientation updates.
*
* <p>When orientation information is updated the new orientation is send to the client using the
* {@link DartMessenger}. This latest value can also be retrieved through the {@link
* #getVideoOrientation()} accessor.
*
* <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() {
if (broadcastReceiver != null) {
return;
}
broadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleUIOrientationChange();
}
};
activity.registerReceiver(broadcastReceiver, orientationIntentFilter);
broadcastReceiver.onReceive(activity, null);
}
/** Stops listening for orientation updates. */
public void stop() {
if (broadcastReceiver == null) {
return;
}
activity.unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
/**
* Returns the device's photo orientation in degrees based on the sensor orientation and the last
* known UI orientation.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @return The device's photo orientation in degrees.
*/
public int getPhotoOrientation() {
return this.getPhotoOrientation(this.lastOrientation);
}
/**
* Returns the device's photo orientation in degrees based on the sensor orientation and the
* supplied {@link PlatformChannel.DeviceOrientation} value.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
* into degrees.
* @return The device's photo orientation in degrees.
*/
public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) {
int angle = 0;
// Fallback to device orientation when the orientation value is null.
if (orientation == null) {
orientation = getUIOrientation();
}
switch (orientation) {
case PORTRAIT_UP:
angle = 90;
break;
case PORTRAIT_DOWN:
angle = 270;
break;
case LANDSCAPE_LEFT:
angle = isFrontFacing ? 180 : 0;
break;
case LANDSCAPE_RIGHT:
angle = isFrontFacing ? 0 : 180;
break;
}
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X).
// This has to be taken into account so the JPEG is rotated properly.
// For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS.
// For devices with orientation of 270, the JPEG is rotated 180 degrees instead.
return (angle + sensorOrientation + 270) % 360;
}
/**
* Returns the device's video orientation in degrees based on the sensor orientation and the last
* known UI orientation.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @return The device's video orientation in degrees.
*/
public int getVideoOrientation() {
return this.getVideoOrientation(this.lastOrientation);
}
/**
* Returns the device's video orientation in degrees based on the sensor orientation and the
* supplied {@link PlatformChannel.DeviceOrientation} value.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
* into degrees.
* @return The device's video orientation in degrees.
*/
public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) {
int angle = 0;
// Fallback to device orientation when the orientation value is null.
if (orientation == null) {
orientation = getUIOrientation();
}
switch (orientation) {
case PORTRAIT_UP:
angle = 0;
break;
case PORTRAIT_DOWN:
angle = 180;
break;
case LANDSCAPE_LEFT:
angle = 90;
break;
case LANDSCAPE_RIGHT:
angle = 270;
break;
}
if (isFrontFacing) {
angle *= -1;
}
return (angle + sensorOrientation + 360) % 360;
}
/** @return the last received UI orientation. */
public PlatformChannel.DeviceOrientation getLastUIOrientation() {
return this.lastOrientation;
}
/**
* Handles orientation changes based on change events triggered by the OrientationIntentFilter.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*/
@VisibleForTesting
void handleUIOrientationChange() {
PlatformChannel.DeviceOrientation orientation = getUIOrientation();
handleOrientationChange(orientation, lastOrientation, messenger);
lastOrientation = orientation;
}
/**
* Handles orientation changes coming from either the device's sensors or the
* OrientationIntentFilter.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*/
@VisibleForTesting
static void handleOrientationChange(
DeviceOrientation newOrientation,
DeviceOrientation previousOrientation,
DartMessenger messenger) {
if (!newOrientation.equals(previousOrientation)) {
messenger.sendDeviceOrientationChangeEvent(newOrientation);
}
}
/**
* Gets the current user interface orientation.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @return The current user interface orientation.
*/
@VisibleForTesting
PlatformChannel.DeviceOrientation getUIOrientation() {
final int rotation = getDisplay().getRotation();
final int orientation = activity.getResources().getConfiguration().orientation;
switch (orientation) {
case Configuration.ORIENTATION_PORTRAIT:
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
} else {
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
}
case Configuration.ORIENTATION_LANDSCAPE:
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
} else {
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
}
default:
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
}
}
/**
* Calculates the sensor orientation based on the supplied angle.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @param angle Orientation angle.
* @return The sensor orientation based on the supplied angle.
*/
@VisibleForTesting
PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) {
final int tolerance = 45;
angle += tolerance;
// Orientation is 0 in the default orientation mode. This is portrait-mode for phones
// and landscape for tablets. We have to compensate for this by calculating the default
// orientation, and apply an offset accordingly.
int defaultDeviceOrientation = getDeviceDefaultOrientation();
if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
angle += 90;
}
// Determine the orientation
angle = angle % 360;
return new PlatformChannel.DeviceOrientation[] {
PlatformChannel.DeviceOrientation.PORTRAIT_UP,
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT,
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN,
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT,
}
[angle / 90];
}
/**
* Gets the default orientation of the device.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @return The default orientation of the device.
*/
@VisibleForTesting
int getDeviceDefaultOrientation() {
Configuration config = activity.getResources().getConfiguration();
int rotation = getDisplay().getRotation();
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
return Configuration.ORIENTATION_LANDSCAPE;
} else {
return Configuration.ORIENTATION_PORTRAIT;
}
}
/**
* Gets an instance of the Android {@link android.view.Display}.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @return An instance of the Android {@link android.view.Display}.
*/
@SuppressWarnings("deprecation")
@VisibleForTesting
Display getDisplay() {
return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
}
}