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 7673975..70d71e3 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,728 +1,736 @@ /* 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.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.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.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.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.BusInfoWindow import it.reyboz.bustorino.map.BusPositionUtils import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.map.LocationOverlay import it.reyboz.bustorino.map.MapViewModel import it.reyboz.bustorino.map.MarkerUtils import it.reyboz.bustorino.viewmodels.LinesViewModel import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel 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 lateinit var lineID: String 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 lateinit var stopsRecyclerView: RecyclerView //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 //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 //position of live markers private val busPositionMarkersByTrip = HashMap() private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap() private val liveBusViewModel: MQTTPositionsViewModel 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) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val titleTextView = rootView.findViewById(R.id.titleTextView) titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) 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 viewModel.setMapShowing(false) liveBusViewModel.stopPositionsListening() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) } else{ stopsRecyclerView.visibility = View.GONE map.visibility = View.VISIBLE viewModel.setMapShowing(true) liveBusViewModel.requestPosUpdates(lineID) switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) } } viewModel.setRouteIDQuery(lineID) 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) } } 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 } //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){ if(pair.second!=null && pair.second?.pattern !=null){ val dir = pair.second?.pattern?.directionId if(dir !=null && dir == currentPattern.directionId){ outmap.set(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.set(tripId, -10) } } Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n updateBusPositionsInMap(outmap) } //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.downloadTripsFromMato( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } return rootView } 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) - /* - object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", - arrayOf("https://basemap.nationalmap.gov/ArcG IS/rest/services/USGSTopo/MapServer/tile/" )) { - override fun getTileURLString(pMapTileIndex: Long) : String{ - return baseUrl + - MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + - "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; - } + + 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.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 = 11.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){ currentPatterns = patterns.sortedBy { p-> p.pattern.code } 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 = 16f 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(); 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 liveBusViewModel.requestPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) 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.stopPositionsListening() 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 78b56bf..934224b 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,891 +1,889 @@ 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.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; 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.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 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 OPTION_SHOW_LEGEND = "show_legend"; 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"; - /// UI ELEMENTS // - private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean instanceStateSaved = 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 int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; 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 (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 pendingNearbyStopsRequest = 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 || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; boolean resCoarse = result.get(Manifest.permission.ACCESS_COARSE_LOCATION); boolean resFine = result.get(Manifest.permission.ACCESS_FINE_LOCATION); Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && 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(pendingNearbyStopsRequest){ showNearbyFragmentIfNeeded(cr); pendingNearbyStopsRequest = false; } } if(pendingNearbyStopsRequest) pendingNearbyStopsRequest=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); } } @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"); pendingNearbyStopsRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @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); - addToFavorites = root.findViewById(R.id.addToFavorites); + /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); 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)); hideHintButton.setOnClickListener(this::onHideHint); 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")); 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(getContext()); + 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); } instanceStateSaved = false; } @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); instanceStateSaved = 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){ //We want the nearby bus stops! //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsRequest = true; } else { showNearbyFragmentIfNeeded(cr); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // 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 Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); 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); } } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * 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()); } /** * 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()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } @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); // 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(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing() &&!instanceStateSaved) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because we saved instanceState"); } } @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 (pendingNearbyStopsRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } 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()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } 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()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); actuallyShowNearbyStopsFragment(); pendingNearbyStopsRequest = 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/map/LocationOverlay.java b/app/src/main/java/it/reyboz/bustorino/map/LocationOverlay.java index d5c48ab..cd0bd59 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/LocationOverlay.java +++ b/app/src/main/java/it/reyboz/bustorino/map/LocationOverlay.java @@ -1,63 +1,86 @@ /* BusTO - Map components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.map; +import android.content.Context; +import android.util.Log; import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import org.osmdroid.views.overlay.mylocation.IMyLocationProvider; import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; public class LocationOverlay extends MyLocationNewOverlay { + private final static String DEBUG_TAG = "BusTOLocationOverlay"; final OverlayCallbacks callbacks; public LocationOverlay(MapView mapView, OverlayCallbacks callbacks) { super(mapView); this.callbacks = callbacks; } public LocationOverlay(IMyLocationProvider myLocationProvider, MapView mapView, OverlayCallbacks callbacks) { super(myLocationProvider, mapView); this.callbacks = callbacks; } @Override public void enableFollowLocation() { super.enableFollowLocation(); callbacks.onEnableFollowMyLocation(); } @Override public void disableFollowLocation() { super.disableFollowLocation(); callbacks.onDisableFollowMyLocation(); } + public static LocationOverlay createLocationOverlay(boolean enableLocation, MapView map, Context context, OverlayCallbacks locationCallbacks){ + if(context== null) { + Log.d(DEBUG_TAG, "Cannot start location overlay, context is null"); + return null; + } + // Location Overlay + // from OpenBikeSharing (THANK GOD) + Log.d(DEBUG_TAG, "Starting position overlay"); + GpsMyLocationProvider imlp = new GpsMyLocationProvider(context.getApplicationContext()); + imlp.setLocationUpdateMinDistance(5); + imlp.setLocationUpdateMinTime(2000); + + final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); + if (enableLocation) overlay.enableMyLocation(); + //overlay.setOptionsMenuEnabled(true); + + return overlay; + } + public interface OverlayCallbacks{ /** * Called right after disableFollowMyLocation */ void onDisableFollowMyLocation(); /** * Called right after enableFollowMyLocation */ void onEnableFollowMyLocation(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.java b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.java deleted file mode 100644 index 93264f6..0000000 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - 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.Criteria; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; -import android.os.Bundle; -import android.util.Log; -import android.widget.Toast; - -import androidx.core.content.ContextCompat; -import androidx.core.location.LocationManagerCompat; -import androidx.core.location.LocationRequestCompat; - -import it.reyboz.bustorino.util.LocationCriteria; -import it.reyboz.bustorino.util.Permissions; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.ListIterator; - -/** - * Singleton class used to access location. Possibly extended with other location sources. - */ -public class AppLocationManager implements LocationListener { - - public static final int LOCATION_GPS_AVAILABLE = 22; - public static final int LOCATION_UNAVAILABLE = -22; - - private final Context appContext; - private final LocationManager locMan; - public static final String DEBUG_TAG = "BUSTO LocAdapter"; - private final String BUNDLE_LOCATION = "location"; - private static AppLocationManager instance; - private int oldGPSLocStatus = LOCATION_UNAVAILABLE; - private int minimum_time_milli = -1; - - private final ArrayList> requestersRef = new ArrayList<>(); - - - private AppLocationManager(Context context) { - this.appContext = context.getApplicationContext(); - locMan = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - boolean isLocationPermissionGiven = checkLocationPermission(context); - } - - public static AppLocationManager getInstance(Context con) { - if(instance==null) instance = new AppLocationManager(con); - return instance; - } - - public static boolean checkLocationPermission(Context context){ - return ContextCompat.checkSelfPermission(context, - Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED; - } - - - private void requestGPSPositionUpdates() throws SecurityException{ - final int timeinterval = (minimum_time_milli>0 && minimum_time_milli> iter = requestersRef.listIterator(); - while(iter.hasNext()){ - final LocationRequester cReq = iter.next().get(); - if(cReq==null) iter.remove(); - else { - minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); - } - } - Log.d(DEBUG_TAG,"Updated requesters, got "+requestersRef.size()+" listeners to update every "+minimum_time_milli+" ms at least"); - } - - - public void addLocationRequestFor(LocationRequester req){ - boolean present = false; - minimum_time_milli = Integer.MAX_VALUE; - int countNull = 0; - ListIterator> iter = requestersRef.listIterator(); - while(iter.hasNext()){ - final LocationRequester cReq = iter.next().get(); - if(cReq==null) { - countNull++; - iter.remove(); - } else if(cReq.equals(req)){ - present = true; - minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); - } - } - Log.d(DEBUG_TAG, countNull+" listeners have been removed because null"); - if(!present) { - WeakReference newref = new WeakReference<>(req); - requestersRef.add(newref); - minimum_time_milli = Math.min(req.getLocationCriteria().getTimeInterval(),minimum_time_milli); - Log.d(DEBUG_TAG,"Added new stop requester, instance of "+req.getClass().getSimpleName()); - } - if(requestersRef.size()>0){ - Log.d(DEBUG_TAG,"Requesting location updates"); - requestGPSPositionUpdates(); - - } - - } - public void removeLocationRequestFor(LocationRequester req){ - minimum_time_milli = Integer.MAX_VALUE; - ListIterator> iter = requestersRef.listIterator(); - while(iter.hasNext()){ - final LocationRequester cReq = iter.next().get(); - if(cReq==null || cReq.equals(req)) iter.remove(); - else { - minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); - } - } - if(requestersRef.size()<=0){ - locMan.removeUpdates(this); - } else { - requestGPSPositionUpdates(); - } - } - private void sendLocationStatusToAll(int status){ - ListIterator> iter = requestersRef.listIterator(); - while(iter.hasNext()){ - final LocationRequester cReq = iter.next().get(); - if(cReq==null) iter.remove(); - else cReq.onLocationStatusChanged(status); - } - } - public boolean isRequesterRegistered(LocationRequester requester){ - for(WeakReference regRef: requestersRef){ - if(regRef.get()!=null && regRef.get() ==requester) return true; - } - return false; - } - - @Override - public void onLocationChanged(Location location) { - Log.d(DEBUG_TAG,"found location:\nlat: "+location.getLatitude()+" lon: "+location.getLongitude()+"\naccuracy: "+location.getAccuracy()); - ListIterator> iter = requestersRef.listIterator(); - int new_min_interval = Integer.MAX_VALUE; - while(iter.hasNext()){ - final LocationRequester requester = iter.next().get(); - if(requester==null) iter.remove(); - else{ - final long timeNow = System.currentTimeMillis(); - final LocationCriteria criteria = requester.getLocationCriteria(); - if(location.getAccuracy()criteria.getTimeInterval()){ - requester.onLocationChanged(location); - Log.d("AppLocationManager","Updating position for instance of requester "+requester.getClass().getSimpleName()); - } - //update minimum time interval - new_min_interval = Math.min(requester.getLocationCriteria().getTimeInterval(),new_min_interval); - } - } - minimum_time_milli = new_min_interval; - if(requestersRef.size()==0){ - //stop requesting the position - locMan.removeUpdates(this); - } - - - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - //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 - public void onProviderEnabled(String provider) { - cleanAndUpdateRequesters(); - requestGPSPositionUpdates(); - Log.d(DEBUG_TAG, "Provider: "+provider+" enabled"); - for(WeakReference req: requestersRef){ - if(req.get()==null) continue; - req.get().onLocationProviderAvailable(); - } - - } - - @Override - public void onProviderDisabled(String provider) { - cleanAndUpdateRequesters(); - for(WeakReference req: requestersRef){ - if(req.get()==null) continue; - req.get().onLocationDisabled(); - } - //locMan.removeUpdates(this); - Log.d(DEBUG_TAG, "Provider: "+provider+" disabled"); - } - - public boolean anyLocationProviderMatchesCriteria(Criteria cr) { - return Permissions.anyLocationProviderMatchesCriteria(locMan, cr, true); - } - /** - * Interface to be implemented to get the location request - */ - public interface LocationRequester{ - /** - * Do something with the newly obtained location - * @param loc the obtained location - */ - void onLocationChanged(Location loc); - - /** - * Inform the requester that the GPS status has changed - * @param status new status - */ - void onLocationStatusChanged(int status); - - /** - * We have a location provider available - */ - void onLocationProviderAvailable(); - /** - * Called when location is disabled - */ - void 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 - */ - long getLastUpdateTimeMillis(); - - /** - * Get the specifications for the location - * @return fully parsed LocationCriteria - */ - LocationCriteria getLocationCriteria(); - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt new file mode 100644 index 0000000..a617ef5 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt @@ -0,0 +1,274 @@ +/* + 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 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. + */ +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? { + if (instance == null) instance = AppLocationManager(con) + 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/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt index fced032..9d07f4f 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -1,55 +1,45 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import org.osmdroid.util.BoundingBox import java.util.ArrayList import java.util.concurrent.Executors class StopsMapViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) - /* - private val boundingBoxLiveData = MutableLiveData() - fun setStopBoundingBox(bb: BoundingBox){ - boundingBoxLiveData.value = bb - } - - */ val stopsInBoundingBox = MutableLiveData>() private val callback = - OldDataRepository.Callback> { result -> - result.let { - if(it.isSuccess){ - stopsInBoundingBox.postValue(it.result) + OldDataRepository.Callback> { res -> + if(res.isSuccess){ + stopsInBoundingBox.postValue(res.result) Log.d(DEBUG_TAG, "Setting value of stops in bounding box") } - - } } fun requestStopsInBoundingBox(bb: BoundingBox) { bb.let { Log.d(DEBUG_TAG, "Launching stop request") oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) } } companion object{ private const val DEBUG_TAG = "BusTOStopMapViewModel" } } \ No newline at end of file