diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index d51329a..a53fcb9 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,304 +1,304 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; import java.util.*; public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.item_arrivals_nearby_card; //private List stops; private @NonNull GPSPoint userPosition; private FragmentListenerMain listener; private List< RouteWithStop > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable private final String KEY_CAPITALIZE; private NameCapitalize capit; public ArrivalsStopAdapter(@Nullable List< RouteWithStop > routesPairList,@NonNull FragmentListenerMain fragmentListener, @NonNull Context con, @NonNull GPSPoint pos) { listener = fragmentListener; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); this.capit = NameCapitalize.getCapitalize(capitalizeKey); } @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS final var stopRoutePair = routesPairList.get(position); if(stopRoutePair != null){ final Stop stop = stopRoutePair.getStop(); final Route r = stopRoutePair.getRoute(); final Double distance = stop.getDistanceFromLocation(userPosition.getLatitude(), userPosition.longitude); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getDisplayCode()); holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); - holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); + holder.arrivalsTextView.setText(r.getPassaggiToString(0,3,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return routesPairList.size(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); capit = NameCapitalize.getCapitalize(k); notifyDataSetChanged(); } } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } /* public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } } */ public void updatePosition(@NonNull GPSPoint pos){ userPosition = pos; } public void setRoutesPairListAndPosition(@NonNull List newList, @Nullable GPSPoint pos) { if(pos!=null) updatePosition(pos); if (routesPairList == null) { routesPairList = new ArrayList<>(newList); notifyItemRangeInserted(0, newList.size()); return; } DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return routesPairList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { RouteWithStop oldItem = routesPairList.get(oldItemPosition); RouteWithStop newItem = newList.get(newItemPosition); // usa un ID univoco return oldItem.getId().equals(newItem.getId()); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { RouteWithStop oldItem = routesPairList.get(oldItemPosition); RouteWithStop newItem = newList.get(newItemPosition); // confronta il contenuto return oldItem.equals(newItem); } } ); routesPairList.clear(); routesPairList.addAll(newList); diffResult.dispatchUpdatesTo(this); /*if (this.routesPairList == null || routesPairList.size() == 0){ routesPairList = mRoutesPairList; notifyDataSetChanged(); } else{ final var indexMapIn = getRouteIndexMap(mRoutesPairList); final var indexMapExisting = getRouteIndexMap(routesPairList); //List> oldList = routesPairList; routesPairList = mRoutesPairList; /* for (Pair pair: indexMapIn.keySet()){ final Integer posIn = indexMapIn.get(pair); if (posIn == null) continue; if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemMoved(posExisting, posIn); indexMapExisting.remove(pair); } else{ //INSERT IT //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemInserted(posIn); } }// //REMOVE OLD STOPS for (Pair pair: indexMapExisting.keySet()) { final Integer posExisting = indexMapExisting.get(pair); if (posExisting == null) continue; //routesPairList.remove(posExisting.intValue()); notifyItemRemoved(posExisting); } */ } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ //Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) var iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final var stopRoutePair = iterator.next(); stopRoutePair.getRoute(); final Pair routeNameDirection = new Pair<>(stopRoutePair.getRoute().getName(), stopRoutePair.getRoute().destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } private static HashMap getRouteIndexMap(List routesPairList){ final HashMap myMap = new HashMap<>(); for (int i=0; i. */ package it.reyboz.bustorino.adapters; +import android.content.Context; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.GPSPoint; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.util.StopSorterByDistance; import it.reyboz.bustorino.fragments.FragmentListenerMain; import java.util.Collections; import java.util.List; public class SquareStopAdapter extends RecyclerView.Adapter { private final static int layoutRes = R.layout.item_stop_nearby_card; //private List stops; private @Nullable GPSPoint userPosition; private FragmentListenerMain listener; private List stops; public SquareStopAdapter(@Nullable List stopList, FragmentListenerMain fragmentListener, @Nullable GPSPoint pos) { listener = fragmentListener; userPosition = pos; stops = stopList; } @Override public SquareViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); //sort the stops by distance if(stops != null && stops.size() > 0) Collections.sort(stops,new StopSorterByDistance(userPosition)); return new SquareViewHolder(view); } @Override public void onBindViewHolder(SquareViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(stops==null || stops.size() == 0) return; //NO STOPS final Stop stop = stops.get(position); + final Context context = holder.itemView.getContext(); if(stop!=null){ if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ Double distance = stop.getDistanceFromLocation(userPosition); holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } - holder.stopNameView.setText(stop.getStopDisplayName()); + holder.stopNameView.setText(context.getString( + R.string.two_strings_format,"", stop.getStopDisplayName())); + // stop.ID +" - "+ stop.getStopDisplayName()); holder.stopIDView.setText(stop.ID); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { holder.routesView.setVisibility(View.GONE); } else { - holder.routesView.setText(whatStopsHere); + holder.routesView.setText(context.getString(R.string.lines_fill, whatStopsHere)); holder.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return stops.size(); } class SquareViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView stopIDView; TextView stopNameView; TextView routesView; TextView distancetextView; String stopID; SquareViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); stopIDView = (TextView) holdView.findViewById(R.id.stop_numberText); stopNameView = (TextView) holdView.findViewById(R.id.stop_nameText); routesView = (TextView) holdView.findViewById(R.id.stop_linesText); distancetextView = (TextView) holdView.findViewById(R.id.stop_distanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void setStops(List newStops) { // this.stops = stops; DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return stops.size(); } @Override public int getNewListSize() { return newStops.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { var oldItem = stops.get(oldItemPosition); var newItem = newStops.get(newItemPosition); // usa un ID univoco return oldItem.ID.equals(newItem.ID); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { var oldItem = stops.get(oldItemPosition); var newItem = newStops.get(newItemPosition); return oldItem.equals(newItem); } }); this.stops.clear(); this.stops.addAll(newStops); diffResult.dispatchUpdatesTo(this); } public void setUserPosition(@Nullable GPSPoint userPosition) { this.userPosition = userPosition; } /* @Override public Stop getItem(int position) { return stops.get(position); } */ } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt index b34d6dc..458f800 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt @@ -1,715 +1,858 @@ /* 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.content.Context +import android.content.res.ColorStateList import android.location.Location import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.AppCompatButton -import androidx.core.util.Pair +import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import com.google.android.material.button.MaterialButton import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.ArrivalsStopAdapter import it.reyboz.bustorino.adapters.SquareStopAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.DatabaseUpdate import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.middleware.FusedNativeLocationProvider import it.reyboz.bustorino.middleware.FusedNativeLocationProvider.LocationUpdateListener import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.util.Permissions.Companion.bothLocationPermissionsGranted import it.reyboz.bustorino.util.StopSorterByDistance +import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel import java.util.* +import java.util.concurrent.atomic.AtomicBoolean class NearbyStopsFragment : ScreenBaseFragment() { override fun getBaseViewForSnackBar(): View? { return null } enum class FragType(val num: Int) { STOPS(1), ARRIVALS(2); companion object { @JvmStatic fun fromNum(i: Int): FragType { when (i) { 1 -> return STOPS 2 -> return ARRIVALS else -> throw IllegalArgumentException("type not recognized") } } } } private enum class LocationShowingStatus { - SEARCHING, POSITION_FOUND, DISABLED, NO_PERMISSION + SEARCHING, LOCATION_FOUND, DISABLED, NO_PERMISSION //NO_STOPS_NEARBY } private var mListener: FragmentListenerMain? = null - private var fragment_type = FragType.STOPS + private var fragmentType = FragType.STOPS private lateinit var gridRecyclerView: RecyclerView private var dataAdapter: SquareStopAdapter? = null private var gridLayoutManager: AutoFitGridLayoutManager? = null private var lastPosition: GPSPoint? = null private var circlingProgressBar: ProgressBar? = null private lateinit var flatProgressBar: ProgressBar //protected SharedPreferences globalSharedPref; //private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private var messageTextView: TextView? = null private lateinit var enableLocationButton : MaterialButton private var titleTextView: TextView? = null private var loadingTextView: TextView? = null private var scrollListener: CommonScrollListener? = null private var switchButton: AppCompatButton? = null private var firstLocForStops = true private var firstLocForArrivals = true private var stopsMaxDistance = -3 private var stopsMinNumber = -1 //These are useful for the case of nearby arrivals private var arrivalsStopAdapter: ArrivalsStopAdapter? = null - private var currentNearbyStops = ArrayList() + private var currentNearbyStops: ArrayList? = null private var showingStatus = LocationShowingStatus.NO_PERMISSION private var isLocationEnabled = false + private var dataShownInAdapter = AtomicBoolean(false) + private var noDataMessageId = R.string.no_stops_nearby + private var loadingDataMessageId = R.string.position_searching_message + private val locationUpdateListener: LocationUpdateListener = object : LocationUpdateListener { override fun onLocationUpdate(location: Location) { - updateLocationViewModel(location) + + if (location.getAccuracy() < MIN_ACCURACY) { + lastPosition = GPSPoint(location.getLatitude(), location.getLongitude()) + viewModel.setLastLocation(location) + } else{ + Log.d(DEBUG_TAG, "Refusing location ${location.latitude},${location.longitude} because accuracy: ${location.accuracy} > MIN_ACCURACY=$MIN_ACCURACY") + } } override fun onFusedStatusChanged(isEnabled: Boolean) { Log.d(DEBUG_TAG, "Location provider is enabled: " + isEnabled) isLocationEnabled = isEnabled - if (isEnabled) { - setShowingStatus(LocationShowingStatus.SEARCHING) - } else { - setShowingStatus(LocationShowingStatus.DISABLED) - } + if(!dataShownInAdapter.get()) + if (isEnabled) { + setShowingStatus(LocationShowingStatus.SEARCHING) + } else { + setShowingStatus(LocationShowingStatus.DISABLED) + } } } private val locationPermissionLauncher = getPositionRequestLauncher(){ granted -> startLocationUpdatesByType() setShowingStatus(LocationShowingStatus.SEARCHING) } // Two different settings for the location provider private val locationOptionsArrivals = FusedNativeLocationProvider.Options(5 * 1000L, 25f) private val locationOptionsStops = FusedNativeLocationProvider.Options(1000L, 5f) private var locationProvider: FusedNativeLocationProvider? = null /*private val arrivalsListener: ArrivalsListener = object : ArrivalsListener { override fun setProgress(completedRequests: Int, pendingRequests: Int) { if (pendingRequests == 0) { flatProgressBar.setIndeterminate(true) flatProgressBar.setVisibility(View.GONE) } else { flatProgressBar.setIndeterminate(false) flatProgressBar.progress = completedRequests } } /*override fun onAllRequestsCancelled() { if (flatProgressBar != null) flatProgressBar!!.setVisibility(View.GONE) } */ override fun showCompletedArrivals(completedPalinas: ArrayList) { showArrivalsInRecycler(completedPalinas) } } */ //ViewModel private val viewModel : NearbyStopsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let{ setFragmentType(FragType.fromNum(it.getInt(FRAGMENT_TYPE_KEY))) } //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 nearbyArrivalsDownloader = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); locationProvider = FusedNativeLocationProvider(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Inflate the layout for this fragment val context = requireContext() val root = inflater.inflate(R.layout.fragment_nearby_stops, container, false) gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView) gridLayoutManager = AutoFitGridLayoutManager( context.getApplicationContext(), utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat()).toInt() ) gridRecyclerView.setLayoutManager(gridLayoutManager) gridRecyclerView.setHasFixedSize(false) circlingProgressBar = root.findViewById(R.id.circularProgressBar) flatProgressBar = root.findViewById(R.id.horizontalProgressBar) messageTextView = root.findViewById(R.id.messageTextView) enableLocationButton = root.findViewById(R.id.grantLocationButton) titleTextView = root.findViewById(R.id.titleTextView) loadingTextView = root.findViewById(R.id.positionLoadingTextView) switchButton = root.findViewById(R.id.switchButton) scrollListener = CommonScrollListener(mListener, false) - switchButton!!.setOnClickListener(View.OnClickListener { v: View? -> switchFragmentType() }) + switchButton!!.setOnClickListener { v: View? -> switchFragmentType() } if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "onCreateView") val appContext = requireContext().applicationContext DatabaseUpdate.watchUpdateWorkStatus(context, this){ workInfos -> if (workInfos.isEmpty()) { viewModel.setDBUpdateRunning(false) return@watchUpdateWorkStatus } val wi = workInfos.get(0) if (wi.state == WorkInfo.State.RUNNING && locationProvider!!.isRunning()) { locationProvider!!.stopUpdates() viewModel.setDBUpdateRunning(true) } else { //start the request checkPermissionLocationStart() viewModel.setDBUpdateRunning(false) //actually restart request } } //add location listener locationProvider!!.addListener(locationUpdateListener) enableLocationButton.setOnClickListener { locationPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS) } return root } private fun checkPermissionLocationStart(){ Log.d(DEBUG_TAG, "Check permission and start location updates") if (bothLocationPermissionsGranted(requireContext())) { if (!locationProvider!!.isRunning()) { startLocationUpdatesByType() setShowingStatus(LocationShowingStatus.SEARCHING) + } else{ + Log.w(DEBUG_TAG, "Asked to check and start location updates, but provider is already running") } } else { setShowingStatus(LocationShowingStatus.NO_PERMISSION) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) gridRecyclerView.setVisibility(View.INVISIBLE) gridRecyclerView.addOnScrollListener(scrollListener!!) //mListener?.readyGUIfor(FragmentKind.NEARBY_STOPS) //observe the livedata viewModel.stopsAtDistance.observe(getViewLifecycleOwner()) {stops -> Log.d(DEBUG_TAG, "Received " + stops.size + " stops nearby") var distance = viewModel.distanceMtLiveData.getValue() if (distance == null) { distance = 40 } if ((stops.size < stopsMinNumber && distance <= stopsMaxDistance)) { - viewModel.setDistance(distance + 40) + viewModel.setDistance(distance + 50) //viewModel.requestStopsAtDistance(distance, true); //Log.d(DEBUG_TAG, "Doubling distance now!"); return@observe // THIS WORKS AS AN `else` } - if (!stops.isEmpty()) { - currentNearbyStops = stops - setShowingStatus(LocationShowingStatus.POSITION_FOUND) - showStopsInViews(currentNearbyStops, lastPosition) + displayStopsOrRequestArrivals(stops) + } + + viewModel.locationLiveData.observe(getViewLifecycleOwner()) {loc -> + if(loc!=null){ + setShowingStatus(LocationShowingStatus.LOCATION_FOUND) } + //the stops are updated manually } viewModel.downloadingArrivals.observe(viewLifecycleOwner){ running -> + flatProgressBar.isIndeterminate = true if(!running) flatProgressBar.visibility = View.GONE else flatProgressBar.visibility = View.VISIBLE } - viewModel.progressPerc.observe(viewLifecycleOwner){ progress -> + /*viewModel.progressPerc.observe(viewLifecycleOwner){ progress -> flatProgressBar.isIndeterminate = false flatProgressBar.progress = progress flatProgressBar.max = 100 if (progress<100){ flatProgressBar.visibility = View.VISIBLE } } + */ + viewModel.arrivalsDecoupled.observe(viewLifecycleOwner){ stoprouteList -> if (getContext() == null) { Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached") return@observe } - val context = requireContext() - if (firstLocForArrivals) { - mListener?.let{ - lastPosition?.let{ pos -> - arrivalsStopAdapter = ArrivalsStopAdapter(stoprouteList, it, context, pos) - gridRecyclerView.setAdapter(arrivalsStopAdapter) - firstLocForArrivals = false - } - } - } else { - lastPosition?.let{ pos -> - arrivalsStopAdapter?.setRoutesPairListAndPosition(stoprouteList, pos) - } - } + showArrivals(stoprouteList) //arrivalsStopAdapter.notifyDataSetChanged(); - setShowingStatus(LocationShowingStatus.POSITION_FOUND) - showRecyclerHidingLoadMessage() + //showRecyclerHidingLoadMessage() //if (mListener != null) mListener!!.readyGUIfor(FragmentKind.NEARBY_ARRIVALS) } //added - checkPermissionLocationStart() + //checkPermissionLocationStart() + } + + private fun displayStopsOrRequestArrivals(stops: ArrayList){ + if (!stops.isEmpty()) { + currentNearbyStops = stops + //displayStopsOrLaunchArrivalsRequest(stops, lastPosition) + viewModel.locationLiveData.value?.let{loc -> + when(fragmentType){ + FragType.STOPS->{ + + lastPosition?.let{loc -> + Collections.sort(stops, StopSorterByDistance(loc)) + showStopsInRecycler(stops,loc) + } + + } + FragType.ARRIVALS -> { + viewModel.requestArrivalsForStops(stops) + } + } + } + } else{ + showNoStopsMessage() + viewModel.cancelAllArrivalsRequests() + } + } + + private fun showArrivals(stoprouteList: ArrayList){ + val context = requireContext() + if (firstLocForArrivals) { + mListener?.let{ + lastPosition?.let{ pos -> + arrivalsStopAdapter = ArrivalsStopAdapter(stoprouteList, it, context, pos) + gridRecyclerView.setAdapter(arrivalsStopAdapter) + firstLocForArrivals = false + } + } + } else { + lastPosition?.let{ pos -> + arrivalsStopAdapter?.setRoutesPairListAndPosition(stoprouteList, pos) + } + } + dataShownInAdapter.set(true) } /** * Internal bit used to start location updates */ private fun startLocationUpdatesByType() { - when (fragment_type) { + when (fragmentType) { FragType.STOPS -> locationProvider!!.startUpdates(locationOptionsStops) FragType.ARRIVALS -> locationProvider!!.startUpdates(locationOptionsArrivals) } } /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ private fun setFragmentType(type: FragType) { - val isChanged = fragment_type != type - this.fragment_type = type - /*switch(type){ - case ARRIVALS: - TIME_INTERVAL_REQUESTS = 5*1000; - break; - case STOPS: - TIME_INTERVAL_REQUESTS = 1000; - - } + val isChanged = fragmentType != type + this.fragmentType = type - */ if (isChanged) { startLocationUpdatesByType() - setShowingStatus(LocationShowingStatus.SEARCHING) } } /** * Set the location in the view model if it is good * @param location new location */ + /* private fun updateLocationViewModel(location: Location, accuracy: Float = 150f) { - if (location.getAccuracy() < accuracy) { - lastPosition = GPSPoint(location.getLatitude(), location.getLongitude()) - //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); - viewModel.setLastLocation(location) - } + } + */ + private fun setShowingStatus(newStatus: LocationShowingStatus) { var newStatus = newStatus if (newStatus == showingStatus) { return } if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Changing showing status from $showingStatus to $newStatus") if (!isLocationEnabled && newStatus != LocationShowingStatus.NO_PERMISSION) { Log.d(DEBUG_TAG, "asked to show status: $newStatus but the position is disabled") newStatus = LocationShowingStatus.DISABLED } when (newStatus) { - LocationShowingStatus.POSITION_FOUND -> { + LocationShowingStatus.LOCATION_FOUND -> { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.VISIBLE) messageTextView!!.setVisibility(View.GONE) enableLocationButton.setVisibility(View.GONE) - } LocationShowingStatus.NO_PERMISSION -> { circlingProgressBar?.setVisibility(View.GONE) + flatProgressBar.setVisibility(View.GONE) loadingTextView?.setVisibility(View.GONE) messageTextView?.setText(R.string.enable_position_message_nearby) messageTextView?.setVisibility(View.VISIBLE) enableLocationButton.setVisibility(View.VISIBLE) } LocationShowingStatus.DISABLED -> { //if (showingStatus== LocationShowingStatus.SEARCHING){ circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) + flatProgressBar.setVisibility(View.GONE) //} messageTextView!!.setText(R.string.enable_location_message) messageTextView!!.setVisibility(View.VISIBLE) enableLocationButton.setVisibility(View.GONE) } - LocationShowingStatus.SEARCHING -> { circlingProgressBar!!.setVisibility(View.VISIBLE) - loadingTextView!!.setVisibility(View.VISIBLE) + flatProgressBar.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.GONE) messageTextView!!.setVisibility(View.GONE) enableLocationButton.setVisibility(View.GONE) + loadingTextView?.apply { + setText(loadingDataMessageId) + visibility = View.VISIBLE + } } } showingStatus = newStatus } override fun onAttach(context: Context) { super.onAttach(context) if (context is FragmentListenerMain) { mListener = context as FragmentListenerMain } else { throw RuntimeException( context .toString() + " must implement OnFragmentInteractionListener" ) } Log.d(DEBUG_TAG, "OnAttach called") //viewModel = ViewModelProvider(this).get(NearbyStopsViewModel::class.java) } override fun onPause() { super.onPause() //gridRecyclerView.setAdapter(null) Log.d(DEBUG_TAG, "On paused called") locationProvider!!.stopUpdates() } override fun onResume() { super.onResume() //fix view if we were showing the stops or the arrivals - prepareForFragmentType() - when (fragment_type) { + loadPreferencesStops() + setGuiForFragmentType(fragmentType) + //if(lastPosition == null){ + viewModel.locationLiveData.value?.let{loc -> lastPosition = loc } + //} + + if(bothLocationPermissionsGranted(requireContext())){ + locationProvider?.apply { + if(isLocationEnabled()){ + //location is enabled, start updates + startLocationUpdatesByType() + if(lastPosition == null){ + setShowingStatus(LocationShowingStatus.SEARCHING) + } + } else{ + setShowingStatus(LocationShowingStatus.DISABLED) + } + } + + } else{ + setShowingStatus(LocationShowingStatus.NO_PERMISSION) + } + //setupDataAndLayoutByFragmentType() + + mListener!!.enableRefreshLayout(false) + + if(fragmentType == FragType.ARRIVALS){ + viewModel.arrivalsDecoupled.value?.let{ + //re-do the adapter + firstLocForArrivals = true + showArrivals(it) + } + } else if(fragmentType == FragType.STOPS) { + viewModel.stopsAtDistance.value?.let { + //remake the adapter + firstLocForStops = true + displayStopsOrRequestArrivals(it) + } + } + + /* + when (fragmentType) { FragType.STOPS -> if (dataAdapter != null) { //gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) } FragType.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") stopsMinNumber = 5 stopsMaxDistance = 600 - return + } //Re-read preferences + + + } + private fun loadPreferencesStops(){ val shpr = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext()) //For some reason, they are all saved as strings stopsMaxDistance = shpr.getInt(getString(R.string.pref_key_radius_recents), 600) var isMinStopInt = true try { stopsMinNumber = shpr.getInt(getString(R.string.pref_key_num_recents), 5) } catch (ex: ClassCastException) { isMinStopInt = false } if (!isMinStopInt) try { stopsMinNumber = shpr.getString(getString(R.string.pref_key_num_recents), "5")!!.toInt() } catch (ex: NumberFormatException) { stopsMinNumber = 5 } if (BuildConfig.DEBUG) Log.d( DEBUG_TAG, "Max distance for stops: $stopsMaxDistance, Min number of stops: $stopsMinNumber" ) - - //checkPermissionLocationStart() } override fun onDetach() { super.onDetach() mListener = null //if (arrivalsManager != null) arrivalsManager!!.cancelAllRequests() } override fun onStart() { super.onStart() if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "onStart called") //checkPermissionLocationStart() } /** * Display the stops, or run new set of requests for arrivals */ - private fun showStopsInViews(stops: ArrayList, location: GPSPoint?) { + /*private fun displayStopsOrLaunchArrivalsRequest(stops: ArrayList, location: GPSPoint) { if (stops.isEmpty()) { setNoStopsLayout() return } - if (location == null) { - // we could do something better, but it's better to do this for now - return - } - - /*var minDistance = Double.POSITIVE_INFINITY - for (s in stops) { - minDistance = min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude())) - } - - */ - - //quick trial to hopefully always get the stops in the correct order - Collections.sort(stops, StopSorterByDistance(location)) + when (fragment_type) { - FragType.STOPS -> showStopsInRecycler(stops) + FragType.STOPS -> { + setShowingStatus(LocationShowingStatus.STOPS_FOUND) + showStopsInRecycler(stops, location) + } FragType.ARRIVALS -> { - //don't do anything if we're not attached - /*context?.let{ - if (arrivalsManager == null) arrivalsManager = - NearbyArrivalsDownloader(it.applicationContext, arrivalsListener) - arrivalsManager!!.requestArrivalsForStops(stops) - } - - */ viewModel.requestArrivalsForStops(stops) + //setShowingStatus(LocationShowingStatus.SEARCHING) + viewModel.arrivalsDecoupled.value?.let{ + + } } } } - /** - * To enable targeting from the Button */ - fun switchFragmentType(v: View?) { - switchFragmentType() - } + /** * Call when you need to switch the type of fragment */ private fun switchFragmentType() { - when (fragment_type) { - FragType.ARRIVALS -> setFragmentType(FragType.STOPS) - FragType.STOPS -> setFragmentType(FragType.ARRIVALS) - else -> {} + when (fragmentType) { + FragType.ARRIVALS -> { + viewModel.cancelAllArrivalsRequests() + setFragmentType(FragType.STOPS) + //when switching from arrivals + firstLocForStops = true + } + FragType.STOPS -> { + setFragmentType(FragType.ARRIVALS) + firstLocForArrivals = true + } + } + //now it's switched + setGuiForFragmentType(fragmentType) + + viewModel.stopsAtDistance.value?.let{ + //re-issue update, triggering chain + viewModel.stopsAtDistance.value = it + } + /*if(fragmentType == FragType.ARRIVALS) { + viewModel.stopsAtDistance.value?.let{ + viewModel.requestArrivalsForStops(it) + } } - prepareForFragmentType() - //locManager.removeLocationRequestFor(fragmentLocationListener); - //locManager.addLocationRequestFor(fragmentLocationListener); - if (lastPosition != null) { - // we have at least one fix on the position - showStopsInViews(currentNearbyStops, lastPosition) + + */ + // + //setupDataAndLayoutByFragmentType() + } + private fun setGuiForFragmentType(fragmentType: FragType) { + when (fragmentType) { + FragType.STOPS ->{ + switchButton!!.text = getString(R.string.show_arrivals) + titleTextView!!.text = getString(R.string.nearby_stops_message) + noDataMessageId = R.string.no_stops_nearby + loadingDataMessageId = R.string.position_searching_message + //switchButton?.backgroundTintList = ColorStateList.valueOf( + // ViewUtils.getColorFromTheme(requireContext(), R.attr.colorPrimaryDark)) + + } + FragType.ARRIVALS ->{ + titleTextView!!.text = getString(R.string.nearby_arrivals_message) + switchButton!!.text = getString(R.string.show_stops) + noDataMessageId = R.string.no_stops_nearby_arrivals + loadingDataMessageId = R.string.searching_arrivals_indefinite + //switchButton?.backgroundTintList = ColorStateList.valueOf( + // ResourcesCompat.getColor(resources, R.color.light_blue_900, requireActivity().theme)) + } } } /** * Prepare the views for the set fragment type */ - private fun prepareForFragmentType() { - if (fragment_type == FragType.STOPS) { + /*private fun setupDataAndLayoutByFragmentType() { + + var dataAvailable = false + if (fragmentType == FragType.STOPS) { switchButton!!.text = getString(R.string.show_arrivals) titleTextView!!.text = getString(R.string.nearby_stops_message) - - dataAdapter?.let{ gridRecyclerView.adapter = it - + viewModel.stopsAtDistance.value?.let { stops-> + // if data adapter is not null set stops, otherwise + dataAdapter?.setStops(stops) ?: lastPosition?.let{ pos -> + dataAdapter = SquareStopAdapter(stops, mListener, pos) + } + Log.d(DEBUG_TAG, "Found ${stops.size} stops") + } + dataAdapter?.let{ + gridRecyclerView.adapter = it + dataAvailable = true } - mListener?.readyGUIfor(FragmentKind.NEARBY_STOPS) - } else if (fragment_type == FragType.ARRIVALS) { + } else if (fragmentType == FragType.ARRIVALS) { titleTextView!!.text = getString(R.string.nearby_arrivals_message) switchButton!!.text = getString(R.string.show_stops) val arrivalsSorted = viewModel.arrivalsDecoupled.value arrivalsSorted?.let{ - lastPosition?.let{pos -> - arrivalsStopAdapter = ArrivalsStopAdapter(it,mListener!!,requireContext(), pos ) - } + arrivalsStopAdapter?.setRoutesPairListAndPosition( + it, lastPosition) ?: lastPosition?.let{pos -> + arrivalsStopAdapter = ArrivalsStopAdapter(it, mListener!!, requireContext(), pos) + } } + arrivalsStopAdapter?.let{ gridRecyclerView.setAdapter(it) } - mListener?.readyGUIfor(FragmentKind.NEARBY_ARRIVALS) } + if(gridRecyclerView.adapter == null){ + flatProgressBar.isIndeterminate = true + flatProgressBar.visibility = View.VISIBLE + } else{ + flatProgressBar.visibility = View.GONE + flatProgressBar.isIndeterminate = false + } + } + */ + //useful methods /**//// GUI METHODS //////// */ - private fun showStopsInRecycler(stops: MutableList?) { + private fun showStopsInRecycler(stops: MutableList, location: GPSPoint) { + // hide the progress bar + flatProgressBar.visibility = View.GONE + + Collections.sort(stops, StopSorterByDistance(location)) if (dataAdapter == null) { dataAdapter = SquareStopAdapter(stops, mListener, lastPosition) - gridRecyclerView!!.setAdapter(dataAdapter) firstLocForStops = false } else { dataAdapter!!.setUserPosition(lastPosition) dataAdapter!!.setStops(stops) } + gridRecyclerView.setAdapter(dataAdapter) + + if(gridRecyclerView.visibility != View.VISIBLE){ + if(showingStatus == LocationShowingStatus.LOCATION_FOUND) + Log.e(DEBUG_TAG, "Visualization error: the recyclerView is not visible but location status is $showingStatus") + else{ + Log.w(DEBUG_TAG, "Grid recyclerView should be visible for the stops, setting status ${LocationShowingStatus.LOCATION_FOUND}") + setShowingStatus(LocationShowingStatus.LOCATION_FOUND) + } + } + dataShownInAdapter.set(true) //showRecyclerHidingLoadMessage(); - if (gridRecyclerView!!.getVisibility() != View.VISIBLE) { + /*if (gridRecyclerView!!.getVisibility() != View.VISIBLE) { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView!!.setVisibility(View.VISIBLE) } messageTextView!!.setVisibility(View.GONE) - } - - private fun showArrivalsInRecycler(routesPairList: List>) { - + */ } - private fun 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 fun showRecyclerHidingLoadMessage() { + /*private fun showRecyclerHidingLoadMessage() { if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.VISIBLE) } messageTextView!!.setVisibility(View.GONE) - } /* + } + + */ + + private fun showNoStopsMessage(){ + messageTextView!!.setVisibility(View.VISIBLE) + messageTextView!!.setText(noDataMessageId) + flatProgressBar.visibility = View.GONE + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + enableLocationButton.setVisibility(View.GONE) + } + + /* * Local locationListener, to use for the GPS */ /* class FragmentLocationListener implements LocationListenerCompat { private long lastUpdateTime = -1; public boolean isRegistered = false; @Override public void onLocationChanged(@NonNull Location location) { if(viewModel==null){ return; } if(location.getAccuracy()<200) { lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); viewModel.setLastLocation(location); } 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 String provider, int status, @Nullable Bundle extras) { LocationListenerCompat.super.onStatusChanged(provider, status, extras); } } */ companion object { private const val DEBUG_TAG = "NearbyStopsFragment" private const val FRAGMENT_TYPE_KEY = "FragmentType" const val FRAGMENT_TAG: String = "NearbyStopsFrag" const val COLUMN_WIDTH_DP: Int = 250 + const val MIN_ACCURACY = 200.0 /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ @JvmStatic fun newInstance(type: FragType): NearbyStopsFragment { //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); val fragment = NearbyStopsFragment() val args = Bundle(1) args.putInt(FRAGMENT_TYPE_KEY, type.num) fragment.setArguments(args) return fragment } } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt index 1a3bcbf..6a30a53 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt @@ -1,354 +1,359 @@ /* BusTO - View Model components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.viewmodels import android.app.Application import android.location.Location import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import com.android.volley.Response import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.mato.MapiArrivalRequest import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.util.StopSorterByDistance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong import kotlin.time.Duration.Companion.seconds class NearbyStopsViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, application) val arrivalsNearby = MutableLiveData>() // map of arrivals by stopID private val arrivalsMapStopID = ConcurrentHashMap() val progressPerc = MutableLiveData() + //val fragmentType = MutableLiveData() + val downloadingArrivals = MutableLiveData() val lastTimeFinished = AtomicLong(0) private var job : Job? = null /*private val arrivalsListener = object : NearbyArrivalsDownloader.ArrivalsListener { override fun setProgress(completedRequests: Int, pendingRequests: Int) { val totalReq = completedRequests + pendingRequests progressPerc.postValue( (completedRequests * 100) / totalReq ) if(pendingRequests == 0) downloadingArrivals.postValue(false) } override fun onAllRequestsCancelled() { downloadingArrivals.postValue(false) } override fun showCompletedArrivals(completedPalinas: ArrayList) { arrivalsNearby.postValue(completedPalinas) } } */ //private val nearbyArrivalsDownloader = NearbyArrivalsDownloader(application,arrivalsListener ) private val volleyManager = NetworkVolleyManager.getInstance(application) /** * Response listener for the requests */ private val responseListener = Response.Listener { p -> val key = p.ID arrivalsMapStopID[key] = p arrivalsNearby.postValue(arrivalsMapStopID.values.toList()) val c = completedRequests.incrementAndGet() val r = runningRequests.decrementAndGet() updateProgressPost(c,errorRequests.get(), totalReqs.get()) if(r==0){ setFinishedPost() } } /** * Error listener for the requests */ private val errorListener = Response.ErrorListener { error -> val e = errorRequests.incrementAndGet() val r = runningRequests.decrementAndGet() updateProgressPost(completedRequests.get(), e, totalReqs.get()) if(r==0){ setFinishedPost() } if(BuildConfig.DEBUG) Log.d(DEBUG_TAG,"query to palina, error "+ error.toString()) } private fun setFinishedPost(){ downloadingArrivals.postValue(false) //val time = System.currentTimeMillis() //lastTimeFinished.set(time) job?.cancel() job = viewModelScope.launch { delay(30.seconds) launch(Dispatchers.Main) { Log.d(DEBUG_TAG, "Updating arrivals from job") stopsAtDistance.value?.let { requestArrivalsForStops(it) } } } } private val totalReqs = AtomicInteger(0) private val completedRequests = AtomicInteger(0) private val errorRequests = AtomicInteger(0) private val runningRequests = AtomicInteger(0) + fun cancelAllArrivalsRequests() { + volleyManager.requestQueue.cancelAll(REQUEST_TAG) + } /** * Run new batch of requests */ fun requestArrivalsForStops(stops: List) { //nearbyArrivalsDownloader.requestArrivalsForStops(stops) if(runningRequests.get() > 0) { - volleyManager.requestQueue.cancelAll(REQUEST_TAG) + cancelAllArrivalsRequests() } val currentDate = Date() val timeRange = 3600 val departures = 10 totalReqs.set(stops.size) runningRequests.set(stops.size) completedRequests.set(0) errorRequests.set(0) arrivalsMapStopID.clear() for (s in stops) { val req = MapiArrivalRequest(s.ID, currentDate, timeRange, departures, responseListener, errorListener) req.setTag(REQUEST_TAG) volleyManager.addToRequestQueue(req) } downloadingArrivals.value = (true) } private fun updateProgressPost(completed: Int, error: Int, total: Int) { val done = completed + error progressPerc.postValue( (done * 100) / total ) } //// ------- LOCATION STUFF --------- val locationLiveData = MutableLiveData() val distanceMtLiveData = MutableLiveData(40) val stopsAtDistance = MediatorLiveData>() private val dbUpdateRunning = MutableLiveData(false) private val callback = OldDataRepository.Callback> { res -> if(res.isSuccess){ stopsAtDistance.postValue(res.result) if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Setting value of stops in bounding box") } } fun setLastLocation(location: Location) { locationLiveData.value = GPSPoint(location.latitude, location.longitude) } fun setDistance(distance: Int) { distanceMtLiveData.value = distance } fun setDBUpdateRunning(running: Boolean) { dbUpdateRunning.value = (running) } /** * Request stop in location [latitude], [longitude], at distance [distanceMeters] * If [saveValues] is true, store the position and the distance used */ fun requestStopsAtDistance(latitude: Double, longitude: Double, distanceMeters: Int, saveValues: Boolean){ if(saveValues){ locationLiveData.postValue(GPSPoint(latitude, longitude)) distanceMtLiveData.postValue(distanceMeters) } oldRepo.requestStopsWithinDistance(latitude, longitude, distanceMeters, callback) } /** * Request stops using the previously saved location */ fun requestStopsAtDistance(distanceMeters: Int, saveValue: Boolean){ if(saveValue){ distanceMtLiveData.postValue(distanceMeters) } oldRepo.requestStopsWithinDistance( locationLiveData.value!!.latitude, locationLiveData.value!!.longitude, distanceMeters, callback) } fun requestStopsCheckDBRunning(position: GPSPoint, distanceMt: Int){ if(dbUpdateRunning.value==null || !(dbUpdateRunning.value!!)){ oldRepo.requestStopsWithinDistance(position.latitude, position.longitude, distanceMt, callback) } else{ Log.d(DEBUG_TAG, "Database update is running, cannot do it") } } fun postLocation(location: Location){ locationLiveData.postValue(GPSPoint(location.latitude, location.longitude)) } fun postLocation(location: GPSPoint){ locationLiveData.postValue(location) } fun postLastDistance(distanceMeters: Int){ distanceMtLiveData.postValue(distanceMeters) } init { stopsAtDistance.addSource(locationLiveData){ point-> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "New location: $point") val distance = distanceMtLiveData.value ?: 40 //oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, distance, callback) requestStopsCheckDBRunning(point, distance) } stopsAtDistance.addSource(distanceMtLiveData){ dist-> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "New distance: $dist") if(locationLiveData.value != null){ val point: GPSPoint = locationLiveData.value!! //oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, dist, callback) requestStopsCheckDBRunning(point, dist) } else{ Log.d(DEBUG_TAG, "Modified distance but locationLiveData value is null") } } stopsAtDistance.addSource(dbUpdateRunning){ running -> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "DB update running: $running") if(!running) { reRequestStops() } } } private fun reRequestStops(){ var req = false locationLiveData.value?.let{ point -> distanceMtLiveData.value ?.let { dist-> req = true oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, dist, callback) } } if(!req){ Log.w(DEBUG_TAG, "Requested to rerun stops, but position or distance (or both) are null") } } val arrivalsDecoupled = arrivalsNearby.map { palinas -> locationLiveData.value?.let { loc -> Collections.sort(palinas, StopSorterByDistance(loc)) } var routesPairList = ArrayList(10) //int maxNum = Math.min(MAX_STOPS, stopList.size()); for (p in palinas) { //if there are no routes available, skip stop if (p.queryAllRoutes().isEmpty()) continue for (r in p.queryAllRoutes()) { //if there are no routes, should not do anything if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(RouteWithStop(p, r)) } } val pos = locationLiveData.value if(pos != null) { routesPairList.sortWith { p1, p2 -> comparePairsRoutesArrivals(p1,p2,pos) } } routesPairList } fun comparePairsRoutesArrivals(pair1: RouteWithStop, pair2: RouteWithStop, pos: GPSPoint) : Int{ var delta = 0 val stop1: Stop = pair1.stop val stop2: Stop = pair2.stop val dist1 = utils.measuredistanceBetween( pos.latitude, pos.longitude, stop1.getLatitude()!!, stop1.getLongitude()!! ) val dist2 = utils.measuredistanceBetween( pos.latitude, pos.longitude, stop2.getLatitude()!!, stop2.getLongitude()!! ) val passaggi1 = pair1.route.passaggi val passaggi2 = pair2.route.passaggi if (passaggi1.size <= 0 || passaggi2.size <= 0) { Log.e("ArrivalsStopAdapter", "Cannot compare: No arrivals in one of the stops") } else { Collections.sort(passaggi1) Collections.sort(passaggi2) /*int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh; if(deltaOre>12) deltaOre -= 24; else if (deltaOre<-12) deltaOre += 24; delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm; */ delta = passaggi1[0]!!.getDifferenceMinutes(passaggi2[0]!!).toInt() } delta += ((dist1 - dist2) * MINUTI_PER_METRO * DISTANCE_MULTIPLIER).toInt() return delta } override fun onCleared() { - volleyManager.requestQueue.cancelAll(REQUEST_TAG) + cancelAllArrivalsRequests() super.onCleared() } companion object{ private const val DEBUG_TAG = "BusTO-NearbyStopVwModel" const val REQUEST_TAG: String = "NearbyArrivals" const val MINUTI_PER_METRO: Double = 6.0 / 100 //v = 5km/h const val DISTANCE_MULTIPLIER: Double = 2.0 / 3 } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nearby_stops.xml b/app/src/main/res/layout/fragment_nearby_stops.xml index f14fbd6..df40454 100644 --- a/app/src/main/res/layout/fragment_nearby_stops.xml +++ b/app/src/main/res/layout/fragment_nearby_stops.xml @@ -1,118 +1,168 @@ - + + + + android:layout_marginBottom="5dp" + app:layout_constraintBottom_toTopOf="@id/nearbyTopSeparator" + /> + + android:verticalSpacing="10dp" + app:layout_constraintTop_toBottomOf="@id/nearbyTopSeparator" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> - + diff --git a/app/src/main/res/layout/item_arrivals_nearby_card.xml b/app/src/main/res/layout/item_arrivals_nearby_card.xml index 2970e36..3068829 100644 --- a/app/src/main/res/layout/item_arrivals_nearby_card.xml +++ b/app/src/main/res/layout/item_arrivals_nearby_card.xml @@ -1,111 +1,116 @@ + android:layout_margin="5dp" android:padding="6dp" +> + android:layout_marginStart="10dp" + android:gravity="center_vertical" + /> - \ No newline at end of file diff --git a/app/src/main/res/layout/item_stop_nearby_card.xml b/app/src/main/res/layout/item_stop_nearby_card.xml index 8182544..ceaccad 100644 --- a/app/src/main/res/layout/item_stop_nearby_card.xml +++ b/app/src/main/res/layout/item_stop_nearby_card.xml @@ -1,124 +1,110 @@ - + app:cardCornerRadius="5dp" + app:strokeWidth="1dp" + app:strokeColor="@color/card_border" + app:cardBackgroundColor="@color/card_background" + android:layout_marginTop="6dp" + android:layout_marginBottom="8dp" + android:layout_marginStart="5dp" + android:layout_marginEnd="5dp" + android:padding="4dp"> - - + + android:layout_height="wrap_content" + android:text="7261" + android:textSize="19sp" + android:textIsSelectable="true" + android:fontFamily="@font/lato_bold" + android:textColor="@color/stop_number_color" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:layout_marginBottom="6dp" + android:layout_marginTop="10dp" + android:layout_marginStart="13dp" + android:layout_marginEnd="20dp"/> - - + - - + + - - \ No newline at end of file + android:fontFamily="@font/lato_regular" + android:layout_marginTop="8dp" + android:layout_marginBottom="10dp" + android:layout_marginEnd="15dp" + + app:layout_constraintTop_toBottomOf="@id/stop_nameText" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/stop_distanceTextView" + android:layout_marginStart="50dp" + + /> + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index ff98671..b0a9830 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,31 +1,35 @@ #ababab #0b6fc1 #bfbfbf #1a4db2 #3770e2 #c34322 #861313 #3657b0 #2270c3F @color/grey_900 @color/grey_900 @color/orange_700_night #666666 + @color/grey_800 @color/grey_400 + @color/light_blue_600 + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 434e46d..83324ae 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,96 +1,101 @@ #ff9800 #E77D13 #F57C00 #f57200 #e66b00 #cc6600 #994d00 #2196F3 + #0b6fc1 + #2a65e8 #2060dd #8A4247 #FFF3E0 #fff5e5 #e08700 #d16900 #2378e8 #0079f5 #2a968b #0067ff #2F59CC #CC5E43 #548017 #228b22 #0ABA34 #009688 #00a38d #009480 #00a887 #4DB6AC #80cbc4 #008175 #F5F5F5 #dddddd #f8f8f8 #bababa #acacac #757575 #444 #353535 #303030 #f2f2f2 #DE0908 #b30000 #dd441f #b30d0d #f15000 #f2621a #f47333 #2060DD #FFFFFF #000000 #1c1c1c #3f3f3f @color/blue_mid_2 @color/red_dark @color/blue_extra #3089e8 #FF039BE5 #FF01579B #FF40C4FF #FF00B0FF #66000000 #555 #cccccc @color/grey_500 #00000000 @color/orange_700 @color/grey_200 @color/grey_050 + @color/grey_200 @color/orange_500 @color/blue_extraurbano @color/metro_red @color/orange_icons_10light @color/grey_400 @color/orange_750_l45 @color/grey_700 @color/blue_700 + @color/light_blue_900 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6684ecc..2965fd1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,416 +1,418 @@ 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 has 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 Arrivals at: Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines No lines found in this category No lines match the searched name Destination: Lines: %1$s Line %1$s Line %1$s towards: Stop %1$s Vehicle %1$s No timetable found No QR code found, try using another app to scan Scan QR code at the bus stop Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Open the wiki 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, an open source and 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!


Introductory tutorial

If you want to see the introduction again, use the button below:

]]>
News and Updates

On the Telegram channel, you can find information about the latest app updates

]]>
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 for icons and support and design libraries.
- Other icons from Bootstrap, Feather, and Hero Icons.
- All the contributors, and the beta testers, too!


If you want more information or want to contribute to development, use the buttons below! ]]>
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 Show line details Show full direction Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Near me Nearby stops Nearby arrivals 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 + Loading arrival times + No stops nearby to search arrival times for 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 location to show it on the map Allow access to location to show stops nearby Please enable location on the device No GPS receiver found on the device! Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Show introduction Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s Loading arrivals from %1$s GTT App GTT Website 5T Torino website Muoversi a Torino 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 BusTO - live position service Live positions Showing activity related to the live positions service MaTO live bus positions service is running 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 Home Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Filter by name Launching database update Downloading data from MaTO server Downloading realtime alerts data Capitalize directions @string/directions_capitalize_no_change @string/directions_capitalize_everything @string/directions_capitalize_first_letter 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_home_text @string/near_me_title @string/nav_favorites_text @string/nav_map_text @string/lines Light Dark Change it with the system Set dark or light theme Source of real time positions for buses and trams MaTO (updated more frequently, might have errors) GTFS RT (less frequently updated, more accurate) @string/positions_source_mato_descr @string/positions_source_gtfsrt_descr "You are too far, not showing your position Style of the map Versatiles (vector) OSM legacy (raster, lighter) @string/map_style_versatiles @string/map_style_legacy_raster 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 Missing permission To use this functionality, the application needs access to the location, which now can only be granted in the system settings. Open settings OK, close the tutorial Close the tutorial Enable notifications Notifications enabled Backup and restore Backup Restore Backup or restore data Data saved Backup completed Backup to file Import data from backup Backup has been imported Check at least one item to import! Import favorites from backup Import preferences from backup Hello blank fragment No map app present to show the stop! Direction is already shown Loading destination… Destination unknown Service for positions working normally No positions received Error connecting to the server Error parsing the server response Error: network response is not as expected Connecting... MaTO GTFS RT Live positions source: Switch source Clear bus positions when switching live positions source Updated: %1$s Alerts for line %1$s: Alerts for stop %1$s: No alerts in your language, showing in %1$s Italian English Checking new alerts now Press back again to close the app
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 75c8645..43597e3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,103 +1,106 @@ + \ No newline at end of file