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; /// /// 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) /// [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; /// /// 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) /// [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; /// polls location provider only at the requested update intervall to reduce load 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("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("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("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("getOrientation"); } bool locationServiceAvailable = _gpsInstance.Call("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("lastKnownLocationNetwork"); AndroidJavaObject locGps = _gpsInstance.Get("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("getLatitude"); double lng = location.Call("getLongitude"); Utils.Vector2d newLatLng = new Utils.Vector2d(lat, lng); bool coordinatesUpdated = !newLatLng.Equals(_currentLocation.LatitudeLongitude); _currentLocation.LatitudeLongitude = newLatLng; float newAccuracy = location.Call("getAccuracy"); bool accuracyUpdated = newAccuracy != _currentLocation.Accuracy; _currentLocation.Accuracy = newAccuracy; // divide by 1000. Android returns milliseconds, we work with seconds long newTimestamp = location.Call("getTime") / 1000; bool timestampUpdated = newTimestamp != _currentLocation.Timestamp; _currentLocation.Timestamp = newTimestamp; string newProvider = location.Call("getProvider"); bool providerUpdated = newProvider != _currentLocation.Provider; _currentLocation.Provider = newProvider; bool hasBearing = location.Call("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("getBearing"); _currentLocation.IsUserHeadingUpdated = newHeading != _currentLocation.UserHeading; _currentLocation.UserHeading = newHeading; } float? newSpeed = location.Call("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("getIsNetworkEnabled"); bool gpsEnabled = _gpsInstance.Call("getIsGpsEnabled"); if (!gpsEnabled) { _currentLocation.HasGpsFix = null; _currentLocation.SatellitesInView = null; _currentLocation.SatellitesUsed = null; } else { _currentLocation.HasGpsFix = _gpsInstance.Get("hasGpsFix"); _currentLocation.SatellitesInView = _gpsInstance.Get("satellitesInView"); _currentLocation.SatellitesUsed = _gpsInstance.Get("satellitesUsedInFix"); } } /// /// If GPS and network location are available use the newer/better one /// /// /// private void populateWithBetterLocation(AndroidJavaObject locGps, AndroidJavaObject locNetwork) { // check which location is fresher long timestampGps = locGps.Call("getTime"); long timestampNet = locNetwork.Call("getTime"); if (timestampGps > timestampNet) { populateCurrentLocation(locGps); return; } // check which location has better accuracy float accuracyGps = locGps.Call("getAccuracy"); float accuracyNet = locNetwork.Call("getAccuracy"); if (accuracyGps < accuracyNet) { populateCurrentLocation(locGps); return; } // default to network populateCurrentLocation(locNetwork); } #if UNITY_ANDROID private string time2str(AndroidJavaObject loc) { long time = loc.Call("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("getLatitude"); double lng = loc.Call("getLongitude"); return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000} / {1:0.00000000}", lat, lng); } catch (Exception ex) { return ex.ToString(); } } #endif } }