diff --git a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt index 2116ca483d..53c34fbd07 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt @@ -25,6 +25,7 @@ class TelegramApplication : Application(), OsmandHelperListener { lateinit var showLocationHelper: ShowLocationHelper private set lateinit var notificationHelper: NotificationHelper private set lateinit var osmandHelper: OsmandAidlHelper private set + lateinit var locationProvider: TelegramLocationProvider private set var telegramService: TelegramService? = null @@ -43,6 +44,7 @@ class TelegramApplication : Application(), OsmandHelperListener { shareLocationHelper = ShareLocationHelper(this) showLocationHelper = ShowLocationHelper(this) notificationHelper = NotificationHelper(this) + locationProvider = TelegramLocationProvider(this) if (settings.hasAnyChatToShareLocation() && AndroidUtils.isLocationPermissionAvailable(this)) { shareLocationHelper.startSharingLocation() diff --git a/OsmAnd-telegram/src/net/osmand/telegram/TelegramLocationProvider.kt b/OsmAnd-telegram/src/net/osmand/telegram/TelegramLocationProvider.kt new file mode 100644 index 0000000000..18bf39723a --- /dev/null +++ b/OsmAnd-telegram/src/net/osmand/telegram/TelegramLocationProvider.kt @@ -0,0 +1,490 @@ +package net.osmand.telegram + +import android.annotation.SuppressLint +import android.content.Context +import android.hardware.* +import android.location.GpsStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import net.osmand.PlatformUtil +import net.osmand.data.LatLon +import net.osmand.telegram.utils.AndroidUtils +import net.osmand.util.MapUtils +import java.util.* + +class TelegramLocationProvider(private val app: TelegramApplication) : SensorEventListener { + + private var lastTimeGPSLocationFixed: Long = 0 + private var gpsSignalLost: Boolean = false + + private var sensorRegistered = false + private val mGravs = FloatArray(3) + private val mGeoMags = FloatArray(3) + private var previousCorrectionValue = 360f + + internal var avgValSin = 0f + internal var avgValCos = 0f + internal var lastValSin = 0f + internal var lastValCos = 0f + private val previousCompassValuesA = FloatArray(50) + private val previousCompassValuesB = FloatArray(50) + private var previousCompassIndA = 0 + private var previousCompassIndB = 0 + private var inUpdateValue = false + + @get:Synchronized + var heading: Float? = null + private set + + // Current screen orientation + private var currentScreenOrientation: Int = 0 + + var lastKnownLocation: net.osmand.Location? = null + private set + + val gpsInfo = GPSInfo() + + private val locationListeners = ArrayList() + private val compassListeners = ArrayList() + private var gpsStatusListener: GpsStatus.Listener? = null + private val mRotationM = FloatArray(9) + private var agpsDataLastTimeDownloaded: Long = 0 + private val useMagneticFieldSensorCompass = false + + // note, passive provider is from API_LEVEL 8 but it is a constant, we can check for it. + // constant should not be changed in future + // LocationManager.PASSIVE_PROVIDER + // put passive provider to first place + // find location + val firstTimeRunDefaultLocation: net.osmand.Location? + @SuppressLint("MissingPermission") + get() { + if (!AndroidUtils.isLocationPermissionAvailable(app)) { + return null + } + val service = app.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val ps = service.getProviders(true) ?: return null + val providers = ArrayList(ps) + val passiveFirst = providers.indexOf("passive") + if (passiveFirst > -1) { + providers.add(0, providers.removeAt(passiveFirst)) + } + for (provider in providers) { + val location = convertLocation(service.getLastKnownLocation(provider)) + if (location != null) { + return location + } + } + return null + } + + + private val gpsListener = object : LocationListener { + override fun onLocationChanged(location: Location?) { + if (location != null) { + lastTimeGPSLocationFixed = location.time + } + setLocation(convertLocation(location)) + } + + override fun onProviderDisabled(provider: String) {} + + override fun onProviderEnabled(provider: String) {} + + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + } + private val networkListeners = LinkedList() + + val lastKnownLocationLatLon: LatLon? + get() = if (lastKnownLocation != null) { + LatLon(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + } else { + null + } + + interface TelegramLocationListener { + fun updateLocation(location: net.osmand.Location?) + } + + interface TelegramCompassListener { + fun updateCompassValue(value: Float) + } + + @SuppressLint("MissingPermission") + fun resumeAllUpdates() { + val service = app.getSystemService(Context.LOCATION_SERVICE) as LocationManager + if (app.isInternetConnectionAvailable) { + if (System.currentTimeMillis() - agpsDataLastTimeDownloaded > AGPS_TO_REDOWNLOAD) { + //force an updated check for internet connectivity here before destroying A-GPS-data + if (app.isInternetConnectionAvailable(true)) { + redownloadAGPS() + } + } + } + if (AndroidUtils.isLocationPermissionAvailable(app)) { + service.addGpsStatusListener(getGpsStatusListener(service)) + try { + service.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + GPS_TIMEOUT_REQUEST.toLong(), + GPS_DIST_REQUEST.toFloat(), + gpsListener + ) + } catch (e: IllegalArgumentException) { + Log.d(PlatformUtil.TAG, "GPS location provider not available") //$NON-NLS-1$ + } + + // try to always ask for network provide : it is faster way to find location + + val providers = service.getProviders(true) ?: return + for (provider in providers) { + if (provider == null || provider == LocationManager.GPS_PROVIDER) { + continue + } + try { + val networkListener = NetworkListener() + service.requestLocationUpdates( + provider, + GPS_TIMEOUT_REQUEST.toLong(), + GPS_DIST_REQUEST.toFloat(), + networkListener + ) + networkListeners.add(networkListener) + } catch (e: IllegalArgumentException) { + Log.d( + PlatformUtil.TAG, + "$provider location provider not available" + ) //$NON-NLS-1$ + } + + } + } + + registerOrUnregisterCompassListener(true) + } + + fun redownloadAGPS() { + try { + val service = app.getSystemService(Context.LOCATION_SERVICE) as LocationManager + service.sendExtraCommand(LocationManager.GPS_PROVIDER, "delete_aiding_data", null) + val bundle = Bundle() + service.sendExtraCommand("gps", "force_xtra_injection", bundle) + service.sendExtraCommand("gps", "force_time_injection", bundle) + agpsDataLastTimeDownloaded = System.currentTimeMillis() + } catch (e: Exception) { + agpsDataLastTimeDownloaded = 0L + e.printStackTrace() + } + + } + + private fun getGpsStatusListener(service: LocationManager): GpsStatus.Listener { + gpsStatusListener = object : GpsStatus.Listener { + private var gpsStatus: GpsStatus? = null + + @SuppressLint("MissingPermission") + override fun onGpsStatusChanged(event: Int) { + gpsStatus = service.getGpsStatus(gpsStatus) + updateGPSInfo(gpsStatus) + updateLocation(lastKnownLocation) + } + } + return gpsStatusListener!! + } + + private fun updateGPSInfo(s: GpsStatus?) { + var fixed = false + var n = 0 + var u = 0 + if (s != null) { + val iterator = s.satellites.iterator() + while (iterator.hasNext()) { + val g = iterator.next() + n++ + if (g.usedInFix()) { + u++ + fixed = true + } + } + } + gpsInfo.fixed = fixed + gpsInfo.foundSatellites = n + gpsInfo.usedSatellites = u + } + + fun updateScreenOrientation(orientation: Int) { + currentScreenOrientation = orientation + } + + fun addLocationListener(listener: TelegramLocationListener) { + if (!locationListeners.contains(listener)) { + locationListeners.add(listener) + } + } + + fun removeLocationListener(listener: TelegramLocationListener) { + locationListeners.remove(listener) + } + + fun addCompassListener(listener: TelegramCompassListener) { + if (!compassListeners.contains(listener)) { + compassListeners.add(listener) + } + } + + fun removeCompassListener(listener: TelegramCompassListener) { + compassListeners.remove(listener) + } + + @Synchronized + fun registerOrUnregisterCompassListener(register: Boolean) { + if (sensorRegistered && !register) { + Log.d(PlatformUtil.TAG, "Disable sensor") //$NON-NLS-1$ + (app.getSystemService(Context.SENSOR_SERVICE) as SensorManager).unregisterListener(this) + sensorRegistered = false + heading = null + } else if (!sensorRegistered && register) { + Log.d(PlatformUtil.TAG, "Enable sensor") //$NON-NLS-1$ + val sensorMgr = app.getSystemService(Context.SENSOR_SERVICE) as SensorManager + if (useMagneticFieldSensorCompass) { + var s: Sensor? = sensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + if (s == null || !sensorMgr.registerListener( + this, + s, + SensorManager.SENSOR_DELAY_UI + ) + ) { + Log.e(PlatformUtil.TAG, "Sensor accelerometer could not be enabled") + } + s = sensorMgr.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + if (s == null || !sensorMgr.registerListener( + this, + s, + SensorManager.SENSOR_DELAY_UI + ) + ) { + Log.e(PlatformUtil.TAG, "Sensor magnetic field could not be enabled") + } + } else { + val s = sensorMgr.getDefaultSensor(Sensor.TYPE_ORIENTATION) + if (s == null || !sensorMgr.registerListener( + this, + s, + SensorManager.SENSOR_DELAY_UI + ) + ) { + Log.e(PlatformUtil.TAG, "Sensor orientation could not be enabled") + } + } + sensorRegistered = true + } + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + + override fun onSensorChanged(event: SensorEvent) { + // Attention : sensor produces a lot of events & can hang the system + if (inUpdateValue) { + return + } + synchronized(this) { + if (!sensorRegistered) { + return + } + inUpdateValue = true + try { + var `val` = 0f + when (event.sensor.type) { + Sensor.TYPE_ACCELEROMETER -> System.arraycopy(event.values, 0, mGravs, 0, 3) + Sensor.TYPE_MAGNETIC_FIELD -> System.arraycopy(event.values, 0, mGeoMags, 0, 3) + Sensor.TYPE_ORIENTATION -> `val` = event.values[0] + else -> return + } + if (useMagneticFieldSensorCompass) { + if (mGravs != null && mGeoMags != null) { + val success = + SensorManager.getRotationMatrix(mRotationM, null, mGravs, mGeoMags) + if (!success) { + return + } + val orientation = SensorManager.getOrientation(mRotationM, FloatArray(3)) + `val` = Math.toDegrees(orientation[0].toDouble()).toFloat() + } else { + return + } + } + `val` = calcScreenOrientationCorrection(`val`) + `val` = calcGeoMagneticCorrection(`val`) + + val valRad = (`val` / 180f * Math.PI).toFloat() + lastValSin = Math.sin(valRad.toDouble()).toFloat() + lastValCos = Math.cos(valRad.toDouble()).toFloat() + + avgValSin = lastValSin + avgValCos = lastValCos + + updateCompassVal() + } finally { + inUpdateValue = false + } + } + } + + private fun calcGeoMagneticCorrection(value: Float): Float { + var res = value + if (previousCorrectionValue == 360f && lastKnownLocation != null) { + val l = lastKnownLocation + val gf = GeomagneticField( + l!!.latitude.toFloat(), l.longitude.toFloat(), l.altitude.toFloat(), + System.currentTimeMillis() + ) + previousCorrectionValue = gf.declination + } + if (previousCorrectionValue != 360f) { + res += previousCorrectionValue + } + return res + } + + private fun calcScreenOrientationCorrection(value: Float): Float { + var res = value + when (currentScreenOrientation) { + 1 -> res += 90f + 2 -> res += 180f + 3 -> res -= 90f + } + return res + } + + private fun filterCompassValue() { + if (heading == null && previousCompassIndA == 0) { + Arrays.fill(previousCompassValuesA, lastValSin) + Arrays.fill(previousCompassValuesB, lastValCos) + avgValSin = lastValSin + avgValCos = lastValCos + } else { + val l = previousCompassValuesA.size + previousCompassIndA = (previousCompassIndA + 1) % l + previousCompassIndB = (previousCompassIndB + 1) % l + // update average + avgValSin += (-previousCompassValuesA[previousCompassIndA] + lastValSin) / l + previousCompassValuesA[previousCompassIndA] = lastValSin + avgValCos += (-previousCompassValuesB[previousCompassIndB] + lastValCos) / l + previousCompassValuesB[previousCompassIndB] = lastValCos + } + } + + private fun updateCompassVal() { + heading = getAngle(avgValSin, avgValCos) + for (c in compassListeners) { + c.updateCompassValue(heading!!) + } + } + + private fun getAngle(sinA: Float, cosA: Float) = MapUtils.unifyRotationTo360( + (Math.atan2(sinA.toDouble(), cosA.toDouble()) * 180 / Math.PI).toFloat() + ) + + private fun updateLocation(loc: net.osmand.Location?) { + for (l in locationListeners) { + l.updateLocation(loc) + } + } + + private fun useOnlyGPS() = + System.currentTimeMillis() - lastTimeGPSLocationFixed < NOT_SWITCH_TO_NETWORK_WHEN_GPS_LOST_MS + + // Working with location checkListeners + private inner class NetworkListener : LocationListener { + + override fun onLocationChanged(location: Location) { + if (!useOnlyGPS()) { + setLocation(convertLocation(location)) + } + } + + override fun onProviderDisabled(provider: String) {} + + override fun onProviderEnabled(provider: String) {} + + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + } + + private fun stopLocationRequests() { + val service = app.getSystemService(Context.LOCATION_SERVICE) as LocationManager + service.removeGpsStatusListener(gpsStatusListener) + service.removeUpdates(gpsListener) + while (!networkListeners.isEmpty()) { + service.removeUpdates(networkListeners.poll()) + } + } + + fun pauseAllUpdates() { + stopLocationRequests() + registerOrUnregisterCompassListener(false) + } + + private fun setLocation(location: net.osmand.Location?) { + if (location == null) { + updateGPSInfo(null) + } + if (location != null) { + if (gpsSignalLost) { + gpsSignalLost = false + } + } + this.lastKnownLocation = location + + // Update information + updateLocation(this.lastKnownLocation) + } + + fun checkIfLastKnownLocationIsValid() { + val loc = lastKnownLocation + if (loc != null && System.currentTimeMillis() - loc.time > INTERVAL_TO_CLEAR_SET_LOCATION) { + setLocation(null) + } + } + + class GPSInfo { + var foundSatellites = 0 + var usedSatellites = 0 + var fixed = false + } + + companion object { + + private const val INTERVAL_TO_CLEAR_SET_LOCATION = 30 * 1000 + + private const val GPS_TIMEOUT_REQUEST = 0 + private const val GPS_DIST_REQUEST = 0 + private const val NOT_SWITCH_TO_NETWORK_WHEN_GPS_LOST_MS = 12000 + private const val AGPS_TO_REDOWNLOAD = 16L * 60 * 60 * 1000 // 16 hours + + fun convertLocation(l: Location?): net.osmand.Location? { + if (l == null) { + return null + } + val r = net.osmand.Location(l.provider) + r.latitude = l.latitude + r.longitude = l.longitude + r.time = l.time + if (l.hasAccuracy()) { + r.accuracy = l.accuracy + } + if (l.hasSpeed()) { + r.speed = l.speed + } + if (l.hasAltitude()) { + r.altitude = l.altitude + } + if (l.hasBearing()) { + r.bearing = l.bearing + } + return r + } + } +} diff --git a/OsmAnd-telegram/src/net/osmand/telegram/ui/MainActivity.kt b/OsmAnd-telegram/src/net/osmand/telegram/ui/MainActivity.kt index 02de7ff700..cf99430efe 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/ui/MainActivity.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/ui/MainActivity.kt @@ -147,9 +147,14 @@ class MainActivity : AppCompatActivity(), TelegramListener, ActionButtonsListene invalidateOptionsMenu() updateTitle() - if (settings.hasAnyChatToShareLocation() && !AndroidUtils.isLocationPermissionAvailable(this)) { + app.locationProvider.checkIfLastKnownLocationIsValid() + + if (AndroidUtils.isLocationPermissionAvailable(this)) { + app.locationProvider.resumeAllUpdates() + } else { requestLocationPermission() - } else if (settings.hasAnyChatToShowOnMap() && osmandHelper.isOsmandNotInstalled()) { + } + if (settings.hasAnyChatToShowOnMap() && osmandHelper.isOsmandNotInstalled()) { showOsmandMissingDialog() } } @@ -158,6 +163,8 @@ class MainActivity : AppCompatActivity(), TelegramListener, ActionButtonsListene super.onPause() telegramHelper.listener = null + app.locationProvider.pauseAllUpdates() + paused = true } @@ -365,12 +372,16 @@ class MainActivity : AppCompatActivity(), TelegramListener, ActionButtonsListene override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (grantResults.isEmpty()) { + return + } when (requestCode) { PERMISSION_REQUEST_LOCATION -> { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (settings.hasAnyChatToShareLocation()) { app.shareLocationHelper.startSharingLocation() } + app.locationProvider.resumeAllUpdates() } else { settings.stopSharingLocationToChats() app.shareLocationHelper.stopSharingLocation()