diff --git a/app/build.gradle b/app/build.gradle --- a/app/build.gradle +++ b/app/build.gradle @@ -140,7 +140,7 @@ implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' //maplibre - implementation 'org.maplibre.gl:android-sdk:12.0.1' + implementation 'org.maplibre.gl:android-sdk:13.1.0' implementation 'org.maplibre.gl:android-sdk-turf:6.0.1' implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2' 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 . + */ 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 . + */ 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 . + */ 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,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 . + */ package it.reyboz.bustorino.fragments import android.animation.ValueAnimator @@ -28,6 +45,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 @@ -48,7 +66,13 @@ import org.maplibre.android.camera.CameraPosition import org.maplibre.android.geometry.LatLng import org.maplibre.android.location.LocationComponent +import org.maplibre.android.location.LocationComponentActivationOptions import org.maplibre.android.location.LocationComponentOptions +import org.maplibre.android.location.engine.LocationEngineCallback +import org.maplibre.android.location.engine.LocationEngineResult +import org.maplibre.android.location.engine.MapLibreFusedLocationEngineImpl +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.MapView import org.maplibre.android.maps.OnMapReadyCallback @@ -87,6 +111,8 @@ protected lateinit var sharedPreferences: SharedPreferences protected lateinit var bottomSheetBehavior: BottomSheetBehavior + protected lateinit var locationEngine: MapLibreFusedLocationEngineImpl + private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key -> /*when(key){ @@ -134,7 +160,8 @@ //private lateinit var symbolManager: SymbolManager protected val mapStateViewModel: MapStateViewModel by viewModels() - + protected var locationInitialized = false + protected var mapInitialized = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -181,6 +208,8 @@ } } + + @Deprecated("Deprecated in Java") override fun onLowMemory() { mapView.onLowMemory() @@ -398,6 +427,28 @@ } } + abstract fun onMapLocationComponentInitialized() + + @SuppressLint("MissingPermission") + protected fun initializeMapLocationComponent(map: MapLibreMap, context: Context, style: Style?){ + val mStyle = style ?: map.style + mStyle?.let{ style -> + locationComponent = map.locationComponent + val options = LocationComponentActivationOptions.builder(context, style) + .useDefaultLocationEngine(true) + .build() + locationComponent.activateLocationComponent(options) + locationComponent.isLocationComponentEnabled = true + //locationComponent.cameraMode = CameraMode.TRACKING + //locationComponent.renderMode = RenderMode.COMPASS + locationInitialized = true + //UI Elements + // setLocationIconEnabled(true) + //setFollowingUser(true) + onMapLocationComponentInitialized() + } + } + /** * Update function for the bus positions 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 . + */ package it.reyboz.bustorino.fragments import android.content.Context 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 @@ -89,7 +89,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 @@ -232,7 +232,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, "") @@ -528,6 +528,10 @@ } + override fun onMapLocationComponentInitialized() { + //THIS IS USEFUL ONLY IN MapLibreFragment, the new initialization routine has not been implemented yet TODO + } + /** * Switch position icon from activ */ @@ -569,7 +573,7 @@ initSymbolManager(mapReady, style) toRunWhenMapReady?.run() toRunWhenMapReady = null - mapInitialized.set(true) + mapInitialized = true if(patternShown!=null){ viewModel.stopsForPatternLiveData.value?.let { @@ -922,7 +926,7 @@ } private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stopsToSort: List, 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 { 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 . + */ 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 . + */ 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,27 @@ +/* + 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 . +*/ 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 +31,15 @@ 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 @@ -41,18 +53,18 @@ import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds +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.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 +80,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,72 +87,65 @@ // 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 + private var zoomedToFirstLocation = false - //TODO: Rewrite this mess using LocationEngineProvider in MapLibre - private val positionRequestLauncher = registerForActivityResult, Map>( - 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) + //TODO: Rewrite this mess using LocationEngineProvider in MapLibre + private 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() + } + } + ) + + protected val mapLibreLocationCallback = object : LocationEngineCallback { + override fun onSuccess(result: LocationEngineResult) { + val location: Location? = result.lastLocation + location?.let { + if(BuildConfig.DEBUG) + Log.d(DEBUG_TAG, "Lat: ${it.latitude}, Lon: ${it.longitude}") + + if(mapInitialized && !zoomedToFirstLocation) { + map?.apply{ + animateCamera(CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder().target(LatLng(it.latitude, it.longitude)).build()), + 1000) + locationComponent.cameraMode = CameraMode.TRACKING } - 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") + zoomedToFirstLocation = true } - }) - private val showUserPositionRequestLauncher = - registerForActivityResult, Map>( - 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") - }) + else{ + stopsViewModel.lastUserLocation = location + } + } + } + + override fun onFailure(exception: Exception) { + Log.e(DEBUG_TAG, "Errore: ${exception.message}") + } + } //BUS POSITIONS private var usingMQTTPositions = true // THIS IS INSIDE VIEW MODEL NOW @@ -200,8 +204,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) @@ -239,8 +243,10 @@ setFollowingUser(!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 +257,8 @@ // PERMISSIONS REQUESTED AFTER MAP SETUP } + */ + // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { @@ -310,7 +318,6 @@ //setupLayers(style) addImagesStyle(style) - initMapUserLocation(style, mapReady, requireContext()) //init stop layer with this val stopsInCache = stopsViewModel.getAllStopsLoaded() if(stopsInCache.isEmpty()) @@ -321,11 +328,22 @@ // Start observing data now that everything is set up observeStops() + + //enable location + val hasGps = deviceHasGpsProvider() + if(hasGps) { + if (Permissions.bothLocationPermissionsGranted(context)) { + initializeMapLocationComponent(mapReady, context, style) + } else { + setLocationIconEnabled(false) + setFollowingUser(false) + //positionRequestResponder.launch(Permissions.LOCATION_PERMISSIONS) + } + } } 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 +360,20 @@ mapReady.addOnCameraMoveStartedListener { v-> if(v== MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE){ //the user is moving the map - isUserMovingCamera = true + //isUserMovingCamera = true + setFollowingUser(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 +398,24 @@ } 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 + val lastLoc = stopsViewModel.lastUserLocation + val targetLoc = if(lastLoc == null) + LatLng(DEFAULT_CENTER_LAT,DEFAULT_CENTER_LON) + else + LatLng(lastLoc.latitude, lastLoc.longitude) + + 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{ @@ -627,9 +653,9 @@ livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> 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,6 +666,44 @@ // ------ LOCATION STUFF ----- + + + + @SuppressLint("MissingPermission") + override fun onMapLocationComponentInitialized() { + locationComponent.cameraMode = CameraMode.TRACKING + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.locationEngine?.apply{ + Log.d(DEBUG_TAG, "Map Location engine: it") + //it(mapLibreLocationCallback) + // this is only called once + getLastLocation(object : LocationEngineCallback { + override fun onSuccess(res: LocationEngineResult?) { + res?.let { + res.lastLocation?.let{ loc -> + if(mapInitialized) + map?.cameraPosition = CameraPosition.Builder().target(LatLng(loc.latitude, loc.longitude)).build() + else + stopsViewModel.lastUserLocation = loc + } + } + } + + override fun onFailure(p0: java.lang.Exception) { + Log.e(DEBUG_TAG, "Failed to get the last location", p0) + } + + }) + requestLocationUpdates(LocationEngineRequest.Builder(500).setDisplacement(20.0f).build(), + mapLibreLocationCallback, null) + } + + //UI Elements + setLocationIconEnabled(true) + setFollowingUser(true) + } + + /* @SuppressLint("MissingPermission") private fun requestInitialUserLocation() { val provider : String = LocationManager.GPS_PROVIDER//getBestLocationProvider() @@ -673,10 +737,11 @@ } - + */ /** * Handles logic of enabling the user location on the map */ + /* @SuppressLint("MissingPermission") private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) { if (enabled) { @@ -713,13 +778,21 @@ } + */ + + @SuppressLint("MissingPermission") + private fun setMapLocationEnabled(enabled: Boolean){ + map?.locationComponent?.isLocationComponentEnabled = enabled + //map?.cameraPosition = + setLocationIconEnabled(enabled) + } private 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)) } @@ -733,27 +806,40 @@ private fun setFollowingUser(following: Boolean){ updateFollowingIcon(following) followingUserLocation = following - if(following) - ignoreCameraMovementForFollowing = true + //if(following) + // ignoreCameraMovementForFollowing = true } /** - * Method used for enabling / disabling the location + * Method used for enabling / disabling the location from the buttons */ 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) + val enabled = if(locationInitialized) locationComponent.isLocationComponentEnabled else false + val context = context ?: return + if(enabled) { + setMapLocationEnabled(false) + } + else if(deviceHasGpsProvider()) { + if(Permissions.bothLocationPermissionsGranted(context)){ + setMapLocationEnabled(true) } else{ - Log.w(DEBUG_TAG, "Cannot find location, no GPS") + Log.d(DEBUG_TAG, "Requesting permissions to show location") + if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ + Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_LONG).show() + positionRequestResponder.launch(Permissions.LOCATION_PERMISSIONS) + } else{ + //cannot show the dialog anymore, go to the settings + openShowAppSettingsLocationDialog() + } + } + } else{ + context.let { + Toast.makeText(it, R.string.no_gps_on_device, Toast.LENGTH_SHORT).show() } - } + + } @@ -776,6 +862,8 @@ private const val STOP_ACTIVE_IMG = "Stop-active" const val FRAGMENT_TAG = "BusTOMapFragment" + private const val PERM_LOC_COARSE = Manifest.permission.ACCESS_COARSE_LOCATION + private const val PERM_LOC_FINE = Manifest.permission.ACCESS_FINE_LOCATION private const val LOCATION_PERMISSION_REQUEST_CODE = 981202 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,25 @@ }); } + /** + * Show alert dialog to enable location permission + */ + protected void openShowAppSettingsLocationDialog(){ + Context mContext = getContext(); + if (mContext==null) return; + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + + 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, i) -> { + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", mContext.getPackageName(), null)); + startActivity(intent); + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + public interface LocationRequestListener{ void onPermissionResult(boolean isCoarseGranted, boolean isFineGranted); } 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 . + */ package it.reyboz.bustorino.viewmodels import android.app.Application 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 . + */ 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 . + */ 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 . + */ package it.reyboz.bustorino.viewmodels import android.app.Application 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 @@ Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device + No GPS receiver found on the device! Database update in progress… Updating the database Force database update @@ -238,6 +239,7 @@ Asked for %1$s permission too many times Cannot use the map with the storage permission! + storage 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 @@ Grant location permission Location permission granted Location permission has not been granted + Missing permission + + To use this functionality, the application needs access to the location, which can not only be granted in the system settings. + Open settings OK, close the tutorial Close the tutorial