diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt --- a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt @@ -11,11 +11,11 @@ import java.lang.ref.WeakReference class RouteAdapter(val routes: List, - click: onItemClick, - private val layoutId: Int = R.layout.line_title_header) : + click: ItemClicker, + private val layoutId: Int = R.layout.line_title_header) : RecyclerView.Adapter() { - val clickreference: WeakReference + val clickreference: WeakReference init { clickreference = WeakReference(click) } @@ -53,7 +53,7 @@ } } - fun interface onItemClick{ + fun interface ItemClicker{ fun onRouteItemClicked(gtfsRoute: GtfsRoute) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt --- a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt @@ -7,10 +7,18 @@ import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Palina +import java.lang.ref.WeakReference -class RouteOnlyLineAdapter (val routeNames: List) : +class RouteOnlyLineAdapter (val routeNames: List, + onItemClick: OnClick?) : RecyclerView.Adapter() { + + private val clickreference: WeakReference? + init { + clickreference = if(onItemClick!=null) WeakReference(onItemClick) else null + } + /** * Provide a reference to the type of views that you are using * (custom ViewHolder) @@ -23,7 +31,7 @@ textView = view.findViewById(R.id.routeBallID) } } - constructor(palina: Palina, showOnlyEmpty: Boolean): this(palina.routesNamesWithNoPassages) + constructor(palina: Palina, showOnlyEmpty: Boolean): this(palina.routesNamesWithNoPassages, null) // Create new views (invoked by the layout manager) override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { @@ -40,9 +48,15 @@ // Get element from your dataset at this position and replace the // contents of the view with that element viewHolder.textView.text = routeNames[position] + viewHolder.itemView.setOnClickListener{ + clickreference?.get()?.onItemClick(position, routeNames[position]) + } } // Return the size of your dataset (invoked by the layout manager) override fun getItemCount() = routeNames.size + fun interface OnClick{ + fun onItemClick(index: Int, name: String) + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt --- a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt @@ -35,4 +35,8 @@ fun getAllRoutes(): LiveData>{ return gtfsDao.getAllRoutes() } + + fun getRouteFromGtfsId(gtfsId: String): LiveData{ + return gtfsDao.getRouteByGtfsID(gtfsId) + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java --- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java +++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java @@ -19,12 +19,16 @@ import android.content.Context; import android.content.SharedPreferences; +import android.util.Log; import it.reyboz.bustorino.R; import static android.content.Context.MODE_PRIVATE; import androidx.preference.PreferenceManager; +import java.util.HashSet; +import java.util.Set; + /** * Static class for commonly used SharedPreference operations */ @@ -33,6 +37,8 @@ public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version"; public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run"; + public static final String PREF_FAVORITE_LINES = "pref_favorite_lines"; + public static SharedPreferences getMainSharedPreferences(Context context){ return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE); } @@ -59,4 +65,27 @@ final SharedPreferences pref = getMainSharedPreferences(con); return pref.getBoolean(PREF_INTRO_ACTIVITY_RUN, false); } + + public static boolean addOrRemoveLineToFavorites(Context con, String gtfsLineId, boolean addToFavorites){ + final SharedPreferences pref = getMainSharedPreferences(con); + final HashSet favorites = new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); + boolean modified = true; + if(addToFavorites) + favorites.add(gtfsLineId); + else if(favorites.contains(gtfsLineId)) + favorites.remove(gtfsLineId); + else + modified = false; // we are not changing anything + if(modified) { + final SharedPreferences.Editor editor = pref.edit(); + editor.putStringSet(PREF_FAVORITE_LINES, favorites); + editor.apply(); + } + return modified; + } + + public static HashSet getFavoritesLinesGtfsIDs(Context con){ + final SharedPreferences pref = getMainSharedPreferences(con); + return new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt --- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -26,8 +26,8 @@ @Query("SELECT * FROM "+GtfsRoute.DB_TABLE) fun getAllRoutes() : LiveData> - @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} IN (:routeGtfsIds)") - fun getRoutesByIDs(routeGtfsIds: List): LiveData> + @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} LIKE :gtfsId") + fun getRouteByGtfsID(gtfsId: String) : LiveData @Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE) diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -409,7 +409,7 @@ final ArrayList routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages(); Collections.sort(routesWithNoPassages, new LinesNameSorter()); - noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages); + noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages, null); if(noArrivalsRecyclerView!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); //hide the views if there are no empty routes diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -20,6 +20,7 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context +import android.content.SharedPreferences import android.graphics.Paint import android.os.Bundle import android.util.Log @@ -45,6 +46,7 @@ import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker +import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops @@ -67,7 +69,7 @@ class LinesDetailFragment() : ScreenBaseFragment() { - private lateinit var lineID: String + private var lineID = "" private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null @@ -83,7 +85,31 @@ private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton + + private var favoritesButton: ImageButton? = null + private var isLineInFavorite = false + private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged -> + if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener + val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) + newFavorites?.let { + isLineInFavorite = it.contains(lineID) + //if the button has been intialized, change the icon accordingly + favoritesButton?.let { button-> + if(isLineInFavorite) { + button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) + Toast.makeText(context,R.string.favorites_line_add,Toast.LENGTH_SHORT).show() + } else { + button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null)) + Toast.makeText(context,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show() + } + + + } + } + } + private lateinit var stopsRecyclerView: RecyclerView + private lateinit var descripTextView: TextView //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { @@ -93,7 +119,7 @@ viewModel.shouldShowMessage=false } stop?.let { - fragmentListener?.requestArrivalsForStopID(it.ID) + fragmentListener.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") @@ -142,12 +168,27 @@ val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") switchButton = rootView.findViewById(R.id.switchImageButton) + favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) + descripTextView = rootView.findViewById(R.id.lineDescripTextView) + descripTextView.visibility = View.INVISIBLE val titleTextView = rootView.findViewById(R.id.titleTextView) - titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) + favoritesButton?.isClickable = true + favoritesButton?.setOnClickListener { + if(lineID.isNotEmpty()) + PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite) + } + val preferences = PreferencesHolder.getMainSharedPreferences(requireContext()) + val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) + if(favorites!=null && favorites.contains(lineID)){ + favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) + isLineInFavorite = true + } + preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor) + patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter @@ -195,6 +236,10 @@ showStopsAsList(stops) } } + viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> + descripTextView.text = route.longName + descripTextView.visibility = View.VISIBLE + } if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ val patt = viewModel.selectedPatternLiveData.value!! Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") @@ -310,7 +355,7 @@ // add ability to zoom with 2 fingers it.setMultiTouchControls(true) - it.minZoomLevel = 11.0 + it.minZoomLevel = 12.0 //map controller setup val mapController = it.controller diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -15,7 +15,9 @@ import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter +import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.util.LinesNameSorter @@ -29,11 +31,12 @@ private val viewModel: LinesGridShowingViewModel by viewModels() //private lateinit var gridLayoutManager: AutoFitGridLayoutManager - + private lateinit var favoritesRecyclerView: RecyclerView private lateinit var urbanRecyclerView: RecyclerView private lateinit var extraurbanRecyclerView: RecyclerView private lateinit var touristRecyclerView: RecyclerView + private lateinit var favoritesTitle: TextView private lateinit var urbanLinesTitle: TextView private lateinit var extrurbanLinesTitle: TextView private lateinit var touristLinesTitle: TextView @@ -53,7 +56,7 @@ return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } - private val routeClickListener = RouteAdapter.onItemClick { + private val routeClickListener = RouteAdapter.ItemClicker { fragmentListener.showLineOnMap(it.gtfsId) } private val arrows = HashMap() @@ -66,10 +69,12 @@ ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) + favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView) urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) + favoritesTitle = rootView.findViewById(R.id.favoritesTitleView) urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) @@ -77,6 +82,7 @@ arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) + arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites) //show urban expanded by default val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) @@ -87,6 +93,13 @@ ) recyView.layoutManager = gridLayoutManager } + //init favorites recyclerview + val gridLayoutManager = AutoFitGridLayoutManager( + requireContext().applicationContext, + (utils.convertDipToPixels(context, 70f)).toInt() + ) + favoritesRecyclerView.layoutManager = gridLayoutManager + viewModel.routesLiveData.observe(viewLifecycleOwner){ //routesList = ArrayList(it) @@ -116,6 +129,15 @@ } } + viewModel.favoritesLines.observe(viewLifecycleOwner){ routes-> + val routesNames = routes.map { it.shortName } + //create new item click listener every time + val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> + val r = routes[pos] + fragmentListener.showLineOnMap(r.gtfsId) + } + favoritesRecyclerView.adapter = adapter + } //onClicks urbanLinesTitle.setOnClickListener { @@ -127,14 +149,33 @@ touristLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_TOUR) } + favoritesTitle.setOnClickListener { + closeOpenFavorites() + } + arrows[AG_FAV]?.setOnClickListener { + closeOpenFavorites() + } //arrows onClicks - for(k in arrows.keys){ + for(k in Companion.AGENCIES){ //k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) } } + return rootView } + private fun closeOpenFavorites(){ + if(favoritesRecyclerView.visibility == View.VISIBLE){ + //close it + favoritesRecyclerView.visibility = View.GONE + setOpen(arrows[AG_FAV]!!, false) + viewModel.favoritesExpanded.value = false + } else{ + favoritesRecyclerView.visibility = View.VISIBLE + setOpen(arrows[AG_FAV]!!, true) + viewModel.favoritesExpanded.value = true + } + } private fun openLinesAndCloseOthersIfNeeded(agency: String){ if(openRecyclerView!="" && openRecyclerView!= agency) { @@ -205,6 +246,20 @@ override fun onResume() { super.onResume() + val pref = PreferencesHolder.getMainSharedPreferences(requireContext()) + val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) + res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))} + //restore state + viewModel.favoritesExpanded.value?.let { + if(!it){ + //close it + favoritesRecyclerView.visibility = View.GONE + setOpen(arrows[AG_FAV]!!, false) + } else{ + favoritesRecyclerView.visibility = View.VISIBLE + setOpen(arrows[AG_FAV]!!, true) + } + } viewModel.isUrbanExpanded.value?.let { if(it) { urbanRecyclerView.visibility = View.VISIBLE @@ -246,6 +301,7 @@ companion object { private const val COLUMN_WIDTH_DP=200 + private const val AG_FAV = "fav" private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" private const val AG_TOUR ="gtt:T" diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt @@ -4,10 +4,11 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.map 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 it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.util.LinesNameSorter class LinesGridShowingViewModel(application: Application) : AndroidViewModel(application) { @@ -24,4 +25,27 @@ val isUrbanExpanded = MutableLiveData(true) val isExtraUrbanExpanded = MutableLiveData(false) val isTouristExpanded = MutableLiveData(false) + val favoritesExpanded = MutableLiveData(true) + + val favoritesLinesIDs = MutableLiveData>() + + private val linesNameSorter = LinesNameSorter() + private val linesComparator = Comparator { a,b -> + return@Comparator linesNameSorter.compare(a.shortName, b.shortName) + } + + fun setFavoritesLinesIDs(linesIds: HashSet){ + favoritesLinesIDs.value = linesIds + } + + val favoritesLines = favoritesLinesIDs.map {lineIds -> + val linesList = ArrayList() + if (lineIds.size == 0 || routesLiveData.value==null) return@map linesList + for(line in routesLiveData.value!!){ + if(lineIds.contains(line.gtfsId)) + linesList.add(line) + } + linesList.sortWith(linesComparator) + return@map linesList + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt @@ -52,6 +52,9 @@ gtfsRepo.getPatternsWithStopsForRouteID(it) } + val gtfsRoute = routeIDToSearch.switchMap { + gtfsRepo.getRouteFromGtfsId(it) + } fun setRouteIDQuery(routeID: String){ diff --git a/app/src/main/res/drawable/ic_star_filled.xml b/app/src/main/res/drawable/ic_star_filled.xml --- a/app/src/main/res/drawable/ic_star_filled.xml +++ b/app/src/main/res/drawable/ic_star_filled.xml @@ -1,6 +1,6 @@ + + + + + --> - + + + + + /> Tocca per cambiare Fonte posizioni in tempo reale di bus e tram - MaTO (aggiornate più spesso, può non funzionare) GTFS RT (più stabile) + Linea aggiunta ai preferiti + Linea rimossa dai preferiti + Preferite + Tocca a lungo la fermata per le opzioni Rimuovi tutti i trip GTFS diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,7 @@ Extra urban lines Tourist lines - Heading to: + Destination: Lines: %1$s Line: %1$s No timetable found @@ -55,6 +55,9 @@ 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