diff --git a/Samples/Samples/Samples.csproj b/Samples/Samples/Samples.csproj
index 61b0a6a5c..3c2e46b67 100644
--- a/Samples/Samples/Samples.csproj
+++ b/Samples/Samples/Samples.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/Samples/Samples/View/AllSensorsPage.xaml b/Samples/Samples/View/AllSensorsPage.xaml
index 93db3fe83..5064198b3 100644
--- a/Samples/Samples/View/AllSensorsPage.xaml
+++ b/Samples/Samples/View/AllSensorsPage.xaml
@@ -141,6 +141,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/Samples/View/OrientationSensorPage.xaml b/Samples/Samples/View/OrientationSensorPage.xaml
new file mode 100644
index 000000000..24c4fabe2
--- /dev/null
+++ b/Samples/Samples/View/OrientationSensorPage.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/OrientationSensorPage.xaml.cs b/Samples/Samples/View/OrientationSensorPage.xaml.cs
new file mode 100644
index 000000000..1ee1df418
--- /dev/null
+++ b/Samples/Samples/View/OrientationSensorPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class OrientationSensorPage : BasePage
+ {
+ public OrientationSensorPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/HomeViewModel.cs b/Samples/Samples/ViewModel/HomeViewModel.cs
index adeeb97e2..b10c48879 100644
--- a/Samples/Samples/ViewModel/HomeViewModel.cs
+++ b/Samples/Samples/ViewModel/HomeViewModel.cs
@@ -28,8 +28,8 @@ public HomeViewModel()
"📏",
"All Sensors",
typeof(AllSensorsPage),
- "Have a look at the accelerometer, compass, gyroscope and magnetometer.",
- new[] { "accelerometer", "compass", "gyroscope", "magnetometer", "sensors", "hardware", "device" }),
+ "Have a look at the accelerometer, compass, gyroscope, magnetometer, and orientation sensors.",
+ new[] { "accelerometer", "compass", "gyroscope", "magnetometer", "orientation", "sensors", "hardware", "device" }),
new SampleItem(
"📦",
"App Info",
@@ -120,6 +120,12 @@ public HomeViewModel()
typeof(MagnetometerPage),
"Detect device's orientation relative to Earth's magnetic field.",
new[] { "compass", "magnetometer", "sensors", "hardware", "device" }),
+ new SampleItem(
+ "📏",
+ "Orientation Sensor",
+ typeof(OrientationSensorPage),
+ "Retrieve orientation of the device in 3D space.",
+ new[] { "orientation", "sensors", "hardware", "device" }),
new SampleItem(
"📞",
"Phone Dialer",
diff --git a/Samples/Samples/ViewModel/OrientationSensorViewModel.cs b/Samples/Samples/ViewModel/OrientationSensorViewModel.cs
new file mode 100644
index 000000000..61873fecc
--- /dev/null
+++ b/Samples/Samples/ViewModel/OrientationSensorViewModel.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ class OrientationSensorViewModel : BaseViewModel
+ {
+ double x;
+ double y;
+ double z;
+ double w;
+ bool isActive;
+ int speed = 2;
+
+ public OrientationSensorViewModel()
+ {
+ StartCommand = new Command(OnStart);
+ StopCommand = new Command(OnStop);
+ }
+
+ public ICommand StartCommand { get; }
+
+ public ICommand StopCommand { get; }
+
+ public double X
+ {
+ get => x;
+ set => SetProperty(ref x, value);
+ }
+
+ public double Y
+ {
+ get => y;
+ set => SetProperty(ref y, value);
+ }
+
+ public double Z
+ {
+ get => z;
+ set => SetProperty(ref z, value);
+ }
+
+ public double W
+ {
+ get => w;
+ set => SetProperty(ref w, value);
+ }
+
+ public bool IsActive
+ {
+ get => isActive;
+ set => SetProperty(ref isActive, value);
+ }
+
+ public List Speeds { get; } =
+ new List
+ {
+ "Fastest",
+ "Game",
+ "Normal",
+ "User Interface"
+ };
+
+ public int Speed
+ {
+ get => speed;
+ set => SetProperty(ref speed, value);
+ }
+
+ public override void OnAppearing()
+ {
+ OrientationSensor.ReadingChanged += OnReadingChanged;
+ base.OnAppearing();
+ }
+
+ public override void OnDisappearing()
+ {
+ OnStop();
+ OrientationSensor.ReadingChanged -= OnReadingChanged;
+
+ base.OnDisappearing();
+ }
+
+ async void OnStart()
+ {
+ try
+ {
+ OrientationSensor.Start((SensorSpeed)Speed);
+ IsActive = true;
+ }
+ catch (Exception ex)
+ {
+ await DisplayAlertAsync($"Unable to start orientation sensor: {ex.Message}");
+ }
+ }
+
+ void OnStop()
+ {
+ IsActive = false;
+ Accelerometer.Stop();
+ }
+
+ void OnReadingChanged(OrientationSensorChangedEventArgs e)
+ {
+ var data = e.Reading;
+ switch ((SensorSpeed)Speed)
+ {
+ case SensorSpeed.Fastest:
+ case SensorSpeed.Game:
+ Platform.BeginInvokeOnMainThread(() =>
+ {
+ X = data.Orientation.X;
+ Y = data.Orientation.Y;
+ Z = data.Orientation.Z;
+ W = data.Orientation.W;
+ });
+ break;
+ default:
+ X = data.Orientation.X;
+ Y = data.Orientation.Y;
+ Z = data.Orientation.Z;
+ W = data.Orientation.W;
+ break;
+ }
+ }
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.android.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.android.cs
new file mode 100644
index 000000000..0c27fb603
--- /dev/null
+++ b/Xamarin.Essentials/OrientationSensor/OrientationSensor.android.cs
@@ -0,0 +1,65 @@
+using Android.Hardware;
+using Android.Runtime;
+
+namespace Xamarin.Essentials
+{
+ public static partial class OrientationSensor
+ {
+ internal static bool IsSupported =>
+ Platform.SensorManager?.GetDefaultSensor(SensorType.RotationVector) != null;
+
+ static OrientationSensorListener listener;
+ static Sensor orientationSensor;
+
+ internal static void PlatformStart(SensorSpeed sensorSpeed)
+ {
+ var delay = SensorDelay.Normal;
+ switch (sensorSpeed)
+ {
+ case SensorSpeed.Normal:
+ delay = SensorDelay.Normal;
+ break;
+ case SensorSpeed.Fastest:
+ delay = SensorDelay.Fastest;
+ break;
+ case SensorSpeed.Game:
+ delay = SensorDelay.Game;
+ break;
+ case SensorSpeed.Ui:
+ delay = SensorDelay.Ui;
+ break;
+ }
+
+ listener = new OrientationSensorListener();
+ orientationSensor = Platform.SensorManager.GetDefaultSensor(SensorType.RotationVector);
+ Platform.SensorManager.RegisterListener(listener, orientationSensor, delay);
+ }
+
+ internal static void PlatformStop()
+ {
+ if (listener == null || orientationSensor == null)
+ return;
+
+ Platform.SensorManager.UnregisterListener(listener, orientationSensor);
+ listener.Dispose();
+ listener = null;
+ }
+ }
+
+ class OrientationSensorListener : Java.Lang.Object, ISensorEventListener
+ {
+ internal OrientationSensorListener()
+ {
+ }
+
+ void ISensorEventListener.OnAccuracyChanged(Sensor sensor, [GeneratedEnum] SensorStatus accuracy)
+ {
+ }
+
+ void ISensorEventListener.OnSensorChanged(SensorEvent e)
+ {
+ var data = new OrientationSensorData(e.Values[0], e.Values[1], e.Values[2], e.Values[3]);
+ OrientationSensor.OnChanged(data);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.ios.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.ios.cs
new file mode 100644
index 000000000..03cdf6d70
--- /dev/null
+++ b/Xamarin.Essentials/OrientationSensor/OrientationSensor.ios.cs
@@ -0,0 +1,53 @@
+using CoreMotion;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class OrientationSensor
+ {
+ // Timing intervales to match android sensor speeds in seconds
+ // https://stackoverflow.com/questions/10044158/android-sensors
+ internal const double FastestInterval = .02;
+ internal const double GameInterval = .04;
+ internal const double UiInterval = .08;
+ internal const double NormalInterval = .225;
+
+ internal static bool IsSupported =>
+ Platform.MotionManager?.DeviceMotionAvailable ?? false;
+
+ internal static void PlatformStart(SensorSpeed sensorSpeed)
+ {
+ var manager = Platform.MotionManager;
+ switch (sensorSpeed)
+ {
+ case SensorSpeed.Fastest:
+ manager.DeviceMotionUpdateInterval = FastestInterval;
+ break;
+ case SensorSpeed.Game:
+ manager.DeviceMotionUpdateInterval = GameInterval;
+ break;
+ case SensorSpeed.Normal:
+ manager.DeviceMotionUpdateInterval = NormalInterval;
+ break;
+ case SensorSpeed.Ui:
+ manager.DeviceMotionUpdateInterval = UiInterval;
+ break;
+ }
+
+ manager.StartDeviceMotionUpdates(Platform.GetCurrentQueue(), DataUpdated);
+ }
+
+ static void DataUpdated(CMDeviceMotion data, NSError error)
+ {
+ if (data == null)
+ return;
+
+ var field = data.Attitude.Quaternion;
+ var rotationData = new OrientationSensorData(field.x, field.y, field.z, field.w);
+ OnChanged(rotationData);
+ }
+
+ internal static void PlatformStop() =>
+ Platform.MotionManager?.StopDeviceMotionUpdates();
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.cs
new file mode 100644
index 000000000..7f6c4906b
--- /dev/null
+++ b/Xamarin.Essentials/OrientationSensor/OrientationSensor.netstandard.cs
@@ -0,0 +1,14 @@
+namespace Xamarin.Essentials
+{
+ public static partial class OrientationSensor
+ {
+ internal static bool IsSupported =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static void PlatformStart(SensorSpeed sensorSpeed) =>
+ throw new NotImplementedInReferenceAssemblyException();
+
+ static void PlatformStop() =>
+ throw new NotImplementedInReferenceAssemblyException();
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.shared.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.shared.cs
new file mode 100644
index 000000000..9a83f4333
--- /dev/null
+++ b/Xamarin.Essentials/OrientationSensor/OrientationSensor.shared.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Numerics;
+
+namespace Xamarin.Essentials
+{
+ public static partial class OrientationSensor
+ {
+ static bool useSyncContext;
+
+ public static event OrientationSensorChangedEventHandler ReadingChanged;
+
+ public static bool IsMonitoring { get; private set; }
+
+ public static void Start(SensorSpeed sensorSpeed)
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+ if (IsMonitoring)
+ return;
+
+ IsMonitoring = true;
+ useSyncContext = sensorSpeed == SensorSpeed.Normal || sensorSpeed == SensorSpeed.Ui;
+
+ try
+ {
+ PlatformStart(sensorSpeed);
+ }
+ catch
+ {
+ IsMonitoring = false;
+ throw;
+ }
+ }
+
+ public static void Stop()
+ {
+ if (!IsSupported)
+ throw new FeatureNotSupportedException();
+
+ if (!IsMonitoring)
+ return;
+
+ IsMonitoring = false;
+
+ try
+ {
+ PlatformStop();
+ }
+ catch
+ {
+ IsMonitoring = true;
+ throw;
+ }
+ }
+
+ internal static void OnChanged(OrientationSensorData reading)
+ => OnChanged(new OrientationSensorChangedEventArgs(reading));
+
+ internal static void OnChanged(OrientationSensorChangedEventArgs e)
+ {
+ var handler = ReadingChanged;
+ if (handler == null)
+ return;
+
+ if (useSyncContext)
+ Platform.BeginInvokeOnMainThread(() => handler?.Invoke(e));
+ else
+ handler?.Invoke(e);
+ }
+ }
+
+ public delegate void OrientationSensorChangedEventHandler(OrientationSensorChangedEventArgs e);
+
+ public class OrientationSensorChangedEventArgs : EventArgs
+ {
+ internal OrientationSensorChangedEventArgs(OrientationSensorData reading) => Reading = reading;
+
+ public OrientationSensorData Reading { get; }
+ }
+
+ public struct OrientationSensorData
+ {
+ internal OrientationSensorData(double x, double y, double z, double w)
+ : this((float)x, (float)y, (float)z, (float)w)
+ {
+ }
+
+ internal OrientationSensorData(float x, float y, float z, float w)
+ {
+ Orientation = new Quaternion(x, y, z, w);
+ }
+
+ public Quaternion Orientation { get; }
+ }
+}
diff --git a/Xamarin.Essentials/OrientationSensor/OrientationSensor.uwp.cs b/Xamarin.Essentials/OrientationSensor/OrientationSensor.uwp.cs
new file mode 100644
index 000000000..a13ecb19a
--- /dev/null
+++ b/Xamarin.Essentials/OrientationSensor/OrientationSensor.uwp.cs
@@ -0,0 +1,54 @@
+using Windows.Devices.Sensors;
+using WindowsOrientationSensor = Windows.Devices.Sensors.OrientationSensor;
+
+namespace Xamarin.Essentials
+{
+ public static partial class OrientationSensor
+ {
+ // Magic numbers from https://docs.microsoft.com/en-us/uwp/api/windows.devices.sensors.compass.reportinterval#Windows_Devices_Sensors_Compass_ReportInterval
+ internal const uint FastestInterval = 8;
+ internal const uint GameInterval = 22;
+ internal const uint NormalInterval = 33;
+
+ // keep around a reference so we can stop this same instance
+ static WindowsOrientationSensor sensor;
+
+ internal static WindowsOrientationSensor DefaultSensor =>
+ WindowsOrientationSensor.GetDefault();
+
+ internal static bool IsSupported =>
+ DefaultSensor != null;
+
+ internal static void PlatformStart(SensorSpeed sensorSpeed)
+ {
+ sensor = DefaultSensor;
+
+ var interval = NormalInterval;
+ switch (sensorSpeed)
+ {
+ case SensorSpeed.Fastest:
+ interval = FastestInterval;
+ break;
+ case SensorSpeed.Game:
+ interval = GameInterval;
+ break;
+ }
+
+ sensor.ReportInterval = sensor.MinimumReportInterval >= interval ? sensor.MinimumReportInterval : interval;
+
+ sensor.ReadingChanged += DataUpdated;
+ }
+
+ static void DataUpdated(object sender, OrientationSensorReadingChangedEventArgs e)
+ {
+ var reading = e.Reading;
+ var data = new OrientationSensorData(reading.Quaternion.X, reading.Quaternion.Y, reading.Quaternion.Z, reading.Quaternion.W);
+ OnChanged(data);
+ }
+
+ internal static void PlatformStop()
+ {
+ sensor.ReadingChanged -= DataUpdated;
+ }
+ }
+}