diff --git a/app/build.gradle b/app/build.gradle index f45bc9dd0..08d97d015 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,6 +123,7 @@ dependencies { kapt 'androidx.room:room-compiler:2.3.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.github.omadahealth:swipy:1.2.3' implementation 'de.cketti.library.changelog:ckchangelog:1.2.2' implementation 'com.google.android:flexbox:0.3.2' @@ -132,9 +133,9 @@ dependencies { implementation "com.mikepenz:fastadapter-commons:$fastadapterVersion" implementation "com.mikepenz:fastadapter-extensions-expandable:$fastadapterVersion" implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0' - implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0' - implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-locationlayer:0.4.0' - implementation 'com.mapzen.android:lost:3.0.4' + implementation('org.maplibre.gl:android-sdk:9.5.2') { + exclude group: 'com.google.android.gms' // no proprietary Google libraries + } // only added because of lint bug Timber 4.6.0 implementation 'com.jakewharton.timber:timber:4.7.0' diff --git a/app/src/main/java/de/grobox/transportr/AppModule.java b/app/src/main/java/de/grobox/transportr/AppModule.java index 766ad9010..4e862d716 100644 --- a/app/src/main/java/de/grobox/transportr/AppModule.java +++ b/app/src/main/java/de/grobox/transportr/AppModule.java @@ -27,7 +27,7 @@ import de.grobox.transportr.data.locations.LocationRepository; import de.grobox.transportr.data.searches.SearchesDao; import de.grobox.transportr.data.searches.SearchesRepository; -import de.grobox.transportr.map.GpsController; +import de.grobox.transportr.map.PositionController; import de.grobox.transportr.networks.TransportNetworkManager; import de.grobox.transportr.settings.SettingsManager; @@ -70,8 +70,8 @@ SearchesRepository searchesRepository(SearchesDao searchesDao, LocationDao locat } @Provides - GpsController gpsController() { - return new GpsController(application.getApplicationContext()); + PositionController gpsController() { + return new PositionController(application.getApplicationContext()); } } diff --git a/app/src/main/java/de/grobox/transportr/TransportrApplication.kt b/app/src/main/java/de/grobox/transportr/TransportrApplication.kt index 5e69126ca..539ceea37 100644 --- a/app/src/main/java/de/grobox/transportr/TransportrApplication.kt +++ b/app/src/main/java/de/grobox/transportr/TransportrApplication.kt @@ -20,7 +20,7 @@ package de.grobox.transportr import android.app.Application import com.mapbox.mapboxsdk.Mapbox -import com.mapbox.services.android.telemetry.MapboxTelemetry +import com.mapbox.mapboxsdk.WellKnownTileServer open class TransportrApplication : Application() { lateinit var component: AppComponent @@ -29,11 +29,7 @@ open class TransportrApplication : Application() { override fun onCreate() { super.onCreate() - Mapbox.getInstance( - applicationContext, - "pk.eyJ1IjoidG92b2s3IiwiYSI6ImNpeTA1OG82YjAwN3YycXA5cWJ6NThmcWIifQ.QpURhF9y7XBMLmWhELsOnw" - ) - MapboxTelemetry.getInstance().isTelemetryEnabled = false + Mapbox.getInstance(applicationContext) component = createComponent() } @@ -43,4 +39,4 @@ open class TransportrApplication : Application() { .appModule(AppModule(this)) .build() } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/grobox/transportr/locations/LocationLiveData.kt b/app/src/main/java/de/grobox/transportr/locations/LocationLiveData.kt deleted file mode 100644 index ec6df47fc..000000000 --- a/app/src/main/java/de/grobox/transportr/locations/LocationLiveData.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Transportr - * - * Copyright (c) 2013 - 2021 Torsten Grote - * - * This program is Free Software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.grobox.transportr.locations - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.annotation.SuppressLint -import android.content.Context -import android.location.Location -import androidx.annotation.RequiresPermission -import androidx.annotation.WorkerThread -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import com.mapbox.services.android.telemetry.location.LocationEngineListener -import com.mapbox.services.android.telemetry.location.LocationEnginePriority.BALANCED_POWER_ACCURACY -import com.mapbox.services.android.telemetry.location.LostLocationEngine -import de.grobox.transportr.locations.ReverseGeocoder.ReverseGeocoderCallback -import de.grobox.transportr.map.hasLocationProviders - - -class LocationLiveData(private val context: Context) : LiveData(), LocationEngineListener, ReverseGeocoderCallback { - - private val locationEngine: LostLocationEngine = LostLocationEngine(context) - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun observe(owner: LifecycleOwner, observer: Observer) { - super.observe(owner, observer) - } - - @SuppressLint("MissingPermission") - override fun onActive() { - super.onActive() - - if (hasLocationProviders(context)) { - locationEngine.priority = BALANCED_POWER_ACCURACY - locationEngine.interval = 5000 - locationEngine.activate() - locationEngine.addLocationEngineListener(this) - // work-around for https://github.com/mapbox/mapbox-plugins-android/issues/371 - locationEngine.requestLocationUpdates() - } else { - value = null - } - } - - override fun onInactive() { - super.onInactive() - locationEngine.removeLocationUpdates() - locationEngine.removeLocationEngineListener(this) - locationEngine.deactivate() - } - - @SuppressLint("MissingPermission") - override fun onConnected() { - locationEngine.requestLocationUpdates() - } - - override fun onLocationChanged(location: Location) { - locationEngine.removeLocationUpdates() - Thread { - val geoCoder = ReverseGeocoder(context, this) - geoCoder.findLocation(location) - }.start() - } - - @WorkerThread - override fun onLocationRetrieved(location: WrapLocation) { - postValue(location) - } - -} diff --git a/app/src/main/java/de/grobox/transportr/map/BaseMapFragment.kt b/app/src/main/java/de/grobox/transportr/map/BaseMapFragment.kt index 0c2c2fd23..4838aa2e8 100644 --- a/app/src/main/java/de/grobox/transportr/map/BaseMapFragment.kt +++ b/app/src/main/java/de/grobox/transportr/map/BaseMapFragment.kt @@ -20,14 +20,15 @@ package de.grobox.transportr.map import android.os.Bundle -import android.text.Html import android.text.method.LinkMovementMethod -import androidx.annotation.CallSuper -import androidx.annotation.LayoutRes import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import com.mapbox.mapboxsdk.camera.CameraUpdateFactory import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLngBounds @@ -43,10 +44,16 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { private lateinit var attribution: TextView protected var map: MapboxMap? = null protected var mapPadding: Int = 0 + protected var mapInset: MapPadding = MapPadding() @get:LayoutRes protected abstract val layout: Int + // Returns the Jawg url depending on the style given (jawg-streets by default) + // taken from https://www.jawg.io/docs/integration/maplibre-gl-android/simple-map/ + private fun makeStyleUrl(style: String = "jawg-streets") = + "${getString(R.string.jawg_styles_url) + style}.json?access-token=${getString(R.string.jawg_access_token)}" + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) @@ -64,7 +71,7 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { mapView.onCreate(savedInstanceState) mapView.getMapAsync(this) attribution.movementMethod = LinkMovementMethod.getInstance() - attribution.text = Html.fromHtml(getString(R.string.map_attribution, getString(R.string.map_attribution_improve))) + attribution.text = HtmlCompat.fromHtml(getString(R.string.map_attribution, getString(R.string.map_attribution_improve)), FROM_HTML_MODE_LEGACY) } override fun onStart() { @@ -78,9 +85,9 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { activity?.run { // work-around to force update map style after theme switching obtainStyledAttributes(intArrayOf(R.attr.mapStyle)).apply { - val mapStyle = getString(0) - if (mapStyle != null && mapboxMap.styleUrl != mapStyle) { - mapboxMap.setStyleUrl(mapStyle) + val mapStyle = getString(0)?.let { makeStyleUrl(it) } + if (mapStyle != null && mapboxMap.style?.uri != mapStyle) { + mapboxMap.setStyle(mapStyle) } recycle() } @@ -120,6 +127,8 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { protected open fun animateTo(latLng: LatLng?, zoom: Int) { if (latLng == null) return map?.let { map -> + val padding = mapInset + mapPadding + map.moveCamera(CameraUpdateFactory.paddingTo(padding.left.toDouble(), padding.top.toDouble(), padding.right.toDouble(), padding.bottom.toDouble())) val update = if (map.cameraPosition.zoom < zoom) CameraUpdateFactory.newLatLngZoom( latLng, zoom.toDouble() @@ -130,7 +139,8 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { protected open fun zoomToBounds(latLngBounds: LatLngBounds?, animate: Boolean) { if (latLngBounds == null) return - val update = CameraUpdateFactory.newLatLngBounds(latLngBounds, mapPadding) + val padding = mapInset + mapPadding + val update = CameraUpdateFactory.newLatLngBounds(latLngBounds, padding.left, padding.top, padding.right, padding.bottom) map?.let { map -> if (animate) { map.easeCamera(update) @@ -148,4 +158,23 @@ abstract class BaseMapFragment : TransportrFragment(), OnMapReadyCallback { zoomToBounds(latLngBounds, true) } + protected fun setPadding(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0) { + // store map padding to be retained even after CameraBoundsUpdates + // and update directly for subsequent camera updates in MapDrawer + mapInset = MapPadding(left, top, right, bottom) + map?.moveCamera(CameraUpdateFactory.paddingTo(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble())) + } + + data class MapPadding( + val left: Int = 0, + val top: Int = 0, + val right: Int = 0, + val bottom: Int = 0 + ) { + constructor(padding: DoubleArray) : this(padding[0].toInt(), padding[1].toInt(), padding[2].toInt(), padding[3].toInt()) + + operator fun plus(other: Int) = + MapPadding(left + other, top + other, right + other, bottom + other) + } + } diff --git a/app/src/main/java/de/grobox/transportr/map/GpsController.kt b/app/src/main/java/de/grobox/transportr/map/GpsController.kt deleted file mode 100644 index 6e9bf8ee8..000000000 --- a/app/src/main/java/de/grobox/transportr/map/GpsController.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Transportr - * - * Copyright (c) 2013 - 2021 Torsten Grote - * - * This program is Free Software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.grobox.transportr.map - - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import android.content.Context -import android.location.Location -import android.location.LocationManager -import androidx.annotation.WorkerThread -import de.grobox.transportr.AbstractManager -import de.grobox.transportr.locations.ReverseGeocoder -import de.grobox.transportr.locations.ReverseGeocoder.ReverseGeocoderCallback -import de.grobox.transportr.locations.WrapLocation -import de.grobox.transportr.map.GpsController.Companion.GPS_FIX_EXPIRY -import de.schildbach.pte.dto.Location.coord -import java.util.* -import java.util.concurrent.TimeUnit - -internal data class GpsState(var hasFix: Boolean, var isOld: Boolean, var isTracking: Boolean) - -internal class GpsController(val context: Context) : AbstractManager(), ReverseGeocoderCallback { - - companion object { - internal val GPS_FIX_EXPIRY = TimeUnit.SECONDS.toMillis(3) - } - - private val geoCoder = ReverseGeocoder(context, this) - - private val gpsState = MutableLiveData() - - private var location: Location? = null - private var lastLocation: Location? = null - private var wrapLocation: WrapLocation? = null - - init { - gpsState.value = GpsState(false, false, false) - } - - @WorkerThread - override fun onLocationRetrieved(location: WrapLocation) { - runOnUiThread { wrapLocation = location } - } - - fun setLocation(newLocation: Location?, useGeoCoder: Boolean) { - if (newLocation == null) return - - if (location == null || location!!.distanceTo(newLocation) > 50) { - // store location and last location - lastLocation = location - location = newLocation - - if (useGeoCoder) { - // check if we need to use the reverse geo coder - val isNew = wrapLocation.let { it == null || !it.isSamePlace(newLocation.latitude, newLocation.longitude) } - if (isNew) geoCoder.findLocation(newLocation) - } - } - // set new FAB state if location is recent - if (!newLocation.isOld()) { - updateGpsState(hasFix = true, isOld = false) - } - } - - internal fun updateGpsState(hasFix: Boolean? = null, isOld: Boolean? = null, isTracking: Boolean? = null) { - val newState = GpsState( - hasFix ?: gpsState.value!!.hasFix, - isOld ?: gpsState.value!!.isOld, - isTracking ?: gpsState.value!!.isTracking - ) - // only set a new value if it is different from the old - if (newState != gpsState.value) gpsState.value = newState - } - - fun getGpsState(): LiveData = gpsState - - fun getWrapLocation(): WrapLocation? { - if (wrapLocation == null) { - location?.let { return it.toWrapLocation() } - } - wrapLocation?.let { return it } - return null - } - -} - -fun Location.toWrapLocation(): WrapLocation? { - if (latitude == 0.0 && longitude == 0.0) return null - val loc = coord((latitude * 1E6).toInt(), (longitude * 1E6).toInt()) - return WrapLocation(loc) -} - -fun Location.isOld(): Boolean { - return Date().time > time + GPS_FIX_EXPIRY -} - -fun hasLocationProviders(context: Context): Boolean { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - val activeProviders = locationManager.getProviders(true) - - return activeProviders.size > 1 || (activeProviders.size == 1 && activeProviders[0] != "passive") -} diff --git a/app/src/main/java/de/grobox/transportr/map/GpsMapFragment.kt b/app/src/main/java/de/grobox/transportr/map/GpsMapFragment.kt index 002081046..6a009c181 100644 --- a/app/src/main/java/de/grobox/transportr/map/GpsMapFragment.kt +++ b/app/src/main/java/de/grobox/transportr/map/GpsMapFragment.kt @@ -21,193 +21,133 @@ package de.grobox.transportr.map import android.Manifest.permission.ACCESS_FINE_LOCATION import android.annotation.SuppressLint -import androidx.lifecycle.Observer import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.ColorStateList -import android.graphics.BlendMode -import android.graphics.BlendModeColorFilter import android.graphics.PorterDuff -import android.location.Location -import android.os.Build import android.os.Bundle -import android.os.CountDownTimer -import androidx.annotation.CallSuper -import androidx.annotation.RequiresPermission -import com.google.android.material.floatingactionbutton.FloatingActionButton -import androidx.core.content.ContextCompat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory.newLatLng -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory.newLatLngZoom -import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.geometry.LatLngBounds +import androidx.annotation.CallSuper +import androidx.core.content.ContextCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.mapbox.mapboxsdk.location.LocationComponent +import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions +import com.mapbox.mapboxsdk.location.LocationComponentOptions +import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener +import com.mapbox.mapboxsdk.location.modes.CameraMode +import com.mapbox.mapboxsdk.location.modes.RenderMode import com.mapbox.mapboxsdk.maps.MapboxMap -import com.mapbox.mapboxsdk.plugins.locationlayer.LocationLayerMode -import com.mapbox.mapboxsdk.plugins.locationlayer.LocationLayerPlugin -import com.mapbox.services.android.telemetry.location.LocationEngineListener -import com.mapbox.services.android.telemetry.location.LocationEnginePriority -import com.mapbox.services.android.telemetry.location.LostLocationEngine import de.grobox.transportr.R -import de.grobox.transportr.map.GpsController.Companion.GPS_FIX_EXPIRY +import de.grobox.transportr.map.GpsMapViewModel.GpsFabState +import de.grobox.transportr.map.PositionController.Companion.FIX_EXPIRY +import de.grobox.transportr.map.PositionController.PositionState import de.grobox.transportr.utils.Constants.REQUEST_LOCATION_PERMISSION -import java.util.concurrent.TimeUnit.SECONDS -abstract class GpsMapFragment : BaseMapFragment(), LocationEngineListener { +internal abstract class GpsMapFragment : BaseMapFragment() { - internal lateinit var gpsController: GpsController + internal abstract val viewModel: ViewModel - private var locationPlugin: LocationLayerPlugin? = null - private var locationEngine: LostLocationEngine? = null + private var locationComponent: LocationComponent? = null private lateinit var gpsFab: FloatingActionButton - protected open var useGeoCoder = false - companion object { const val LOCATION_ZOOM = 14 } - private val timer = object : CountDownTimer(Long.MAX_VALUE, GPS_FIX_EXPIRY) { - override fun onTick(millisUntilFinished: Long) { - val location = locationPlugin?.lastKnownLocation - if (location == null) gpsController.updateGpsState(hasFix = false) - else if (location.isOld()) { - gpsController.updateGpsState(isOld = true) - } - } - - override fun onFinish() {} - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val v = super.onCreateView(inflater, container, savedInstanceState) as View gpsFab = v.findViewById(R.id.gpsFab) gpsFab.setOnClickListener { onGpsFabClick() } + viewModel.gpsFabState.observe(viewLifecycleOwner) { state -> + // Floating GPS Action Button Style + val (iconColor, backgroundColor) = when (state) { + GpsFabState.TRACKING -> Pair( + ContextCompat.getColor(context, R.color.fabForegroundFollow), + ColorStateList.valueOf(ContextCompat.getColor(context, R.color.fabBackground)) + ) + GpsFabState.ENABLED -> Pair( + ContextCompat.getColor(context, R.color.fabForegroundMoved), + ColorStateList.valueOf(ContextCompat.getColor(context, R.color.fabBackgroundMoved)) + ) + else -> Pair( + ContextCompat.getColor(context, R.color.fabForegroundInitial), + ColorStateList.valueOf(ContextCompat.getColor(context, R.color.fabBackground)) + ) + } + gpsFab.drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN) + gpsFab.backgroundTintList = backgroundColor + } + return v } @CallSuper - @SuppressLint("MissingPermission") - override fun onStart() { - super.onStart() - locationPlugin?.onStart() - locationEngine?.let { - it.addLocationEngineListener(this) - // work-around for https://github.com/mapbox/mapbox-plugins-android/issues/371 - it.requestLocationUpdates() - } - timer.start() + override fun onMapReady(mapboxMap: MapboxMap) { + super.onMapReady(mapboxMap) + activateLocationComponent() } - @CallSuper - override fun onStop() { - locationPlugin?.onStop() - locationEngine?.let { - it.removeLocationEngineListener(this) - it.removeLocationUpdates() - } - super.onStop() - timer.cancel() - } + private fun activateLocationComponent() { + // Check if permissions are enabled and if not request + if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED) { - override fun onDestroyView() { - super.onDestroyView() - locationEngine?.deactivate() - } + locationComponent = map?.locationComponent + map?.getStyle { style -> + locationComponent?.apply { - @CallSuper - override fun onMapReady(mapboxMap: MapboxMap) { - super.onMapReady(mapboxMap) - enableLocationPlugin() + activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(LocationComponentOptions.builder(context).staleStateTimeout(FIX_EXPIRY).build()) + .useDefaultLocationEngine(false) + .build() + ) - locationEngine?.let { - if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED) { - gpsController.setLocation(it.lastLocation, useGeoCoder) - } - } + addOnCameraTrackingChangedListener(object : OnCameraTrackingChangedListener { + override fun onCameraTrackingDismissed() {} + + override fun onCameraTrackingChanged(currentMode: Int) { + viewModel.isCameraTracking.value = currentMode == CameraMode.TRACKING + } + }) - gpsController.getGpsState().observe(this, Observer { onNewGpsState(it!!) }) - map?.addOnScrollListener { - gpsController.getGpsState().value?.let { - if (it.isTracking) { - gpsController.updateGpsState(isTracking = false) + addOnLocationStaleListener { + viewModel.isPositionStale.value = it + } + + cameraMode = CameraMode.NONE + renderMode = RenderMode.COMPASS } - } - } - } - private fun enableLocationPlugin() { - // Check if permissions are enabled and if not request - if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED) { - // Create an instance of LOST location engine - initializeLocationEngine() + viewModel.positionController.position.observe(viewLifecycleOwner) { + locationComponent?.forceLocationUpdate(it) + } - if (locationPlugin == null && map != null) { - locationPlugin = LocationLayerPlugin(mapView, map!!, locationEngine, R.style.LocationLayer) - locationPlugin!!.setLocationLayerEnabled(LocationLayerMode.COMPASS) + viewModel.positionController.positionState.observe(viewLifecycleOwner) { + locationComponent?.isLocationComponentEnabled = when (it) { + PositionState.ENABLED -> true + else -> false + } + } } } else { requestPermission() } } - @RequiresPermission(ACCESS_FINE_LOCATION) - private fun initializeLocationEngine() { - locationEngine = LostLocationEngine(context) - locationEngine?.let { - it.priority = LocationEnginePriority.HIGH_ACCURACY - it.interval = SECONDS.toMillis(1).toInt() - it.smallestDisplacement = 0f - it.activate() - it.addLocationEngineListener(this) - } - } - - @SuppressLint("MissingPermission") - override fun onConnected() { - locationEngine?.requestLocationUpdates() - } - + @SuppressLint("MissingPermission") //todo: remove private fun onGpsFabClick() { - if (ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) { - Toast.makeText(context, R.string.permission_denied_gps, Toast.LENGTH_SHORT).show() - return - } - if (!hasLocationProviders(context)) { - Toast.makeText(context, R.string.warning_gps_off, Toast.LENGTH_SHORT).show() - return - } - val location = locationPlugin?.lastKnownLocation - if (location == null) { - Toast.makeText(context, R.string.warning_no_gps_fix, Toast.LENGTH_SHORT).show() - return - } - map?.let { map -> - val latLng = LatLng(location.latitude, location.longitude) - val update = if (map.cameraPosition.zoom < LOCATION_ZOOM) newLatLngZoom(latLng, LOCATION_ZOOM.toDouble()) else newLatLng(latLng) - map.easeCamera(update, 750) - gpsController.updateGpsState(isTracking = true) - } - } - - private fun onNewGpsState(gpsState: GpsState) { - // Floating GPS Action Button Style - var iconColor = ContextCompat.getColor(context, R.color.fabForegroundInitial) - var backgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.fabBackground)) - if (gpsState.hasFix && !gpsState.isOld && !gpsState.isTracking) { - iconColor = ContextCompat.getColor(context, R.color.fabForegroundMoved) - backgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.fabBackgroundMoved)) - } else if (gpsState.hasFix && !gpsState.isOld && gpsState.isTracking) { - iconColor = ContextCompat.getColor(context, R.color.fabForegroundFollow) + when (viewModel.positionController.positionState.value) { + PositionState.DENIED -> Toast.makeText(context, R.string.permission_denied_gps, Toast.LENGTH_SHORT).show() + PositionState.DISABLED -> Toast.makeText(context, R.string.warning_gps_off, Toast.LENGTH_SHORT).show() + PositionState.ENABLED -> { + locationComponent?.lastKnownLocation?.let { + map?.zoomToMyLocation() + } ?: Toast.makeText(context, R.string.warning_no_gps_fix, Toast.LENGTH_SHORT).show() + } } - gpsFab.drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN) - gpsFab.backgroundTintList = backgroundColor - - // Location Marker Icon Style - locationPlugin?.applyStyle(if (gpsState.isOld) R.style.LocationLayerOld else R.style.LocationLayer) } private fun requestPermission() { @@ -219,34 +159,19 @@ abstract class GpsMapFragment : BaseMapFragment(), LocationEngineListener { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (requestCode != REQUEST_LOCATION_PERMISSION) return if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) { - enableLocationPlugin() + viewModel.positionController.permissionGranted() + activateLocationComponent() } } - override fun onLocationChanged(location: Location) { - if (gpsController.getGpsState().value!!.isTracking) { - map?.animateCamera(newLatLng(LatLng(location.latitude, location.longitude))) - } - gpsController.setLocation(location, useGeoCoder) - } - - fun zoomToMyLocation() { - val location = getLastKnownLocation() ?: return - val latLng = LatLng(location.latitude, location.longitude) - val update = CameraUpdateFactory.newLatLngZoom(latLng, LOCATION_ZOOM.toDouble()) - map?.moveCamera(update) - } - - override fun animateTo(latLng: LatLng?, zoom: Int) { - gpsController.updateGpsState(isTracking = false) - super.animateTo(latLng, zoom) - } - - override fun zoomToBounds(latLngBounds: LatLngBounds?, animate: Boolean) { - gpsController.updateGpsState(isTracking = false) - super.zoomToBounds(latLngBounds, animate) + protected fun MapboxMap.zoomToMyLocation() { + locationComponent.setCameraMode( + CameraMode.TRACKING, 750, + if (cameraPosition.zoom < LOCATION_ZOOM) LOCATION_ZOOM.toDouble() else null, + null, null, null + ) } - protected fun getLastKnownLocation() = locationPlugin?.lastKnownLocation + protected fun getLastKnownLocation() = viewModel.positionController.position.value } diff --git a/app/src/main/java/de/grobox/transportr/map/GpsMapViewModel.kt b/app/src/main/java/de/grobox/transportr/map/GpsMapViewModel.kt new file mode 100644 index 000000000..fa5a754df --- /dev/null +++ b/app/src/main/java/de/grobox/transportr/map/GpsMapViewModel.kt @@ -0,0 +1,72 @@ +/* + * Transportr + * + * Copyright (c) 2013 - 2021 Torsten Grote + * + * This program is Free Software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.grobox.transportr.map + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.grobox.transportr.map.GpsMapViewModel.GpsFabState +import de.grobox.transportr.map.PositionController.PositionState + +internal interface GpsMapViewModel { + + val positionController: PositionController + + //todo: change to LiveData + val isCameraTracking: MutableLiveData + val isPositionStale: MutableLiveData + val gpsFabState: LiveData + + enum class GpsFabState { + DISABLED, + ENABLED, + TRACKING + } +} + +internal class GpsMapViewModelImpl(override val positionController: PositionController) : GpsMapViewModel { + override val isCameraTracking = MutableLiveData() + override val isPositionStale = MutableLiveData() + override val gpsFabState = MediatorLiveData().apply { + var state = PositionState.DISABLED + var isTracking = false + var isStale = false + fun update() { + value = when { + state == PositionState.DENIED || state == PositionState.DISABLED || isStale -> GpsFabState.DISABLED + state == PositionState.ENABLED && !isTracking -> GpsFabState.ENABLED + state == PositionState.ENABLED && isTracking -> GpsFabState.TRACKING + else -> value + } + } + addSource(positionController.positionState) { + state = it + update() + } + addSource(isCameraTracking) { + isTracking = it + update() + } + addSource(isPositionStale) { + isStale = it + update() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/grobox/transportr/map/MapActivity.java b/app/src/main/java/de/grobox/transportr/map/MapActivity.java index f8fd7c38d..6713762c1 100644 --- a/app/src/main/java/de/grobox/transportr/map/MapActivity.java +++ b/app/src/main/java/de/grobox/transportr/map/MapActivity.java @@ -53,6 +53,7 @@ import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN; +import static de.grobox.transportr.locations.WrapLocation.WrapType.GPS; import static de.grobox.transportr.trips.search.DirectionsActivity.ACTION_SEARCH; import static de.grobox.transportr.utils.Constants.WRAP_LOCATION; import static de.grobox.transportr.utils.IntentUtils.findDirections; @@ -65,7 +66,7 @@ public class MapActivity extends DrawerActivity implements LocationViewListener @Inject ViewModelProvider.Factory viewModelFactory; private MapViewModel viewModel; - private GpsController gpsController; + private PositionController positionController; private LocationView search; private BottomSheetBehavior bottomSheetBehavior; @@ -105,7 +106,7 @@ public void onSlide(@NonNull View bottomSheet, float slideOffset) { // get view model and observe data viewModel = new ViewModelProvider(this, viewModelFactory).get(MapViewModel.class); - gpsController = viewModel.getGpsController(); + positionController = viewModel.getPositionController(); viewModel.getTransportNetwork().observe(this, this::onTransportNetworkChanged); viewModel.getHome().observe(this, homeLocation -> search.setHomeLocation(homeLocation)); viewModel.getWork().observe(this, workLocation -> search.setWorkLocation(workLocation)); @@ -120,7 +121,7 @@ public void onSlide(@NonNull View bottomSheet, float slideOffset) { FloatingActionButton directionsFab = findViewById(R.id.directionsFab); directionsFab.setOnClickListener(view -> { - WrapLocation from = gpsController.getWrapLocation(); + WrapLocation from = new WrapLocation(GPS); //locationController.getWrapLocation(); //TODO!!! WrapLocation to = null; if (locationFragment != null && locationFragmentVisible()) { to = locationFragment.getLocation(); diff --git a/app/src/main/java/de/grobox/transportr/map/MapDrawer.kt b/app/src/main/java/de/grobox/transportr/map/MapDrawer.kt index d78f034f8..2df7ef747 100644 --- a/app/src/main/java/de/grobox/transportr/map/MapDrawer.kt +++ b/app/src/main/java/de/grobox/transportr/map/MapDrawer.kt @@ -53,8 +53,8 @@ internal abstract class MapDrawer(protected val context: Context) { protected fun zoomToBounds(map: MapboxMap, builder: LatLngBounds.Builder, animate: Boolean) { try { val latLngBounds = builder.build() - val padding = context.resources.getDimensionPixelSize(R.dimen.mapPadding) - val cameraUpdate = CameraUpdateFactory.newLatLngBounds(latLngBounds, padding) + val padding = BaseMapFragment.MapPadding(map.cameraPosition.padding) + context.resources.getDimensionPixelSize(R.dimen.mapPadding) + val cameraUpdate = CameraUpdateFactory.newLatLngBounds(latLngBounds, padding.left, padding.top, padding.right, padding.bottom) if (animate) { map.easeCamera(cameraUpdate, 750) } else { @@ -72,4 +72,4 @@ internal abstract class MapDrawer(protected val context: Context) { return iconFactory.fromBitmap(bitmap) } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/grobox/transportr/map/MapFragment.kt b/app/src/main/java/de/grobox/transportr/map/MapFragment.kt index 2fb1c8e7b..271046a14 100644 --- a/app/src/main/java/de/grobox/transportr/map/MapFragment.kt +++ b/app/src/main/java/de/grobox/transportr/map/MapFragment.kt @@ -46,30 +46,26 @@ import de.schildbach.pte.dto.NearbyLocationsResult import de.schildbach.pte.dto.NearbyLocationsResult.Status.OK import javax.inject.Inject -class MapFragment : GpsMapFragment(), LoaderCallbacks, OnMarkerClickListener { +internal class MapFragment : GpsMapFragment(), LoaderCallbacks, OnMarkerClickListener { @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: MapViewModel + override lateinit var viewModel: MapViewModel private lateinit var nearbyStationsDrawer: NearbyStationsDrawer private var selectedLocationMarker: Marker? = null - override var useGeoCoder: Boolean = true - override val layout: Int @LayoutRes get() = R.layout.fragment_map override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val v = super.onCreateView(inflater, container, savedInstanceState) - component.inject(this) - viewModel = ViewModelProvider(activity!!, viewModelFactory).get(MapViewModel::class.java) - viewModel.transportNetwork.observe(viewLifecycleOwner, Observer { onTransportNetworkChanged(it) }) - gpsController = viewModel.gpsController + + val v = super.onCreateView(inflater, container, savedInstanceState) + viewModel.transportNetwork.observe(viewLifecycleOwner) { onTransportNetworkChanged(it) } nearbyStationsDrawer = NearbyStationsDrawer(context) @@ -83,8 +79,8 @@ class MapFragment : GpsMapFragment(), LoaderCallbacks, On val args = NearbyLocationsLoader.getBundle(location, 0) LoaderManager.getInstance(this).initLoader(LOADER_NEARBY_STATIONS, args, this) - mapboxMap.addOnMapClickListener { viewModel.mapClicked.call() } - mapboxMap.addOnMapLongClickListener { point -> viewModel.selectLocation(WrapLocation(point)) } + mapboxMap.addOnMapClickListener { viewModel.mapClicked.call(); false } + mapboxMap.addOnMapLongClickListener { point -> viewModel.selectLocation(WrapLocation(point)); false } mapboxMap.setOnMarkerClickListener(this) if (viewModel.transportNetworkWasChanged || mapboxMap.isInitialPosition()) { @@ -105,14 +101,14 @@ class MapFragment : GpsMapFragment(), LoaderCallbacks, On private fun zoomInOnFreshStart() { // zoom to favorite locations or only current location, if no favorites exist - viewModel.liveBounds.observe(this, Observer { bounds -> + viewModel.liveBounds.observe(this) { bounds -> if (bounds != null) { zoomToBounds(bounds) } else if (getLastKnownLocation() != null) { - zoomToMyLocation() + map?.zoomToMyLocation() } viewModel.liveBounds.removeObservers(this) - }) + } } override fun onMarkerClick(marker: Marker): Boolean { diff --git a/app/src/main/java/de/grobox/transportr/map/MapViewModel.kt b/app/src/main/java/de/grobox/transportr/map/MapViewModel.kt index 5ccb8f9a9..6bafb04f4 100644 --- a/app/src/main/java/de/grobox/transportr/map/MapViewModel.kt +++ b/app/src/main/java/de/grobox/transportr/map/MapViewModel.kt @@ -48,7 +48,8 @@ internal class MapViewModel @Inject internal constructor( transportNetworkManager: TransportNetworkManager, locationRepository: LocationRepository, searchesRepository: SearchesRepository, - val gpsController: GpsController) : SavedSearchesViewModel(application, transportNetworkManager, locationRepository, searchesRepository) { + override val positionController: PositionController + ) : SavedSearchesViewModel(application, transportNetworkManager, locationRepository, searchesRepository), GpsMapViewModel by GpsMapViewModelImpl(positionController) { private val peekHeight = MutableLiveData() private val selectedLocationClicked = MutableLiveData() @@ -84,7 +85,6 @@ internal class MapViewModel @Inject internal constructor( selectedLocation.value = location // do not reset the selected location right away, will break incoming geo intent // the observing fragment will call clearSelectedLocation() instead when it is done - gpsController.updateGpsState(isTracking = false) } fun clearSelectedLocation() { @@ -131,7 +131,7 @@ internal class MapViewModel @Inject internal constructor( .toMutableSet() home.value?.let { if (it.hasLocation()) points.add(it.latLng) } work.value?.let { if (it.hasLocation()) points.add(it.latLng) } - gpsController.getWrapLocation()?.let { if (it.hasLocation()) points.add(it.latLng) } + //locationController.getWrapLocation()?.let { if (it.hasLocation()) points.add(it.latLng) } //TODO!!!! if (points.size < 2) { updatedLiveBounds.setValue(null) } else { diff --git a/app/src/main/java/de/grobox/transportr/map/PositionController.kt b/app/src/main/java/de/grobox/transportr/map/PositionController.kt new file mode 100644 index 000000000..fa6da8eda --- /dev/null +++ b/app/src/main/java/de/grobox/transportr/map/PositionController.kt @@ -0,0 +1,178 @@ +/* + * Transportr + * + * Copyright (c) 2013 - 2021 Torsten Grote + * + * This program is Free Software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.grobox.transportr.map + + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.location.LocationManager.GPS_PROVIDER +import android.location.LocationManager.NETWORK_PROVIDER +import android.os.Bundle +import android.os.Looper +import androidx.annotation.RequiresPermission +import androidx.annotation.WorkerThread +import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.grobox.transportr.AbstractManager +import de.grobox.transportr.locations.ReverseGeocoder +import de.grobox.transportr.locations.WrapLocation +import de.grobox.transportr.utils.NotifyingLiveData +import java.util.concurrent.TimeUnit + +//todo: maybe move listeners to internal private class instead +internal class PositionController(val context: Context) + : AbstractManager(), NotifyingLiveData.OnActivationCallback, ReverseGeocoder.ReverseGeocoderCallback, LocationListener { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + private val geoCoder = ReverseGeocoder(context, this) + + private val _position = NotifyingLiveData(this) + private val _positionState = MutableLiveData() + private val _positionName = MediatorLiveData().apply { + addSource(_position) { + geoCoder.findLocation(it) + } + } + + val position: LiveData = _position + val positionState: LiveData = _positionState + val positionName: LiveData = _positionName + + init { + _positionState.value = when { + ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED -> + PositionState.DENIED + LocationManagerCompat.isLocationEnabled(locationManager) -> + PositionState.ENABLED + else -> PositionState.DISABLED + } + } + + fun permissionGranted() { + _positionState.value = PositionState.DISABLED + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override fun onActive() { + //todo: check this without having the permission! + for (provider in LOCATION_PROVIDERS) { + locationManager.requestLocationUpdates(provider, MIN_UPDATE_INTERVAL, MIN_UPDATE_DISTANCE, this, Looper.getMainLooper()) + } + } + + override fun onInactive() { + for (provider in LOCATION_PROVIDERS) { + locationManager.removeUpdates(this) + } + } + + override fun onLocationChanged(location: Location) { + if (isBetterPosition(location, _position.value)) { + _position.value = location + } + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) { + if (provider == GPS_PROVIDER && LocationManagerCompat.isLocationEnabled(locationManager)) { + _positionState.value = PositionState.ENABLED + } + } + + override fun onProviderDisabled(provider: String) { + if (provider == GPS_PROVIDER && !LocationManagerCompat.isLocationEnabled(locationManager)) { + _positionState.value = PositionState.DISABLED + } + } + + @WorkerThread + override fun onLocationRetrieved(location: WrapLocation) { + _positionName.postValue(location) + } + + companion object { + val LOCATION_PROVIDERS = arrayOf(GPS_PROVIDER, NETWORK_PROVIDER) + val FIX_EXPIRY = TimeUnit.SECONDS.toMillis(5) + val MIN_UPDATE_INTERVAL = TimeUnit.SECONDS.toMillis(1) + const val MIN_UPDATE_DISTANCE = 0.0f + const val ACCURACY_THRESHOLD_METERS = 200 + + /** + * Determines whether one position reading is better than the current position fix + * + * + * (c) https://developer.android.com/guide/topics/location/strategies + * + * @param position The new Location that you want to evaluate + * @param currentBestPosition The current Location fix, to which you want to compare the new one + */ + fun isBetterPosition(position: Location?, currentBestPosition: Location?): Boolean { + if (position == null) { + return false + } + if (currentBestPosition == null) { + // A new location is always better than no location + return true + } + + // Check whether the new location fix is newer or older + val timeDelta = position.time - currentBestPosition.time + val isSignificantlyNewer = timeDelta > TimeUnit.MINUTES.toMillis(2) + val isSignificantlyOlder = timeDelta < -TimeUnit.MINUTES.toMillis(2) + val isNewer = timeDelta > 0 + + // Check whether the new location fix is more or less accurate + val accuracyDelta = (position.accuracy - currentBestPosition.accuracy).toInt() + val isLessAccurate = accuracyDelta > 0 + val isMoreAccurate = accuracyDelta < 0 + val isSignificantlyLessAccurate = accuracyDelta > ACCURACY_THRESHOLD_METERS + + // Check if the old and new location are from the same provider + val isFromSameProvider = position.provider.equals(currentBestPosition.provider) + + // Determine location quality using a combination of timeliness and accuracy + return when { + // the user has likely moved + isSignificantlyNewer -> true + // If the new location is more than two minutes older, it must be worse + isSignificantlyOlder -> return false + isMoreAccurate -> true + isNewer && !isLessAccurate -> true + isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true + else -> false + } + } + } + + enum class PositionState { + DENIED, + DISABLED, + ENABLED, + } +} diff --git a/app/src/main/java/de/grobox/transportr/map/SavedSearchesFragment.kt b/app/src/main/java/de/grobox/transportr/map/SavedSearchesFragment.kt index bd2ea49ed..f62380fae 100644 --- a/app/src/main/java/de/grobox/transportr/map/SavedSearchesFragment.kt +++ b/app/src/main/java/de/grobox/transportr/map/SavedSearchesFragment.kt @@ -49,7 +49,7 @@ internal class SavedSearchesFragment : FavoriteTripsFragment() { } override fun onSpecialLocationClicked(location: WrapLocation) { - val from = viewModel.gpsController.getWrapLocation() ?: WrapLocation(GPS) + val from = /* viewModel.locationController.getWrapLocation() ?:*/ WrapLocation(GPS) //TODO findDirections(context, from, null, location, true, true) } diff --git a/app/src/main/java/de/grobox/transportr/trips/detail/TripDetailViewModel.kt b/app/src/main/java/de/grobox/transportr/trips/detail/TripDetailViewModel.kt index b351ddf0e..ce9a28600 100644 --- a/app/src/main/java/de/grobox/transportr/trips/detail/TripDetailViewModel.kt +++ b/app/src/main/java/de/grobox/transportr/trips/detail/TripDetailViewModel.kt @@ -27,7 +27,9 @@ import com.mapbox.mapboxsdk.geometry.LatLngBounds import de.grobox.transportr.R import de.grobox.transportr.TransportrApplication import de.grobox.transportr.locations.WrapLocation -import de.grobox.transportr.map.GpsController +import de.grobox.transportr.map.GpsMapViewModel +import de.grobox.transportr.map.GpsMapViewModelImpl +import de.grobox.transportr.map.PositionController import de.grobox.transportr.networks.TransportNetworkManager import de.grobox.transportr.networks.TransportNetworkViewModel import de.grobox.transportr.settings.SettingsManager @@ -40,11 +42,13 @@ import de.schildbach.pte.dto.Trip import de.schildbach.pte.dto.Trip.Leg import javax.inject.Inject -class TripDetailViewModel @Inject internal constructor( - application: TransportrApplication, - transportNetworkManager: TransportNetworkManager, - val gpsController: GpsController, - private val settingsManager: SettingsManager) : TransportNetworkViewModel(application, transportNetworkManager), LegClickListener { +internal class TripDetailViewModel +@Inject internal constructor( + application: TransportrApplication, + transportNetworkManager: TransportNetworkManager, + override val positionController: PositionController, + private val settingsManager: SettingsManager +) : TransportNetworkViewModel(application, transportNetworkManager), LegClickListener, GpsMapViewModel by GpsMapViewModelImpl(positionController) { enum class SheetState { BOTTOM, MIDDLE, EXPANDED @@ -112,5 +116,4 @@ class TripDetailViewModel @Inject internal constructor( TripReloader(network.networkProvider, settingsManager, query, trip, errorString, tripReloadError) .reload() } - } diff --git a/app/src/main/java/de/grobox/transportr/trips/detail/TripMapFragment.kt b/app/src/main/java/de/grobox/transportr/trips/detail/TripMapFragment.kt index 563b0d8e7..df31093f7 100644 --- a/app/src/main/java/de/grobox/transportr/trips/detail/TripMapFragment.kt +++ b/app/src/main/java/de/grobox/transportr/trips/detail/TripMapFragment.kt @@ -34,7 +34,7 @@ import de.grobox.transportr.map.GpsMapFragment import de.schildbach.pte.dto.Trip import javax.inject.Inject -class TripMapFragment : GpsMapFragment() { +internal class TripMapFragment : GpsMapFragment() { companion object { @JvmField @@ -48,31 +48,24 @@ class TripMapFragment : GpsMapFragment() { @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: TripDetailViewModel + override lateinit var viewModel: TripDetailViewModel override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val v = super.onCreateView(inflater, container, savedInstanceState) - component.inject(this) viewModel = ViewModelProvider(activity!!, viewModelFactory).get(TripDetailViewModel::class.java) - gpsController = viewModel.gpsController - return v + return super.onCreateView(inflater, container, savedInstanceState) } override fun onMapReady(mapboxMap: MapboxMap) { super.onMapReady(mapboxMap) - // set padding, so everything gets centered in top half of screen - val metrics = DisplayMetrics() - activity!!.windowManager.defaultDisplay.getMetrics(metrics) - val topPadding = mapPadding / 2 - val bottomPadding = mapView.height / 4 - map!!.setPadding(0, topPadding, 0, bottomPadding) + // set bottom padding, so everything gets centered in top half of screen + setPadding(bottom = mapView.height / 2) - viewModel.getTrip().observe(this, Observer { onTripChanged(it) }) - viewModel.getZoomLocation().observe(this, Observer { this.animateTo(it) }) - viewModel.getZoomLeg().observe(this, Observer { this.animateToBounds(it) }) + viewModel.getTrip().observe(this) { onTripChanged(it) } + viewModel.getZoomLocation().observe(this) { this.animateTo(it) } + viewModel.getZoomLeg().observe(this) { this.animateToBounds(it) } } private fun onTripChanged(trip: Trip?) { diff --git a/app/src/main/java/de/grobox/transportr/trips/search/DirectionsViewModel.kt b/app/src/main/java/de/grobox/transportr/trips/search/DirectionsViewModel.kt index 9273ccfe2..cd396b4c9 100644 --- a/app/src/main/java/de/grobox/transportr/trips/search/DirectionsViewModel.kt +++ b/app/src/main/java/de/grobox/transportr/trips/search/DirectionsViewModel.kt @@ -26,9 +26,9 @@ import de.grobox.transportr.data.locations.FavoriteLocation.FavLocationType import de.grobox.transportr.data.locations.LocationRepository import de.grobox.transportr.data.searches.SearchesRepository import de.grobox.transportr.favorites.trips.SavedSearchesViewModel -import de.grobox.transportr.locations.LocationLiveData import de.grobox.transportr.locations.LocationView.LocationViewListener import de.grobox.transportr.locations.WrapLocation +import de.grobox.transportr.map.PositionController import de.grobox.transportr.networks.TransportNetworkManager import de.grobox.transportr.networks.getTransportNetwork import de.grobox.transportr.settings.SettingsManager @@ -47,7 +47,7 @@ import javax.inject.Inject class DirectionsViewModel @Inject internal constructor( application: TransportrApplication, transportNetworkManager: TransportNetworkManager, settingsManager: SettingsManager, - locationRepository: LocationRepository, searchesRepository: SearchesRepository + locationRepository: LocationRepository, searchesRepository: SearchesRepository, private val positionController: PositionController ) : SavedSearchesViewModel(application, transportNetworkManager, locationRepository, searchesRepository), TimeDateListener, LocationViewListener { private val _tripsRepository: TripsRepository @@ -55,7 +55,7 @@ class DirectionsViewModel @Inject internal constructor( private val _viaLocation = MutableLiveData() val viaSupported: LiveData private val _toLocation = MutableLiveData() - val locationLiveData = LocationLiveData(application.applicationContext) + val locationLiveData = positionController.positionName val findGpsLocation = MutableLiveData() val timeUpdate = LiveTrigger() private val _now = MutableLiveData(true) diff --git a/app/src/main/java/de/grobox/transportr/utils/NotifyingLiveData.kt b/app/src/main/java/de/grobox/transportr/utils/NotifyingLiveData.kt new file mode 100644 index 000000000..96101f7b2 --- /dev/null +++ b/app/src/main/java/de/grobox/transportr/utils/NotifyingLiveData.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.grobox.transportr.utils + +import androidx.annotation.MainThread +import javax.annotation.ParametersAreNonnullByDefault +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import de.grobox.transportr.utils.NotifyingLiveData +import java.util.concurrent.atomic.AtomicBoolean + +//todo: change description! +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + * + * + * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + * + * + * Note that only one observer is going to be notified of changes. + */ +@ParametersAreNonnullByDefault +class NotifyingLiveData(private val callback: OnActivationCallback) : MutableLiveData() { + + @MainThread + override fun onActive() { + super.onActive() + callback.onActive() + } + + override fun onInactive() { + super.onInactive() + callback.onInactive() + } + + interface OnActivationCallback { + fun onActive() + fun onInactive() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index c1e9e90c5..9d491aff2 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -8,24 +8,17 @@ - - + tools:text="© JawgMaps | © OSM Contributors"/> - \ No newline at end of file + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 13cf411b2..a5d229a76 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -8,7 +8,7 @@ @color/cardview_dark_background #ff424242 - @string/mapbox_style_dark + @string/jawg_style_dark #99000000 @color/cardview_dark_background diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4eb6955a..7873d90ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,7 +79,12 @@ and always knows where you are to not miss where to get off the bus. This app is Free Software, so you can freely use, study, share and improve it. Contributions are encouraged and appreciated. If you would like to contribute, please visit the homepage for more information. Visit Website - © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://www.mapbox.com/about/maps/">Mapbox</a> <b><a href="https://www.mapbox.com/map-feedback/">%1$s</a></b> + https://api.jawg.io/styles/ + KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ + jawg-light + jawg-dark + + <a href="http://jawg.io" title="Tiles Courtesy of Jawg Maps">© <b>Jawg</b>Maps</a>, <a href="https://www.openstreetmap.org/copyright" title="OpenStreetMap is open data licensed under ODbL">© OSM contributors</a> | <b><a href="https://www.openstreetmap.org/fixthemap">%1$s</a></b> Improve this map Open Navigation Drawer diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0b206c6b9..4504122fa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -53,21 +53,10 @@ - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d21281ec3..95cdcc27d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -19,7 +19,7 @@