You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mapbox-sdk/Unity/Location/DeviceLocationProviderAndro...

421 lines
12 KiB

1 year ago
namespace Mapbox.Unity.Location
{
using UnityEngine;
using System.Collections;
using System.Globalization;
using System;
using System.IO;
using System.Text;
using Mapbox.Utils;
public class DeviceLocationProviderAndroidNative : AbstractLocationProvider, IDisposable
{
[SerializeField]
[Tooltip("Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")]
public float _desiredAccuracyInMeters = 5.0f;
/// <summary>
/// The minimum distance (measured in meters) a device must move laterally before location is updated.
/// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener)
/// </summary>
[SerializeField]
[Tooltip("The minimum distance (measured in meters) a device must move laterally before location is updated. Higher values like 500 imply less overhead.")]
float _updateDistanceInMeters = 0.0f;
/// <summary>
/// The minimum time interval between location updates, in milliseconds.
/// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener)
/// </summary>
[SerializeField]
[Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")]
long _updateTimeInMilliSeconds = 1000;
private WaitForSeconds _wait1sec;
private WaitForSeconds _wait5sec;
private WaitForSeconds _wait60sec;
/// <summary>polls location provider only at the requested update intervall to reduce load</summary>
private WaitForSeconds _waitUpdateTime;
private bool _disposed;
private static object _lock = new object();
private Coroutine _pollLocation;
private AndroidJavaObject _activityContext = null;
private AndroidJavaObject _gpsInstance;
private AndroidJavaObject _sensorInstance;
~DeviceLocationProviderAndroidNative() { Dispose(false); }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposeManagedResources)
{
if (!_disposed)
{
if (disposeManagedResources)
{
shutdown();
}
_disposed = true;
}
}
private void shutdown()
{
try
{
lock (_lock)
{
if (null != _gpsInstance)
{
_gpsInstance.Call("stopLocationListeners");
_gpsInstance.Dispose();
_gpsInstance = null;
}
if (null != _sensorInstance)
{
_sensorInstance.Call("stopSensorListeners");
_sensorInstance.Dispose();
_sensorInstance = null;
}
}
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
protected virtual void OnDestroy() { shutdown(); }
protected virtual void OnDisable() { shutdown(); }
protected virtual void Awake()
{
// safe measures to not run when disabled or not selected as location provider
if (!enabled) { return; }
if (!transform.gameObject.activeInHierarchy) { return; }
_wait1sec = new WaitForSeconds(1);
_wait5sec = new WaitForSeconds(5);
_wait60sec = new WaitForSeconds(60);
// throttle if entered update intervall is unreasonably low
_waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f);
_currentLocation.IsLocationServiceEnabled = false;
_currentLocation.IsLocationServiceInitializing = true;
if (Application.platform == RuntimePlatform.Android)
{
getActivityContext();
getGpsInstance(true);
getSensorInstance();
if (_pollLocation == null)
{
_pollLocation = StartCoroutine(locationRoutine());
}
}
}
private void getActivityContext()
{
using (AndroidJavaClass activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
_activityContext = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
}
if (null == _activityContext)
{
Debug.LogError("Could not get UnityPlayer activity");
return;
}
}
private void getGpsInstance(bool showToastMessages = false)
{
if (null == _activityContext) { return; }
using (AndroidJavaClass androidGps = new AndroidJavaClass("com.mapbox.android.unity.AndroidGps"))
{
if (null == androidGps)
{
Debug.LogError("Could not get class 'AndroidGps'");
return;
}
_gpsInstance = androidGps.CallStatic<AndroidJavaObject>("instance", _activityContext);
if (null == _gpsInstance)
{
Debug.LogError("Could not get 'AndroidGps' instance");
return;
}
_activityContext.Call("runOnUiThread", new AndroidJavaRunnable(() => { _gpsInstance.Call("showMessage", "starting location listeners"); }));
_gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds);
}
}
private void getSensorInstance()
{
if (null == _activityContext) { return; }
using (AndroidJavaClass androidSensors = new AndroidJavaClass("com.mapbox.android.unity.AndroidSensors"))
{
if (null == androidSensors)
{
Debug.LogError("Could not get class 'AndroidSensors'");
return;
}
_sensorInstance = androidSensors.CallStatic<AndroidJavaObject>("instance", _activityContext);
if (null == _sensorInstance)
{
Debug.LogError("Could not get 'AndroidSensors' instance");
return;
}
_sensorInstance.Call("startSensorListeners");
}
}
private IEnumerator locationRoutine()
{
while (true)
{
// couldn't get player activity, wait and retry
if (null == _activityContext)
{
SendLocation(_currentLocation);
yield return _wait60sec;
getActivityContext();
continue;
}
// couldn't get gps plugin instance, wait and retry
if (null == _gpsInstance)
{
SendLocation(_currentLocation);
yield return _wait60sec;
getGpsInstance();
continue;
}
// update device orientation
if (null != _sensorInstance)
{
_currentLocation.DeviceOrientation = _sensorInstance.Call<float>("getOrientation");
}
bool locationServiceAvailable = _gpsInstance.Call<bool>("getIsLocationServiceAvailable");
_currentLocation.IsLocationServiceEnabled = locationServiceAvailable;
// app might have been started with location OFF but switched on after start
// check from time to time
if (!locationServiceAvailable)
{
_currentLocation.IsLocationServiceInitializing = true;
_currentLocation.Accuracy = 0;
_currentLocation.HasGpsFix = false;
_currentLocation.SatellitesInView = 0;
_currentLocation.SatellitesUsed = 0;
SendLocation(_currentLocation);
_gpsInstance.Call("stopLocationListeners");
yield return _wait5sec;
_gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds);
yield return _wait1sec;
continue;
}
// if we got till here it means location services are running
_currentLocation.IsLocationServiceInitializing = false;
try
{
AndroidJavaObject locNetwork = _gpsInstance.Get<AndroidJavaObject>("lastKnownLocationNetwork");
AndroidJavaObject locGps = _gpsInstance.Get<AndroidJavaObject>("lastKnownLocationGps");
// easy cases: neither or either gps location or network location available
if (null == locGps & null == locNetwork) { populateCurrentLocation(null); }
if (null != locGps && null == locNetwork) { populateCurrentLocation(locGps); }
if (null == locGps && null != locNetwork) { populateCurrentLocation(locNetwork); }
// gps- and network location available: figure out which one to use
if (null != locGps && null != locNetwork) { populateWithBetterLocation(locGps, locNetwork); }
_currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow);
SendLocation(_currentLocation);
}
catch (Exception ex)
{
Debug.LogErrorFormat("GPS plugin error: " + ex.ToString());
}
yield return _waitUpdateTime;
}
}
private void populateCurrentLocation(AndroidJavaObject location)
{
if (null == location)
{
_currentLocation.IsLocationUpdated = false;
_currentLocation.IsUserHeadingUpdated = false;
return;
}
double lat = location.Call<double>("getLatitude");
double lng = location.Call<double>("getLongitude");
Utils.Vector2d newLatLng = new Utils.Vector2d(lat, lng);
bool coordinatesUpdated = !newLatLng.Equals(_currentLocation.LatitudeLongitude);
_currentLocation.LatitudeLongitude = newLatLng;
float newAccuracy = location.Call<float>("getAccuracy");
bool accuracyUpdated = newAccuracy != _currentLocation.Accuracy;
_currentLocation.Accuracy = newAccuracy;
// divide by 1000. Android returns milliseconds, we work with seconds
long newTimestamp = location.Call<long>("getTime") / 1000;
bool timestampUpdated = newTimestamp != _currentLocation.Timestamp;
_currentLocation.Timestamp = newTimestamp;
string newProvider = location.Call<string>("getProvider");
bool providerUpdated = newProvider != _currentLocation.Provider;
_currentLocation.Provider = newProvider;
bool hasBearing = location.Call<bool>("hasBearing");
// only evalute bearing when location object actually has a bearing
// Android populates bearing (which is not equal to device orientation)
// only when the device is moving.
// Otherwise it is set to '0.0'
// https://developer.android.com/reference/android/location/Location.html#getBearing()
// We don't want that when we rotate a map according to the direction
// thes user is moving, thus don't update 'heading' with '0.0'
if (!hasBearing)
{
_currentLocation.IsUserHeadingUpdated = false;
}
else
{
float newHeading = location.Call<float>("getBearing");
_currentLocation.IsUserHeadingUpdated = newHeading != _currentLocation.UserHeading;
_currentLocation.UserHeading = newHeading;
}
float? newSpeed = location.Call<float>("getSpeed");
bool speedUpdated = newSpeed != _currentLocation.SpeedMetersPerSecond;
_currentLocation.SpeedMetersPerSecond = newSpeed;
// flag location as updated if any of below conditions evaluates to true
// Debug.LogFormat("coords:{0} acc:{1} time:{2} speed:{3}", coordinatesUpdated, accuracyUpdated, timestampUpdated, speedUpdated);
_currentLocation.IsLocationUpdated =
providerUpdated
|| coordinatesUpdated
|| accuracyUpdated
|| timestampUpdated
|| speedUpdated;
// Un-comment if required. Throws a warning right now.
//bool networkEnabled = _gpsInstance.Call<bool>("getIsNetworkEnabled");
bool gpsEnabled = _gpsInstance.Call<bool>("getIsGpsEnabled");
if (!gpsEnabled)
{
_currentLocation.HasGpsFix = null;
_currentLocation.SatellitesInView = null;
_currentLocation.SatellitesUsed = null;
}
else
{
_currentLocation.HasGpsFix = _gpsInstance.Get<bool>("hasGpsFix");
_currentLocation.SatellitesInView = _gpsInstance.Get<int>("satellitesInView");
_currentLocation.SatellitesUsed = _gpsInstance.Get<int>("satellitesUsedInFix");
}
}
/// <summary>
/// If GPS and network location are available use the newer/better one
/// </summary>
/// <param name="locGps"></param>
/// <param name="locNetwork"></param>
private void populateWithBetterLocation(AndroidJavaObject locGps, AndroidJavaObject locNetwork)
{
// check which location is fresher
long timestampGps = locGps.Call<long>("getTime");
long timestampNet = locNetwork.Call<long>("getTime");
if (timestampGps > timestampNet)
{
populateCurrentLocation(locGps);
return;
}
// check which location has better accuracy
float accuracyGps = locGps.Call<float>("getAccuracy");
float accuracyNet = locNetwork.Call<float>("getAccuracy");
if (accuracyGps < accuracyNet)
{
populateCurrentLocation(locGps);
return;
}
// default to network
populateCurrentLocation(locNetwork);
}
#if UNITY_ANDROID
private string time2str(AndroidJavaObject loc)
{
long time = loc.Call<long>("getTime");
DateTime dtPlugin = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromMilliseconds(time));
return dtPlugin.ToString("yyyyMMdd HHmmss");
}
private string loc2str(AndroidJavaObject loc)
{
if (null == loc) { return "loc: NULL"; }
try
{
double lat = loc.Call<double>("getLatitude");
double lng = loc.Call<double>("getLongitude");
return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000} / {1:0.00000000}", lat, lng);
}
catch (Exception ex)
{
return ex.ToString();
}
}
#endif
}
}