diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index ce58ca4..5a15293 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,860 +1,906 @@ /* 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.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.graphics.Paint 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.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.FiveTNormalizer import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.* import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.middleware.LocationUtils +import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.viewmodels.LinesViewModel import it.reyboz.bustorino.viewmodels.LivePositionsViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.FolderOverlay import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList class LinesDetailFragment() : ScreenBaseFragment() { private var lineID = "" private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List private lateinit var map: MapView private var viewingPattern: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() private val mapViewModel: MapViewModel by viewModels() private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton private var favoritesButton: ImageButton? = null - private lateinit var locationIcon: ImageButton + private var locationIcon: ImageButton? = null private var isLineInFavorite = false private var appContext: Context? = null + private var isLocationPermissionOK = false private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged -> if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) newFavorites?.let {favorites-> isLineInFavorite = favorites.contains(lineID) //if the button has been intialized, change the icon accordingly favoritesButton?.let { button-> //avoid crashes if fragment not attached if(context==null) return@let if(isLineInFavorite) { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()} } else { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null)) appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()} } } } } private lateinit var stopsRecyclerView: RecyclerView private lateinit var descripTextView: TextView //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { fragmentListener.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(fragmentListener == null){ Log.e(DEBUG_TAG, "Fragment listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { TODO("Not yet implemented") } } private var polyline: Polyline? = null //private var stopPosList = ArrayList() private lateinit var stopsOverlay: FolderOverlay private lateinit var locationOverlay: LocationOverlay + private val locationOverlayResponder = object : LocationOverlay.OverlayCallbacks{ + override fun onDisableFollowMyLocation() { + Log.d(DEBUG_TAG, "Follow location disabled") + } + + override fun onEnableFollowMyLocation() { + Log.d(DEBUG_TAG, "Follow location enabled") + } + } + //location request responder + private val locationRequestResLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ res -> + //onActivityResult(res: map) + if(res[Permissions.LOCATION_PERMISSIONS[0]] ==true || res[Permissions.LOCATION_PERMISSIONS[1]] ==true) + locationIcon?.let { onPositionIconButtonClick(it) } + else{ + context?.let { Toast.makeText(it,R.string.location_permission_not_granted, Toast.LENGTH_SHORT).show() } + } + } //fragment actions private lateinit var fragmentListener: CommonFragmentListener private val stopTouchResponder = TouchResponder { stopID, stopName -> Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") fragmentListener.requestArrivalsForStopID(stopID) } private var showOnTopOfLine = true private var recyclerInitDone = false private var useMQTTPositions = true //position of live markers private val busPositionMarkersByTrip = HashMap() private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap() private val liveBusViewModel: LivePositionsViewModel by viewModels() @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") switchButton = rootView.findViewById(R.id.switchImageButton) locationIcon = rootView.findViewById(R.id.locationEnableIcon) favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) descripTextView = rootView.findViewById(R.id.lineDescripTextView) descripTextView.visibility = View.INVISIBLE val titleTextView = rootView.findViewById(R.id.titleTextView) titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay( GtfsUtils.getLineNameFromGtfsID(lineID), true) favoritesButton?.isClickable = true favoritesButton?.setOnClickListener { if(lineID.isNotEmpty()) PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite) } val preferences = PreferencesHolder.getMainSharedPreferences(requireContext()) val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) if(favorites!=null && favorites.contains(lineID)){ favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) isLineInFavorite = true } appContext = requireContext().applicationContext preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter initializeMap(rootView) initializeRecyclerView() switchButton.setOnClickListener{ if(map.visibility == View.VISIBLE){ map.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE - locationIcon.visibility = View.GONE + locationIcon?.visibility = View.GONE viewModel.setMapShowing(false) liveBusViewModel.stopMatoUpdates() //map.overlayManager.remove(busPositionsOverlay) switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) } else{ stopsRecyclerView.visibility = View.GONE map.visibility = View.VISIBLE - locationIcon.visibility = View.VISIBLE + locationIcon?.visibility = View.VISIBLE viewModel.setMapShowing(true) //map.overlayManager.add(busPositionsOverlay) //map. if(useMQTTPositions) liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else liveBusViewModel.requestGTFSUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) } } - locationIcon.setOnClickListener { - if(locationOverlay.isMyLocationEnabled){ - //switch off - locationOverlay.disableMyLocation() - locationIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) - //show message - Toast.makeText(requireContext(),R.string.location_disabled,Toast.LENGTH_SHORT).show() - } else{ - //switch on - locationOverlay.enableMyLocation() - locationIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) - //show message - Toast.makeText(requireContext(),R.string.location_enabled,Toast.LENGTH_SHORT).show() - } + locationIcon?.let {view -> + if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext())) + setLocationIconEnabled(false) + //set click Listener + view.setOnClickListener(this::onPositionIconButtonClick) } + //set + + viewModel.setRouteIDQuery(lineID) val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> if(map.visibility ==View.VISIBLE) showPatternWithStopsOnMap(stops) else{ if(stopsRecyclerView.visibility==View.VISIBLE) showStopsAsList(stops) } } viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> if(route == null){ //need to close the fragment activity?.supportFragmentManager?.popBackStack() return@observe } descripTextView.text = route.longName descripTextView.visibility = View.VISIBLE } if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ val patt = viewModel.selectedPatternLiveData.value!! Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") showPattern(patt) pausedFragment = false } Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") //listeners patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) //viewModel.setPatternToDisplay(patternWithStops) setPatternAndReqStops(patternWithStops) Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") if(map?.visibility == View.VISIBLE) { busPositionsOverlay.closeAllInfoWindows() busPositionsOverlay.items.clear() busPositionMarkersByTrip.clear() stopAnimations() tripMarkersAnimators.clear() liveBusViewModel.retriggerPositionUpdate() } } override fun onNothingSelected(p0: AdapterView<*>?) { } } //live bus positions liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ if(map.visibility == View.GONE || viewingPattern ==null){ //DO NOTHING return@observe } val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID) //filter buses with direction, show those only with the same direction val outmap = HashMap>() val currentPattern = viewingPattern!!.pattern val numUpds = it.entries.size Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") val patternsDirections = HashMap() for((tripId, pair) in it.entries){ //remove trips with wrong line ideas if(pair.first.routeID!=filtdLineID) continue if(pair.second!=null && pair.second?.pattern !=null){ val dir = pair.second?.pattern?.directionId if(dir !=null && dir == currentPattern.directionId){ outmap[tripId] = pair } patternsDirections.set(tripId,if (dir!=null) dir else -10) } else{ outmap[tripId] = pair //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") patternsDirections[tripId] = -10 } } Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n updateBusPositionsInMap(outmap) //if not using MQTT positions if(!useMQTTPositions){ liveBusViewModel.requestDelayedGTFSUpdates(2000) } } //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.downloadTripsFromMato( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } return rootView } + private fun setLocationIconEnabled(setTrue: Boolean){ + if(setTrue) + locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) + else + locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + } + + /** + * Switch position icon from activ + */ + private fun onPositionIconButtonClick(view: View){ + if(locationOverlay.isMyLocationEnabled){ + //switch off + locationOverlay.disableMyLocation() + //set image on respective button + setLocationIconEnabled(false) + if(context!=null) { + if (LocationUtils.isLocationEnabled(context)) { + //show message + Toast.makeText(context, R.string.location_disabled, Toast.LENGTH_SHORT).show() + } + } + } else{ + //switch on + locationOverlay.enableMyLocation() + if(context!=null) { + if(!Permissions.anyLocationPermissionsGranted(context)) { + locationRequestResLauncher.launch(Permissions.LOCATION_PERMISSIONS) + Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() + } + else if (LocationUtils.isLocationEnabled(context)) { + //set image on button + setLocationIconEnabled(true) + //show message + Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show() + } else{ + Toast.makeText(context, R.string.map_location_disabled_device, Toast.LENGTH_SHORT).show() + } + } + } + } + private fun initializeMap(rootView : View){ val ctx = requireContext().applicationContext Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) map = rootView.findViewById(R.id.lineMap) map.let { it.setTileSource(TileSourceFactory.MAPNIK) - locationOverlay = LocationOverlay.createLocationOverlay(true, it, requireContext(), object : LocationOverlay.OverlayCallbacks{ - override fun onDisableFollowMyLocation() { - Log.d(DEBUG_TAG, "Follow location disabled") - } - - override fun onEnableFollowMyLocation() { - Log.d(DEBUG_TAG, "Follow location enabled") - } - - }) + locationOverlay = LocationOverlay.createLocationOverlay(true, it, requireContext(), locationOverlayResponder) locationOverlay.disableFollowLocation() stopsOverlay = FolderOverlay() busPositionsOverlay = FolderOverlay() //map.setTilesScaledToDpi(true); //map.setTilesScaledToDpi(true); it.setFlingEnabled(true) it.setUseDataConnection(true) // add ability to zoom with 2 fingers it.setMultiTouchControls(true) it.minZoomLevel = 12.0 //map controller setup val mapController = it.controller var zoom = 12.0 var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") zoom = mapViewModel.currentZoom.value!! centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) /*viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) - */ - } mapController.setZoom(zoom) mapController.setCenter(centerMap) Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") //map.invalidate() it.overlayManager.add(stopsOverlay) it.overlayManager.add(locationOverlay) it.overlayManager.add(busPositionsOverlay) zoomToCurrentPattern() firstInit = false - } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } private fun stopAnimations(){ for(anim in tripMarkersAnimators.values){ anim.cancel() } } private fun savePatternsToShow(patterns: List){ val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops -> if(p1.pattern.directionId != p2.pattern.directionId) return@Comparator p1.pattern.directionId - p2.pattern.directionId else return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size) } currentPatterns = patterns.sortedWith(patternsSorter) patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } viewingPattern?.let { showPattern(it) } } /** * Called when the position of the spinner is updated */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } private fun showPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in currentPatterns.indices){ if(currentPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0) patternsSpinner.setSelection(pos) //set pattern setPatternAndReqStops(patternWs) } private fun zoomToCurrentPattern(){ var pointsList: List if(viewingPattern==null) { Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") if(polyline!=null) pointsList = polyline!!.actualPoints else { Log.d(DEBUG_TAG, "The polyline is null") return } }else{ val pattern = viewingPattern!!.pattern pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) } var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } val del = 0.008 //map.controller.c Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) } private fun showPatternWithStopsOnMap(stops: List){ Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") if(viewingPattern==null || map == null) return val pattern = viewingPattern!!.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } //val polyLine=Polyline(map) //polyLine.setPoints(pointsList) //save points if(map.overlayManager.contains(polyline)){ map.overlayManager.remove(polyline) } polyline = Polyline(map, false) polyline!!.setPoints(pointsList) //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) polyline!!.infoWindow = null val paint = Paint() paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) paint.isAntiAlias = true paint.strokeWidth = 13f paint.style = Paint.Style.FILL_AND_STROKE paint.strokeJoin = Paint.Join.ROUND paint.strokeCap = Paint.Cap.ROUND polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) map.overlayManager.add(0,polyline!!) stopsOverlay.closeAllInfoWindows() stopsOverlay.items.clear() val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) for(s in stops){ val gp = if (showOnTopOfLine) findOptimalPosition(s,pointsList) else GeoPoint(s.latitude!!,s.longitude!!) val marker = MarkerUtils.makeMarker( gp, s.ID, s.stopDefaultName, s.routesThatStopHereToString(), map,stopTouchResponder, stopIcon, R.layout.linedetail_stop_infowindow, R.color.line_drawn_poly ) marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) stopsOverlay.add(marker) } //POINTS LIST IS NOT IN ORDER ANY MORE //if(!map.overlayManager.contains(stopsOverlay)){ // map.overlayManager.add(stopsOverlay) //} polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> Log.d(DEBUG_TAG, "clicked") true }) //map.controller.zoomToB//#animateTo(pointsList[0]) val del = 0.008 map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) //map.invalidate() } private fun initializeRecyclerView(){ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager } private fun showStopsAsList(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") val setNewAdapter = true if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } /** * Remove bus marker from overlay associated with tripID */ private fun removeBusMarker(tripID: String){ if(!busPositionMarkersByTrip.containsKey(tripID)){ Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") return } val marker = busPositionMarkersByTrip[tripID] busPositionsOverlay.remove(marker) busPositionMarkersByTrip.remove(tripID) val animator = tripMarkersAnimators[tripID] animator?.let{ it.cancel() tripMarkersAnimators.remove(tripID) } } private fun showPatternWithStop(patternId: String){ //var index = 0 Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") for (i in currentPatterns.indices){ val pattStop = currentPatterns[i] if(pattStop.pattern.code == patternId){ Log.d(DEBUG_TAG, "Pattern found in position $i") //setPatternAndReqStops(pattStop) patternsSpinner.setSelection(i) break } } } /** * draw the position of the buses in the map. Copied from MapFragment */ private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> ) { //Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses") //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); // cleanup the patterns // at first run, the buses which have no direction are still displayed. If those become missing in the data, // it becomes clear that they don't have the same direction val currentBusesTripsIds = HashSet(busPositionMarkersByTrip.keys) for (tripID in currentBusesTripsIds){ if (!tripsPatterns.keys.contains(tripID)){ //the tripId is not in the updates anymore, remove it removeBusMarker(tripID) } } val noPatternsTrips = ArrayList() for (tripID in tripsPatterns.keys) { val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue var marker: Marker? = null //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)) { //check if the trip direction ID is the same, if not remove if(tripWithPatternStops?.pattern != null && tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ removeBusMarker(tripID) } else { //need to change the position of the marker marker = busPositionMarkersByTrip.get(tripID)!! BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) // Set the pattern to add the info if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { val window = marker.infoWindow as BusInfoWindow if (window.pattern == null && tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.pattern) } } } } else { //marker is not there, need to make it //if (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") marker = Marker(map) //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! //mdraw.setBounds(0,0,28,28); marker.icon = mdraw var markerPattern: MatoPattern? = null if (tripWithPatternStops != null) { if (tripWithPatternStops.pattern != null) markerPattern = tripWithPatternStops.pattern } marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { // set pattern to show if(it!=null) showPatternWithStop(it.code) } //marker.infoWindow as BusInfoWindow marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) // the overlay is null when it's not attached yet? // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if (busPositionsOverlay != null) { busPositionsOverlay.add(marker) busPositionMarkersByTrip.put(tripID, marker) } } } if (noPatternsTrips.size > 0) { Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") } } override fun onResume() { super.onResume() Log.d(DEBUG_TAG, "Resetting paused from onResume") pausedFragment = false val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") //separate paths if(useMQTTPositions) liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else liveBusViewModel.requestGTFSUpdates() if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") val controller = map.controller viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) } //controller.setZoom() } //initialize GUI here fragmentListener.readyGUIfor(FragmentKind.LINES) } override fun onPause() { super.onPause() liveBusViewModel.stopMatoUpdates() pausedFragment = true //save map val center = map.mapCenter mapViewModel.currentLat.value = center.latitude mapViewModel.currentLong.value = center.longitude mapViewModel.currentZoom.value = map.zoomLevel.toDouble() } override fun getBaseViewForSnackBar(): View? { return null } companion object { private const val LINEID_KEY="lineID" fun newInstance() = LinesDetailFragment() const val DEBUG_TAG="LinesDetailFragment" fun makeArgs(lineID: String): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) return b } @JvmStatic private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) throw IllegalArgumentException() val sLat = stop.latitude!! val sLong = stop.longitude!! if(pointsList.size < 2) return pointsList[0] pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } val p1 = pointsList[0] val p2 = pointsList[1] if (p1.longitude == p2.longitude){ //Log.e(DEBUG_TAG, "Same longitude") return GeoPoint(sLat, p1.longitude) } else if (p1.latitude == p2.latitude){ //Log.d(DEBUG_TAG, "Same latitude") return GeoPoint(p2.latitude,sLong) } val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) val cR = p1.latitude - p1.longitude * m val longNew = (minv * sLong + sLat -cR ) / (m+minv) val latNew = (m*longNew + cR) //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") return GeoPoint(latNew,longNew) } private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 2d96048..6d043e9 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,872 +1,881 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; +import org.jetbrains.annotations.NotNull; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; - private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 + //private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE! private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// - FragmentManager fragMan; + FragmentManager childFragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); - if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { - ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); + if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { + ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) - && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ + || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsFragmentRequest){ - showNearbyFragmentIfNeeded(cr); + showNearbyFragmentIfPossible(); pendingNearbyStopsFragmentRequest = false; } } if(pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest =false; } }); private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfNeeded(cr); + showNearbyFragmentIfPossible(); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsFragmentRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfNeeded(cr); + showNearbyFragmentIfPossible(); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff - fragMan = getChildFragmentManager(); - fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); + childFragMan = getChildFragmentManager(); + childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(requireContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){ Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { - showNearbyFragmentIfNeeded(cr); + showNearbyFragmentIfPossible(); } } else { //The Introductory Activity is about to be started, hence pause the request and show later pendingIntroRun = true; } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (locationManager == null) locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ //request position permission if needed if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { - showNearbyFragmentIfNeeded(cr); + showNearbyFragmentIfPossible(); } //deactivate flag pendingIntroRun = false; } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); - //this is the second time we are attaching this fragment + //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); + //TODO: if we come back to this from another fragment, and the user has given again the permission + // for the Location, we should show the Nearby Stops + if(!suppressArrivalsReload && pendingStopID==null){ + //none of the following cases are true + // check if we are showing any fragment + final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); + if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ + //we are not showing anything + if(Permissions.anyLocationPermissionsGranted(getContext())){ + showNearbyFragmentIfPossible(); + } + } + } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); - return (fragment!= null && fragment.isVisible()); + return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); - final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); + final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); - FragmentTransaction ft = fragMan.beginTransaction(); + FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void showLineOnMap(String routeGtfsId) { //pass to activity mListener.showLineOnMap(routeGtfsId); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); prepareGUIForBusLines(); new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } - private void showNearbyFragmentIfNeeded(Criteria cr){ - if(isNearbyFragmentShown()) { + private void showNearbyFragmentIfPossible() { + if (isNearbyFragmentShown()) { //nothing to do - Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); + Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } - if(getContext()==null){ + if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } - AppLocationManager appLocationManager = AppLocationManager.getInstance(getContext()); - final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); - if (haveProviders - && fragmentHelper.getLastSuccessfullySearchedBusStop() == null - && !fragMan.isDestroyed()) { + if (fragmentHelper.getLastSuccessfullySearchedBusStop() == null + && !childFragMan.isDestroyed()) { //Go ahead with the request actuallyShowNearbyStopsFragment(); pendingNearbyStopsFragmentRequest = false; - } else if(!haveProviders){ - Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /* * Run location requests separately and asynchronously class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 7bd2380..1be3f1f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,591 +1,678 @@ /* BusTO - Fragments components Copyright (C) 2018 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.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.location.Location; +import android.location.LocationManager; import android.os.Bundle; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.location.LocationListenerCompat; +import androidx.core.location.LocationManagerCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.RecyclerView; import androidx.work.WorkInfo; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.adapters.SquareStopAdapter; import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; import it.reyboz.bustorino.util.LocationCriteria; +import it.reyboz.bustorino.util.Permissions; import it.reyboz.bustorino.util.StopSorterByDistance; import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel; import org.jetbrains.annotations.NotNull; import java.util.*; public class NearbyStopsFragment extends Fragment { public enum FragType{ STOPS(1), ARRIVALS(2); private final int num; FragType(int num){ this.num = num; } public static FragType fromNum(int i){ switch (i){ case 1: return STOPS; case 2: return ARRIVALS; default: throw new IllegalArgumentException("type not recognized"); } } } + private enum LocationShowingStatus {SEARCHING, FIRST_FIX, DISABLED, NO_PERMISSION} private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; //public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; private FragType fragment_type = FragType.STOPS; public final static String FRAGMENT_TAG="NearbyStopsFrag"; - //data Bundle - private final String BUNDLE_LOCATION = "location"; - private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; private GPSPoint lastPosition = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance = 10; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView, loadingTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; - private AppLocationManager locManager; + private LocationManager locManager; //These are useful for the case of nearby arrivals private NearbyArrivalsDownloader arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; private boolean dbUpdateRunning = false; private ArrayList currentNearbyStops = new ArrayList<>(); private NearbyArrivalsDownloader nearbyArrivalsDownloader; + private LocationShowingStatus showingStatus = LocationShowingStatus.NO_PERMISSION; + private final NearbyArrivalsDownloader.ArrivalsListener arrivalsListener = new NearbyArrivalsDownloader.ArrivalsListener() { @Override public void setProgress(int completedRequests, int pendingRequests) { if(flatProgressBar!=null) { if (pendingRequests == 0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } else { flatProgressBar.setIndeterminate(false); flatProgressBar.setProgress(completedRequests); } } } @Override public void onAllRequestsCancelled() { if(flatProgressBar!=null) flatProgressBar.setVisibility(View.GONE); } @Override public void showCompletedArrivals(ArrayList completedPalinas) { showArrivalsInRecycler(completedPalinas); } }; //ViewModel private NearbyStopsViewModel viewModel; public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ public static NearbyStopsFragment newInstance(FragType type) { //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); args.putInt(FRAGMENT_TYPE_KEY,type.num); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { setFragmentType(FragType.fromNum(getArguments().getInt(FRAGMENT_TYPE_KEY))); } - locManager = AppLocationManager.getInstance(getContext()); + locManager = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE); fragmentLocationListener = new FragmentLocationListener(); if (getContext()!=null) { globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } nearbyArrivalsDownloader = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.circularProgressBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); titleTextView = root.findViewById(R.id.titleTextView); loadingTextView = root.findViewById(R.id.positionLoadingTextView); switchButton = root.findViewById(R.id.switchButton); scrollListener = new CommonScrollListener(mListener,false); switchButton.setOnClickListener(v -> switchFragmentType()); Log.d(DEBUG_TAG, "onCreateView"); + final Context appContext =requireContext().getApplicationContext(); DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { + @SuppressLint("MissingPermission") @Override public void onChanged(List workInfos) { if(workInfos.isEmpty()) return; WorkInfo wi = workInfos.get(0); - if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { - locManager.removeLocationRequestFor(fragmentLocationListener); + if (wi.getState() == WorkInfo.State.RUNNING && fragmentLocationListener.isRegistered) { + locManager.removeUpdates(fragmentLocationListener); + fragmentLocationListener.isRegistered = true; dbUpdateRunning = true; } else{ //start the request - if(!locManager.isRequesterRegistered(fragmentLocationListener)) - locManager.addLocationRequestFor(fragmentLocationListener); + if(!fragmentLocationListener.isRegistered){ + requestLocationUpdates(); + } dbUpdateRunning = false; } } }); //observe the livedata viewModel.getStopsAtDistance().observe(getViewLifecycleOwner(), stops -> { if (!dbUpdateRunning && (stops.size() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { distance = distance + 40; viewModel.requestStopsAtDistance(distance, true); //Log.d(DEBUG_TAG, "Doubling distance now!"); return; } if(!stops.isEmpty()) { Log.d(DEBUG_TAG, "Showing "+stops.size()+" stops nearby"); currentNearbyStops =stops; showStopsInViews(currentNearbyStops, lastPosition); } }); + if(Permissions.anyLocationPermissionsGranted(appContext)){ + setShowingStatus(LocationShowingStatus.SEARCHING); + } else { + setShowingStatus(LocationShowingStatus.NO_PERMISSION); + + } return root; } + //because linter is stupid and cannot look inside *anyLocationPermissionGranted* + @SuppressLint("MissingPermission") + private boolean requestLocationUpdates(){ + if(Permissions.anyLocationPermissionsGranted(requireContext())) { + locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, + 3000, 10.0f, fragmentLocationListener + ); + fragmentLocationListener.isRegistered = true; + return true; + } else return false; + } + /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ private void setFragmentType(FragType type){ this.fragment_type = type; switch(type){ case ARRIVALS: TIME_INTERVAL_REQUESTS = 5*1000; break; case STOPS: TIME_INTERVAL_REQUESTS = 1000; } } + private void setShowingStatus(@NonNull LocationShowingStatus newStatus){ + if(newStatus == showingStatus){ + Log.d(DEBUG_TAG, "Asked to set new displaying status but it's the same"); + return; + } + switch (newStatus){ + case FIRST_FIX: + circlingProgressBar.setVisibility(View.GONE); + loadingTextView.setVisibility(View.GONE); + gridRecyclerView.setVisibility(View.VISIBLE); + messageTextView.setVisibility(View.GONE); + break; + case NO_PERMISSION: + circlingProgressBar.setVisibility(View.GONE); + loadingTextView.setVisibility(View.GONE); + messageTextView.setText(R.string.enable_position_message_nearby); + messageTextView.setVisibility(View.VISIBLE); + break; + case DISABLED: + if (showingStatus== LocationShowingStatus.SEARCHING){ + circlingProgressBar.setVisibility(View.GONE); + loadingTextView.setVisibility(View.GONE); + } + messageTextView.setText(R.string.enableGpsText); + messageTextView.setVisibility(View.VISIBLE); + break; + case SEARCHING: + circlingProgressBar.setVisibility(View.VISIBLE); + loadingTextView.setVisibility(View.VISIBLE); + gridRecyclerView.setVisibility(View.GONE); + messageTextView.setVisibility(View.GONE); + } + showingStatus = newStatus; + } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context + " must implement OnFragmentInteractionListener"); } Log.d(DEBUG_TAG, "OnAttach called"); viewModel = new ViewModelProvider(this).get(NearbyStopsViewModel.class); + + } @Override public void onPause() { super.onPause(); gridRecyclerView.setAdapter(null); - locManager.removeLocationRequestFor(fragmentLocationListener); + locManager.removeUpdates(fragmentLocationListener); + fragmentLocationListener.isRegistered = false; Log.d(DEBUG_TAG,"On paused called"); } @Override public void onResume() { super.onResume(); try{ - if(!dbUpdateRunning && !locManager.isRequesterRegistered(fragmentLocationListener)) - locManager.addLocationRequestFor(fragmentLocationListener); + if(!dbUpdateRunning && !fragmentLocationListener.isRegistered) { + requestLocationUpdates(); + } } catch (SecurityException ex){ //ignored //try another location provider } //fix view if we were showing the stops or the arrivals prepareForFragmentType(); switch(fragment_type){ case STOPS: if(dataAdapter!=null){ //gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); } break; case ARRIVALS: if(arrivalsStopAdapter!=null){ //gridRecyclerView.setAdapter(arrivalsStopAdapter); circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); } } mListener.enableRefreshLayout(false); Log.d(DEBUG_TAG,"OnResume called"); if(getContext()==null){ Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now"); MIN_NUM_STOPS = 5; MAX_DISTANCE = 600; return; } //Re-read preferences SharedPreferences shpr = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()); //For some reason, they are all saved as strings MAX_DISTANCE = shpr.getInt(getString(R.string.pref_key_radius_recents),600); boolean isMinStopInt = true; try{ MIN_NUM_STOPS = shpr.getInt(getString(R.string.pref_key_num_recents), 5); } catch (ClassCastException ex){ isMinStopInt = false; } if(!isMinStopInt) try { MIN_NUM_STOPS = Integer.parseInt(shpr.getString(getString(R.string.pref_key_num_recents), "5")); } catch (NumberFormatException ex){ MIN_NUM_STOPS = 5; } if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ ", Min number of stops: "+MIN_NUM_STOPS); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); gridRecyclerView.setVisibility(View.INVISIBLE); gridRecyclerView.addOnScrollListener(scrollListener); } @Override public void onDetach() { super.onDetach(); mListener = null; if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); } /** * Display the stops, or run new set of requests for arrivals */ private void showStopsInViews(ArrayList stops, GPSPoint location){ if (stops.isEmpty()) { setNoStopsLayout(); return; } double minDistance = Double.POSITIVE_INFINITY; for(Stop s: stops){ minDistance = Math.min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude())); } //quick trial to hopefully always get the stops in the correct order Collections.sort(stops,new StopSorterByDistance(location)); switch (fragment_type){ case STOPS: showStopsInRecycler(stops); break; case ARRIVALS: if(getContext()==null) break; //don't do anything if we're not attached if(arrivalsManager==null) arrivalsManager = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); arrivalsManager.requestArrivalsForStops(stops); /*flatProgressBar.setVisibility(View.VISIBLE); flatProgressBar.setProgress(0); flatProgressBar.setIndeterminate(false); */ //for the moment, be satisfied with only one location //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); break; default: } } /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ switch (fragment_type){ case ARRIVALS: setFragmentType(FragType.STOPS); break; case STOPS: setFragmentType(FragType.ARRIVALS); break; default: } prepareForFragmentType(); fragmentLocationListener.lastUpdateTime = -1; //locManager.removeLocationRequestFor(fragmentLocationListener); //locManager.addLocationRequestFor(fragmentLocationListener); showStopsInViews(currentNearbyStops, lastPosition); } /** * Prepare the views for the set fragment type */ private void prepareForFragmentType(){ if(fragment_type==FragType.STOPS){ switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); } else if (fragment_type==FragType.ARRIVALS){ titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) gridRecyclerView.setAdapter(arrivalsStopAdapter); } } //useful methods /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ if(firstLocForStops) { dataAdapter = new SquareStopAdapter(stops, mListener, lastPosition); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); dataAdapter.setUserPosition(lastPosition); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ Collections.sort(palinas,new StopSorterByDistance(lastPosition)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop if(p.queryAllRoutes().isEmpty()) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); return; } if(firstLocForArrivals){ arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastPosition); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastPosition); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); } /** * Local locationListener, to use for the GPS */ - class FragmentLocationListener implements AppLocationManager.LocationRequester{ + class FragmentLocationListener implements LocationListenerCompat { - private int oldLocStatus = -2; - private LocationCriteria cr; private long lastUpdateTime = -1; - + public boolean isRegistered = false; @Override public void onLocationChanged(Location location) { //set adapter if(location==null){ Log.e(DEBUG_TAG, "Location is null, cannot request stops"); return; } else if(viewModel==null){ return; } if(location.getAccuracy()<200 && !dbUpdateRunning) { if(viewModel.getDistanceMtLiveData().getValue()==null){ //never run request distance = 40; } lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); } lastUpdateTime = System.currentTimeMillis(); Log.d("BusTO:NearPositListen","can start request for stops: "+ !dbUpdateRunning); } + @Override + public void onProviderEnabled(@NonNull String provider) { + Log.d(DEBUG_TAG, "Location provider "+provider+" enabled"); + if(provider.equals(LocationManager.GPS_PROVIDER)){ + setShowingStatus(LocationShowingStatus.SEARCHING); + } + } + + @Override + public void onProviderDisabled(@NonNull String provider) { + Log.d(DEBUG_TAG, "Location provider "+provider+" disabled"); + if(provider.equals(LocationManager.GPS_PROVIDER)) { + setShowingStatus(LocationShowingStatus.DISABLED); + } + } + + @Override + public void onStatusChanged(@NonNull @NotNull String provider, int status, @Nullable @org.jetbrains.annotations.Nullable Bundle extras) { + LocationListenerCompat.super.onStatusChanged(provider, status, extras); + } + /* @Override public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override public @NotNull LocationCriteria getLocationCriteria() { return new LocationCriteria(200,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } @Override public void onLocationProviderAvailable() { } @Override public void onLocationDisabled() { } + + */ } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index 9624939..48834f5 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -1,65 +1,90 @@ package it.reyboz.bustorino.fragments; +import android.Manifest; import android.content.Context; import android.content.SharedPreferences; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.BuildConfig; +import java.util.Map; + import static android.content.Context.MODE_PRIVATE; public abstract class ScreenBaseFragment extends Fragment { protected final static String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs"; protected void setOption(String optionName, boolean value) { Context mContext = getContext(); SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { Context mContext = getContext(); assert mContext != null; return getOption(mContext, optionName, optDefault); } protected void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Toast.makeText(getContext(), messageID, length).show(); } public void hideKeyboard() { if (getActivity()==null) return; View view = getActivity().getCurrentFocus(); if (view != null) { ((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } /** * Find the view on which the snackbar should be shown * @return a view or null if you don't want the snackbar shown */ @Nullable public abstract View getBaseViewForSnackBar(); public static boolean getOption(Context context, String optionName, boolean optDefault){ SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE); return preferences.getBoolean(optionName, optDefault); } public static void setOption(Context context,String optionName, boolean value) { SharedPreferences.Editor editor = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.apply(); } + public ActivityResultLauncher getPositionRequestLauncher(LocationRequestListener listener){ + return registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() { + @Override + public void onActivityResult(Map result) { + if (result == null) return; + + if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || + result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) + return; + final boolean coarseGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)); + final boolean fineGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION)); + + listener.onPermissionResult(coarseGranted, fineGranted); + } + }); + } + + public interface LocationRequestListener{ + void onPermissionResult(boolean isCoarseGranted, boolean isFineGranted); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt index a617ef5..0ce8a07 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt @@ -1,274 +1,277 @@ /* BusTO (middleware) Copyright (C) 2019 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.middleware import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.location.* import android.os.Bundle import android.util.Log import androidx.core.content.ContextCompat +import androidx.core.location.LocationListenerCompat import it.reyboz.bustorino.util.LocationCriteria import it.reyboz.bustorino.util.Permissions import java.lang.ref.WeakReference import kotlin.math.min /** * Singleton class used to access location. Possibly extended with other location sources. + * + * 2024: This is far too much. We need to simplify the whole mechanism (no more singleton) */ class AppLocationManager private constructor(context: Context) : LocationListener { private val appContext: Context private val locMan: LocationManager private val BUNDLE_LOCATION = "location" private var oldGPSLocStatus = LOCATION_UNAVAILABLE private var minimum_time_milli = -1 private val requestersRef = ArrayList>() init { appContext = context.applicationContext locMan = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager } @Throws(SecurityException::class) private fun requestGPSPositionUpdates(): Boolean { val timeinterval = if (minimum_time_milli > 0 && minimum_time_milli < Int.MAX_VALUE) minimum_time_milli else 2000 locMan.removeUpdates(this) if (!checkLocationPermission(appContext)){ Log.e(DEBUG_TAG, "No location permission!!") return false } if (locMan.allProviders.contains("gps")) locMan.requestLocationUpdates( LocationManager.GPS_PROVIDER, timeinterval.toLong(), 5f, this ) /*LocationManagerCompat.requestLocationUpdates(locMan, LocationManager.GPS_PROVIDER, new LocationRequestCompat.Builder(timeinterval).setMinUpdateDistanceMeters(5.F).build(),this, ); TODO: find a way to do this */ return true } private fun cleanAndUpdateRequesters() { minimum_time_milli = Int.MAX_VALUE val iter = requestersRef.listIterator() while (iter.hasNext()) { val cReq = iter.next().get() if (cReq == null) iter.remove() else { minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) .toInt() } } Log.d( DEBUG_TAG, "Updated requesters, got " + requestersRef.size + " listeners to update every " + minimum_time_milli + " ms at least" ) } fun addLocationRequestFor(req: LocationRequester) { var present = false minimum_time_milli = Int.MAX_VALUE var countNull = 0 val iter = requestersRef.listIterator() while (iter.hasNext()) { val cReq = iter.next().get() if (cReq == null) { countNull++ iter.remove() } else if (cReq == req) { present = true minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) .toInt() } } Log.d(DEBUG_TAG, "$countNull listeners have been removed because null") if (!present) { val newref = WeakReference(req) requestersRef.add(newref) minimum_time_milli = min(req.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) .toInt() Log.d(DEBUG_TAG, "Added new stop requester, instance of " + req.javaClass.simpleName) } if (requestersRef.size > 0) { Log.d(DEBUG_TAG, "Requesting location updates") requestGPSPositionUpdates() } } fun removeLocationRequestFor(req: LocationRequester) { minimum_time_milli = Int.MAX_VALUE val iter = requestersRef.listIterator() while (iter.hasNext()) { val cReq = iter.next().get() if (cReq == null || cReq == req) iter.remove() else { minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) .toInt() } } if (requestersRef.size <= 0) { locMan.removeUpdates(this) } } private fun sendLocationStatusToAll(status: Int) { val iter = requestersRef.listIterator() while (iter.hasNext()) { val cReq = iter.next().get() if (cReq == null) iter.remove() else cReq.onLocationStatusChanged(status) } } fun isRequesterRegistered(requester: LocationRequester): Boolean { for (regRef in requestersRef) { if (regRef.get() != null && regRef.get() === requester) return true } return false } override fun onLocationChanged(location: Location) { Log.d( DEBUG_TAG, "found location: \nlat: ${location.latitude} lon: ${location.longitude} accuracy: ${location.accuracy}" ) val iter = requestersRef.listIterator() var new_min_interval = Int.MAX_VALUE while (iter.hasNext()) { val requester = iter.next().get() if (requester == null) iter.remove() else { val timeNow = System.currentTimeMillis() val criteria = requester.locationCriteria if (location.accuracy < criteria.minAccuracy && timeNow - requester.lastUpdateTimeMillis > criteria.timeInterval ) { requester.onLocationChanged(location) Log.d( "AppLocationManager", "Updating position for instance of requester " + requester.javaClass.simpleName ) } //update minimum time interval new_min_interval = min(requester.locationCriteria.timeInterval.toDouble(), new_min_interval.toDouble()) .toInt() } } minimum_time_milli = new_min_interval if (requestersRef.size == 0) { //stop requesting the position locMan.removeUpdates(this) } } override fun onStatusChanged(provider: String, status: Int, extras: Bundle) { //IF ANOTHER LOCATION SOURCE IS READY, USE IT //OTHERWISE, SIGNAL THAT WE HAVE NO LOCATION if (oldGPSLocStatus != status) { if (status == LocationProvider.OUT_OF_SERVICE || status == LocationProvider.TEMPORARILY_UNAVAILABLE) { sendLocationStatusToAll(LOCATION_UNAVAILABLE) } else if (status == LocationProvider.AVAILABLE) { sendLocationStatusToAll(LOCATION_GPS_AVAILABLE) } oldGPSLocStatus = status } Log.d(DEBUG_TAG, "Provider status changed: $provider status: $status") } override fun onProviderEnabled(provider: String) { cleanAndUpdateRequesters() requestGPSPositionUpdates() Log.d(DEBUG_TAG, "Provider: $provider enabled") for (req in requestersRef) { if (req.get() == null) continue req.get()!!.onLocationProviderAvailable() } } override fun onProviderDisabled(provider: String) { cleanAndUpdateRequesters() for (req in requestersRef) { if (req.get() == null) continue req.get()!!.onLocationDisabled() } //locMan.removeUpdates(this); 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 */ interface LocationRequester { /** * Do something with the newly obtained location * @param loc the obtained location */ fun onLocationChanged(loc: Location?) /** * Inform the requester that the GPS status has changed * @param status new status */ fun onLocationStatusChanged(status: Int) /** * We have a location provider available */ fun onLocationProviderAvailable() /** * Called when location is disabled */ fun onLocationDisabled() /** * Give the last time of update the requester has * Set it to -1 in order to receive each new location * @return the time for update in milliseconds since epoch */ val lastUpdateTimeMillis: Long /** * Get the specifications for the location * @return fully parsed LocationCriteria */ val locationCriteria: LocationCriteria } companion object { const val LOCATION_GPS_AVAILABLE = 22 const val LOCATION_UNAVAILABLE = -22 private const val DEBUG_TAG = "BUSTO LocAdapter" private var instance: AppLocationManager? = null @JvmStatic - fun getInstance(con: Context): AppLocationManager? { + fun getInstance(con: Context): AppLocationManager { if (instance == null) instance = AppLocationManager(con) - return instance + return instance!! } fun checkLocationPermission(context: Context?): Boolean { return ContextCompat.checkSelfPermission( context!!, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED } } } diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/LocationUtils.java b/app/src/main/java/it/reyboz/bustorino/middleware/LocationUtils.java new file mode 100644 index 0000000..b84d486 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/LocationUtils.java @@ -0,0 +1,28 @@ +package it.reyboz.bustorino.middleware; + +import android.content.Context; +import android.location.LocationManager; +import android.os.Build; +import android.provider.Settings; +import androidx.core.content.ContextCompat; + +public class LocationUtils { + + public static LocationManager getSystemLocationManager(Context context){ + return ContextCompat.getSystemService(context, LocationManager.class); + } + + //thanks to https://stackoverflow.com/questions/10311834/how-to-check-if-location-services-are-enabled + public static Boolean isLocationEnabled(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // This is a new method provided in API 28 + LocationManager lm = getSystemLocationManager(context); + return lm.isLocationEnabled(); + } else { + // This was deprecated in API 28 + int mode = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE, + Settings.Secure.LOCATION_MODE_OFF); + return (mode != Settings.Secure.LOCATION_MODE_OFF); + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/util/Permissions.java b/app/src/main/java/it/reyboz/bustorino/util/Permissions.java index 87044a0..8bbb696 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/Permissions.java +++ b/app/src/main/java/it/reyboz/bustorino/util/Permissions.java @@ -1,73 +1,73 @@ 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 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.size() > 0; + 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/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 227c936..6aec75d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,274 +1,277 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Si No Prossimo Precedente Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linee urbane Linee extraurbane Linee turistiche Direzione: Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:


Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!


Come funziona?

Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito www.gtt.to.it, www.5t.torino.it, muoversiatorino.it "per uso personale" e altre fonti Open Data (aperto.comune.torino.it).


Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori e i beta tester!


Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.


Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.

Tutti i dati utilizzati dall\'app provengono direttamente da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Canale Telegram La mia posizione Segui posizione Attiva o disattiva posizione Posizione attivata Posizione disattivata + La posizione è disabilitata sul dispositivo Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Nessun passaggio per le linee: Canale default delle notifiche Operazioni sul database Informazioni sul database (aggiornamento) Downloading trips from MaTO server Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Download dati dal server MaTO Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Fonte posizioni in tempo reale di bus e tram MaTO (aggiornate più spesso, può non funzionare) GTFS RT (più stabile) Linea aggiunta ai preferiti Linea rimossa dai preferiti Preferite Tocca a lungo la fermata per le opzioni Rimuovi i dati dei trip (libera spazio) Tutti i trip GTFS sono rimossi dal database Mostra introduzione open source per il trasporto pubblico di Torino. Stai usando un\'app indipendente, senza pubblicità e senza nessun tracciamento. ]]> Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]> preferiti toccando la stella a fianco del nome.]]> fermate più vicine a te direttamente nella schermata principale...]]> posizioni in tempo reale dei bus e tram (in blu)]]> Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Informazioni per sapere di più sull\'app e il team di sviluppo.]]> Capito, chiudi introduzione Chiudi introduzione Abilita accesso alla posizione Accesso alla posizione abilitato + Accesso alla posizione non consentito dall\'utente + Abilita notifiche Notifiche abilitate
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 810a652..4697434 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,305 +1,308 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines Destination: Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons and Volley framework.
- Android app components.
- All the contributors, and the beta testers, too!


Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update - Allow access to position to show it on the map - Please enable GPS + Allow access to location to show it on the map + Allow access to location to show stops nearby + Please enable location on the device Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Center on my location Follow me Enable or disable location Location enabled Location disabled + Location is disabled on device Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database Downloading trips from MaTO server 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. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Downloading data from MaTO server Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might be offline) GTFS RT (more stable, less frequently updated) Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted + Location permission has not been granted OK, close the tutorial Close the tutorial Enable notifications Notifications enabled