Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

Commit

Permalink
GH-126: Finish Shake Detector API (#693)
Browse files Browse the repository at this point in the history
* Update CONTRIBUTING.md (#692)

* GH-126 Detect Shake API (#666)

* Added shaken support

* name vhange

* check-in

* doc update

* Implements the Shake API inside Accelerometer Class;
Change sample to AccelorometerViewModel

* Fix the sample project

* Update the docs

* Added Queue mechanism based off seismic with tests as well. Something is not  right yet with calulating isaccelerating.

* Finalize shake detection!!!

* Re-generated the docs.

* Fix the VM after merge
  • Loading branch information
jamesmontemagno authored Feb 19, 2019
1 parent 46d0d42 commit ead1eba
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 39 deletions.
66 changes: 63 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion Samples/Samples/View/AccelerometerPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</views:BasePage.BindingContext>

<StackLayout>
<Label Text="Retrieve acceleration data of the device in 3D space." FontAttributes="Bold" Margin="12" />
<Label Text="Retrieve acceleration data of the device in 3D space. Additionally, this page will detect if you shake your device." FontAttributes="Bold" Margin="12" />

<ScrollView>
<Grid Padding="12,0,12,12">
Expand All @@ -20,6 +20,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
Expand All @@ -40,6 +41,8 @@

<Button Grid.Row="5" Grid.Column="1" Text="Stop" Command="{Binding StopCommand}"
IsEnabled="{Binding IsActive}" />

<Label Grid.Row="6" Grid.ColumnSpan="2" Text="{Binding ShakeTime}"/>
</Grid>
</ScrollView>
</StackLayout>
Expand Down
14 changes: 13 additions & 1 deletion Samples/Samples/ViewModel/AccelerometerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class AccelerometerViewModel : BaseViewModel
double x;
double y;
double z;
string shakeTime = string.Empty;
bool isActive;
int speed = 0;

Expand All @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down
11 changes: 9 additions & 2 deletions Samples/Samples/ViewModel/BrowserViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -114,6 +114,13 @@ async void OpenUri()
{
IsBusy = false;
}

Color? GetColor(int index)
{
return index <= 0
? (Color?)null
: (System.Drawing.Color)colorDictionary[AllColors[index]];
}
}
}
}
1 change: 0 additions & 1 deletion Samples/Samples/ViewModel/DeviceInfoViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public DisplayInfo ScreenMetrics
public override void OnAppearing()
{
base.OnAppearing();

DeviceDisplay.MainDisplayInfoChanged += OnScreenMetricsChanged;
ScreenMetrics = DeviceDisplay.MainDisplayInfo;
}
Expand Down
53 changes: 52 additions & 1 deletion Tests/Accelerometer_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Xamarin.Essentials;
using System;
using Xamarin.Essentials;
using Xunit;

namespace Tests
Expand Down Expand Up @@ -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();
}
}
39 changes: 39 additions & 0 deletions Xamarin.Essentials/Accelerometer/Accelerometer.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccelerometerChangedEventArgs> ReadingChanged;

public static event EventHandler ShakeDetected;

public static bool IsMonitoring { get; private set; }

public static void Start(SensorSpeed sensorSpeed)
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit ead1eba

Please sign in to comment.