diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt index 29bc817..5572e7c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -1,788 +1,792 @@ /* 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.database.Cursor 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.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.viewmodels.ArrivalsViewModel import java.util.* class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { private var DEBUG_TAG = DEBUG_TAG_ALL private lateinit var stopID: String //private set private var stopName: String? = null private var prefs: DBStatusManager? = null private var listener: OnDBUpdateStatusChangeListener? = null private var justCreated = false private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false private var stopIsInFavorites = false //Views protected lateinit var addToFavorites: ImageButton protected lateinit var timesSourceTextView: TextView protected lateinit var messageTextView: TextView protected lateinit var arrivalsRecyclerView: RecyclerView private var mListAdapter: PalinaAdapter? = null private lateinit var resultsLayout : LinearLayout private lateinit var loadingMessageTextView: TextView private lateinit var progressBar: ProgressBar private lateinit var howDoesItWorkTextView: TextView private lateinit var hideHintButton: Button //private NestedScrollView theScrollView; protected lateinit var noArrivalsRecyclerView: RecyclerView private var noArrivalsAdapter: RouteOnlyLineAdapter? = null private var noArrivalsTitleView: TextView? = null private var layoutManager: GridLayoutManager? = null //private View canaryEndView; private var fetchers: List = ArrayList() private val arrivalsViewModel : ArrivalsViewModel by viewModels() private var reloadOnResume = true fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName: String? Log.d(DEBUG_TAG, "Make toast for line " + route.name) routeName = FiveTNormalizer.routeInternalToDisplay(route.name) if (routeName == null) { routeName = route.displayCode } if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { Log.d( DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" ) if (route.gtfsId != null) { mListener.showLineOnMap(route.gtfsId, stopID) } else { val gtfsID = FiveTNormalizer.getGtfsRouteID(route) Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") mListener.showLineOnMap(gtfsID, stopID) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) addToFavorites = root.findViewById(R.id.addToFavorites) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) timesSourceTextView = root.findViewById(R.id.timesSourceTextView) timesSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing) //mListener.requestArrivalsForStopID(stopID) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } timesSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites toggleLastStopToFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } noArrivalsRecyclerView.setLayoutManager(layoutManager) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times lastUpdatedPalina?.let { showArrivalsSources(it) } /*if (lastUpdatedPalina?.queryAllRoutes() != null && lastUpdatedPalina!!.queryAllRoutes()!!.size >0){ showArrivalsSources(lastUpdatedPalina!!) } else{ Log.d(DEBUG_TAG, "No routes names") } */ arrivalsViewModel.palinaLiveData.observe(viewLifecycleOwner){ mListener.toggleSpinner(false) if(arrivalsViewModel.resultLiveData.value==Fetcher.Result.OK){ //the result is true changeUIFirstSearchActive(false) updateFragmentData(it) } else{ progressBar.visibility=View.INVISIBLE - loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) + // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times + if (stopName==null) + loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) + else + loadingMessageTextView.text = getString(R.string.no_arrivals_stop) } } arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "Using arrivals source: $it") val srcString = getDisplayArrivalsSource(it,requireContext()) loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString) } arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res -> when (res) { Fetcher.Result.OK -> {} Fetcher.Result.CLIENT_OFFLINE -> showToastMessage(R.string.network_error, true) Fetcher.Result.SERVER_ERROR -> { if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true) } else { showToastMessage(R.string.network_error, true) } showToastMessage(R.string.internal_error,true) } Fetcher.Result.PARSER_ERROR -> showShortToast(R.string.internal_error) Fetcher.Result.QUERY_TOO_SHORT -> showShortToast(R.string.query_too_short) Fetcher.Result.EMPTY_RESULT_SET -> showShortToast(R.string.no_arrivals_stop) Fetcher.Result.NOT_FOUND -> showShortToast(R.string.no_bus_stop_have_this_name) else -> showShortToast(R.string.internal_error) } } return root } private fun showShortToast(id: Int) = showToastMessage(id,true) private fun changeUIFirstSearchActive(yes: Boolean){ if(yes){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } else{ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } } override fun onResume() { super.onResume() val loaderManager = loaderManager Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina") /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ /*if(lastUpdatedPalina!=null){ updateFragmentData(null); showArrivalsSources(lastUpdatedPalina); }*/ mListener.readyGUIfor(FragmentKind.ARRIVALS) //fix bug when the list adapter is null mListAdapter?.let { resetListAdapter(it) } if (noArrivalsAdapter != null) { noArrivalsRecyclerView.adapter = noArrivalsAdapter } if (stopID.isNotEmpty()) { if (!justCreated) { fetchers = utils.getDefaultArrivalsFetchers(context) adjustFetchersToSource() if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID) } else { //start first search requestArrivalsForTheFragment() changeUIFirstSearchActive(true) justCreated = false } //start the loader if (prefs!!.isDBUpdating(true)) { prefs!!.registerListener() } else { Log.d(DEBUG_TAG, "Restarting loader for stop") loaderManager.restartLoader( loaderFavId, arguments, this ) } updateMessage() } if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) { showHints() } } override fun onStart() { super.onStart() if (needUpdateOnAttach) { updateFragmentData(null) needUpdateOnAttach = false } } override fun onPause() { if (listener != null) prefs!!.unregisterListener() super.onPause() val loaderManager = loaderManager Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders()) loaderManager.destroyLoader(loaderFavId) } override fun onAttach(context: Context) { super.onAttach(context) //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context) } fun reloadsOnResume(): Boolean { return reloadOnResume } fun setReloadOnResume(reloadOnResume: Boolean) { this.reloadOnResume = reloadOnResume } // HINT "HOW TO USE" private fun showHints() { howDoesItWorkTextView.visibility = View.VISIBLE hideHintButton.visibility = View.VISIBLE //actionHelpMenuItem.setVisible(false); } private fun hideHints() { howDoesItWorkTextView.visibility = View.GONE hideHintButton.visibility = View.GONE //actionHelpMenuItem.setVisible(true); } fun onHideHint(v: View?) { hideHints() setOption(requireContext(), OPTION_SHOW_LEGEND, false) } /*val currentFetchersAsArray: Array get() { val arr = arrayOfNulls(fetchers!!.size) fetchers!!.toArray(arr) return arr } */ fun getCurrentFetchersAsArray(): Array { val r= fetchers.toTypedArray() //?: emptyArray() return r } private fun rotateFetchers() { Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers") fetchers?.let { Collections.rotate(it, -1) } Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers") } /** * Update the UI with the new data * @param p the full Palina */ fun updateFragmentData(p: Palina?) { if (p != null) lastUpdatedPalina = p if (!isAdded) { //defer update at next show if (p == null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null") else needUpdateOnAttach = true } else { val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) showArrivalsSources(lastUpdatedPalina!!) resetListAdapter(adapter) val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView!!.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) noArrivalsRecyclerView!!.adapter = noArrivalsAdapter noArrivalsRecyclerView!!.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) timesSourceTextView.text = base_message timesSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { timesSourceTextView.visibility = View.VISIBLE } else { timesSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { setTextViewMessage(getString(R.string.passages, message)) } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader when (id) { loaderFavId -> { builder.appendPath("favorites").appendPath(stopID) cl = CursorLoader(requireContext(), builder.build(), UserDB.getFavoritesColumnNamesAsArray, null, null, null) } loaderStopId -> { builder.appendPath("stop").appendPath(stopID) cl = CursorLoader( requireContext(), builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME), null, null, null ) } else -> { cl = CursorLoader(requireContext(), builder.build(), null, null,null,null) Log.d(DEBUG_TAG, "This is probably going to crash") } } cl.setUpdateThrottle(500) return cl } override fun onLoadFinished(loader: Loader, data: Cursor) { when (loader.id) { loaderFavId -> { val colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]) if (data.count > 0) { // IT'S IN FAVORITES data.moveToFirst() val probableName = data.getString(colUserName) stopIsInFavorites = true if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop //update the message in the textview updateMessage() } else { stopIsInFavorites = false } updateStarIcon() if (stopName == null) { //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment$tag", "Stop wasn't in the favorites and has no name, looking in the DB") loaderManager.restartLoader( loaderStopId, arguments, this ) } } loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ) if (index == -1) { Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...") } stopName = data.getString(index) updateMessage() } else { Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } } } override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } protected fun resetListAdapter(adapter: PalinaAdapter) { mListAdapter = adapter arrivalsRecyclerView.adapter = adapter arrivalsRecyclerView.visibility = View.VISIBLE } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } fun toggleLastStopToFavorites() { val stop: Stop? = lastUpdatedPalina if (stop != null) { // toggle the status in background AsyncStopFavoriteAction( requireContext().applicationContext, AsyncStopFavoriteAction.Action.TOGGLE ) { v: Boolean -> updateStarIconFromLastBusStop(v) }.execute(stop) } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true) } } /** * Update the star "Add to favorite" icon */ fun updateStarIconFromLastBusStop(toggleDone: Boolean) { stopIsInFavorites = if (stopIsInFavorites) !toggleDone else toggleDone updateStarIcon() // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ fun updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID.isEmpty()) { addToFavorites.visibility = View.INVISIBLE } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled) } else { addToFavorites.setImageResource(R.drawable.ic_star_outline) } addToFavorites.visibility = View.VISIBLE } } override fun onDestroyView() { //arrivalsRecyclerView = null if (arguments != null) { requireArguments().putString(SOURCES_TEXT, timesSourceTextView.text.toString()) requireArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.text.toString()) } super.onDestroyView() } override fun getBaseViewForSnackBar(): View? { return null } fun isFragmentForTheSameStop(p: Palina): Boolean { return if (tag != null) tag == getFragmentTag(p) else false } /** * Request arrivals in the fragment */ fun requestArrivalsForTheFragment(){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() //AsyncArrivalsSearcher(, getCurrentFetchersAsArray(), context).execute(stopID) context?.let { mListener.toggleSpinner(true) val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" } //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray()) //val workManager = WorkManager.getInstance(it) //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest) arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray()) //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID") } } companion object { private const val OPTION_SHOW_LEGEND = "show_legend" private const val KEY_STOP_ID = "stopid" private const val KEY_STOP_NAME = "stopname" private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment" private const val loaderFavId = 2 private const val loaderStopId = 1 const val STOP_TITLE: String = "messageExtra" private const val SOURCES_TEXT = "sources_textview_message" @JvmStatic @JvmOverloads fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment { val fragment = ArrivalsFragment() val args = Bundle() args.putString(KEY_STOP_ID, stopID) //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null) { args.putString(KEY_STOP_NAME, stopName) } fragment.arguments = args return fragment } @JvmStatic fun getFragmentTag(p: Palina): String { return "palina_" + p.ID } @JvmStatic fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID" @JvmStatic fun getDisplayArrivalsSource(source: Source, context: Context): String{ return when (source) { Source.GTTJSON -> context.getString(R.string.gttjsonfetcher) Source.FiveTAPI -> context.getString(R.string.fivetapifetcher) Source.FiveTScraper -> context.getString(R.string.fivetscraper) Source.MatoAPI -> context.getString(R.string.source_mato) Source.UNDETERMINED -> //Don't show the view context.getString(R.string.undetermined_source) } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt index f62e93b..43f407f 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt @@ -1,184 +1,184 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.content.Context import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.viewModelScope import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.mato.MatoAPIFetcher import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.middleware.RecursionHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicReference class ArrivalsViewModel(application: Application): AndroidViewModel(application) { // Arrivals of palina val appContext: Context init { appContext = application.applicationContext } val palinaLiveData = MediatorLiveData() val sourcesLiveData = MediatorLiveData() val resultLiveData = MediatorLiveData() val currentFetchers = MediatorLiveData>() fun requestArrivalsForStop(stopId: String, fetchers: List){ val context = appContext //application.applicationContext currentFetchers.value = fetchers viewModelScope.launch(Dispatchers.IO){ runArrivalsFetching(stopId, fetchers, context) } } fun requestArrivalsForStop(stopId: String, fetchersSources: Array){ val fetchers = constructFetchersFromStrList(fetchersSources) requestArrivalsForStop(stopId, fetchers) } private suspend fun runArrivalsFetching(stopId: String, fetchers: List, appContext: Context) { if (fetchers.isEmpty()) { //do nothing return } // Equivalente del doInBackground nell'AsyncTask val recursionHelper = RecursionHelper(fetchers.toTypedArray()) - var resultPalina = Palina(stopId) + var resultPalina : Palina? = null val stringBuilder = StringBuilder() for (f in fetchers) { stringBuilder.append("") stringBuilder.append(f.javaClass.simpleName) stringBuilder.append("; ") } Log.d(DEBUG_TAG, "Using fetchers: $stringBuilder") val resultRef = AtomicReference() while (recursionHelper.valid()) { val fetcher = recursionHelper.getAndMoveForward() sourcesLiveData.postValue(fetcher.sourceForFetcher) if (fetcher is MatoAPIFetcher) { fetcher.appContext = appContext } Log.d(DEBUG_TAG, "Using the ArrivalsFetcher: ${fetcher.javaClass}") // Verifica se è un fetcher per MetroStop da saltare try { if (fetcher is FiveTAPIFetcher && stopId.toInt() >= 8200) { continue } } catch (ex: NumberFormatException) { Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures") } // Legge i tempi di arrivo val palina = fetcher.ReadArrivalTimesAll(stopId, resultRef) Log.d(DEBUG_TAG, "Arrivals fetcher: $fetcher\n\tProgress: ${resultRef.get()}") // Gestione del FiveTAPIFetcher per ottenere le direzioni if (fetcher is FiveTAPIFetcher) { val branchResultRef = AtomicReference() val branches = fetcher.getDirectionsForStop(stopId, branchResultRef) Log.d(DEBUG_TAG, "FiveTArrivals fetcher: $fetcher\n\tDetails req: ${branchResultRef.get()}") if (branchResultRef.get() == Fetcher.Result.OK) { palina.addInfoFromRoutes(branches) // Inserisce i dati nel database viewModelScope.launch(Dispatchers.IO) { //modify the DB in another coroutine in the background NextGenDB.insertBranchesIntoDB(appContext,branches) } } else { resultRef.set(Fetcher.Result.NOT_FOUND) } } // Unisce percorsi duplicati palina.mergeDuplicateRoutes(0) if (resultRef.get() == Fetcher.Result.OK && palina.getTotalNumberOfPassages() == 0) { resultRef.set(Fetcher.Result.EMPTY_RESULT_SET) Log.d(DEBUG_TAG, "Setting empty results") } //reportProgress resultLiveData.postValue(resultRef.get()) // Se è un MatoAPIFetcher con risultati validi, salviamo i dati if (resultPalina == null && fetcher is MatoAPIFetcher && palina.queryAllRoutes().size > 0) { resultPalina = palina } // Se abbiamo un risultato OK, restituiamo la palina if (resultRef.get() == Fetcher.Result.OK) { //set data resultLiveData.postValue(Fetcher.Result.OK) palinaLiveData.postValue(palina) //TODO: Rotate the fetchers appropriately return } //end Fetchers loop } // Se arriviamo qui, tutti i fetcher hanno fallito //failedAll = true // Se abbiamo comunque una palina, la restituiamo if (resultPalina != null) { resultLiveData.postValue(resultRef.get()) palinaLiveData.postValue(resultPalina) } } companion object{ const val DEBUG_TAG="BusTO-ArrivalsViMo" @JvmStatic fun getFetcherFromStrSource(src:String): ArrivalsFetcher?{ val srcEnum = Passaggio.Source.valueOf(src) val fe: ArrivalsFetcher? = when(srcEnum){ Passaggio.Source.FiveTAPI -> FiveTAPIFetcher() Passaggio.Source.GTTJSON -> GTTJSONFetcher() Passaggio.Source.FiveTScraper -> FiveTScraperFetcher() Passaggio.Source.MatoAPI -> MatoAPIFetcher() Passaggio.Source.UNDETERMINED -> null null -> null } return fe } @JvmStatic fun constructFetchersFromStrList(sources: Array): List{ val fetchers = mutableListOf() for(s in sources){ val fe = getFetcherFromStrSource(s) if(fe!=null){ fetchers.add(fe) } else{ Log.d(DEBUG_TAG, "Cannot convert fetcher source $s to a fetcher") } } return fetchers } } } \ 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 d1103d1..3d3e277 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,376 +1,376 @@ 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 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 Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about - Contribute + 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 technical information or to contribute to development, use the buttons below! + 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 Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel 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 app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database 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 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 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_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might 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 OK, close the tutorial Close the tutorial Enable notifications Notifications enabled Backup and restore Import/export preferences Data saved 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