|
|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using GeoSus.Client;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
@@ -16,6 +16,292 @@ namespace Subsystems
|
|
|
|
|
Running,
|
|
|
|
|
Failed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Position source backend. Selectable at runtime via the GPS overlay
|
|
|
|
|
/// "Source" button so the user can recover when one path misbehaves on
|
|
|
|
|
/// their phone:
|
|
|
|
|
/// Auto - JNI: subscribe to gps + network, pick most recent fix.
|
|
|
|
|
/// GpsOnly - JNI: subscribe to gps only (network's frequent indoor
|
|
|
|
|
/// fixes don't drown out the slower-but-precise gps fix).
|
|
|
|
|
/// NetworkOnly - JNI: subscribe to network only (cell tower / WiFi).
|
|
|
|
|
/// Useful indoors when no satellite lock is possible.
|
|
|
|
|
/// UnityInput - Unity's Input.location wrapper. Verified to hang on
|
|
|
|
|
/// Mi 9T / A20e (which is why JNI exists), but works on
|
|
|
|
|
/// newer Android where the JNI streaming-callbacks path
|
|
|
|
|
/// silently doesn't fire (MIUI/HyperOS battery saver,
|
|
|
|
|
/// approximate-vs-precise permission split, minDistance
|
|
|
|
|
/// gating on stationary phones).
|
|
|
|
|
/// EditorWasd - WASD-driven simulated position. Available regardless
|
|
|
|
|
/// of testMode flag so desktop builds and editor sessions
|
|
|
|
|
/// can navigate the map without real GPS.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public enum PositionSource
|
|
|
|
|
{
|
|
|
|
|
Auto,
|
|
|
|
|
GpsOnly,
|
|
|
|
|
NetworkOnly,
|
|
|
|
|
UnityInput,
|
|
|
|
|
EditorWasd,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Bridges android.location.LocationListener to managed code. The method
|
|
|
|
|
/// names here must match Java's LocationListener interface exactly so
|
|
|
|
|
/// AndroidJavaProxy's reflection dispatcher can find them.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal class AndroidLocationProxy : AndroidJavaProxy
|
|
|
|
|
{
|
|
|
|
|
public AndroidLocationProvider Owner { get; set; }
|
|
|
|
|
public AndroidLocationProxy() : base("android.location.LocationListener") { }
|
|
|
|
|
|
|
|
|
|
// Called by Android each time a new fix arrives from the registered provider.
|
|
|
|
|
public void onLocationChanged(AndroidJavaObject location)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (location == null) return;
|
|
|
|
|
double lat = location.Call<double>("getLatitude");
|
|
|
|
|
double lon = location.Call<double>("getLongitude");
|
|
|
|
|
long t = location.Call<long>("getTime");
|
|
|
|
|
string provider = "";
|
|
|
|
|
try { provider = location.Call<string>("getProvider"); } catch { }
|
|
|
|
|
// Streaming callbacks are LIVE (never cached). The cached path
|
|
|
|
|
// calls UpdateLocation directly with isCached=true.
|
|
|
|
|
Owner?.UpdateLocation(lat, lon, t, provider, isCached: false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GPS-JNI] onLocationChanged failed: " + ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Required by the LocationListener interface even if we don't use them.
|
|
|
|
|
// Missing methods cause java.lang.AbstractMethodError at runtime.
|
|
|
|
|
public void onStatusChanged(string provider, int status, AndroidJavaObject extras) { }
|
|
|
|
|
public void onProviderEnabled(string provider) { }
|
|
|
|
|
public void onProviderDisabled(string provider) { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Direct wrapper around android.location.LocationManager via JNI, used as
|
|
|
|
|
/// a replacement for Unity's Input.location on Android when the user picks
|
|
|
|
|
/// Auto/GpsOnly/NetworkOnly. Subscribed providers are configurable so the
|
|
|
|
|
/// position-source picker can rewire live without restart.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal class AndroidLocationProvider
|
|
|
|
|
{
|
|
|
|
|
private AndroidJavaObject _activity;
|
|
|
|
|
private AndroidJavaObject _locationManager;
|
|
|
|
|
private AndroidLocationProxy _gpsListener;
|
|
|
|
|
private AndroidLocationProxy _networkListener;
|
|
|
|
|
private double _lat, _lon;
|
|
|
|
|
private long _lastTimeMillis;
|
|
|
|
|
private long _lastLiveTimeMillis; // Time of most recent NON-cached fix.
|
|
|
|
|
private bool _hasFix;
|
|
|
|
|
private bool _hasLiveFix; // True once any streaming callback fired.
|
|
|
|
|
private string _activeProvider = "";
|
|
|
|
|
|
|
|
|
|
// Captured at Initialize() so the diagnostic can report
|
|
|
|
|
// "GPS provider DISABLED, only network enabled" etc.
|
|
|
|
|
private bool _gpsProviderEnabled;
|
|
|
|
|
private bool _networkProviderEnabled;
|
|
|
|
|
private bool _gpsLastKnownExists;
|
|
|
|
|
private bool _networkLastKnownExists;
|
|
|
|
|
private string _enabledProvidersList = "";
|
|
|
|
|
|
|
|
|
|
// Subscription scope - set in Initialize, used in Shutdown to know
|
|
|
|
|
// which listeners we registered.
|
|
|
|
|
private bool _subscribedGps;
|
|
|
|
|
private bool _subscribedNetwork;
|
|
|
|
|
|
|
|
|
|
public bool HasFix => _hasFix;
|
|
|
|
|
public bool HasLiveFix => _hasLiveFix;
|
|
|
|
|
public long LastLiveTimeMillis => _lastLiveTimeMillis;
|
|
|
|
|
public long LastTimeMillis => _lastTimeMillis;
|
|
|
|
|
public double Lat => _lat;
|
|
|
|
|
public double Lon => _lon;
|
|
|
|
|
public string ActiveProvider => _activeProvider;
|
|
|
|
|
public bool GpsProviderEnabled => _gpsProviderEnabled;
|
|
|
|
|
public bool NetworkProviderEnabled => _networkProviderEnabled;
|
|
|
|
|
public bool GpsLastKnownExists => _gpsLastKnownExists;
|
|
|
|
|
public bool NetworkLastKnownExists => _networkLastKnownExists;
|
|
|
|
|
public string EnabledProvidersList => _enabledProvidersList;
|
|
|
|
|
public bool SubscribedGps => _subscribedGps;
|
|
|
|
|
public bool SubscribedNetwork => _subscribedNetwork;
|
|
|
|
|
|
|
|
|
|
public bool Initialize(out string error, bool useGps, bool useNetwork)
|
|
|
|
|
{
|
|
|
|
|
error = "";
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
|
|
|
|
{
|
|
|
|
|
_activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
|
|
|
|
|
}
|
|
|
|
|
if (_activity == null) { error = "no current activity"; return false; }
|
|
|
|
|
|
|
|
|
|
_locationManager = _activity.Call<AndroidJavaObject>("getSystemService", "location");
|
|
|
|
|
if (_locationManager == null) { error = "getSystemService(\"location\") returned null"; return false; }
|
|
|
|
|
|
|
|
|
|
// Capture provider enable state up front so the diagnostic
|
|
|
|
|
// can distinguish "provider disabled at OS level" from
|
|
|
|
|
// "provider enabled but produced no fix yet".
|
|
|
|
|
_gpsProviderEnabled = SafeIsProviderEnabled("gps");
|
|
|
|
|
_networkProviderEnabled = SafeIsProviderEnabled("network");
|
|
|
|
|
_enabledProvidersList = SafeGetEnabledProviders();
|
|
|
|
|
|
|
|
|
|
Debug.Log($"[GPS-JNI] init useGps={useGps} useNetwork={useNetwork} gps enabled={_gpsProviderEnabled} network enabled={_networkProviderEnabled} all enabled=[{_enabledProvidersList}]");
|
|
|
|
|
|
|
|
|
|
// Try cached last-known fixes from the providers we're about
|
|
|
|
|
// to subscribe to. If the OS already knows where we are
|
|
|
|
|
// (e.g. from another app that recently used GPS), we get a
|
|
|
|
|
// fix at zero cost and zero wait time. Tagged isCached so
|
|
|
|
|
// the diagnostic can mark them and we know we still need
|
|
|
|
|
// to wait for a streaming callback.
|
|
|
|
|
if (useNetwork) TryLastKnown("network", out _networkLastKnownExists);
|
|
|
|
|
if (useGps) TryLastKnown("gps", out _gpsLastKnownExists);
|
|
|
|
|
|
|
|
|
|
_subscribedGps = useGps;
|
|
|
|
|
_subscribedNetwork = useNetwork;
|
|
|
|
|
|
|
|
|
|
if (useGps) _gpsListener = new AndroidLocationProxy { Owner = this };
|
|
|
|
|
if (useNetwork) _networkListener = new AndroidLocationProxy { Owner = this };
|
|
|
|
|
|
|
|
|
|
// requestLocationUpdates must be called on a thread with a
|
|
|
|
|
// Looper. Use the Activity's UI thread, which always has one.
|
|
|
|
|
// minTime=1000ms, minDistance=0f - we want updates on every
|
|
|
|
|
// fix the OS produces. Previously this was 1f which gated
|
|
|
|
|
// out updates from a stationary phone (MIUI/newer Android
|
|
|
|
|
// are stricter about this and that's the suspected cause of
|
|
|
|
|
// "via gps (cached)" sticking forever).
|
|
|
|
|
_activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
|
|
|
|
|
{
|
|
|
|
|
if (useGps)
|
|
|
|
|
{
|
|
|
|
|
try { _locationManager.Call("requestLocationUpdates", "gps", 1000L, 0f, _gpsListener); }
|
|
|
|
|
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] gps subscribe failed: " + ex.Message); }
|
|
|
|
|
}
|
|
|
|
|
if (useNetwork)
|
|
|
|
|
{
|
|
|
|
|
try { _locationManager.Call("requestLocationUpdates", "network", 1000L, 0f, _networkListener); }
|
|
|
|
|
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] network subscribe failed: " + ex.Message); }
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
error = "JNI init exception: " + ex.Message;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TryLastKnown(string provider, out bool nonNullReturned)
|
|
|
|
|
{
|
|
|
|
|
nonNullReturned = false;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var loc = _locationManager.Call<AndroidJavaObject>("getLastKnownLocation", provider);
|
|
|
|
|
if (loc != null)
|
|
|
|
|
{
|
|
|
|
|
nonNullReturned = true;
|
|
|
|
|
double lat = loc.Call<double>("getLatitude");
|
|
|
|
|
double lon = loc.Call<double>("getLongitude");
|
|
|
|
|
long t = loc.Call<long>("getTime");
|
|
|
|
|
UpdateLocation(lat, lon, t, provider, isCached: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"[GPS-JNI] getLastKnownLocation({provider}) failed: " + ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SafeIsProviderEnabled(string provider)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return _locationManager.Call<bool>("isProviderEnabled", provider);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"[GPS-JNI] isProviderEnabled({provider}) failed: " + ex.Message);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build a comma-separated list of currently-enabled providers via
|
|
|
|
|
// LocationManager.getProviders(true). We iterate the returned
|
|
|
|
|
// java.util.List by index because AndroidJavaObject does not
|
|
|
|
|
// implement IEnumerable.
|
|
|
|
|
string SafeGetEnabledProviders()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var list = _locationManager.Call<AndroidJavaObject>("getProviders", true);
|
|
|
|
|
if (list == null) return "";
|
|
|
|
|
int size = list.Call<int>("size");
|
|
|
|
|
var parts = new System.Text.StringBuilder();
|
|
|
|
|
for (int i = 0; i < size; i++)
|
|
|
|
|
{
|
|
|
|
|
var name = list.Call<string>("get", i);
|
|
|
|
|
if (i > 0) parts.Append(",");
|
|
|
|
|
parts.Append(name);
|
|
|
|
|
}
|
|
|
|
|
return parts.ToString();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GPS-JNI] getProviders failed: " + ex.Message);
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void UpdateLocation(double lat, double lon, long timeMillis, string provider, bool isCached)
|
|
|
|
|
{
|
|
|
|
|
// Ignore older fixes if a newer one is already in hand. This lets
|
|
|
|
|
// both gps + network listeners feed us without ping-ponging
|
|
|
|
|
// between stale and fresh data.
|
|
|
|
|
if (timeMillis < _lastTimeMillis) return;
|
|
|
|
|
_lat = lat;
|
|
|
|
|
_lon = lon;
|
|
|
|
|
_lastTimeMillis = timeMillis;
|
|
|
|
|
// Active-provider name carries cached/live state in the diagnostic
|
|
|
|
|
// banner so the user can see at a glance whether streaming has
|
|
|
|
|
// kicked in or we're still on the initial cached snapshot.
|
|
|
|
|
_activeProvider = (provider ?? "") + (isCached ? " (cached)" : "");
|
|
|
|
|
_hasFix = true;
|
|
|
|
|
if (!isCached)
|
|
|
|
|
{
|
|
|
|
|
_hasLiveFix = true;
|
|
|
|
|
_lastLiveTimeMillis = timeMillis;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Shutdown()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_locationManager != null)
|
|
|
|
|
{
|
|
|
|
|
if (_gpsListener != null) _locationManager.Call("removeUpdates", _gpsListener);
|
|
|
|
|
if (_networkListener != null) _locationManager.Call("removeUpdates", _networkListener);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GPS-JNI] Shutdown failed: " + ex.Message);
|
|
|
|
|
}
|
|
|
|
|
_gpsListener = null;
|
|
|
|
|
_networkListener = null;
|
|
|
|
|
_locationManager = null;
|
|
|
|
|
_activity = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
public static class PositonExtensions
|
|
|
|
|
{
|
|
|
|
|
public static Position ToLocal(this Position position, Position center)
|
|
|
|
|
@@ -51,34 +337,283 @@ namespace Subsystems
|
|
|
|
|
private GameObject _player;
|
|
|
|
|
private bool _testMode;
|
|
|
|
|
|
|
|
|
|
// PlayerPrefs key for the user's chosen position source. Persists
|
|
|
|
|
// across app restarts so a user who flipped to UnityInput because
|
|
|
|
|
// their phone hated the JNI path doesn't have to flip again every
|
|
|
|
|
// launch.
|
|
|
|
|
private const string PrefsSourceKey = "PositionSource_v1";
|
|
|
|
|
private PositionSource _currentSource = PositionSource.Auto;
|
|
|
|
|
|
|
|
|
|
// When the multi-client editor test mode picks a non-host bot as
|
|
|
|
|
// active, we need the host's WASD path to NOT also move. Set true
|
|
|
|
|
// by GameManager when active slot != 0.
|
|
|
|
|
public bool SuppressWasd = false;
|
|
|
|
|
|
|
|
|
|
private GPSState _GPSState = GPSState.Uninitialized;
|
|
|
|
|
private float _speed = 0.00001f;
|
|
|
|
|
private Position _mapCenter;
|
|
|
|
|
private CoroutineHost _coroutineHost = new CoroutineHost();
|
|
|
|
|
private CoroutineHost _coroutineHost;
|
|
|
|
|
|
|
|
|
|
private int _gpsRetryCount = 0;
|
|
|
|
|
private const int _maxGpsRetries = 5;
|
|
|
|
|
private float _lastPositionSendTime;
|
|
|
|
|
private const float _positionKeepAliveSeconds = 1.0f;
|
|
|
|
|
|
|
|
|
|
// Diagnostic state. We capture *why* GPS init failed so the UI can
|
|
|
|
|
// surface it to the user without requiring logcat. Older Android
|
|
|
|
|
// phones (Mi 9T, A20e) hit silent failure modes that are impossible
|
|
|
|
|
// to distinguish from "still warming up" without this.
|
|
|
|
|
private string _lastGpsError = "";
|
|
|
|
|
private float _gpsInitStartTime = -1f;
|
|
|
|
|
// Bump from the original 20s. Cold-start GPS on older Android can
|
|
|
|
|
// easily exceed 20s indoors or under cloud cover - by the time the
|
|
|
|
|
// user notices nothing is happening, we've already given up.
|
|
|
|
|
private const int _gpsInitTimeoutSeconds = 60;
|
|
|
|
|
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
// JNI-backed location provider, used for Auto/GpsOnly/NetworkOnly.
|
|
|
|
|
// UnityInput uses Input.location instead and leaves this null.
|
|
|
|
|
private AndroidLocationProvider _androidProvider;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/// <summary>Last known GPS position (for CreateLobby centre point)</summary>
|
|
|
|
|
public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null;
|
|
|
|
|
|
|
|
|
|
/// <summary>Current GPS state machine value (debug/diagnostic).</summary>
|
|
|
|
|
public string GpsStateName => _GPSState.ToString();
|
|
|
|
|
|
|
|
|
|
/// <summary>Last GPS error reason captured during init (empty if none).</summary>
|
|
|
|
|
public string LastGpsError => _lastGpsError ?? "";
|
|
|
|
|
|
|
|
|
|
/// <summary>Retry count out of max (debug/diagnostic).</summary>
|
|
|
|
|
public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}";
|
|
|
|
|
|
|
|
|
|
/// <summary>Currently selected position source (for UI cycle button).</summary>
|
|
|
|
|
public PositionSource CurrentSource => _currentSource;
|
|
|
|
|
|
|
|
|
|
/// <summary>Display name for the current source (for UI label).</summary>
|
|
|
|
|
public string CurrentSourceName
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
switch (_currentSource)
|
|
|
|
|
{
|
|
|
|
|
case PositionSource.Auto: return "Auto (GPS+Net)";
|
|
|
|
|
case PositionSource.GpsOnly: return "GPS only";
|
|
|
|
|
case PositionSource.NetworkOnly: return "Network only";
|
|
|
|
|
case PositionSource.UnityInput: return "Unity Input";
|
|
|
|
|
case PositionSource.EditorWasd: return "WASD";
|
|
|
|
|
default: return _currentSource.ToString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Human-readable one-line GPS status for on-screen overlay. Designed
|
|
|
|
|
/// to be visible without ADB so users can self-diagnose permission
|
|
|
|
|
/// vs. timeout vs. device-disabled vs. running-but-no-fix-yet.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string GpsDiagnostic
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_currentSource == PositionSource.EditorWasd)
|
|
|
|
|
{
|
|
|
|
|
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
|
|
|
|
|
return "WASD: waiting for map center";
|
|
|
|
|
return $"WASD lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (_GPSState)
|
|
|
|
|
{
|
|
|
|
|
case GPSState.Uninitialized:
|
|
|
|
|
return "Uninitialized (will start on first lobby action)";
|
|
|
|
|
case GPSState.Initializing:
|
|
|
|
|
{
|
|
|
|
|
float elapsed = _gpsInitStartTime >= 0 ? Time.time - _gpsInitStartTime : 0;
|
|
|
|
|
string providers = "";
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
if (_androidProvider != null && !string.IsNullOrEmpty(_androidProvider.EnabledProvidersList))
|
|
|
|
|
providers = $" providers=[{_androidProvider.EnabledProvidersList}]";
|
|
|
|
|
#endif
|
|
|
|
|
return $"Initializing ({elapsed:F1}s / max {_gpsInitTimeoutSeconds}s){providers}";
|
|
|
|
|
}
|
|
|
|
|
case GPSState.Running:
|
|
|
|
|
{
|
|
|
|
|
string suffix = "";
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
if (_androidProvider != null)
|
|
|
|
|
{
|
|
|
|
|
string p = _androidProvider.ActiveProvider;
|
|
|
|
|
if (!string.IsNullOrEmpty(p)) suffix = " via " + p;
|
|
|
|
|
// Show how stale the most recent fix is (ms-level
|
|
|
|
|
// resolution) so "stuck on cached" is obvious at
|
|
|
|
|
// a glance: "via gps (cached) [no live, 47s old]".
|
|
|
|
|
if (!_androidProvider.HasLiveFix)
|
|
|
|
|
{
|
|
|
|
|
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
|
|
|
|
|
long ageMs = now - _androidProvider.LastTimeMillis;
|
|
|
|
|
if (_androidProvider.LastTimeMillis > 0 && ageMs > 0)
|
|
|
|
|
suffix += $" [no live, {ageMs / 1000}s old]";
|
|
|
|
|
else
|
|
|
|
|
suffix += " [no live]";
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
|
|
|
|
|
long ageMs = now - _androidProvider.LastLiveTimeMillis;
|
|
|
|
|
if (ageMs > 5000) suffix += $" [live {ageMs / 1000}s old]";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
|
|
|
|
|
return "Running but no fix yet (waiting for satellites)" + suffix;
|
|
|
|
|
return $"Running lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}" + suffix;
|
|
|
|
|
}
|
|
|
|
|
case GPSState.Failed:
|
|
|
|
|
return $"Failed: {(_lastGpsError ?? "unknown")} (retries {GpsRetryProgress})";
|
|
|
|
|
default:
|
|
|
|
|
return "?";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
|
|
|
|
|
{
|
|
|
|
|
_gameClient = gameClient;
|
|
|
|
|
_player = player;
|
|
|
|
|
_testMode = testMode;
|
|
|
|
|
// CoroutineHost needs a MonoBehaviour on a real GameObject
|
|
|
|
|
var hostGO = new UnityEngine.GameObject("_CoroutineHost");
|
|
|
|
|
UnityEngine.Object.DontDestroyOnLoad(hostGO);
|
|
|
|
|
_coroutineHost = hostGO.AddComponent<CoroutineHost>();
|
|
|
|
|
|
|
|
|
|
// Restore the user's last picked source. Default depends on
|
|
|
|
|
// platform: editor defaults to EditorWasd (no GPS hardware in
|
|
|
|
|
// editor anyway); device defaults to Auto.
|
|
|
|
|
string saved = PlayerPrefs.GetString(PrefsSourceKey, "");
|
|
|
|
|
if (!string.IsNullOrEmpty(saved) && Enum.TryParse(saved, out PositionSource parsed))
|
|
|
|
|
{
|
|
|
|
|
_currentSource = parsed;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
_currentSource = PositionSource.EditorWasd;
|
|
|
|
|
#else
|
|
|
|
|
_currentSource = PositionSource.Auto;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legacy testMode flag forces EditorWasd. New code paths should
|
|
|
|
|
// use SwitchPositionSource(EditorWasd) instead, but we keep the
|
|
|
|
|
// old behavior for backward compatibility with the inspector flag.
|
|
|
|
|
if (_testMode) _currentSource = PositionSource.EditorWasd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Called from OnSceneLoaded when Client.unity loads so the
|
|
|
|
|
/// Player capsule (which lives in Client.unity) can be wired at runtime.</summary>
|
|
|
|
|
public void SetPlayerObject(GameObject player) { _player = player; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Switch the active position source backend live. Tears down the
|
|
|
|
|
/// current backend's listeners (JNI proxies, Input.location), resets
|
|
|
|
|
/// the state machine, and kicks off init for the new source. Persists
|
|
|
|
|
/// the choice to PlayerPrefs.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void SwitchPositionSource(PositionSource newSource)
|
|
|
|
|
{
|
|
|
|
|
if (_currentSource == newSource) return;
|
|
|
|
|
Debug.Log($"[GPS] SwitchPositionSource {_currentSource} -> {newSource}");
|
|
|
|
|
|
|
|
|
|
// Tear down whatever's running.
|
|
|
|
|
ShutdownCurrentBackend();
|
|
|
|
|
|
|
|
|
|
_currentSource = newSource;
|
|
|
|
|
PlayerPrefs.SetString(PrefsSourceKey, newSource.ToString());
|
|
|
|
|
PlayerPrefs.Save();
|
|
|
|
|
|
|
|
|
|
_GPSState = GPSState.Uninitialized;
|
|
|
|
|
_gpsRetryCount = 0;
|
|
|
|
|
_lastGpsError = "";
|
|
|
|
|
_gpsInitStartTime = -1f;
|
|
|
|
|
// Don't clear _currentPosition - the user has presumably been
|
|
|
|
|
// playing somewhere. Map markers/avatar position can stay until
|
|
|
|
|
// the next fix arrives from the new source.
|
|
|
|
|
|
|
|
|
|
EnsureGPSStarted();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Cycle through the available sources for tap-to-cycle UI.</summary>
|
|
|
|
|
public void CycleNextPositionSource()
|
|
|
|
|
{
|
|
|
|
|
var values = (PositionSource[])Enum.GetValues(typeof(PositionSource));
|
|
|
|
|
int idx = Array.IndexOf(values, _currentSource);
|
|
|
|
|
var next = values[(idx + 1) % values.Length];
|
|
|
|
|
SwitchPositionSource(next);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ShutdownCurrentBackend()
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
if (_androidProvider != null)
|
|
|
|
|
{
|
|
|
|
|
_androidProvider.Shutdown();
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
// Stop Unity Input.location too, in case it was running.
|
|
|
|
|
try { Input.location.Stop(); } catch { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Kick off GPS initialization if it hasn't started yet. Safe to call
|
|
|
|
|
/// repeatedly. Hosts must call this from the lobby setup screen so
|
|
|
|
|
/// that by the time they click "Create Lobby" we have a real GPS
|
|
|
|
|
/// fix to use as the play-area center, instead of falling back to
|
|
|
|
|
/// the hardcoded coordinates.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void EnsureGPSStarted()
|
|
|
|
|
{
|
|
|
|
|
if (_currentSource == PositionSource.EditorWasd) return;
|
|
|
|
|
if (_coroutineHost == null) return;
|
|
|
|
|
// Allow tapping "Create Lobby" again (or any caller of this
|
|
|
|
|
// method) to retry from Failed up to _maxGpsRetries times.
|
|
|
|
|
if (_GPSState == GPSState.Uninitialized)
|
|
|
|
|
{
|
|
|
|
|
_coroutineHost.StartCoroutine(InitiallizeGPS());
|
|
|
|
|
}
|
|
|
|
|
else if (_GPSState == GPSState.Failed && _gpsRetryCount < _maxGpsRetries)
|
|
|
|
|
{
|
|
|
|
|
_gpsRetryCount++;
|
|
|
|
|
_coroutineHost.StartCoroutine(InitiallizeGPS());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public void positionCheck()
|
|
|
|
|
{
|
|
|
|
|
var state = _gameClient?.CurrentLobbyState;
|
|
|
|
|
if (state == null || state.Phase != GamePhase.Playing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_gameClient.CurrentLobbyState.Phase == GamePhase.Playing)
|
|
|
|
|
if (_currentSource == PositionSource.EditorWasd)
|
|
|
|
|
{
|
|
|
|
|
if (_testMode)
|
|
|
|
|
if (_currentPosition == new Position(0, 0))
|
|
|
|
|
{
|
|
|
|
|
if (state.MapData == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (_currentPosition == null || _currentPosition == new Position(0, 0))
|
|
|
|
|
{
|
|
|
|
|
//Init blok
|
|
|
|
|
_currentPosition = _gameClient.CurrentLobbyState.MapData.Center;
|
|
|
|
|
_mapCenter = _gameClient.CurrentLobbyState.MapData.Center;
|
|
|
|
|
_currentPosition = state.MapData.Center;
|
|
|
|
|
_mapCenter = state.MapData.Center;
|
|
|
|
|
_lastSentPosition = _currentPosition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!SuppressWasd)
|
|
|
|
|
TestPlayerPosition();
|
|
|
|
|
else
|
|
|
|
|
TrySendCurrentPosition(); // keep-alive only
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
@@ -93,54 +628,84 @@ namespace Subsystems
|
|
|
|
|
}
|
|
|
|
|
else if (_GPSState == GPSState.Running)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_currentPosition != _lastSentPosition)
|
|
|
|
|
{
|
|
|
|
|
_gameClient.UpdatePosition(_currentPosition);
|
|
|
|
|
_lastSentPosition = _currentPosition;
|
|
|
|
|
_player.transform.position = _currentPosition.ToLocalVector3(_mapCenter);
|
|
|
|
|
_player.transform.rotation = Quaternion.Euler(0, (float)CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), _currentPosition.ToLocalVector3(_mapCenter)), 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log(ex);
|
|
|
|
|
}
|
|
|
|
|
EnsureMapCenter();
|
|
|
|
|
TrySendCurrentPosition();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("GPS failed, trying again...");
|
|
|
|
|
if (_gpsRetryCount < _maxGpsRetries)
|
|
|
|
|
{
|
|
|
|
|
_gpsRetryCount++;
|
|
|
|
|
_GPSState = GPSState.Uninitialized;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("GPS unavailable after max retries. Using last known position.");
|
|
|
|
|
// Keep _GPSState = Failed so we stop retrying
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (NullReferenceException ex) { Debug.Log(ex); }
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureMapCenter()
|
|
|
|
|
{
|
|
|
|
|
if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var md = _gameClient?.CurrentLobbyState?.MapData;
|
|
|
|
|
if (md != null)
|
|
|
|
|
_mapCenter = md.Center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TrySendCurrentPosition()
|
|
|
|
|
{
|
|
|
|
|
bool moved = _currentPosition != _lastSentPosition;
|
|
|
|
|
bool keepAliveDue = (Time.time - _lastPositionSendTime) >= _positionKeepAliveSeconds;
|
|
|
|
|
if (!moved && !keepAliveDue)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var previous = _lastSentPosition;
|
|
|
|
|
_gameClient.UpdatePosition(_currentPosition);
|
|
|
|
|
_lastSentPosition = _currentPosition;
|
|
|
|
|
_lastPositionSendTime = Time.time;
|
|
|
|
|
|
|
|
|
|
if (_player == null || (_mapCenter.Lat == 0 && _mapCenter.Lon == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
|
|
|
|
|
_player.transform.position = localCurrent;
|
|
|
|
|
|
|
|
|
|
if (previous.Lat == 0 && previous.Lon == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var heading = CalculateHeading(previous.ToLocalVector3(_mapCenter), localCurrent);
|
|
|
|
|
if (heading.HasValue)
|
|
|
|
|
_player.transform.rotation = Quaternion.Euler(0, (float)heading.Value, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TestPlayerPosition()
|
|
|
|
|
{
|
|
|
|
|
double x = Input.GetAxis("Horizontal");
|
|
|
|
|
double y = Input.GetAxis("Vertical");
|
|
|
|
|
Debug.Log($"Input: {x}, {y}");
|
|
|
|
|
_currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
|
|
|
|
|
Debug.Log($"Current Position: {_currentPosition.Lat}, {_currentPosition.Lon}");
|
|
|
|
|
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
|
|
|
|
|
Debug.Log($"Local Current Position: {localCurrent}");
|
|
|
|
|
var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
|
|
|
|
|
if (heading != null)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log($"Heading: {heading}");
|
|
|
|
|
if (_player != null)
|
|
|
|
|
_player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
|
|
|
|
|
}
|
|
|
|
|
if (_player != null)
|
|
|
|
|
_player.transform.position = localCurrent;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_currentPosition != _lastSentPosition)
|
|
|
|
|
{
|
|
|
|
|
_gameClient.UpdatePosition(_currentPosition);
|
|
|
|
|
_lastSentPosition = _currentPosition;
|
|
|
|
|
}
|
|
|
|
|
TrySendCurrentPosition();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
@@ -150,105 +715,212 @@ namespace Subsystems
|
|
|
|
|
}
|
|
|
|
|
private double? CalculateHeading(Vector3 first, Vector3 second)
|
|
|
|
|
{
|
|
|
|
|
double? heading = null;
|
|
|
|
|
if ((first - second).magnitude == 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x == second.x && first.z < second.z)
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x == second.x && first.z > second.z)
|
|
|
|
|
{
|
|
|
|
|
return 180;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x > second.x && first.z == second.z)
|
|
|
|
|
{
|
|
|
|
|
return 270;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x < second.x && first.z == second.z)
|
|
|
|
|
{
|
|
|
|
|
return 90;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x < second.x && first.z < second.z)
|
|
|
|
|
{
|
|
|
|
|
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
|
|
|
return (heading * 180) / Math.PI;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x < second.x && first.z > second.z)
|
|
|
|
|
{
|
|
|
|
|
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
|
|
|
return (heading * 180) / Math.PI + 180;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x > second.x && first.z < second.z)
|
|
|
|
|
{
|
|
|
|
|
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
|
|
|
return (heading * 180) / Math.PI - 90;
|
|
|
|
|
}
|
|
|
|
|
else if (first.x > second.x && first.z > second.z)
|
|
|
|
|
{
|
|
|
|
|
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
|
|
|
return (heading * 180) / Math.PI - 90;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if ((first - second).magnitude < 0.0001f) return null;
|
|
|
|
|
float dx = second.x - first.x;
|
|
|
|
|
float dz = second.z - first.z;
|
|
|
|
|
float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg;
|
|
|
|
|
if (heading < 0) heading += 360f;
|
|
|
|
|
return heading;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
IEnumerator InitiallizeGPS()
|
|
|
|
|
{
|
|
|
|
|
_GPSState = GPSState.Initializing;
|
|
|
|
|
_gpsInitStartTime = Time.time;
|
|
|
|
|
_lastGpsError = "";
|
|
|
|
|
|
|
|
|
|
#if UNITY_ANDROID
|
|
|
|
|
// Request fine location permission if not already granted.
|
|
|
|
|
// On Android 12+ a "precise" toggle exists separately from coarse,
|
|
|
|
|
// but Unity's FineLocation request covers both for our purposes.
|
|
|
|
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.FineLocation);
|
|
|
|
|
// Wait up to 10 seconds for user to respond to the permission dialog
|
|
|
|
|
float waited = 0f;
|
|
|
|
|
while (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation) && waited < 10f)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
waited += 0.5f;
|
|
|
|
|
}
|
|
|
|
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
|
|
|
|
|
{
|
|
|
|
|
_lastGpsError = "Permission denied (fine location)";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
// Choose subscription scope based on selected source. UnityInput
|
|
|
|
|
// skips JNI entirely and falls through to the Input.location path
|
|
|
|
|
// below (the same path iOS / editor use).
|
|
|
|
|
if (_currentSource == PositionSource.Auto ||
|
|
|
|
|
_currentSource == PositionSource.GpsOnly ||
|
|
|
|
|
_currentSource == PositionSource.NetworkOnly)
|
|
|
|
|
{
|
|
|
|
|
bool useGps = (_currentSource != PositionSource.NetworkOnly);
|
|
|
|
|
bool useNetwork = (_currentSource != PositionSource.GpsOnly);
|
|
|
|
|
|
|
|
|
|
if (_androidProvider != null)
|
|
|
|
|
{
|
|
|
|
|
_androidProvider.Shutdown();
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
}
|
|
|
|
|
_androidProvider = new AndroidLocationProvider();
|
|
|
|
|
if (!_androidProvider.Initialize(out var initError, useGps, useNetwork))
|
|
|
|
|
{
|
|
|
|
|
_lastGpsError = "Native LocationManager failed: " + initError;
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fast-fail if neither subscribed provider is enabled at OS
|
|
|
|
|
// level. Waiting 60s for fixes from disabled providers is
|
|
|
|
|
// pointless - tell the user immediately what's wrong.
|
|
|
|
|
bool anyUsableEnabled =
|
|
|
|
|
(useGps && _androidProvider.GpsProviderEnabled) ||
|
|
|
|
|
(useNetwork && _androidProvider.NetworkProviderEnabled);
|
|
|
|
|
if (!anyUsableEnabled)
|
|
|
|
|
{
|
|
|
|
|
string which = useGps && useNetwork ? "gps + network"
|
|
|
|
|
: useGps ? "gps"
|
|
|
|
|
: "network";
|
|
|
|
|
_lastGpsError = $"{which} provider DISABLED at OS level. Open Settings > Location and switch it ON. Or tap [Source] to try a different backend.";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_androidProvider.Shutdown();
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for the first fix (cached or live).
|
|
|
|
|
int maxWaitJni = _gpsInitTimeoutSeconds;
|
|
|
|
|
while (!_androidProvider.HasFix && maxWaitJni > 0)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(1);
|
|
|
|
|
maxWaitJni--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_androidProvider.HasFix)
|
|
|
|
|
{
|
|
|
|
|
string enabled = _androidProvider.EnabledProvidersList ?? "";
|
|
|
|
|
string gpsState = _androidProvider.GpsProviderEnabled ? "ON" : "OFF";
|
|
|
|
|
string netState = _androidProvider.NetworkProviderEnabled ? "ON" : "OFF";
|
|
|
|
|
string lastKnown = $"lastKnown[gps={(_androidProvider.GpsLastKnownExists ? "yes" : "no")}, net={(_androidProvider.NetworkLastKnownExists ? "yes" : "no")}]";
|
|
|
|
|
|
|
|
|
|
_lastGpsError = $"Timeout {_gpsInitTimeoutSeconds}s on {_currentSource}. enabled=[{enabled}] gps={gpsState} net={netState} {lastKnown}. Try [Source] cycle to switch backends.";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_androidProvider.Shutdown();
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
|
|
|
|
|
_GPSState = GPSState.Running;
|
|
|
|
|
_gpsRetryCount = 0;
|
|
|
|
|
_coroutineHost.StartCoroutine(AndroidGPSService());
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// _currentSource == UnityInput on Android: fall through to the
|
|
|
|
|
// Input.location path below. This is the recovery path for
|
|
|
|
|
// newer Android phones where JNI's streaming-callbacks don't
|
|
|
|
|
// fire (MIUI/HyperOS background restrictions, approximate-vs-
|
|
|
|
|
// precise permission, minDistance gating on stationary phones).
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// iOS / editor / non-Android / Android-with-UnityInput-source:
|
|
|
|
|
// use Unity's Input.location.
|
|
|
|
|
if (!Input.location.isEnabledByUser)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("Location not enabled on device or app does not have permission to access location");
|
|
|
|
|
_lastGpsError = "Location services not enabled by user";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
// Starts the location service.
|
|
|
|
|
|
|
|
|
|
float desiredAccuracyInMeters = 10f;
|
|
|
|
|
float updateDistanceInMeters = 10f;
|
|
|
|
|
float desiredAccuracyInMeters = 5f;
|
|
|
|
|
float updateDistanceInMeters = 1f;
|
|
|
|
|
|
|
|
|
|
Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
|
|
|
|
|
|
|
|
|
|
// Waits until the location service initializes
|
|
|
|
|
int maxWait = 20;
|
|
|
|
|
int maxWait = _gpsInitTimeoutSeconds;
|
|
|
|
|
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(1);
|
|
|
|
|
maxWait--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the service didn't initialize in 20 seconds this cancels location service use.
|
|
|
|
|
if (maxWait < 1)
|
|
|
|
|
{
|
|
|
|
|
_lastGpsError = $"Timed out after {_gpsInitTimeoutSeconds}s waiting for first fix (try moving outdoors, or tap [Source] to try a different backend)";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
Debug.LogError("Timed out");
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
_GPSState = GPSState.Running;
|
|
|
|
|
yield return _coroutineHost.StartCoroutine(GPSService());
|
|
|
|
|
}
|
|
|
|
|
IEnumerator GPSService()
|
|
|
|
|
{
|
|
|
|
|
// Check if the user has location service enabled.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If the connection failed this cancels location service use.
|
|
|
|
|
if (Input.location.status == LocationServiceStatus.Failed)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("Unable to determine device location");
|
|
|
|
|
_lastGpsError = "Unity Input.location reported Failed status";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// If the connection succeeded, this retrieves the device's current location and displays it in the Console window.
|
|
|
|
|
_currentPosition = new Position(Input.location.lastData.latitude, Input.location.lastData.longitude);
|
|
|
|
|
Debug.Log("Location: " + Input.location.lastData.latitude + " " + Input.location.lastData.longitude + " " + Input.location.lastData.altitude + " " + Input.location.lastData.horizontalAccuracy + " " + Input.location.lastData.timestamp);
|
|
|
|
|
yield return new WaitForSeconds(5f);
|
|
|
|
|
|
|
|
|
|
_GPSState = GPSState.Running;
|
|
|
|
|
_gpsRetryCount = 0;
|
|
|
|
|
_coroutineHost.StartCoroutine(GPSService());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stops the location service if there is no need to query location updates continuously.
|
|
|
|
|
yield return _coroutineHost.StartCoroutine(GPSService());
|
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Mirrors the JNI provider's most recent fix into _currentPosition
|
|
|
|
|
/// every 0.5s so the rest of the game (which polls _currentPosition
|
|
|
|
|
/// indirectly via LastKnownPosition / TrySendCurrentPosition) keeps
|
|
|
|
|
/// working unchanged. Replaces GPSService on Android.
|
|
|
|
|
/// </summary>
|
|
|
|
|
IEnumerator AndroidGPSService()
|
|
|
|
|
{
|
|
|
|
|
while (_GPSState == GPSState.Running && _androidProvider != null)
|
|
|
|
|
{
|
|
|
|
|
if (_androidProvider.HasFix)
|
|
|
|
|
{
|
|
|
|
|
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
|
|
|
|
|
}
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Loop ended (state != Running or provider disposed). Clean up
|
|
|
|
|
// listeners so we don't leak across retries.
|
|
|
|
|
if (_androidProvider != null)
|
|
|
|
|
{
|
|
|
|
|
_androidProvider.Shutdown();
|
|
|
|
|
_androidProvider = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
IEnumerator GPSService()
|
|
|
|
|
{
|
|
|
|
|
while (_GPSState == GPSState.Running)
|
|
|
|
|
{
|
|
|
|
|
if (Input.location.status == LocationServiceStatus.Failed)
|
|
|
|
|
{
|
|
|
|
|
_lastGpsError = "Location service died after init (provider stopped)";
|
|
|
|
|
Debug.LogError("[GPS] " + _lastGpsError);
|
|
|
|
|
_GPSState = GPSState.Failed;
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep current GPS position fresh; sending is throttled in positionCheck().
|
|
|
|
|
var data = Input.location.lastData;
|
|
|
|
|
_currentPosition = new Position(data.latitude, data.longitude);
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|