Page MenuHomeGitPull.it

D237.1778675217.diff
No OneTemporary

Authored By
Unknown
Size
91 KB
Referenced Files
None
Subscribers
None

D237.1778675217.diff

diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2026 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
import android.os.Bundle
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2026 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
import android.os.Bundle
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2024 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
import android.app.Activity
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt
@@ -1,5 +1,23 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2025 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
+import android.Manifest
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
@@ -19,6 +37,9 @@
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
@@ -28,6 +49,7 @@
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.gson.JsonObject
+import it.reyboz.bustorino.BuildConfig
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.FiveTNormalizer
import it.reyboz.bustorino.backend.LivePositionTripPattern
@@ -38,7 +60,10 @@
import it.reyboz.bustorino.backend.utils
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
+import it.reyboz.bustorino.map.MapLibreLocationEngine
import it.reyboz.bustorino.map.MapLibreUtils
+import it.reyboz.bustorino.middleware.FusedNativeLocationProvider
+import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.util.ViewUtils
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import it.reyboz.bustorino.viewmodels.MapStateViewModel
@@ -48,7 +73,10 @@
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.location.LocationComponent
-import org.maplibre.android.location.LocationComponentOptions
+import org.maplibre.android.location.LocationComponentActivationOptions
+import org.maplibre.android.location.engine.LocationEngineCallback
+import org.maplibre.android.location.engine.LocationEngineRequest
+import org.maplibre.android.location.engine.LocationEngineResult
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.OnMapReadyCallback
@@ -67,6 +95,7 @@
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
+import kotlin.time.Duration.Companion.milliseconds
abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback {
protected var map: MapLibreMap? = null
@@ -87,6 +116,11 @@
protected lateinit var sharedPreferences: SharedPreferences
protected lateinit var bottomSheetBehavior: BottomSheetBehavior<RelativeLayout>
+ protected var locationEngine: MapLibreLocationEngine? = null
+ protected lateinit var locationProvider: FusedNativeLocationProvider
+
+ protected var shownToastNoPosition = false
+
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key ->
/*when(key){
@@ -100,6 +134,28 @@
reloadMap()
}
}
+
+ protected val positionRequestResponder = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions(), ActivityResultCallback{ res ->
+ if(!(res.containsKey(PERM_LOC_COARSE)&&res.containsKey(PERM_LOC_FINE))){
+ Log.e(DEBUG_TAG, "Location request does not have the correct keys")
+ } else if(res[PERM_LOC_COARSE]!! && res[PERM_LOC_FINE]!!){
+ //permission OK, init map location
+ val mMap = map
+ if(mMap == null){
+ Log.w(DEBUG_TAG, "Location request completed, but map is null!")
+ }else{
+ initializeMapLocationComponent(mMap,requireContext(), null)
+ }
+ } else{
+ // PERMISSION DENIED
+ // TODO find better way to show the necessity of the permission
+ if(shouldShowRequestPermissionRationale(PERM_LOC_FINE))
+ Toast.makeText(requireContext(),
+ R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
+ }
+ }
+ )
//Bottom sheet behavior in GeneralMapLibreFragment
protected var bottomLayout: RelativeLayout? = null
protected lateinit var stopTitleTextView: TextView
@@ -134,7 +190,39 @@
//private lateinit var symbolManager: SymbolManager
protected val mapStateViewModel: MapStateViewModel by viewModels()
+ protected var locationInitialized = false
+ protected var mapInitialized = false
+ protected var receivedFirstLocation = false
+
+
+ //location callback to decide if to zoom to the user position
+ @SuppressLint("MissingPermission")
+ protected val mapLibreLocationCallback = object : LocationEngineCallback<LocationEngineResult> {
+ override fun onSuccess(result: LocationEngineResult) {
+ val location: Location? = result.lastLocation
+ Log.d(DEBUG_TAG, "Received location $location")
+ location?.let {
+ //check timing of the location
+ val currentTime = System.currentTimeMillis()
+ val discard = (currentTime - it.time) > 90 * 1000.0 // discard if it is Older than 60 seconds
+ if(!discard) {
+ if (!receivedFirstLocation) {
+ onFirstReceivedLocation(it)
+ }
+ receivedFirstLocation = true
+ }
+ }
+ if(receivedFirstLocation){
+ //remove this
+ locationEngine?.removeLocationUpdates(this)
+ }
+ }
+
+ override fun onFailure(exception: Exception) {
+ Log.e(DEBUG_TAG, "Error in getting position: ${exception.message}")
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -181,6 +269,8 @@
}
}
+
+
@Deprecated("Deprecated in Java")
override fun onLowMemory() {
mapView.onLowMemory()
@@ -377,24 +467,47 @@
}
- /**
- * Initialize the map location, but do not enable the component
- */
+
+ abstract fun onMapLocationComponentInitialized()
+
@SuppressLint("MissingPermission")
- protected fun initMapUserLocation(style: Style, map: MapLibreMap, context: Context){
- locationComponent = map.locationComponent
- val locationComponentOptions =
- LocationComponentOptions.builder(context)
- .pulseEnabled(false)
- .build()
- val locationComponentActivationOptions =
- MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
- locationComponent.activateLocationComponent(locationComponentActivationOptions)
- locationComponent.isLocationComponentEnabled = false
+ protected fun setLocationComponentEnabled(enabled: Boolean): Boolean{
+ var changed = false
+ map?.apply {
+ if(locationComponent.isLocationComponentEnabled !=enabled)
+ locationComponent.isLocationComponentEnabled= enabled
+ changed = true}
+
+ return changed
+ }
- lastLocation?.let {
- if (it.accuracy < 200)
- locationComponent.forceLocationUpdate(it)
+ @SuppressLint("MissingPermission")
+ protected fun initializeMapLocationComponent(map: MapLibreMap, context: Context, style: Style?){
+ val mStyle = style ?: map.style
+ if(locationInitialized){
+ Log.w(DEBUG_TAG, "trying to initialize Location Component, but it is already done")
+ return
+ }
+ mStyle?.let{ style ->
+ locationComponent = map.locationComponent
+
+ locationProvider = FusedNativeLocationProvider(context)
+ locationEngine = MapLibreLocationEngine(locationProvider)
+ val options = LocationComponentActivationOptions.builder(context, style)
+ .useDefaultLocationEngine(false)
+ .locationEngine(locationEngine)
+ .build()
+ locationComponent.activateLocationComponent(options)
+ //locationComponent.cameraMode = CameraMode.TRACKING
+ //locationComponent.renderMode = RenderMode.COMPASS
+ locationInitialized = true
+ if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Requesting location updates")
+ locationEngine!!.requestLocationUpdates(LocationEngineRequest.Builder(500).setDisplacement(20.0f).build(),
+ mapLibreLocationCallback, null)
+ // signal to show user location icon as active
+ mapStateViewModel.locationActive.value = true
+ setLocationComponentEnabled(true)
+ onMapLocationComponentInitialized()
}
}
@@ -405,7 +518,6 @@
* Unified version that works with both fragments
*
* @param incomingData Map of updates with optional trip and pattern information
- * @param checkCoordinateValidity If true, validates that coordinates are positive (default: false)
* @param hasVehicleTracking If true, checks if vehShowing is updated and calls callback (default: true)
* @param trackVehicleCallback Optional callback to show vehicle details when vehShowing is updated
*/
@@ -587,7 +699,7 @@
// Schedule delayed update
if(lifecycleOwnerLiveData.value != null)
viewLifecycleOwner.lifecycleScope.launch {
- delay(200)
+ delay(200.milliseconds)
updatePositionsIcons(forced)
}
return
@@ -874,6 +986,76 @@
style.addLayerAbove(selectedBusLayer, BUSES_LAYER_ID)
}
+ /**
+ * Method used for enabling / disabling the location from the buttons
+ */
+ protected fun switchUserLocationStatus(view: View?){
+ val enabled = if(locationInitialized) locationComponent.isLocationComponentEnabled else false
+ val context = context ?: return
+ if(enabled) {
+ setMapLocationEnabled(false)
+ onMapLocationEnabled(false)
+ }
+ else if(deviceHasGpsProvider()) {
+ if(Permissions.bothLocationPermissionsGranted(context)){
+ setMapLocationEnabled(true)
+ onMapLocationEnabled(true)
+ } else{
+ Log.d(DEBUG_TAG, "Requesting permissions to show location")
+ Permissions.getInstance(context).checkRequestLocationPermissions(requireActivity(), positionRequestResponder)
+ }
+ } else{
+ context.let {
+ Toast.makeText(it, R.string.no_gps_on_device, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ }
+
+ @SuppressLint("MissingPermission")
+ protected fun setMapLocationEnabled(enabled: Boolean){
+ map?.locationComponent?.isLocationComponentEnabled = enabled
+ //map?.cameraPosition =
+ mapStateViewModel.locationActive.value = enabled
+ }
+ protected fun checkInitMapLocation(mapReady: MapLibreMap,style: Style, context: Context) {
+ //enable location
+ val hasGps = deviceHasGpsProvider()
+ val permissions = Permissions.getInstance(context)
+ if(hasGps) {
+ if (Permissions.bothLocationPermissionsGranted(context)) {
+ Log.d(DEBUG_TAG, "Have got the location permission, init location component")
+ initializeMapLocationComponent(mapReady, context, style)
+ }else {
+ var req = false
+ activity?.let{
+ req = permissions.checkRequestLocationPermissions(it, positionRequestResponder)
+ }
+ //setLocationIconEnabled(false)
+ //setFollowingUser(false)
+ if(!req) {
+ setMapLocationEnabled(false)
+ onMapLocationEnabled(false)
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Set the UI elements showing that the user location is disabled
+ */
+ abstract fun onMapLocationEnabled(active: Boolean)
+
+ /**
+ * Helper function to actually set the icon
+ */
+ abstract fun setLocationIconEnabled(enabled: Boolean)
+
+ /**
+ * Called when we receive the first fix on the user location
+ */
+ abstract fun onFirstReceivedLocation(location: Location)
protected fun isBottomSheetShowing(): Boolean {
return bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED
@@ -920,6 +1102,13 @@
.build()
}
+ protected fun showToastLocation(enabled: Boolean){
+ val textid = if (enabled) R.string.location_enabled else R.string.location_disabled
+ context?.let{
+ Toast.makeText(it,textid,Toast.LENGTH_SHORT).show()
+ }
+ }
+
companion object{
private const val DEBUG_TAG="GeneralMapLibreFragment"
@@ -951,5 +1140,11 @@
protected const val POLY_ARROWS_SOURCE = "arrows-source"
protected const val POLY_ARROW ="poly-arrow-img"
+ private const val PERM_LOC_COARSE = Manifest.permission.ACCESS_COARSE_LOCATION
+ private const val PERM_LOC_FINE = Manifest.permission.ACCESS_FINE_LOCATION
+
+ //TODO: this is hardcoded, make it modifiable by the user
+ protected const val MAX_DIST_KM = 90.0
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2023 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
import android.content.Context
@@ -35,7 +52,7 @@
private val locationRequestResLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ res ->
//onActivityResult(res: map<String,Boolean>)
- if(res.get(Permissions.LOCATION_PERMISSIONS[0])==true || res.get(Permissions.LOCATION_PERMISSIONS[1])==true)
+ if(res[Permissions.LOCATION_PERMISSIONS[0]] ==true || res[Permissions.LOCATION_PERMISSIONS[1]] ==true)
setInteractButtonState(ButtonState.LOCATION,false)
}
private val notificationsReqLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
@@ -94,7 +111,8 @@
setInteractButtonState(ButtonState.LOCATION, !permGranted)
interactButton.setOnClickListener {
//ask location permission
- locationRequestResLauncher.launch(Permissions.LOCATION_PERMISSIONS)
+ Permissions.getInstance(requireContext()).checkRequestLocationPermissions(requireActivity(), locationRequestResLauncher)
+ //locationRequestResLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
interactButton.visibility = View.VISIBLE
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -18,19 +18,17 @@
package it.reyboz.bustorino.fragments
-import android.Manifest
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
+import android.location.Location
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
-import androidx.activity.result.ActivityResultCallback
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
@@ -52,7 +50,6 @@
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
import it.reyboz.bustorino.map.*
-import it.reyboz.bustorino.middleware.LocationUtils
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LinesViewModel
import it.reyboz.bustorino.viewmodels.MapStateViewModel
@@ -75,7 +72,6 @@
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point
-import java.util.concurrent.atomic.AtomicBoolean
class LinesDetailFragment() : GeneralMapLibreFragment() {
@@ -89,7 +85,7 @@
private var shouldMapLocationBeReactivated = true
private var toRunWhenMapReady : Runnable? = null
- private var mapInitialized = AtomicBoolean(false)
+ //private var mapInitialized = AtomicBoolean(false)
//private var patternsSpinnerState: Parcelable? = null
@@ -183,20 +179,6 @@
private var polyline: LineString? = null
- private val showUserPositionRequestLauncher =
- registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions(),
- ActivityResultCallback { result ->
- if (result == null) {
- Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
- } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
- && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
- // We can use the position, restart location overlay
- if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
- return@ActivityResultCallback ///@registerForActivityResult
- setMapUserLocationEnabled(true, true, enablingPositionFromClick)
- } else Log.w(DEBUG_TAG, "No location permission")
- })
//private var stopPosList = ArrayList<GeoPoint>()
//fragment actions
@@ -207,8 +189,6 @@
private var usingMQTTPositions = true
private var restoredCameraInMap = false
-
-
//position of live markers
private val tripMarkersAnimators = HashMap<String, ObjectAnimator>()
@@ -232,7 +212,7 @@
//isBottomSheetShowing = false
//stopsLayerStarted = false
lastStopsSizeShown = 0
- mapInitialized.set(false)
+ mapInitialized = false
val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false)
//lineID = requireArguments().getString(LINEID_KEY, "")
@@ -294,10 +274,8 @@
}
}
locationIcon?.let {view ->
- if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext()))
- setLocationIconEnabled(false)
//set click Listener
- view.setOnClickListener(this::onPositionIconButtonClick)
+ view.setOnClickListener(this::switchUserLocationStatus)
}
busPositionsIconButton.setOnClickListener {
LivePositionsDialogFragment().show(parentFragmentManager, "LivePositionsDialog")
@@ -373,6 +351,9 @@
descripTextView.text = route.longName
descripTextView.visibility = View.VISIBLE
}
+ mapStateViewModel.locationActive.observe(viewLifecycleOwner) {
+ setLocationIconEnabled(it)
+ }
// enable info button if there are alerts on the line
alertsViewModel.setGtfsLineFilter(lineID)
alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ list ->
@@ -458,7 +439,7 @@
hideStopOrBusBottomSheet()
if(locationComponent.isLocationComponentEnabled){
- locationComponent.isLocationComponentEnabled = false
+ setLocationComponentEnabled(false)
shouldMapLocationBeReactivated = true
} else
shouldMapLocationBeReactivated = false
@@ -481,60 +462,52 @@
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30))
- if(shouldMapLocationBeReactivated && Permissions.bothLocationPermissionsGranted(requireContext())){
- locationComponent.isLocationComponentEnabled = true
+ if(shouldMapLocationBeReactivated){
+ setLocationComponentEnabled(Permissions.bothLocationPermissionsGranted(requireContext()))
}
}
- private fun setLocationIconEnabled(setTrue: Boolean){
- if(setTrue)
+ override fun setLocationIconEnabled(enabled: Boolean){
+ if(enabled) {
locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
- else
- locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
+ }
+ else {
+ locationIcon?.setImageDrawable(ContextCompat.getDrawable(
+ requireContext(),
+ R.drawable.location_circlew_grey
+ )
+ )
+ }
}
- /**
- * Handles logic of enabling the user location on the map
- */
- @SuppressLint("MissingPermission")
- private fun setMapUserLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) {
- if (enabled) {
- val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext())
+ override fun onMapLocationEnabled(active: Boolean) {
+ //extra thing: show the toast
+ showToastLocation(active)
+ }
- if (permissionOk) {
- Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions")
- locationComponent.isLocationComponentEnabled = true
- //locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
+ override fun onMapLocationComponentInitialized() {
+ //enable the position after the first fix
+ //onMapLocationEnabled(true)
+ }
- setLocationIconEnabled(true)
- if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show()
- } else {
- if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
- //TODO: show dialog for permission rationale
- Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
+ @SuppressLint("MissingPermission")
+ override fun onFirstReceivedLocation(location: Location) {
+ if(mapInitialized){
+ val center = map!!.cameraPosition.target
+ val newPos = LatLng(location.latitude, location.longitude)
+ Log.d(DEBUG_TAG, "Center of the map : $center")
+ val newStatus = if(center==null || newPos.distanceTo(center) > 20*1000){
+ Log.d(DEBUG_TAG, "Distance from center of map to location: "+center?.distanceTo(newPos))
+ if(!shownToastNoPosition) context?.let{ c->
+ Toast.makeText(c, R.string.too_far_not_showing_location, Toast.LENGTH_LONG).show()
+ shownToastNoPosition = true
}
- Log.d(DEBUG_TAG, "Requesting permission to show user location")
- enablingPositionFromClick = fromClick
- showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
- }
- } else{
- locationComponent.isLocationComponentEnabled = false
- setLocationIconEnabled(false)
- if (fromClick) {
- Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show()
- //TODO: Cancel the request for the enablement of the position if needed
+ false
+ } else{
+ true
}
- }
-
- }
-
- /**
- * Switch position icon from activ
- */
- private fun onPositionIconButtonClick(view: View){
- if(locationComponent.isLocationComponentEnabled) setMapUserLocationEnabled(false, false, true)
- else{
- setMapUserLocationEnabled(true, false, true)
+ if(!newStatus) setLocationComponentEnabled(newStatus)
+ mapStateViewModel.locationActive.value = newStatus
}
}
@@ -558,8 +531,7 @@
mapStyle = style
//setupLayers(style)
- // Start observing data
- initMapUserLocation(style, mapReady, requireContext())
+ //checkInitMapLocation(mapReady, style,requireContext())
//if(!stopsLayerStarted)
initPolylineStopsLayers(style, null)
@@ -569,7 +541,7 @@
initSymbolManager(mapReady, style)
toRunWhenMapReady?.run()
toRunWhenMapReady = null
- mapInitialized.set(true)
+ mapInitialized = true
if(patternShown!=null){
viewModel.stopsForPatternLiveData.value?.let {
@@ -646,7 +618,8 @@
savedCameraPosition = null
- if(shouldMapLocationBeReactivated) setMapUserLocationEnabled(true, false, false)
+ if(shouldMapLocationBeReactivated)
+ mapReady.style?.let{ checkInitMapLocation(mapReady,it, context)}
}
override fun showOpenStopWithSymbolLayer(): Boolean {
@@ -922,7 +895,7 @@
}
private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stopsToSort: List<Stop>, zoomToPattern: Boolean){
- if(!mapInitialized.get()){
+ if(!mapInitialized){
//set the runnable and do nothing else
Log.d(DEBUG_TAG, "Delaying pattern display to when map is Ready: ${patternWs.pattern.code}")
toRunWhenMapReady = Runnable {
@@ -1104,7 +1077,10 @@
override fun onStop() {
super.onStop()
mapView.onStop()
- shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled
+ if(locationInitialized)
+ shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled
+ else
+ shouldMapLocationBeReactivated = false
}
override fun onDestroyView() {
@@ -1137,7 +1113,8 @@
mapStyle.removeSource(BUSES_SOURCE_ID)
- map?.locationComponent?.isLocationComponentEnabled = false
+ //map?.locationComponent?.isLocationComponentEnabled = false
+ setLocationComponentEnabled(false)
}
override fun getBaseViewForSnackBar(): View? {
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LivePositionsDialogFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LivePositionsDialogFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LivePositionsDialogFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LivePositionsDialogFragment.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2025 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments
import it.reyboz.bustorino.R
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
@@ -1,4 +1,20 @@
-
+/*
+ BusTO - Fragments components
+ Copyright (C) 2021 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.fragments;
import android.Manifest;
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
@@ -1,12 +1,26 @@
+/*
+ BusTO - Fragments components
+ Copyright (C) 2025 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+*/
package it.reyboz.bustorino.fragments
-import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
-import android.content.res.ColorStateList
import android.location.Location
-import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.util.Log
@@ -16,18 +30,13 @@
import android.widget.ImageButton
import android.widget.RelativeLayout
import android.widget.Toast
-import it.reyboz.bustorino.backend.FiveTNormalizer
-import it.reyboz.bustorino.backend.gtfs.GtfsUtils
-import androidx.activity.result.ActivityResultCallback
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.room.concurrent.AtomicBoolean
import com.google.android.material.bottomsheet.BottomSheetBehavior
+import it.reyboz.bustorino.BuildConfig
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
@@ -35,24 +44,22 @@
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import it.reyboz.bustorino.map.MapLibreStyles
-import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.StopsMapViewModel
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
+import org.maplibre.android.location.engine.LocationEngineCallback
+import org.maplibre.android.location.engine.LocationEngineRequest
+import org.maplibre.android.location.engine.LocationEngineResult
import org.maplibre.android.location.modes.CameraMode
+import org.maplibre.android.location.modes.RenderMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.plugins.annotation.Symbol
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
-
-// TODO: Rename parameter arguments, choose names that match
-// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
-private const val STOP_TO_SHOW = "stoptoshow"
-
/**
* A simple [Fragment] subclass.
* Use the [MapLibreFragment.newInstance] factory method to
@@ -68,7 +75,6 @@
private var isUserMovingCamera = false
private var lastStopsSizeShown = 0
private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0)
- private var mapInitCompleted =false
private var stopsRedrawnTimes = 0
//bottom Sheet behavior in GeneralMapLibreFragment
@@ -76,73 +82,13 @@
// Location stuff
private lateinit var locationManager: LocationManager
- private lateinit var showUserPositionButton: ImageButton
+ private lateinit var userLocationButton: ImageButton
private lateinit var centerUserButton: ImageButton
private lateinit var followUserButton: ImageButton
private var followingUserLocation = false
- private var pendingLocationActivation = false
private var ignoreCameraMovementForFollowing = true
- private var enablingPositionFromClick = false
private var restoredMapCamera = AtomicBoolean()
- private var permissionsGranted = false
-
- //TODO: Rewrite this mess using LocationEngineProvider in MapLibre
- private val positionRequestLauncher = registerForActivityResult<Array<String>, Map<String, Boolean>>(
- ActivityResultContracts.RequestMultiplePermissions(), ActivityResultCallback { result ->
- if (result == null) {
- Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
- }else if(!pendingLocationActivation){
- /// SHOULD DO NOTHING HERE
- Log.d(DEBUG_TAG, "Requested location but now there is no pendingLocationActivation")
- } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
- && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
- // We can use the position, restart location overlay
- permissionsGranted = true
- if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
- return@ActivityResultCallback
- val locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
- var lastLoc = stopsViewModel.lastUserLocation
- @SuppressLint("MissingPermission")
- if(lastLoc==null) lastLoc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
- else Log.d(DEBUG_TAG, "Got last location from cache")
-
- //FIRST CASE: I have no GPS
- if( !locationManager.allProviders.contains(LocationManager.GPS_PROVIDER) ){
- setMapLocationEnabled(false, false,false)
-
- }
- else if (lastLoc != null) {
-
- if(LatLng(lastLoc.latitude, lastLoc.longitude).distanceTo(DEFAULT_LATLNG) <= MAX_DIST_KM*1000){
- Log.d(DEBUG_TAG, "Showing the user position")
- setMapLocationEnabled(true, true, false)
- } else{
- setMapLocationEnabled(false, false,false)
- context?.let{Toast.makeText(it,R.string.too_far_not_showing_location, Toast.LENGTH_SHORT).show()}
- }
- } else requestInitialUserLocation()
-
- } else{
- Toast.makeText(requireContext(),R.string.location_disabled, Toast.LENGTH_SHORT).show()
- Log.w(DEBUG_TAG, "No location permission")
- }
- })
- private val showUserPositionRequestLauncher =
- registerForActivityResult<Array<String>, Map<String, Boolean>>(
- ActivityResultContracts.RequestMultiplePermissions(),
- ActivityResultCallback { result ->
- if (result == null) {
- Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
- } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
- && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
- // We can use the position, restart location overlay
- if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
- return@ActivityResultCallback ///@registerForActivityResult
- setMapLocationEnabled(true, true, enablingPositionFromClick)
- } else Log.w(DEBUG_TAG, "No location permission")
- })
-
//BUS POSITIONS
private var usingMQTTPositions = true // THIS IS INSIDE VIEW MODEL NOW
@@ -200,8 +146,8 @@
arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton)
directionsCard = bottomSheet.findViewById(R.id.directionsCardButton)
- showUserPositionButton = rootView.findViewById(R.id.locationEnableIcon)
- showUserPositionButton.setOnClickListener(this::switchUserLocationStatus)
+ userLocationButton = rootView.findViewById(R.id.locationEnableIcon)
+ userLocationButton.setOnClickListener(this::switchUserLocationStatus)
followUserButton = rootView.findViewById(R.id.followUserImageButton)
centerUserButton = rootView.findViewById(R.id.centerMapImageButton)
busPositionsIconButton = rootView.findViewById(R.id.busPositionsImageButton)
@@ -230,17 +176,16 @@
}
followUserButton.setOnClickListener {
// onClick user following button
- if(context!=null && locationComponent.isLocationComponentEnabled){
- if(followingUserLocation)
- locationComponent.cameraMode = CameraMode.NONE
- else locationComponent.cameraMode = CameraMode.TRACKING
- // CameraMode.TRACKING makes the camera move and jump to the location
+ if(context!=null && locationInitialized && locationComponent.isLocationComponentEnabled){
- setFollowingUser(!followingUserLocation)
+ // CameraMode.TRACKING makes the camera move and jump to the location
+ setFollowUserLocation(!followingUserLocation)
}
}
- locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ //locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ /*
if (Permissions.bothLocationPermissionsGranted(requireContext()) && deviceHasGpsProvider()) {
+
requestInitialUserLocation()
} else{
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
@@ -251,6 +196,8 @@
// PERMISSIONS REQUESTED AFTER MAP SETUP
}
+ */
+
// Setup close button
rootView.findViewById<View>(R.id.btnClose).setOnClickListener {
@@ -287,6 +234,8 @@
usingMQTTPositions = useMQTT
}
+ mapStateViewModel.locationActive.observe(viewLifecycleOwner){ setLocationIconEnabled(it)}
+ mapStateViewModel.followingUserPosition.observe(viewLifecycleOwner){ updateFollowingIcon(it)}
Log.d(DEBUG_TAG, "Fragment View Created!")
@@ -310,7 +259,6 @@
//setupLayers(style)
addImagesStyle(style)
- initMapUserLocation(style, mapReady, requireContext())
//init stop layer with this
val stopsInCache = stopsViewModel.getAllStopsLoaded()
if(stopsInCache.isEmpty())
@@ -321,11 +269,12 @@
// Start observing data now that everything is set up
observeStops()
+
+ checkInitMapLocation(mapReady,style, context)
}
mapReady.addOnCameraIdleListener {
- isUserMovingCamera = false
map?.let {
val newBbox = it.projection.visibleRegion.latLngBounds
if ((newBbox.center==lastBBox.center) && (newBbox.latitudeSpan==lastBBox.latitudeSpan) && (newBbox.longitudeSpan==lastBBox.latitudeSpan)){
@@ -342,21 +291,20 @@
mapReady.addOnCameraMoveStartedListener { v->
if(v== MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE){
//the user is moving the map
- isUserMovingCamera = true
+ //isUserMovingCamera = true
+ updateFollowingIcon(false)
}
}
mapReady.addOnMapClickListener { point ->
onMapClickReact(point)
}
-
- mapInitCompleted = true
// we start requesting the bus positions now
observeBusPositionUpdates()
//Restoring data
- if (initialStopToShow!=null){
+ if (initialStopToShow!=null && initialStopToShow?.hasCoords() == true){
val s = initialStopToShow!!
if(s.hasCoords()){
mapReady.cameraPosition = CameraPosition.Builder().target(
@@ -381,15 +329,27 @@
}
if(!boundsRestored){
- mapReady.cameraPosition = CameraPosition.Builder().target(
- LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
- ).zoom(DEFAULT_ZOOM).build()
+ // we have not restored the bounds, open normally in target location
+ // TODO: check that the map is reopened in the same location
+ val lastLoc = mapStateViewModel.locationToShow
+ val defaultLoc = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
+ val proposedLoc = lastLoc?.let{ LatLng(lastLoc.latitude, lastLoc.longitude)}
+ val targetLoc = if(proposedLoc == null || proposedLoc.distanceTo(defaultLoc) > MAX_DIST_KM*1000)
+ defaultLoc
+ else proposedLoc
+
+
+ mapReady.cameraPosition = CameraPosition.Builder().target(targetLoc).zoom(DEFAULT_ZOOM).build()
}
restoredMapCamera.set(boundsRestored)
+
+
}
+ mapInitialized = true
+
+ //pendingLocationActivation = true
+ //positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
- pendingLocationActivation = true
- positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
private fun onMapClickReact(point: LatLng): Boolean{
@@ -527,9 +487,10 @@
}
*/
//save last location
- map?.locationComponent?.lastKnownLocation?.let{
- stopsViewModel.lastUserLocation = it
- }
+ if (locationInitialized)
+ map?.locationComponent?.lastKnownLocation?.let{
+ stopsViewModel.lastUserLocation = it
+ }
}
@@ -542,7 +503,6 @@
mapStyle.removeSource(BUSES_SOURCE_ID)
- //map?.locationComponent?.isLocationComponentEnabled = false
}
override fun getBaseViewForSnackBar(): View? {
return mapView
@@ -627,9 +587,9 @@
livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap<String, Pair<LivePositionUpdate, TripAndPatternWithStops?>> ->
Log.d(
DEBUG_TAG,
- "Have " + data.size + " trip updates, has Map start finished: " + mapInitCompleted
+ "Have " + data.size + " trip updates, has Map start finished: $mapInitialized"
)
- if (mapInitCompleted) updateBusPositionsInMap(data, hasVehicleTracking = true) { veh ->
+ if (mapInitialized) updateBusPositionsInMap(data, hasVehicleTracking = true) { veh ->
showVehicleTripInBottomSheet(veh)
}
if (!isDetached && !livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.requestDelayedGTFSUpdates(
@@ -640,86 +600,87 @@
// ------ LOCATION STUFF -----
+
+
+
@SuppressLint("MissingPermission")
- private fun requestInitialUserLocation() {
- val provider : String = LocationManager.GPS_PROVIDER//getBestLocationProvider()
-
- //provider.let {
- setLocationIconEnabled(true)
- Toast.makeText(requireContext(), R.string.position_searching_message, Toast.LENGTH_SHORT).show()
- locationManager.requestSingleUpdate(provider, object : LocationListener {
- override fun onLocationChanged(location: Location) {
- val userLatLng = LatLng(location.latitude, location.longitude)
- val distanceToTarget = userLatLng.distanceTo(DEFAULT_LATLNG)
-
- if (distanceToTarget <= MAX_DIST_KM*1000.0) {
- map?.let{
- // if we are still waiting for the position to enable
- if(pendingLocationActivation)
- setMapLocationEnabled(true, true, false)
+ override fun onMapLocationComponentInitialized() {
+ //locationComponent.cameraMode = CameraMode.TRACKING
+
+ locationComponent.renderMode = RenderMode.COMPASS
+ locationComponent.locationEngine?.apply{
+ // this is only called once
+ getLastLocation(object : LocationEngineCallback<LocationEngineResult> {
+ override fun onSuccess(res: LocationEngineResult?) {
+ Log.d(DEBUG_TAG, "Got the last location, ${res?.lastLocation}")
+ res?.lastLocation?.let { loc ->
+ if(mapInitialized)
+ map?.cameraPosition = CameraPosition.Builder().target(LatLng(loc.latitude, loc.longitude)).build()
+ else
+ mapStateViewModel.locationToShow = loc
}
- } else {
- Toast.makeText(context, R.string.too_far_not_showing_location, Toast.LENGTH_SHORT).show()
- setMapLocationEnabled(false,false, false)
}
- }
-
- override fun onProviderDisabled(provider: String) {}
- override fun onProviderEnabled(provider: String) {}
- @Deprecated("Deprecated in Java")
- override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
- }, null)
+ override fun onFailure(p0: java.lang.Exception) {
+ Log.e(DEBUG_TAG, "Failed to get the last location", p0)
+ }
+ })
+ }
}
+ override fun onMapLocationEnabled(active: Boolean) {
+ //Extra stuff to do
+ setFollowUserLocation(active)
+ }
- /**
- * Handles logic of enabling the user location on the map
- */
@SuppressLint("MissingPermission")
- private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) {
- if (enabled) {
- val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext())
-
- if (permissionOk) {
- Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions, fromClick: $fromClick")
- locationComponent.isLocationComponentEnabled = true
- if (!restoredMapCamera.get()) {
- locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
- setFollowingUser(true)
+ override fun onFirstReceivedLocation(location: Location) {
+
+ val it = location
+ if(locationInitialized && !receivedFirstLocation) {
+ //only zoom if the user position is close enough to the center
+ val newPoint = LatLng(it.latitude, it.longitude)
+ if(newPoint.distanceTo(LatLng(
+ MapLibreFragment.DEFAULT_CENTER_LAT,
+ MapLibreFragment.DEFAULT_CENTER_LON
+ ))
+ > MAX_DIST_KM * 1000){
+ //show Toast
+ if(!shownToastNoPosition) context?.let{ c->
+ Toast.makeText(c, R.string.too_far_not_showing_location, Toast.LENGTH_LONG).show()
+ shownToastNoPosition = true
}
- setLocationIconEnabled(true)
- if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show()
- pendingLocationActivation =false
- //locationComponent.locationEngine.requestLocationUpdates()
+ setLocationComponentEnabled(false)
+ //Update UI Status
+ mapStateViewModel.locationActive.value = false
+ mapStateViewModel.followingUserPosition.value = false
} else {
- if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
- //TODO: show dialog for permission rationale
- Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
+ map?.apply {
+ animateCamera(
+ CameraUpdateFactory.newCameraPosition(
+ CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()
+ ),
+ 1000
+ )
+ setLocationComponentEnabled(true)
+ locationComponent.cameraMode = CameraMode.TRACKING
+ mapStateViewModel.locationActive.value = true
}
- Log.d(DEBUG_TAG, "Requesting permission to show user location")
- showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
- }
- } else{
- locationComponent.isLocationComponentEnabled = false
- setFollowingUser(false)
- setLocationIconEnabled(false)
- if (fromClick) {
- Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show()
- if(pendingLocationActivation) pendingLocationActivation=false //Cancel the request for the enablement of the position
+ setFollowUserLocation(true)
}
}
-
+ else{
+ //check for this is when the map is used
+ mapStateViewModel.locationToShow = location
+ }
}
-
-
- private fun setLocationIconEnabled(enabled: Boolean){
+ override fun setLocationIconEnabled(enabled: Boolean){
if (enabled)
- showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
+ userLocationButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
else
- showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
+ userLocationButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
}
@@ -730,30 +691,18 @@
followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.walk_circle_inactive))
}
- private fun setFollowingUser(following: Boolean){
- updateFollowingIcon(following)
- followingUserLocation = following
- if(following)
- ignoreCameraMovementForFollowing = true
- }
-
/**
- * Method used for enabling / disabling the location
+ * This sets both the status on the component if it has been activated and the icon in the Fragment
*/
- private fun switchUserLocationStatus(view: View?){
- if(pendingLocationActivation || locationComponent.isLocationComponentEnabled)
- setMapLocationEnabled(false, false, true)
- else{
- if(locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
- pendingLocationActivation = true
- Log.d(DEBUG_TAG, "Request enable location")
- setMapLocationEnabled(true, false, true)
- } else{
- Log.w(DEBUG_TAG, "Cannot find location, no GPS")
- }
-
+ private fun setFollowUserLocation(enabled: Boolean){
+ if(locationInitialized) {
+ if (enabled)
+ locationComponent.cameraMode = CameraMode.TRACKING
+ else locationComponent.cameraMode = CameraMode.NONE
}
+ //update the icon by updating the livedata
+ mapStateViewModel.followingUserPosition.value = enabled
}
@@ -770,7 +719,6 @@
private val DEFAULT_ZOOM = 14.3
private const val POSITION_FOUND_ZOOM = 16.5
private const val NO_POSITION_ZOOM = 17.1
- private const val MAX_DIST_KM = 90.0
private const val DEBUG_TAG = "BusTO-MapLibreFrag"
private const val STOP_ACTIVE_IMG = "Stop-active"
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
@@ -2,7 +2,11 @@
import android.Manifest;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
import android.content.SharedPreferences;
+import android.net.Uri;
+import android.provider.Settings;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
@@ -11,10 +15,12 @@
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import it.reyboz.bustorino.BuildConfig;
+import it.reyboz.bustorino.R;
import java.util.Map;
@@ -98,6 +104,7 @@
});
}
+
public interface LocationRequestListener{
void onPermissionResult(boolean isCoarseGranted, boolean isFineGranted);
}
diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapLibreLocationEngine.kt b/app/src/main/java/it/reyboz/bustorino/map/MapLibreLocationEngine.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/map/MapLibreLocationEngine.kt
@@ -0,0 +1,114 @@
+package it.reyboz.bustorino.map
+
+import android.app.PendingIntent
+import android.os.Looper
+import android.util.Log
+import org.maplibre.android.location.engine.LocationEngine
+import org.maplibre.android.location.engine.LocationEngineCallback
+import org.maplibre.android.location.engine.LocationEngineRequest
+import org.maplibre.android.location.engine.LocationEngineResult
+
+import it.reyboz.bustorino.middleware.FusedNativeLocationProvider
+
+/**
+ * Adattatore che implementa l'interfaccia [LocationEngine] di MapLibre
+ * delegando a [FusedNativeLocationProvider].
+ *
+ * Separa completamente la logica di fusione dei provider (in [FusedNativeLocationProvider])
+ * dalla traduzione nel contratto MapLibre (qui).
+ *
+ * Uso:
+ * val provider = FusedNativeLocationProvider(context)
+ * val engine = MapLibreLocationEngine(provider)
+ * // poi passa engine a LocationComponentActivationOptions
+ */
+class MapLibreLocationEngine(
+ private val provider: FusedNativeLocationProvider,
+) : LocationEngine {
+
+ // Mappa callback MapLibre → listener del provider, per poterli rimuovere
+ private val callbackListeners =
+ HashMap<LocationEngineCallback<LocationEngineResult>, FusedNativeLocationProvider.LocationUpdateListener>()
+
+
+ override fun getLastLocation(callback: LocationEngineCallback<LocationEngineResult>) {
+ val location = provider.getLastLocationFromProviders()
+ if (location != null) {
+ callback.onSuccess(LocationEngineResult.create(location))
+ } else {
+ callback.onFailure(NoLocationException())
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // requestLocationUpdates — overload con Looper (quello usato da MapLibre)
+ // -------------------------------------------------------------------------
+
+ override fun requestLocationUpdates(
+ request: LocationEngineRequest,
+ callback: LocationEngineCallback<LocationEngineResult>,
+ looper: Looper?,
+ ) {
+ val providerListener = FusedNativeLocationProvider.LocationUpdateListener { location ->
+ callback.onSuccess(LocationEngineResult.create(location))
+ }
+
+ callbackListeners[callback] = providerListener
+ provider.addListener(providerListener)
+
+ // Avvia (o riavvia) il provider con i parametri della request MapLibre.
+ // Se il provider è già attivo con altri listener, stop+start lo ri-configura.
+ provider.startUpdates(
+ FusedNativeLocationProvider.Options(
+ minIntervalMs = request.interval,
+ minDisplacementM = request.displacement,
+ looper = looper,
+ )
+ )
+ }
+
+ // -------------------------------------------------------------------------
+ // requestLocationUpdates — overload con PendingIntent (background/geofencing)
+ // -------------------------------------------------------------------------
+
+ override fun requestLocationUpdates(
+ request: LocationEngineRequest,
+ pendingIntent: PendingIntent,
+ ) {
+ // PendingIntent is used for background updates via BroadcastReceiver.
+ // FusedNativeLocationProvider operates in the foreground: delegating to PendingIntent
+ // would require a different architecture (LocationManager.requestLocationUpdates
+ // with native PendingIntent). Not supported in this implementation.
+ throw UnsupportedOperationException(
+ "MapLibreLocationEngine does not support updates via PendingIntent. " +
+ "Use requestLocationUpdates(request, callback, looper) or " +
+ "implement a dedicated BroadcastReceiver."
+ )
+ }
+
+ // -------------------------------------------------------------------------
+ // removeLocationUpdates — overload con callback
+ // -------------------------------------------------------------------------
+
+ override fun removeLocationUpdates(callback: LocationEngineCallback<LocationEngineResult>) {
+ callbackListeners.remove(callback)?.let { providerListener ->
+ provider.removeListener(providerListener)
+ }
+ Log.d(DEBUG_TAG, "Removed location updates callback $callback")
+ }
+
+ // -------------------------------------------------------------------------
+ // removeLocationUpdates — overload con PendingIntent
+ // -------------------------------------------------------------------------
+
+ override fun removeLocationUpdates(pendingIntent: PendingIntent) {
+ throw UnsupportedOperationException(
+ "MapLibreLocationEngine does not support PendingIntent removal."
+ )
+ }
+
+ class NoLocationException : Exception()
+ companion object {
+ const val DEBUG_TAG = "BusTO-MapLocationEngine"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt
--- a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt
+++ b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt
@@ -213,9 +213,6 @@
Log.d(DEBUG_TAG, "Provider: $provider disabled")
}
- fun anyLocationProviderMatchesCriteria(cr: Criteria?): Boolean {
- return Permissions.anyLocationProviderMatchesCriteria(locMan, cr, true)
- }
/**
* Interface to be implemented to get the location request
diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/FusedNativeLocationProvider.kt b/app/src/main/java/it/reyboz/bustorino/middleware/FusedNativeLocationProvider.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/middleware/FusedNativeLocationProvider.kt
@@ -0,0 +1,263 @@
+package it.reyboz.bustorino.middleware
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import it.reyboz.bustorino.BuildConfig
+import it.reyboz.bustorino.util.Permissions
+import java.util.concurrent.CopyOnWriteArraySet
+
+/**
+ * Native Android location provider that fuses GPS_PROVIDER, NETWORK_PROVIDER
+ * and PASSIVE_PROVIDER with no dependency on Google Play Services.
+ *
+ * Standalone class to be used anywhere in the app
+ *
+ */
+class FusedNativeLocationProvider(context: Context) {
+
+ // -------------------------------------------------------------------------
+ // Public interface for location update consumers
+ // -------------------------------------------------------------------------
+
+ fun interface LocationUpdateListener {
+ fun onLocationUpdate(location: Location)
+ }
+
+ /**
+ * Configuration for location updates.
+ *
+ * @param minIntervalMs Minimum interval between updates in ms.
+ * @param minDisplacementM Minimum displacement in meters to trigger an update.
+ * @param looper Thread on which to receive callbacks. Null = main thread.
+ * @param useGps Enables GPS_PROVIDER.
+ * @param useNetwork Enables NETWORK_PROVIDER (WiFi + cell).
+ * @param usePassive Enables PASSIVE_PROVIDER (zero consumption, opportunistic updates).
+ */
+ data class Options(
+ val minIntervalMs: Long = 500L,
+ val minDisplacementM: Float = 5f,
+ val looper: Looper? = null,
+ val useGps: Boolean = true,
+ val useNetwork: Boolean = true,
+ val usePassive: Boolean = true,
+ )
+
+
+ private val locationManager =
+ context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+
+ // List of registered listeners (called on the configured looper)
+ private val listeners = CopyOnWriteArraySet<LocationUpdateListener>()
+
+ // Active Android listeners, one per provider
+ private val activeAndroidListeners = mutableListOf<LocationListener>()
+
+ @Volatile
+ private var bestLocation: Location? = null
+
+ @Volatile
+ private var running = false
+
+ private var runningOptions = Options(500L, 5f, null, true, true, true)
+
+ private val activeProviders = ArrayList<String>()
+
+ private var havePermissions = false
+
+ //private val removedListener = mutableSetOf<LocationUpdateListener>()
+
+ private val handler by lazy { Handler(runningOptions.looper ?: Looper.getMainLooper()) }
+
+ //private var appContext = context.applicationContext
+
+ // -------------------------------------------------------------------------
+ // Public API
+ // -------------------------------------------------------------------------
+
+ /**
+ * Adds a listener. Can be called before or after [startUpdates].
+ */
+ fun addListener(listener: LocationUpdateListener) {
+ if(BuildConfig.DEBUG)
+ Log.d(DEBUG_TAG, "Adding listener $listener")
+ synchronized(listeners) {
+ listeners.add(listener)
+ }
+ }
+
+ /**
+ * Removes a previously registered listener.
+ */
+ fun removeListener(listener: LocationUpdateListener) {
+ if(BuildConfig.DEBUG)
+ Log.d(DEBUG_TAG, "Removing listener $listener")
+ synchronized(listeners){
+ if(listeners.remove(listener)){
+ if(listeners.isEmpty()) stopUpdates()
+ }
+ if(BuildConfig.DEBUG)
+ Log.d(DEBUG_TAG, "Listener now size: ${listeners.size}")
+ }
+ }
+
+ /**
+ * Starts receiving location updates from the enabled providers.
+ * If already running, stops the existing providers first and restarts
+ * them with the new configuration.
+ *
+ * Requires ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION.
+ */
+ @SuppressLint("MissingPermission")
+ fun startUpdates(options: Options?): Boolean {
+ if (running) stopUpdates()
+ if (options!=null){
+ runningOptions = options
+ }
+ val selectedProviders = buildList {
+ if (runningOptions.useGps) add(LocationManager.GPS_PROVIDER)
+ if (runningOptions.useNetwork) add(LocationManager.NETWORK_PROVIDER)
+ if (runningOptions.usePassive) add(LocationManager.PASSIVE_PROVIDER)
+ }
+
+ val effectiveLooper = runningOptions.looper ?: Looper.getMainLooper()
+
+ selectedProviders.forEach { provider ->
+ if (!locationManager.isProviderEnabled(provider)) return@forEach
+
+ val locListener = LocationListener { location ->
+ if (isBetterLocation(location, bestLocation)) {
+ bestLocation = location
+ //Log.d(DEBUG_TAG, "New best location: $bestLocation")
+ notifyListeners(location)
+ }
+ }
+
+ //runCatching {
+ locationManager.requestLocationUpdates(
+ provider,
+ runningOptions.minIntervalMs,
+ runningOptions.minDisplacementM,
+ locListener,
+ effectiveLooper,
+ )
+ activeAndroidListeners.add(locListener)
+ activeProviders.add(provider)
+ //}
+ }
+
+ running = activeAndroidListeners.isNotEmpty()
+ Log.d(DEBUG_TAG, "Started location updates, running: $running, with providers: $activeProviders")
+ return running
+ }
+
+ /**
+ * Stops all updates and releases the Android listeners.
+ * [LocationUpdateListener]s registered via [addListener] are retained:
+ * calling [startUpdates] again will resume delivering updates to them.
+ */
+ private fun stopUpdatesInternal() {
+ if(!running) //we have already done this
+ return
+ Log.d(DEBUG_TAG, "Actually stopping location updates, active providers: $activeProviders")
+ activeAndroidListeners.forEach { listener ->
+ runCatching { locationManager.removeUpdates(listener) }
+ }
+ activeAndroidListeners.clear()
+ running = false
+ activeProviders.clear()
+ }
+
+ /**
+ * Returns the best known location cached by the enabled providers,
+ * without starting continuous updates.
+ *
+ * May return null if no provider has ever acquired a fix
+ * (e.g. first launch, device just turned on).
+ *
+ * Do not use if we do not have the Location permission
+ */
+ @SuppressLint("MissingPermission")
+ fun getLastLocationFromProviders(): Location? {
+ val candidatesLocations = listOf(
+ LocationManager.GPS_PROVIDER,
+ LocationManager.NETWORK_PROVIDER,
+ LocationManager.PASSIVE_PROVIDER,
+ ).mapNotNull { provider ->
+ locationManager.getLastKnownLocation(provider)
+ }
+
+ // Among the candidates, the most accurate wins. On equal accuracy,
+ // the most recent wins.
+ return candidatesLocations.minWithOrNull(
+ compareBy({ it.accuracy }, { -it.time })
+ )
+ }
+
+ //fun getLastReceivedBestLocation(): Location? {
+ // return bestLocation
+ //}
+
+
+ private fun notifyListeners(location: Location) {
+ //synchronized(listeners) {
+ listeners.forEach { it.onLocationUpdate(location) }
+ }
+
+ /**
+ * Public call for stopping the updates
+ */
+ fun stopUpdates() {
+ Log.d(DEBUG_TAG, "Stopping updates")
+ if (Looper.myLooper() == handler.looper) {
+ stopUpdatesInternal()
+ } else {
+ handler.post { stopUpdatesInternal() }
+ }
+ }
+
+
+
+ companion object {
+ private const val TIME_DELAY = 2 * 60 * 1_000L // two minutes
+ private const val ACCURACY_DEGRADATION_THRESHOLD_M = 200f
+ private const val DEBUG_TAG = "BusTO-FusedLocationProv"
+
+ /**
+ * Determines whether [candidate] is a better location than [current].
+ *
+ * Criteria, in priority order:
+ * 1. If the candidate is newer than [TIME_DELAY], always accept it.
+ * 2. If it is significantly older, reject it.
+ * 3. Equal freshness: the one with lower accuracy (tighter radius) wins.
+ * 4. Same provider, same freshness delta, and not degrading too much: accept.
+ */
+ @JvmStatic
+ fun isBetterLocation(candidate: Location, current: Location?): Boolean {
+ if (current == null) return true
+
+ val timeDeltaMs = candidate.time - current.time
+
+ return when {
+ timeDeltaMs > TIME_DELAY -> true // much more recent: accept immediately
+ timeDeltaMs < -TIME_DELAY -> false // much older: reject immediately
+ else -> {
+ val accuracyDeltaM = candidate.accuracy - current.accuracy
+ when {
+ accuracyDeltaM < 0 -> true // more accurate
+ accuracyDeltaM == 0f && timeDeltaMs > 0 -> true // same accuracy, fresher
+ timeDeltaMs > 0
+ && accuracyDeltaM <= ACCURACY_DEGRADATION_THRESHOLD_M
+ && candidate.provider == current.provider -> true
+ else -> false
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/util/Permissions.java b/app/src/main/java/it/reyboz/bustorino/util/Permissions.java
deleted file mode 100644
--- a/app/src/main/java/it/reyboz/bustorino/util/Permissions.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package it.reyboz.bustorino.util;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.location.Criteria;
-import android.location.LocationManager;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.RequiresApi;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import java.util.List;
-
-public class Permissions {
- final static public String DEBUG_TAG = "BusTO -Permissions";
-
- final static public int PERMISSION_REQUEST_POSITION = 33;
- final static public String LOCATION_PERMISSION_GIVEN = "loc_permission";
- final static public int STORAGE_PERMISSION_REQ = 291;
-
- final static public int PERMISSION_OK = 0;
- final static public int PERMISSION_ASKING = 11;
- final static public int PERMISSION_NEG_CANNOT_ASK = -3;
-
- final static public String[] LOCATION_PERMISSIONS={Manifest.permission.ACCESS_COARSE_LOCATION,
- Manifest.permission.ACCESS_FINE_LOCATION};
- //final static public String[] NOTIFICATION_PERMISSION={Manifest.permission.POST_NOTIFICATIONS};
-
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
- public static String[] getNotificationPermissions(){
- return new String[]{Manifest.permission.POST_NOTIFICATIONS};
- }
-
- public static boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) {
- List<String> providers = mng.getProviders(cr, enabled);
- Log.d(DEBUG_TAG, "Getting enabled location providers: ");
- for (String s : providers) {
- Log.d(DEBUG_TAG, "Provider " + s);
- }
- return !providers.isEmpty();
- }
- public static boolean isPermissionGranted(Context con,String permission){
- return ContextCompat.checkSelfPermission(con, permission) == PackageManager.PERMISSION_GRANTED;
- }
-
- public static boolean bothLocationPermissionsGranted(Context con){
- return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) &&
- isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION);
- }
- public static boolean anyLocationPermissionsGranted(Context con){
- return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) ||
- isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION);
- }
-
- public static void assertLocationPermissions(Context con, Activity activity) {
- if(!isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) ||
- !isPermissionGranted(con,Manifest.permission.ACCESS_COARSE_LOCATION)){
- ActivityCompat.requestPermissions(activity,new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_POSITION);
- }
- }
-
- /**
- * Check if the system requires the POST_NOTIFICATION permission to send notifications
- * @return true if required
- */
- public static boolean isNotificationPermissionNeeded(){
- return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU);
- }
-}
diff --git a/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt b/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt
@@ -0,0 +1,156 @@
+package it.reyboz.bustorino.util
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.location.Criteria
+import android.location.LocationManager
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.core.app.ActivityCompat
+import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.startActivity
+import it.reyboz.bustorino.R
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.concurrent.atomics.AtomicInt
+
+class Permissions private constructor(private val appContext: Context) {
+
+ /*
+ @get:RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
+ val notificationPermissions: Array<String>
+ //final static public String[] NOTIFICATION_PERMISSION={Manifest.permission.POST_NOTIFICATIONS};
+ get() = arrayOf<String>(Manifest.permission.POST_NOTIFICATIONS)
+
+ */
+ private var askedTimesLocation = AtomicInteger(0)
+
+ fun anyLocationProviderMatchesCriteria(mng: LocationManager, cr: Criteria, enabled: Boolean): Boolean {
+ val providers = mng.getProviders(cr, enabled)
+ Log.d(DEBUG_TAG, "Getting enabled location providers: ")
+ for (s in providers) {
+ Log.d(DEBUG_TAG, "Provider " + s)
+ }
+ return !providers.isEmpty()
+ }
+
+
+ fun checkRequestLocationPermissions(activity: Activity, launcher: ActivityResultLauncher<Array<String>>): Boolean {
+
+ //activity.getSharedPreferences(, Context.MODE_PRIVATE)
+ var launched = false
+ if(shouldShowRequestPermissionRationale(activity,Manifest.permission.ACCESS_FINE_LOCATION)){
+ Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_LONG).show()
+ } /*else{
+ //cannot show the dialog anymore, go to the settings
+ openShowAppSettingsLocationDialog()
+ }
+ */
+ val reqTimes = askedTimesLocation.getAndIncrement()
+ Log.d(DEBUG_TAG, "Requesting location permissions, asked ${reqTimes} times ")
+ if(reqTimes > 4){
+ openShowAppSettingsLocationDialog()
+ } else{
+ launcher.launch(LOCATION_PERMISSIONS)
+ launched = true
+ }
+ return launched
+ }
+
+ /**
+ * Show alert dialog to enable location permission
+ */
+ fun openShowAppSettingsLocationDialog() {
+ val context = appContext
+ val builder = AlertDialog.Builder(context)
+
+ builder.setTitle(R.string.no_permission_dialog_title)
+ builder.setMessage(R.string.no_permission_dialog_text_location)
+ builder.setPositiveButton(
+ R.string.no_permission_dialog_open,
+ DialogInterface.OnClickListener { dialogInterface: DialogInterface?, i: Int ->
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.setData(Uri.fromParts("package", context.getPackageName(), null))
+ context.startActivity(intent)
+ })
+ builder.setNegativeButton(android.R.string.cancel, null)
+ builder.show()
+ }
+
+
+ fun assertLocationPermissions(con: Context, activity: Activity) {
+ if (!isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) ||
+ !isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION)
+ ) {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf<String>(Manifest.permission.ACCESS_FINE_LOCATION),
+ PERMISSION_REQUEST_POSITION
+ )
+ }
+ }
+
+
+
+ companion object{
+ const val DEBUG_TAG: String = "BusTO -Permissions"
+
+ const val PERMISSION_REQUEST_POSITION: Int = 33
+ const val LOCATION_PERMISSION_GIVEN: String = "loc_permission"
+ const val STORAGE_PERMISSION_REQ: Int = 291
+
+ const val PERMISSION_OK: Int = 0
+ const val PERMISSION_ASKING: Int = 11
+ const val PERMISSION_NEG_CANNOT_ASK: Int = -3
+
+ @JvmField
+ val LOCATION_PERMISSIONS: Array<String> = arrayOf(
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+
+ @JvmStatic
+ fun isPermissionGranted(con: Context, permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(con, permission) == PackageManager.PERMISSION_GRANTED
+ }
+ @JvmStatic
+ fun bothLocationPermissionsGranted(con: Context): Boolean {
+ return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) &&
+ isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION)
+ }
+
+ @JvmStatic
+ fun anyLocationPermissionsGranted(con: Context): Boolean {
+ return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) ||
+ isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION)
+ }
+
+ /**
+ * Check if the system requires the POST_NOTIFICATION permission to send notifications
+ * @return true if required
+ */
+ @JvmStatic
+ fun isNotificationPermissionNeeded() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+
+
+
+ @Volatile
+ private var instance: Permissions? = null
+
+ fun getInstance(context: Context) =
+ instance ?: synchronized(this) {
+ instance ?: Permissions(context.applicationContext).also { instance = it }
+ }
+
+ }
+}
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - View Model components
+ Copyright (C) 2025 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.viewmodels
import android.app.Application
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt
@@ -1,5 +1,6 @@
package it.reyboz.bustorino.viewmodels
+import android.location.Location
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import it.reyboz.bustorino.map.MapCameraState
@@ -33,6 +34,11 @@
return restoreMapState(map, this.savedCameraState)
}
+ var locationToShow: Location? = null
+
+ val locationActive = MutableLiveData(false)
+ val followingUserPosition = MutableLiveData(false)
+
companion object{
fun restoreMapState(map: MapLibreMap, savedCameraState: MapCameraState?): Boolean {
val state = savedCameraState ?: return false
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - View Model components
+ Copyright (C) 2023 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.viewmodels
import android.app.Application
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - View Model components
+ Copyright (C) 2026 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.viewmodels
import android.app.Application
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt
@@ -1,3 +1,20 @@
+/*
+ BusTO - View Model components
+ Copyright (C) 2025 Fabio Mazza
+
+ 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 <http://www.gnu.org/licenses/>.
+ */
package it.reyboz.bustorino.viewmodels
import android.app.Application
@@ -81,9 +98,11 @@
addStopsCallback)
}
}
+
+ //this is only saved at the end, is it really necessary?
var lastUserLocation: Location? = null
companion object{
private const val DEBUG_TAG = "BusTOStopMapViewModel"
}
}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -175,6 +175,7 @@
<string name="enable_position_message_map">Allow access to location to show it on the map</string>
<string name="enable_position_message_nearby">Allow access to location to show stops nearby</string>
<string name="enableGpsText">Please enable location on the device</string>
+ <string name="no_gps_on_device">No GPS receiver found on the device!</string>
<string name="database_update_msg_inapp">Database update in progress&#8230;</string>
<string name="database_update_msg_notif">Updating the database</string>
<string name="database_update_req">Force database update</string>
@@ -238,6 +239,7 @@
<string name="too_many_permission_asks">Asked for %1$s permission too many times</string>
<string name="permission_storage_maps_msg">Cannot use the map with the storage permission!</string>
+
<string name="storage_permission">storage</string>
<string name="message_crash">The application has crashed because you encountered a bug.
\nIf you want, you can help the developers by sending the crash report via email.
@@ -342,6 +344,10 @@
<string name="grant_location_permission">Grant location permission</string>
<string name="location_permission_granted">Location permission granted</string>
<string name="location_permission_not_granted">Location permission has not been granted</string>
+ <string name="no_permission_dialog_title">Missing permission</string>
+ <!-- If more permissions are needed, adapt this string to make if "fillable" -->
+ <string name="no_permission_dialog_text_location">To use this functionality, the application needs access to the location, which can not only be granted in the system settings.</string>
+ <string name="no_permission_dialog_open">Open settings</string>
<string name="close_tutorial">OK, close the tutorial</string>
<string name="close_tutorial_short">Close the tutorial</string>

File Metadata

Mime Type
text/plain
Expires
Wed, May 13, 14:26 (2 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1895856
Default Alt Text
D237.1778675217.diff (91 KB)

Event Timeline