diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 69c22d638..6f0c6feed 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,9 +10,69 @@ Please see our [Code of Conduct](CODE_OF_CONDUCT.md).
You will need to complete a Contribution License Agreement before any pull request can be accepted. Complete the CLA at https://cla.dotnetfoundation.org/.
-## Contributing Code
-
-Check out [A Beginner's Guide for Contributing to Xamarin.Essentials](https://github.com/xamarin/Essentials/wiki/A-Beginner's-Guide-for-Contributing-to-Xamarin.Essentials).
+## Contributing Code - Best Practices
+
+### Enums
+* Always use `Unknown` at index 0 for return types that may have a value that is not known
+* Always use `Default` at index 0 for option types that can use the system default option
+* Follow naming guidelines for tense... `SensorSpeed` not `SensorSpeeds`
+* Assign values (0,1,2,3) for all enums
+
+### Property Names
+* Include units only if one of the platforms includes it in their implementation. For instance HeadingMagneticNorth implies degrees on all platforms, but PressureInHectopascals is needed since platforms don't provide a consistent API for this.
+
+### Units
+* Use the standard units and most well accepted units when possible. For instance Hectopascals are used on UWP/Android and iOS uses Kilopascals so we have chosen Hectopascals.
+
+### Style
+* Prefer using `==` when checking for null instead of `is`
+
+### Exceptions
+
+We currently have different ways of indicating that nothing can be done:
+
+ - do nothing
+ - throw `FeatureNotSupportedException`
+ - throw `PlatformNotSupportedException`
+ - throw `FeatureNotEnabledException`
+
+One case where we do nothing is in Android's energy saver API: if we are not Lollipop, then we just fall through:
+https://github.com/xamarin/Essentials/blob/1.0.0/Xamarin.Essentials/Battery/Battery.android.cs#L12-L48
+
+One case where we throw `FeatureNotSupportedException` is with the sensors: if there is no sensor X, then we throw.
+
+One case (and the only case so far) where we throw `PlatformNotSupportedException` is in Android's text-to-speech API: if we try and speak, but we couldn't initialize, then we throw.
+
+So far, I was able to determine that we throw `FeatureNotSupportedException` for:
+ - the sensors on all platforms if we aren't able to access the hardware
+ - we throw in the start and the stop (this one may be overkill, we can probably first check to see if it is started, and if not then just do nothing)
+ - the Android external browser if there was no browser installed
+ - the email API
+ - Android: if there is no `message/rfc822` intent handler
+ - iOS: (if the mail VC can't send, or if the `mailto:` doesn't have an app, or if trying to send HTML over the `mailto:` protocol
+ - UWP: if the `EmailManager` is not available, or if trying to send HTML
+ - the flashlight API on all platforms if there is no camera flash hardware
+ - the phone dialler
+ - Android / iOS: if the OS can't handle the `tel:` protocol
+ - UWP: the `PhoneCallManager` is missing
+ - the sms API
+ - Android: if there is no `smsto:` intent handler
+ - iOS: (if the message VC can't send
+ - UWP: if the `ChatMessageManager` is not available
+ - the vibration API on UWP if the `VibrationDevice` is not available or if no hardware was found
+
+We throw a `PlatformNotSupportedException` for:
+ - Android when we aren't able to initialize the text-to-speech engine
+
+We throw a `FeatureNotEnabledException` for:
+ - Geolocation if no providers are found
+
+We do "nothing":
+ - the Vibration API on iOS and android never actually checks, it just starts it
+ - the Map API on Android and UWP just starts the URI, assuming that something will be there
+ - the Geolocation API always assumes that there is a GPS and throws a `FeatureNotEnabledException` if there was no way to get the hardware
+ - the KeepScreenOn feature just assumes the window flag will be honoured (probably is, but is there an api level/hardware limit?)
+ - the energy saver API on android pre-Lollipop
## Documentation - mdoc
diff --git a/Samples/Samples/View/AccelerometerPage.xaml b/Samples/Samples/View/AccelerometerPage.xaml
index 917c7a35e..290bd1331 100644
--- a/Samples/Samples/View/AccelerometerPage.xaml
+++ b/Samples/Samples/View/AccelerometerPage.xaml
@@ -9,7 +9,7 @@
-
+
@@ -20,6 +20,7 @@
+
@@ -40,6 +41,8 @@
+
+
diff --git a/Samples/Samples/ViewModel/AccelerometerViewModel.cs b/Samples/Samples/ViewModel/AccelerometerViewModel.cs
index 56924bcdc..08976d86e 100644
--- a/Samples/Samples/ViewModel/AccelerometerViewModel.cs
+++ b/Samples/Samples/ViewModel/AccelerometerViewModel.cs
@@ -11,6 +11,7 @@ public class AccelerometerViewModel : BaseViewModel
double x;
double y;
double z;
+ string shakeTime = string.Empty;
bool isActive;
int speed = 0;
@@ -24,6 +25,12 @@ public AccelerometerViewModel()
public ICommand StopCommand { get; }
+ public string ShakeTime
+ {
+ get => shakeTime;
+ set => SetProperty(ref shakeTime, value);
+ }
+
public double X
{
get => x;
@@ -60,14 +67,19 @@ public int Speed
public override void OnAppearing()
{
Accelerometer.ReadingChanged += OnReadingChanged;
+ Accelerometer.ShakeDetected += Accelerometer_OnShaked;
+
base.OnAppearing();
}
+ void Accelerometer_OnShaked(object sender, EventArgs e) =>
+ ShakeTime = $"Shake detected: {DateTime.Now.ToLongTimeString()}";
+
public override void OnDisappearing()
{
OnStop();
Accelerometer.ReadingChanged -= OnReadingChanged;
-
+ Accelerometer.ShakeDetected -= Accelerometer_OnShaked;
base.OnDisappearing();
}
diff --git a/Samples/Samples/ViewModel/BrowserViewModel.cs b/Samples/Samples/ViewModel/BrowserViewModel.cs
index 6ef4ed8f3..f155a4371 100644
--- a/Samples/Samples/ViewModel/BrowserViewModel.cs
+++ b/Samples/Samples/ViewModel/BrowserViewModel.cs
@@ -101,8 +101,8 @@ async void OpenUri()
{
LaunchMode = (BrowserLaunchMode)BrowserType,
TitleMode = (BrowserTitleMode)BrowserTitleType,
- PreferredToolbarColor = ToolbarColor == 0 ? (Color?)null : (System.Drawing.Color)colorDictionary[AllColors[TitleColor]],
- PreferredControlColor = ControlColor == 0 ? (Color?)null : (System.Drawing.Color)colorDictionary[AllColors[ControlColor]]
+ PreferredToolbarColor = GetColor(ToolbarColor),
+ PreferredControlColor = GetColor(ControlColor)
});
}
catch (Exception e)
@@ -114,6 +114,13 @@ async void OpenUri()
{
IsBusy = false;
}
+
+ Color? GetColor(int index)
+ {
+ return index <= 0
+ ? (Color?)null
+ : (System.Drawing.Color)colorDictionary[AllColors[index]];
+ }
}
}
}
diff --git a/Samples/Samples/ViewModel/DeviceInfoViewModel.cs b/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
index 6fab032d8..a1925d59f 100644
--- a/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
+++ b/Samples/Samples/ViewModel/DeviceInfoViewModel.cs
@@ -31,7 +31,6 @@ public DisplayInfo ScreenMetrics
public override void OnAppearing()
{
base.OnAppearing();
-
DeviceDisplay.MainDisplayInfoChanged += OnScreenMetricsChanged;
ScreenMetrics = DeviceDisplay.MainDisplayInfo;
}
diff --git a/Tests/Accelerometer_Tests.cs b/Tests/Accelerometer_Tests.cs
index 5696f5157..02819833c 100644
--- a/Tests/Accelerometer_Tests.cs
+++ b/Tests/Accelerometer_Tests.cs
@@ -1,4 +1,5 @@
-using Xamarin.Essentials;
+using System;
+using Xamarin.Essentials;
using Xunit;
namespace Tests
@@ -54,5 +55,55 @@ public void Accelerometer_Comparison(
Assert.NotEqual(data1.GetHashCode(), data2.GetHashCode());
}
}
+
+ [Fact]
+ public void InitialShaking()
+ {
+ var q = new AccelerometerQueue();
+ Assert.False(q.IsShaking);
+ }
+
+ [Fact]
+ public void ShakingTests()
+ {
+ var now = DateTime.UtcNow;
+ var q = new AccelerometerQueue();
+ q.Add(GetShakeTime(now, 0), false);
+ q.Add(GetShakeTime(now, .3), false);
+ q.Add(GetShakeTime(now, .6), false);
+ q.Add(GetShakeTime(now, .9), false);
+ Assert.False(q.IsShaking);
+
+ // The oldest two entries will be removed.
+ q.Add(GetShakeTime(now, 1.2), true);
+ q.Add(GetShakeTime(now, 1.5), true);
+ Assert.False(q.IsShaking);
+
+ // Another entry should be removed, now 3 out of 4 are true.
+ q.Add(GetShakeTime(now, 1.8), true);
+ Assert.True(q.IsShaking);
+
+ q.Add(GetShakeTime(now, 2.1), false);
+ Assert.True(q.IsShaking);
+
+ q.Add(GetShakeTime(now, 2.4), false);
+ Assert.False(q.IsShaking);
+ }
+
+ [Fact]
+ public void ClearQueue()
+ {
+ var now = DateTime.UtcNow;
+ var q = new AccelerometerQueue();
+ q.Add(GetShakeTime(now, 0), true);
+ q.Add(GetShakeTime(now, .1), true);
+ q.Add(GetShakeTime(now, .3), true);
+ Assert.True(q.IsShaking);
+ q.Clear();
+ Assert.False(q.IsShaking);
+ }
+
+ long GetShakeTime(DateTime now, double seconds) =>
+ now.AddSeconds(seconds).Nanoseconds();
}
}
diff --git a/Xamarin.Essentials/Accelerometer/Accelerometer.shared.cs b/Xamarin.Essentials/Accelerometer/Accelerometer.shared.cs
index 05cb655a4..9754c8b30 100644
--- a/Xamarin.Essentials/Accelerometer/Accelerometer.shared.cs
+++ b/Xamarin.Essentials/Accelerometer/Accelerometer.shared.cs
@@ -5,10 +5,18 @@ namespace Xamarin.Essentials
{
public static partial class Accelerometer
{
+ const double accelerationThreshold = 169;
+
+ const double gravity = 9.81;
+
+ static readonly AccelerometerQueue queue = new AccelerometerQueue();
+
static bool useSyncContext;
public static event EventHandler ReadingChanged;
+ public static event EventHandler ShakeDetected;
+
public static bool IsMonitoring { get; private set; }
public static void Start(SensorSpeed sensorSpeed)
@@ -63,7 +71,38 @@ internal static void OnChanged(AccelerometerChangedEventArgs e)
MainThread.BeginInvokeOnMainThread(() => ReadingChanged?.Invoke(null, e));
else
ReadingChanged?.Invoke(null, e);
+
+ if (ShakeDetected != null)
+ ProcessShakeEvent(e.Reading.Acceleration);
+ }
+
+ static void ProcessShakeEvent(Vector3 acceleration)
+ {
+ var now = DateTime.UtcNow.Nanoseconds();
+
+ var x = acceleration.X * gravity;
+ var y = acceleration.Y * gravity;
+ var z = acceleration.Z * gravity;
+
+ var g = x.Square() + y.Square() + z.Square();
+ queue.Add(now, g > accelerationThreshold);
+
+ if (queue.IsShaking)
+ {
+ queue.Clear();
+ var args = new EventArgs();
+
+ if (useSyncContext)
+ MainThread.BeginInvokeOnMainThread(() => ShakeDetected?.Invoke(null, args));
+ else
+ ShakeDetected?.Invoke(null, args);
+ }
}
+
+ static double Square(this double q) => q * q;
+
+ internal static long Nanoseconds(this DateTime time) =>
+ (time.Ticks / TimeSpan.TicksPerMillisecond) * 1_000_000;
}
public class AccelerometerChangedEventArgs : EventArgs
diff --git a/Xamarin.Essentials/Accelerometer/AccelerometerQueue.shared.cs b/Xamarin.Essentials/Accelerometer/AccelerometerQueue.shared.cs
new file mode 100644
index 000000000..a8255ef12
--- /dev/null
+++ b/Xamarin.Essentials/Accelerometer/AccelerometerQueue.shared.cs
@@ -0,0 +1,112 @@
+namespace Xamarin.Essentials
+{
+ // Detect if 3/4ths of the accelerometer events in the last half second are accelerating
+ // this means we are free falling or shaking
+ class AccelerometerQueue
+ {
+ readonly AccelerometerDataPool pool = new AccelerometerDataPool();
+
+ // in nanoseconds
+ readonly long maxWindowSize = 500_000_000;
+ readonly long minWindowSize = 250_000_000;
+
+ readonly int minQueueSize = 4;
+
+ AccelerometerSample oldest;
+ AccelerometerSample newest;
+ int sampleCount;
+ int acceleratingCount;
+
+ internal void Add(long timestamp, bool accelerating)
+ {
+ Purge(timestamp - maxWindowSize);
+ var added = pool.Acquire();
+ added.Timestamp = timestamp;
+ added.IsAccelerating = accelerating;
+ added.Next = null;
+
+ if (newest != null)
+ newest.Next = added;
+
+ newest = added;
+
+ if (oldest == null)
+ oldest = added;
+
+ sampleCount++;
+
+ if (accelerating)
+ acceleratingCount++;
+ }
+
+ internal void Clear()
+ {
+ while (oldest != null)
+ {
+ var removed = oldest;
+ oldest = removed.Next;
+ pool.Release(removed);
+ }
+ newest = null;
+ sampleCount = 0;
+ acceleratingCount = 0;
+ }
+
+ void Purge(long cutoff)
+ {
+ while (sampleCount >= minQueueSize &&
+ oldest != null &&
+ cutoff - oldest.Timestamp > 0)
+ {
+ var removed = oldest;
+ if (removed.IsAccelerating)
+ acceleratingCount--;
+
+ sampleCount--;
+ oldest = removed.Next;
+
+ if (oldest == null)
+ newest = null;
+
+ pool.Release(removed);
+ }
+ }
+
+ // Returns true if we have enough samples to detect if we are shaking the device and that more than 3/4th of them are accelerating
+ internal bool IsShaking => newest != null &&
+ oldest != null &&
+ newest.Timestamp - oldest.Timestamp >= minWindowSize &&
+ acceleratingCount >= (sampleCount >> 1) + (sampleCount >> 2);
+
+ internal class AccelerometerSample
+ {
+ public long Timestamp { get; set; }
+
+ public bool IsAccelerating { get; set; }
+
+ public AccelerometerSample Next { get; set; }
+ }
+
+ internal class AccelerometerDataPool
+ {
+ AccelerometerSample head;
+
+ internal AccelerometerSample Acquire()
+ {
+ var aquired = head;
+ if (aquired == null)
+ aquired = new AccelerometerSample();
+ else
+ head = aquired.Next;
+
+ return aquired;
+ }
+
+ internal void Release(AccelerometerSample sample)
+ {
+ sample.Next = head;
+ head = sample;
+ }
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Xamarin.Essentials.csproj b/Xamarin.Essentials/Xamarin.Essentials.csproj
index 3ac4b7e6c..1add8b4bf 100644
--- a/Xamarin.Essentials/Xamarin.Essentials.csproj
+++ b/Xamarin.Essentials/Xamarin.Essentials.csproj
@@ -109,4 +109,4 @@
-
+
\ No newline at end of file
diff --git a/docs/en/FrameworksIndex/xamarin-essentials-android.xml b/docs/en/FrameworksIndex/xamarin-essentials-android.xml
index f9cbafd95..d801d25d4 100644
--- a/docs/en/FrameworksIndex/xamarin-essentials-android.xml
+++ b/docs/en/FrameworksIndex/xamarin-essentials-android.xml
@@ -5,6 +5,7 @@
+
@@ -371,6 +372,7 @@
+
diff --git a/docs/en/FrameworksIndex/xamarin-essentials-ios.xml b/docs/en/FrameworksIndex/xamarin-essentials-ios.xml
index bea741277..7ac30193f 100644
--- a/docs/en/FrameworksIndex/xamarin-essentials-ios.xml
+++ b/docs/en/FrameworksIndex/xamarin-essentials-ios.xml
@@ -5,6 +5,7 @@
+
@@ -372,6 +373,7 @@
+
diff --git a/docs/en/FrameworksIndex/xamarin-essentials-uwp.xml b/docs/en/FrameworksIndex/xamarin-essentials-uwp.xml
index add520c20..565d0bbd5 100644
--- a/docs/en/FrameworksIndex/xamarin-essentials-uwp.xml
+++ b/docs/en/FrameworksIndex/xamarin-essentials-uwp.xml
@@ -5,6 +5,7 @@
+
@@ -371,6 +372,7 @@
+
diff --git a/docs/en/FrameworksIndex/xamarin-essentials.xml b/docs/en/FrameworksIndex/xamarin-essentials.xml
index c783e426f..6d154d427 100644
--- a/docs/en/FrameworksIndex/xamarin-essentials.xml
+++ b/docs/en/FrameworksIndex/xamarin-essentials.xml
@@ -5,6 +5,7 @@
+
@@ -369,6 +370,7 @@
+
diff --git a/docs/en/Xamarin.Essentials/Accelerometer.xml b/docs/en/Xamarin.Essentials/Accelerometer.xml
index 5ef1eaac7..173e0b4e4 100644
--- a/docs/en/Xamarin.Essentials/Accelerometer.xml
+++ b/docs/en/Xamarin.Essentials/Accelerometer.xml
@@ -54,6 +54,25 @@
+
+
+
+
+ Event
+
+ Xamarin.Essentials
+ 1.0.0.0
+
+
+ System.EventHandler
+
+
+ Event triggered when a shake has been detected on the device.
+
+
+
+
+
diff --git a/docs/en/Xamarin.Essentials/Location.xml b/docs/en/Xamarin.Essentials/Location.xml
index 21fabe58a..2bd5e2e00 100644
--- a/docs/en/Xamarin.Essentials/Location.xml
+++ b/docs/en/Xamarin.Essentials/Location.xml
@@ -12,8 +12,7 @@
The latitude, longitude, and time information reported by the device.
-
-
+
@@ -28,8 +27,7 @@
Constructor for a location.
-
-
+
@@ -47,8 +45,7 @@
Location to copy values from.
Constructor for a location.
-
-
+
@@ -68,8 +65,7 @@
Default latitude for location.
Default longitude for location.
Constructor for a location.
-
-
+
@@ -91,8 +87,7 @@
Default longitude for location.
Timestamp for the location (Utc based).
Constructor for a location.
-
-
+
@@ -110,8 +105,7 @@
Gets or sets the accuracy (in meters) of the location.The location accuracy.
-
-
+
@@ -155,8 +149,7 @@
Units to return.
Calculate distance between two locations.Distance between two locations in the unit selected.
-
-
+
@@ -184,8 +177,7 @@
Unit to return.
Calculate distance between two locations.Distance calculated.
-
-
+
@@ -213,8 +205,7 @@
Unit to use.
Calculate distance between two locations.Distance calculated.
-
-
+
@@ -244,8 +235,7 @@
Units to return.
Calculate distance between two locations.Distance between two locations in the unit selected.
-
-
+
@@ -284,7 +274,7 @@
Inform if location is from GPS or from Mock.True if is from Mock and False if from GPS.
-
+
@@ -303,8 +293,7 @@
Gets or sets the latitude of location.Latitude of the location.
-
-
+
@@ -322,8 +311,7 @@
Gets or sets the longitude of location.Longitude of the location.
-
-
+
@@ -341,8 +329,7 @@
Speed in meters per second.Speed measured by the device..
-
-
+
@@ -360,8 +347,7 @@
Gets or sets the timestamp of the location.UTC timestamp.
-
-
+