Commit b6614822 authored by Chris Scott's avatar Chris Scott
parents 820e0d34 0a12aef0
...@@ -70,16 +70,16 @@ A full example could be: ...@@ -70,16 +70,16 @@ A full example could be:
// BackgroundGeoLocation is highly configurable. // BackgroundGeoLocation is highly configurable.
bgGeo.configure(callbackFn, failureFn, { bgGeo.configure(callbackFn, failureFn, {
url: 'http://only.for.android.com/update_location.json', // <-- Android ONLY: your server url to send locations to url: 'http://only.for.android.com/update_location.json', // <-- Android ONLY: your server url to send locations to
params: { // <-- Android ONLY: HTTP POST params sent to your server when persisting locations. params: {
auth_token: 'user_secret_auth_token', auth_token: 'user_secret_auth_token', // <-- Android ONLY: HTTP POST params sent to your server when persisting locations.
foo: 'bar' foo: 'bar' // <-- Android ONLY: HTTP POST params sent to your server when persisting locations.
}, },
headers: { // <-- Android ONLY: Optional HTTP headers sent to your configured #url when persisting locations headers: { // <-- Android ONLY: Optional HTTP headers sent to your configured #url when persisting locations
"X-Foo": "BAR" "X-Foo": "BAR"
}, },
desiredAccuracy: 10, desiredAccuracy: 10,
stationaryRadius: 20, stationaryRadius: 20,
distanceFilter: 30, distanceFilter: 30,
notificationTitle: 'Background tracking', // <-- android only, customize the title of the notification notificationTitle: 'Background tracking', // <-- android only, customize the title of the notification
notificationText: 'ENABLED', // <-- android only, customize the text of the notification notificationText: 'ENABLED', // <-- android only, customize the text of the notification
activityType: 'AutomotiveNavigation', activityType: 'AutomotiveNavigation',
...@@ -99,7 +99,7 @@ NOTE: The plugin includes `org.apache.cordova.geolocation` as a dependency. You ...@@ -99,7 +99,7 @@ NOTE: The plugin includes `org.apache.cordova.geolocation` as a dependency. You
## Behaviour ## Behaviour
The plugin has features allowing you to control the behaviour of background-tracking, striking a balance between accuracy and battery-usage. In stationary-mode, the plugin attempts to descrease its power usage and accuracy by setting up a circular stationary-region of configurable #stationaryRadius. iOS has a nice system [Significant Changes API](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instm/CLLocationManager/startMonitoringSignificantLocationChanges), which allows the os to suspend your app until a cell-tower change is detected (typically 2-3 city-block change) Android uses [LocationManager#addProximityAlert](http://developer.android.com/reference/android/location/LocationManager.html). The plugin has features allowing you to control the behaviour of background-tracking, striking a balance between accuracy and battery-usage. In stationary-mode, the plugin attempts to descrease its power usage and accuracy by setting up a circular stationary-region of configurable #stationaryRadius. iOS has a nice system [Significant Changes API](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instm/CLLocationManager/startMonitoringSignificantLocationChanges), which allows the os to suspend your app until a cell-tower change is detected (typically 2-3 city-block change) Android uses [LocationManager#addProximityAlert](http://developer.android.com/reference/android/location/LocationManager.html). Windows Phone does not have such a API.
When the plugin detects your user has moved beyond his stationary-region, it engages the native platform's geolocation system for aggressive monitoring according to the configured `#desiredAccuracy`, `#distanceFilter` and `#locationTimeout`. The plugin attempts to intelligently scale `#distanceFilter` based upon the current reported speed. Each time `#distanceFilter` is determined to have changed by 5m/s, it recalculates it by squaring the speed rounded-to-nearest-five and adding #distanceFilter (I arbitrarily came up with that formula. Better ideas?). When the plugin detects your user has moved beyond his stationary-region, it engages the native platform's geolocation system for aggressive monitoring according to the configured `#desiredAccuracy`, `#distanceFilter` and `#locationTimeout`. The plugin attempts to intelligently scale `#distanceFilter` based upon the current reported speed. Each time `#distanceFilter` is determined to have changed by 5m/s, it recalculates it by squaring the speed rounded-to-nearest-five and adding #distanceFilter (I arbitrarily came up with that formula. Better ideas?).
...@@ -109,14 +109,19 @@ When the plugin detects your user has moved beyond his stationary-region, it eng ...@@ -109,14 +109,19 @@ When the plugin detects your user has moved beyond his stationary-region, it eng
The plugin works with iOS and Android, however both platforms differ significantly in their interaction with server. The plugin works with iOS and Android, however both platforms differ significantly in their interaction with server.
### iOS ### iOS and WP8
*Only* on iOS will the plugin execute your configured ```callbackFn```. You may manually POST the received ```GeoLocation``` to your server using standard XHR. iOS ignores the @config params ```url```, ```params``` and ```headers```. On iOS and WP8 the plugin will execute your configured ```callbackFn```. You may manually POST the received ```GeoLocation``` to your server using standard XHR. iOS and WP8 ignore the @config params ```url```, ```params``` and ```headers```.
### Android ### Android
Android **WILL NOT** execute your configured ```callbackFn```. The plugin manages sync-ing GeoLocations to your server automatically, using the configured ```url```, ```params``` and ```headers```. Since the Android plugin must run as an autonomous Background Service, disconnected from your the main Android Activity (your foreground application), the background-geolocation plugin will continue to run, even if the foreground Activity is killed due to memory constraints. This is why the Android plugin cannot execute the Javascript ```callbackFn```, since your app is not guaranteed to keep running -- syncing locations to the server must be handled by the plugin. Android **WILL NOT** execute your configured ```callbackFn```. The plugin manages sync-ing GeoLocations to your server automatically, using the configured ```url```, ```params``` and ```headers```. Since the Android plugin must run as an autonomous Background Service, disconnected from your the main Android Activity (your foreground application), the background-geolocation plugin will continue to run, even if the foreground Activity is killed due to memory constraints. This is why the Android plugin cannot execute the Javascript ```callbackFn```, since your app is not guaranteed to keep running -- syncing locations to the server must be handled by the plugin.
### WP8
On WP8 the plugin does not support the Stationairy location and does not implement ```getStationaryLocation()``` and ```onPaceChange()```.
Keep in mind that it is **not** possible to use ```start()``` at the ```pause``` event of Cordova/PhoneGap. WP8 suspend your app immediately and ```start()``` will not be executed. So make sure you fire ```start()``` before the app is closed/minimized.
### Config ### Config
Use the following config-parameters with the #configure method: Use the following config-parameters with the #configure method:
...@@ -134,7 +139,7 @@ When stopped, the minimum distance the device must move beyond the stationary lo ...@@ -134,7 +139,7 @@ When stopped, the minimum distance the device must move beyond the stationary lo
When enabled, the plugin will emit sounds for life-cycle events of background-geolocation! **NOTE iOS**: In addition, you must manually enable the *Audio and Airplay* background mode in *Background Capabilities* to hear these debugging sounds. When enabled, the plugin will emit sounds for life-cycle events of background-geolocation! **NOTE iOS**: In addition, you must manually enable the *Audio and Airplay* background mode in *Background Capabilities* to hear these debugging sounds.
- Exit stationary region: *[ios]* Calendar event notification sound *[android]* dialtone beep-beep-beep - Exit stationary region: *[ios]* Calendar event notification sound *[android]* dialtone beep-beep-beep
- GeoLocation recorded: *[ios]* SMS sent sound, *[android]* tt short beep - GeoLocation recorded: *[ios]* SMS sent sound, *[android]* tt short beep, *[WP8]* High beep, 1 sec.
- Aggressive geolocation engaged: *[ios]* SIRI listening sound, *[android]* none - Aggressive geolocation engaged: *[ios]* SIRI listening sound, *[android]* none
- Passive geolocation engaged: *[ios]* SIRI stop listening sound, *[android]* none - Passive geolocation engaged: *[ios]* SIRI stop listening sound, *[android]* none
- Acquiring stationary location sound: *[ios]* "tick,tick,tick" sound, *[android]* none - Acquiring stationary location sound: *[ios]* "tick,tick,tick" sound, *[android]* none
...@@ -193,7 +198,7 @@ Optional HTTP headers POSTed to your server when persisting locations ...@@ -193,7 +198,7 @@ Optional HTTP headers POSTed to your server when persisting locations
#####`@param {String} notificationText/Title` #####`@param {String} notificationText/Title`
On Android devices it is required to have a notification in the drawer because it's a "foreground service". This gives it high priority, decreasing probability of OS killing it. To customize the title and text of the notification, set these options. On Android devices it is required to have a notification in the drawer because it's a "foreground service". This gives it high priority, decreasing probability of OS killing it. To customize the title and text of the notification, set these options.
#####`@param {Integer} locationTimeout #####`@param {Integer} locationTimeout
The minimum time interval between location updates, in seconds. See [Android docs](http://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(long,%20float,%20android.location.Criteria,%20android.app.PendingIntent)) for more information. The minimum time interval between location updates, in seconds. See [Android docs](http://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(long,%20float,%20android.location.Criteria,%20android.app.PendingIntent)) for more information.
...@@ -204,6 +209,12 @@ The minimum time interval between location updates, in seconds. See [Android do ...@@ -204,6 +209,12 @@ The minimum time interval between location updates, in seconds. See [Android do
Presumably, this affects ios GPS algorithm. See [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/activityType) for more information Presumably, this affects ios GPS algorithm. See [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/activityType) for more information
### WP8 Config
#####`{String} desiredAccuracy`
In Windows Phone, the underlying GeoLocator you can choose to use 'DesiredAccuracy' or 'DesiredAccuracyInMeters'. Since this plugins default configuration accepts meters, the default desiredAccuracy is mapped to the Windows Phone DesiredAccuracyInMeters leaving the DesiredAccuracy enum empty. For more info see the [MS docs](http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.desiredaccuracyinmeters) for more information.
## Licence ## ## Licence ##
The MIT License The MIT License
......
...@@ -69,4 +69,29 @@ ...@@ -69,4 +69,29 @@
<header-file src="src/ios/CDVBackgroundGeoLocation.h" /> <header-file src="src/ios/CDVBackgroundGeoLocation.h" />
</platform> </platform>
<!-- wp8 -->
<platform name="wp8">
<config-file target="config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
<param name="wp-package" value="BackgroundGeoLocation" onload="true" />
<param name="onload" value="true" />
</feature>
</config-file>
<config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Tasks/DefaultTask">
<BackgroundExecution>
<ExecutionType Name="LocationTracking" />
</BackgroundExecution>
</config-file>
<config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Capabilities">
<Capability Name="ID_CAP_LOCATION" />
</config-file>
<source-file src="src/wp8/BackgroundGeoLocation.cs" />
<source-file src="src/wp8/BackgroundGeoLocationOptions.cs" />
<source-file src="src/wp8/DebugAudioNotifier.cs" />
<source-file src="src/wp8/ExtensionMethods.cs" />
<source-file src="src/wp8/IBackgroundGeoLocation.cs" />
</platform>
</plugin> </plugin>
using System;
using Windows.Devices.Geolocation;
using WPCordovaClassLib.Cordova;
using WPCordovaClassLib.Cordova.Commands;
using WPCordovaClassLib.Cordova.JSON;
using System.Diagnostics;
namespace Cordova.Extension.Commands
{
public class BackgroundGeoLocation : BaseCommand, IBackgroundGeoLocation
{
private string ConfigureCallbackToken { get; set; }
private BackgroundGeoLocationOptions BackgroundGeoLocationOptions { get; set; }
/// <summary>
/// Geolocator and RunningInBackground are required properties to run in background
/// For more information read http://msdn.microsoft.com/library/windows/apps/jj662935(v=vs.105).aspx
/// </summary>
public static Geolocator Geolocator { get; set; }
public static bool RunningInBackground { get; set; }
/// <summary>
/// When start() is fired immediate after configure() in javascript, configure may not be finished yet, IsConfigured and IsConfiguring are used to keep track of this
/// </summary>
private bool IsConfigured { get; set; }
private bool IsConfiguring { get; set; }
public BackgroundGeoLocation()
{
IsConfiguring = false;
IsConfigured = false;
}
public void configure(string args)
{
IsConfiguring = true;
ConfigureCallbackToken = CurrentCommandCallbackId;
RunningInBackground = false;
BackgroundGeoLocationOptions = this.ParseBackgroundGeoLocationOptions(args);
IsConfigured = BackgroundGeoLocationOptions.ParsingSucceeded;
IsConfiguring = false;
}
private BackgroundGeoLocationOptions ParseBackgroundGeoLocationOptions(string configureArgs)
{
var parsingSucceeded = true;
var options = JsonHelper.Deserialize<string[]>(configureArgs);
double stationaryRadius;
double distanceFilter;
UInt32 locationTimeout;
UInt32 desiredAccuracy;
bool debug;
if (!double.TryParse(options[3], out stationaryRadius))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for stationaryRadius:{0}", options[3])));
parsingSucceeded = false;
}
if (!double.TryParse(options[4], out distanceFilter))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for distanceFilter:{0}", options[4])));
parsingSucceeded = false;
}
if (!UInt32.TryParse(options[5], out locationTimeout))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for locationTimeout:{0}", options[5])));
parsingSucceeded = false;
}
if (!UInt32.TryParse(options[6], out desiredAccuracy))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for desiredAccuracy:{0}", options[6])));
parsingSucceeded = false;
}
if (!bool.TryParse(options[7], out debug))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for debug:{0}", options[7])));
parsingSucceeded = false;
}
return new BackgroundGeoLocationOptions
{
Url = options[1],
StationaryRadius = stationaryRadius,
DistanceFilterInMeters = distanceFilter,
LocationTimeoutInMilliseconds = locationTimeout,
DesiredAccuracyInMeters = desiredAccuracy,
Debug = debug,
ParsingSucceeded = parsingSucceeded
};
}
public void start(string nothing)
{
while (!IsConfigured && IsConfiguring)
{
// Wait for configure() to complete...
}
if (!IsConfigured || !BackgroundGeoLocationOptions.ParsingSucceeded)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.INVALID_ACTION, "Cannot start: Run configure() with proper values!"));
stop();
return;
}
StopGeolocatorIfActive();
Geolocator = new Geolocator
{
// Default: 50 meters
MovementThreshold = BackgroundGeoLocationOptions.DistanceFilterInMeters,
// JS Interface takes seconds, MS takes miliseconds, default 60 seconds
ReportInterval = BackgroundGeoLocationOptions.LocationTimeoutInMilliseconds * 1000,
// In our case this property has always a value, if left empty or below zero the default will be 100 meter but can be overridden via parameter DesiredAccuracy
DesiredAccuracyInMeters = BackgroundGeoLocationOptions.DesiredAccuracyInMeters
};
Geolocator.PositionChanged += OnGeolocatorOnPositionChanged;
RunningInBackground = true;
}
private void OnGeolocatorOnPositionChanged(Geolocator sender, PositionChangedEventArgs configureCallbackTokenargs)
{
if (Geolocator.LocationStatus == PositionStatus.Disabled || Geolocator.LocationStatus == PositionStatus.NotAvailable)
{
DispatchMessage(PluginResult.Status.ERROR, string.Format("Cannot start: LocationStatus/PositionStatus: {0}! {1}", Geolocator.LocationStatus, IsConfigured), true, ConfigureCallbackToken);
return;
}
var callbackJsonResult = configureCallbackTokenargs.Position.Coordinate.ToJson();
if (BackgroundGeoLocationOptions.Debug)
{
DebugAudioNotifier.GetDebugAudioNotifier().PlaySound(DebugAudioNotifier.Tone.High, TimeSpan.FromSeconds(3));
Debug.WriteLine("PositionChanged token{0}, Coordinates: {1}", ConfigureCallbackToken, callbackJsonResult);
}
DispatchMessage(PluginResult.Status.OK, callbackJsonResult, true, ConfigureCallbackToken);
}
public void stop()
{
RunningInBackground = false;
StopGeolocatorIfActive();
DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
}
private void StopGeolocatorIfActive()
{
if (Geolocator == null) return;
Geolocator.PositionChanged -= OnGeolocatorOnPositionChanged;
Geolocator = null;
}
public void finish()
{
DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
}
public void onPaceChange(bool isMoving)
{
throw new NotImplementedException();
}
public void setConfig(string setConfigArgs)
{
if (string.IsNullOrWhiteSpace(setConfigArgs))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.INVALID_ACTION, "Cannot set config because of an empty input"));
return;
}
var parsingSucceeded = true;
var options = JsonHelper.Deserialize<string[]>(setConfigArgs);
double stationaryRadius;
double distanceFilter;
UInt32 locationTimeout;
UInt32 desiredAccuracy;
if (!double.TryParse(options[0], out stationaryRadius))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for stationaryRadius:{0}", options[2])));
parsingSucceeded = false;
}
if (!double.TryParse(options[1], out distanceFilter))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for distanceFilter:{0}", options[3])));
parsingSucceeded = false;
}
if (!UInt32.TryParse(options[2], out locationTimeout))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for locationTimeout:{0}", options[4])));
parsingSucceeded = false;
}
if (!UInt32.TryParse(options[3], out desiredAccuracy))
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, string.Format("Invalid value for desiredAccuracy:{0}", options[5])));
parsingSucceeded = false;
}
if (!parsingSucceeded) return;
BackgroundGeoLocationOptions.StationaryRadius = stationaryRadius;
BackgroundGeoLocationOptions.DistanceFilterInMeters = distanceFilter;
BackgroundGeoLocationOptions.LocationTimeoutInMilliseconds = locationTimeout * 1000;
BackgroundGeoLocationOptions.DesiredAccuracyInMeters = desiredAccuracy;
DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
}
public void getStationaryLocation()
{
throw new NotImplementedException();
}
private void DispatchMessage(PluginResult.Status status, string message, bool keepCallback, string callBackId)
{
var pluginResult = new PluginResult(status, message) { KeepCallback = keepCallback };
DispatchCommandResult(pluginResult, callBackId);
}
}
}
using System;
namespace Cordova.Extension.Commands
{
public class BackgroundGeoLocationOptions
{
public string Url;
public double StationaryRadius;
public double DistanceFilterInMeters;
public UInt32 LocationTimeoutInMilliseconds;
public UInt32 DesiredAccuracyInMeters;
public bool Debug;
public bool ParsingSucceeded { get; set; }
}
}
\ No newline at end of file
using System;
using System.Windows;
using System.Windows.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
namespace Cordova.Extension.Commands
{
public class DebugAudioNotifier : IDisposable
{
private static DebugAudioNotifier _audioNotifier;
private DynamicSoundEffectInstance _dynamicSound;
private DispatcherTimer _timer;
private TimeSpan _timeLeft;
private const int SampleRate = 48000;
private Tone _frequency = Tone.Low;
private int _bufferSize;
private byte[] _soundBuffer;
private int _totalTime;
public enum Tone
{
Low = 300,
High = 900
}
private DebugAudioNotifier()
{
}
public static DebugAudioNotifier GetDebugAudioNotifier()
{
return _audioNotifier ?? (_audioNotifier = new DebugAudioNotifier());
}
public void PlaySound(Tone tone, TimeSpan duration)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
if (_timer == null)
{
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(33)
};
_timer.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } };
}
if (_timer.IsEnabled) _timer.Stop();
_timeLeft = duration;
FrameworkDispatcher.Update();
_frequency = tone;
_dynamicSound = new DynamicSoundEffectInstance(SampleRate, AudioChannels.Mono);
_dynamicSound.BufferNeeded += dynamicSound_BufferNeeded;
_dynamicSound.Play();
_bufferSize = _dynamicSound.GetSampleSizeInBytes(TimeSpan.FromSeconds(1));
_soundBuffer = new byte[_bufferSize];
_timer.Start();
});
}
private void dynamicSound_BufferNeeded(object sender, EventArgs e)
{
for (var i = 0; i < _bufferSize - 1; i += 2)
{
var time = _totalTime / (double)SampleRate;
var sample = (short)(Math.Sin(2 * Math.PI * (int)_frequency * time) * short.MaxValue);
_soundBuffer[i] = (byte)sample;
_soundBuffer[i + 1] = (byte)(sample >> 8);
_totalTime++;
}
_timeLeft = _timeLeft.Subtract(TimeSpan.FromSeconds(_totalTime / SampleRate));
if (_timeLeft.Ticks <= 0)
{
_totalTime = 0;
return;
}
_dynamicSound.SubmitBuffer(_soundBuffer);
}
public void Dispose()
{
_dynamicSound.Dispose();
}
}
}
\ No newline at end of file
using System;
using System.Globalization;
using Windows.Devices.Geolocation;
namespace Cordova.Extension.Commands
{
public static class ExtensionMethods
{
public static string ToJson(this Geocoordinate geocoordinate)
{
var numberFormatInfo = (NumberFormatInfo)NumberFormatInfo.CurrentInfo.Clone();
numberFormatInfo.NaNSymbol = "0";
numberFormatInfo.NumberDecimalSeparator = ".";
return string.Format("{{ " +
"\"accuracy\": {0}," +
"\"latitude\": {1}," +
"\"longitude\": {2}," +
"\"altitude\": {3}," +
"\"altitudeAccuracy\": {4}," +
"\"heading\": {5}," +
"\"velocity\": {6}," +
"\"timestamp\": {7}" +
"}}"
, geocoordinate.Accuracy.ToString(numberFormatInfo)
, geocoordinate.Latitude.ToString(numberFormatInfo)
, geocoordinate.Longitude.ToString(numberFormatInfo)
, geocoordinate.Altitude.HasValue ? geocoordinate.Altitude.Value.ToString(numberFormatInfo) : "0"
, geocoordinate.AltitudeAccuracy.HasValue ? geocoordinate.AltitudeAccuracy.Value.ToString(numberFormatInfo) : "0"
, geocoordinate.Heading.HasValue ? geocoordinate.Heading.Value.ToString(numberFormatInfo) : "0"
, geocoordinate.Speed.HasValue ? geocoordinate.Speed.Value.ToString(numberFormatInfo) : "0"
, geocoordinate.Timestamp.DateTime.ToJavaScriptMilliseconds());
}
public static long ToJavaScriptMilliseconds(this DateTime dt)
{
return (long)dt
.ToUniversalTime()
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.TotalMilliseconds;
}
}
}
\ No newline at end of file
namespace Cordova.Extension.Commands
{
public interface IBackgroundGeoLocation
{
void configure(string optionsString);
void start(string asd);
void stop();
void finish();
void onPaceChange(bool isMoving);
void setConfig(string config);
void getStationaryLocation();
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment