diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index c2f96f2..04f94cf 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,192 +1,194 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

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

Perché usare BusTO?

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

Come Funziona?

Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".

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

Licenze

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

Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Canale unico delle notifiche Database Informazioni sul database (aggiornamento) Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti + Schermata da mostrare all\'avvio + Tocca per cambiare Tocca a lungo la fermata per le opzioni
diff --git a/res/values/keys.xml b/res/values/keys.xml index 5b99d15..93ad292 100644 --- a/res/values/keys.xml +++ b/res/values/keys.xml @@ -1,6 +1,13 @@ layout_pref pref_update_db_now + + + arrivals + favorites + map + lines + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 31ee1ec..cd70631 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,210 +1,219 @@ BusTO Libre BusTO BusTO dev You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Lines: %1$s Line: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About 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 Welcome!

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

Why use this app?

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

How does it work?

This app will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".

Who worked on BusTO:
- 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 icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

The app and the related source code are released by Valerio Bozzolan 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 hoping to be useful to everyone but without ANY warranty.

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

Get involved! :)

]]>
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 recents is invalid Invalid value, put a valid number Finding the position… No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to position to show it on the map Please enable GPS 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 Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals Default Default channel for notifications Database Notifications on the update of the database Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST + Section to show on startup + Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options + + @string/nav_arrivals_text + @string/nav_favorites_text + @string/nav_map_text + @string/lines + +
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 25c4cd2..6723e68 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,79 +1,89 @@ + + + + diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java index caebbc3..01bc285 100644 --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,628 +1,664 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); + boolean showingArrivalsForStop = false; //database check GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); boolean dataUpdateRequested = false; final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); if (old_version < db_version){ //decide update conditions in the future if(old_version < 2 && db_version >= 2) { dataUpdateRequested = true; DatabaseUpdate.requestDBUpdateWithWork(this, true, true); } PreferencesHolder.setGtfsDBVersion(theShPr, db_version); } Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); requestArrivalsForStopID(busStopID); + showingArrivalsForStop = true; } //Try (hopefully) database update if(!dataUpdateRequested) DatabaseUpdate.requestDBUpdateWithWork(this, false, false); + /* Watch for database update */ final WorkManager workManager = WorkManager.getInstance(this); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment - showMainFragment(); + Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); + Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); + String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); + //if (vl.length() == 0 || vl.equals("arrivals")) { + // showMainFragment(); + Log.d(DEBUG_TAG, "The default screen to open is: "+vl); + if(showingArrivalsForStop){ + showMainFragment(false); + } else if (vl.equals("map")){ + requestMapFragment(false); + } else if(vl.equals("favorites")){ + checkAndShowFavoritesFragment(getSupportFragmentManager(), false); + } else if(vl.equals("lines")){ + showLinesFragment(getSupportFragmentManager(), false, null); + } else{ + showMainFragment(false); + } } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - FavoritesFragment fragment = FavoritesFragment.newInstance(); - ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES) - .addToBackStack("main"); - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - ft.commit(); + checkAndShowFavoritesFragment(getSupportFragmentManager(), true); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); - showMainFragment(); + showMainFragment(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); - final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; - int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); - switch (result) { - case PERMISSION_OK: - createAndShowMapFragment(null); - break; - case PERMISSION_ASKING: - permissionDoneRunnables.put(permission, - () -> createAndShowMapFragment(null)); - break; - case PERMISSION_NEG_CANNOT_ASK: - String storage_perm = getString(R.string.storage_permission); - String text = getString(R.string.too_many_permission_asks, storage_perm); - Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); - } + requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment f = getSupportFragmentManager().findFragmentByTag(LinesFragment.FRAGMENT_TAG); - if(f!=null){ - ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); - }else{ - //use new method - ft.replace(R.id.mainActContentFrame,LinesFragment.class,null,LinesFragment.FRAGMENT_TAG); - } - - ft.setReorderingAllowed(true) - .addToBackStack("lines") - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .commit(); + showLinesFragment(getSupportFragmentManager(), true,null); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { boolean foundFragment = false; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } - shownFrag.getChildFragmentManager().popBackStackImmediate(); + shownFrag.getChildFragmentManager().popBackStack(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); + Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); + Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); } else super.onBackPressed(); } /** * Create and show the SnackBar with the message */ private void createDefaultSnackbar() { View baseView = null; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } - private MainScreenFragment createAndShowMainFragment(){ - FragmentManager fraMan = getSupportFragmentManager(); - - MainScreenFragment fragment = MainScreenFragment.newInstance(); - - FragmentTransaction transaction = fraMan.beginTransaction(); - transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG); - transaction.commit(); - return fragment; - } - /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ - private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment){ - fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment) - .setReorderingAllowed(true) - .addToBackStack(null) + private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ + FragmentTransaction ft = fraMan.beginTransaction() + .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) + .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + if (addToBackStack) ft.addToBackStack(null); + ft.commit(); + } + + private void requestMapFragment(final boolean allowReturn){ + final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; + int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); + switch (result) { + case PERMISSION_OK: + createAndShowMapFragment(null, allowReturn); + break; + case PERMISSION_ASKING: + permissionDoneRunnables.put(permission, + () -> createAndShowMapFragment(null, allowReturn)); + break; + case PERMISSION_NEG_CANNOT_ASK: + String storage_perm = getString(R.string.storage_permission); + String text = getString(R.string.too_many_permission_asks, storage_perm); + Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); + } + } + + private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ + FragmentTransaction ft = fragmentManager.beginTransaction(); + Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); + if(fragment!=null){ + ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); + }else{ + //use new method + ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); + } + if (addToBackStack) + ft.addToBackStack("favorites_main"); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .setReorderingAllowed(false); + ft.commit(); + } + + private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ + FragmentTransaction ft = fragmentManager.beginTransaction(); + Fragment f = fragmentManager.findFragmentByTag(LinesFragment.FRAGMENT_TAG); + if(f!=null){ + ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); + }else{ + //use new method + ft.replace(R.id.mainActContentFrame,LinesFragment.class,fragArgs,LinesFragment.FRAGMENT_TAG); + } + if (addToBackStack) + ft.addToBackStack("lines"); + ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } - private MainScreenFragment showMainFragment(){ + private MainScreenFragment showMainFragment(boolean addToBackStack){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ - mainScreenFragment = createAndShowMainFragment(); + mainScreenFragment = MainScreenFragment.newInstance(); + //mainScreenFragment = createAndShowMainFragment(); + showMainFragment(fraMan, mainScreenFragment, addToBackStack); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; - showMainFragment(fraMan, mainScreenFragment); + showMainFragment(fraMan, mainScreenFragment, addToBackStack); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; default: titleResId = 0; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); - showMainFragment(fraMan, probableFragment); + showMainFragment(fraMan, probableFragment, true); } else { + //createAndShowMainFragment // we have no fragment - probableFragment = createAndShowMainFragment(); + probableFragment = MainScreenFragment.newInstance(); + showMainFragment(fraMan, probableFragment,true); + //probableFragment = createAndShowMainFragment(); } } probableFragment.requestArrivalsForStopID(ID); mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { - createAndShowMapFragment(stop); + createAndShowMapFragment(stop, true); } //Map Fragment stuff - void createAndShowMapFragment(@Nullable Stop stop){ + void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); - ft.addToBackStack(null); + if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_about: startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(getString(R.string.hack_url), activityContext); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; case R.id.action_experiments: startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); default: } return false; } } } diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index 6c94dd7..ecfd6b0 100644 --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,430 +1,430 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.gtfs.GtfsAgency import it.reyboz.bustorino.data.gtfs.GtfsFeed import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPattern import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { var appContext: Context? = null set(value) { field = value!!.applicationContext } constructor(): this(2) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val now = Calendar.getInstance().time var numMinutes = 0 var palina = Palina(stopID) var numPassaggi = 0 var trials = 0 val numDepartures = 4 - while (numPassaggi < minNumPassaggi && trials < 4) { + while (numPassaggi < minNumPassaggi && trials < 2) { //numDepartures+=2 numMinutes += 20 val future = RequestFuture.newFuture() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { if (BuildConfig.DEBUG) for (r in palinaResult.queryAllRoutes()){ Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) } palina = palinaResult numPassaggi = palina.minNumberOfPassages } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude, jsonStop.getString("gtfsId") ) val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) return data } fun getFeedsAndAgencies(context: Context, res: AtomicReference?): Pair, ArrayList> { val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) request.setRetryPolicy(longRetryPolicy) request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) requestQueue.add(request) val feeds = ArrayList() val agencies = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val feedsJSON = resObj.getJSONArray("feeds") for (i in 0 until feedsJSON.length()){ val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) feeds.add(resTup.first) agencies.addAll(resTup.second) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return Pair(feeds,agencies) } fun getRoutes(context: Context, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() params.put("feeds","gtt") val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) request.retryPolicy = longRetryPolicy requestQueue.add(request) val routes = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) routes.add(route) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return routes } fun getPatternsWithStops(context: Context, routesGTFSIds: ArrayList, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() for (r in routesGTFSIds){ if(r.isEmpty()) routesGTFSIds.remove(r) } val routes = JSONArray(routesGTFSIds) params.put("routes",routes) val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) request.retryPolicy = longRetryPolicy request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) requestQueue.add(request) val patterns = ArrayList() //var outObj = "" try { val resObj = future.get(60,TimeUnit.SECONDS) //outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) patterns.addAll(patternList) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } /* var numRequests = 0 for(routeName in routesGTFSIds){ if (!routeName.isEmpty()) numRequests++ } val countDownForRequests = CountDownLatch(numRequests) val lockSave = ReentrantLock() //val countDownFor for (routeName in routesGTFSIds){ val pars = JSONObject() pars.put("") } val goodResponseListener = Response.Listener { } val errorResponseListener = Response.ErrorListener { } */ return patterns } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/FavoritesLiveData.java b/src/it/reyboz/bustorino/data/FavoritesLiveData.java index 07ce144..4f9d2fa 100644 --- a/src/it/reyboz/bustorino/data/FavoritesLiveData.java +++ b/src/it/reyboz/bustorino/data/FavoritesLiveData.java @@ -1,219 +1,222 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.Stop; public class FavoritesLiveData extends LiveData> implements CustomAsyncQueryHandler.AsyncQueryListener { private static final String TAG = "BusTO-FavoritesLiveData"; private final boolean notifyChangesDescendants; @NonNull private final Context mContext; @NonNull private final FavoritesLiveData.ForceLoadContentObserver mObserver; private final CustomAsyncQueryHandler queryHandler; private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( AppDataProvider.FAVORITES).build(); private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=220; @Nullable private List stopsFromFavorites, stopsDone; private boolean isQueryRunning = false; private int stopNeededCount = 0; public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) { super(); mContext = context.getApplicationContext(); mObserver = new FavoritesLiveData.ForceLoadContentObserver(); notifyChangesDescendants = notifyDescendantsChanges; queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this); } private void loadData() { loadData(false); } private static Uri.Builder getStopsBuilder(){ return AppDataProvider.getUriBuilderToComplete().appendPath("stop"); } private void loadData(boolean forceQuery) { - Log.d(TAG, "loadData()"); + Log.d(TAG, "loadData() force: "+forceQuery); if (!forceQuery){ if (getValue()!= null){ //Data already loaded + Log.d(TAG, "Data already loaded"); return; } } if (isQueryRunning){ //we are waiting for data, we will get an update soon + Log.d(TAG, "Query is running, abort"); return; } isQueryRunning = true; queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null); } @Override protected void onActive() { //Log.d(TAG, "onActive()"); - loadData(); + loadData(true); } /** * Clear the data for the cursor */ public void onClear(){ ContentResolver resolver = mContext.getContentResolver(); resolver.unregisterContentObserver(mObserver); } @Override protected void setValue(List stops) { //Log.d("BusTO-FavoritesLiveData","Setting the new values for the stops, have "+ // stops.size()+" stops"); ContentResolver resolver = mContext.getContentResolver(); resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver); super.setValue(stops); } @Override public void onQueryComplete(int token, Object cookie, Cursor cursor) { if (cursor == null){ //Nothing to do Log.e(TAG, "Null cursor for token "+token); return; } if (token == FAV_TOKEN) { stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.getFavoritesColumnNamesAsArray); cursor.close(); //reset counters stopNeededCount = stopsFromFavorites.size(); stopsDone = new ArrayList<>(); if(stopsFromFavorites.size() == 0){ //we don't need to call the other query setValue(stopsDone); + isQueryRunning = false; } else for (int i = 0; i < stopsFromFavorites.size(); i++) { Stop s = stopsFromFavorites.get(i); queryHandler.startQuery(STOPS_TOKEN_BASE + i, null, getStopsBuilder().appendPath(s.ID).build(), NextGenDB.QUERY_COLUMN_stops_all, null, null, null); } } else if(token >= STOPS_TOKEN_BASE){ final int index = token - STOPS_TOKEN_BASE; assert stopsFromFavorites != null; Stop stopUpdate = stopsFromFavorites.get(index); Stop finalStop; List result = NextGenDB.getStopsFromCursorAllFields(cursor); cursor.close(); if (result.size() < 1){ // stop is not in the DB finalStop = stopUpdate; } else { finalStop = result.get(0); if (BuildConfig.DEBUG && !(finalStop.ID.equals(stopUpdate.ID))) { throw new AssertionError("Assertion failed"); } finalStop.setStopUserName(stopUpdate.getStopUserName()); } if (stopsDone!=null) stopsDone.add(finalStop); stopNeededCount--; if (stopNeededCount == 0) { // we have finished the queries isQueryRunning = false; Collections.sort(stopsDone); setValue(stopsDone); } } } /** * Content Observer that forces reload of cursor when data changes * On different thread (new Handler) */ public final class ForceLoadContentObserver extends ContentObserver { public ForceLoadContentObserver() { super(new Handler()); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { Log.d(TAG, "ForceLoadContentObserver.onChange()"); loadData(true); } } } diff --git a/src/it/reyboz/bustorino/data/PreferencesHolder.java b/src/it/reyboz/bustorino/data/PreferencesHolder.java index 611c07b..a0c551a 100644 --- a/src/it/reyboz/bustorino/data/PreferencesHolder.java +++ b/src/it/reyboz/bustorino/data/PreferencesHolder.java @@ -1,45 +1,51 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.Context; import android.content.SharedPreferences; import it.reyboz.bustorino.R; import static android.content.Context.MODE_PRIVATE; +import androidx.preference.PreferenceManager; + /** * Static class for commonly used SharedPreference operations */ public abstract class PreferencesHolder { public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version"; public static SharedPreferences getMainSharedPreferences(Context context){ return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE); } + public static SharedPreferences getAppPreferences(Context con){ + return PreferenceManager.getDefaultSharedPreferences(con); + } + public static int getGtfsDBVersion(SharedPreferences pref){ return pref.getInt(PREF_GTFS_DB_VERSION,-1); } public static void setGtfsDBVersion(SharedPreferences pref,int version){ SharedPreferences.Editor ed = pref.edit(); ed.putInt(PREF_GTFS_DB_VERSION,version); ed.apply(); } } diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java index 344b1fc..31364a2 100644 --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,305 +1,306 @@ /* BusTO - Fragments components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.app.AlertDialog; import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import it.reyboz.bustorino.*; import it.reyboz.bustorino.adapters.StopAdapterListener; import it.reyboz.bustorino.adapters.StopRecyclerAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.FavoritesViewModel; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class FavoritesFragment extends ScreenBaseFragment { private RecyclerView favoriteRecyclerView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; @Nullable private CommonFragmentListener mListener; public static final String FRAGMENT_TAG = "BusTOFavFragment"; private final StopAdapterListener adapterListener = new StopAdapterListener() { @Override public void onTappedStop(Stop stop) { mListener.requestArrivalsForStopID(stop.ID); } @Override public boolean onLongPressOnStop(Stop stop) { Log.d("BusTO-FavoritesFrag", "LongPressOnStop"); return true; } }; public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView); //favoriteListView = root.findViewById(R.id.favoriteListView); /*favoriteRecyclerView.setOn((parent, view, position, id) -> { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ /* Stop busStop = (Stop) parent.getItemAtPosition(position); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); */ LinearLayoutManager llManager = new LinearLayoutManager(getContext()); llManager.setOrientation(LinearLayoutManager.VERTICAL); favoriteRecyclerView.setLayoutManager(llManager); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(), llManager.getOrientation()); favoriteRecyclerView.addItemDecoration(dividerItemDecoration); angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); //register for the context menu registerForContextMenu(favoriteRecyclerView); FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); showStops(new ArrayList<>()); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /* This method is apparently NOT CALLED ANYMORE Called on Android 6 */ @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); Log.d("Favorites Fragment", "Creating context menu "); if (v.getId() == R.id.favoritesRecyclerView) { // if we aren't attached to activity, return null if (getActivity()==null) return; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public void onResume() { super.onResume(); if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); + } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter)) return false; StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter(); Stop busStop = adapter.getStops().get(adapter.getPosition()); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, result -> { }).execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: if (busStop.getLatitude() == null | busStop.getLongitude() == null | mListener==null ) { Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show(); return true; } //GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude()); mListener.showMapCenteredOnStop(busStop); return true; default: return super.onContextItemSelected(item); } } @Nullable @Override public View getBaseViewForSnackBar() { return null; } void showStops(List busStops){ // If no data is found show a friendly message if(BuildConfig.DEBUG) Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list"); if (busStops.size() == 0) { favoriteRecyclerView.setVisibility(View.INVISIBLE); // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); //assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } else { favoriteRecyclerView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * redrwaing everything. */ // Show results favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener, StopRecyclerAdapter.Use.FAVORITES)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> { String busStopUsername = busStopNameText.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.length() == 0) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); } } else { // changed to something // something different? if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); } } launchUpdate(busStop); }); builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> { // delete user name from database busStop.setStopUserName(null); launchUpdate(busStop); }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, result -> { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); } } diff --git a/src/it/reyboz/bustorino/fragments/LinesFragment.kt b/src/it/reyboz/bustorino/fragments/LinesFragment.kt index 76f3926..2007aad 100644 --- a/src/it/reyboz/bustorino/fragments/LinesFragment.kt +++ b/src/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -1,301 +1,392 @@ /* BusTO - Fragments components Copyright (C) 2022 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.os.Bundle +import android.os.Parcelable import android.util.Log import android.view.* import android.widget.* import android.widget.AdapterView.INVALID_POSITION import android.widget.AdapterView.OnItemSelectedListener import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.PatternWithStopsSorter class LinesFragment : ScreenBaseFragment() { companion object { fun newInstance(){ - val fragment = LinesFragment() + LinesFragment() } const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" val patternStopsComparator = PatternWithStopsSorter() } private lateinit var viewModel: LinesViewModel private lateinit var linesSpinner: Spinner private lateinit var patternsSpinner: Spinner private lateinit var currentRoutes: List private lateinit var currentPatterns: List private lateinit var currentPatternStops: List private lateinit var routeDescriptionTextView: TextView private lateinit var stopsRecyclerView: RecyclerView private var linesAdapter: ArrayAdapter? = null private var patternsAdapter: ArrayAdapter? = null private var mListener: CommonFragmentListener? = null private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } - private var firstClick = true; + private var firstClick = true + private var recyclerViewState:Parcelable? = null + private var patternsSpinnerState:Parcelable? = null private val adapterListener = object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { //var r = "" //stop?.let { r= it.stopDisplayName.toString() } if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { mListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(mListener == null){ Log.e(DEBUG_TAG, "Listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { Log.d(DEBUG_TAG, "LongPressOnStop") return true } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + Log.d(DEBUG_TAG, "saveInstanceState bundle: $outState") + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_lines, container, false) linesSpinner = rootView.findViewById(R.id.linesSpinner) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager //allow the context menu to be opened registerForContextMenu(stopsRecyclerView) + Log.d(DEBUG_TAG, "Called onCreateView for LinesFragment") + Log.d(DEBUG_TAG, "OnCreateView, selected line spinner pos: ${linesSpinner.selectedItemPosition}") + Log.d(DEBUG_TAG, "OnCreateView, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") + + //set requests + viewModel.routesGTTLiveData.observe(viewLifecycleOwner) { + setRoutes(it) + } + + viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ + patterns -> + run { + currentPatterns = patterns.sortedBy { p-> p.pattern.code } + //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } + patternsAdapter?.let { + it.clear() + it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) + it.notifyDataSetChanged() + } + + val pos = patternsSpinner.selectedItemPosition + //might be possible that the selectedItem is different (larger than list size) + if(pos!= INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ + val p = currentPatterns[pos] + Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") + setPatternAndReqStops(currentPatterns[pos]) + } + + } + } + + viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops-> + Log.d("BusTO-LinesFragment", "Setting stops from DB") + setCurrentStops(stops) + } if(context!=null) { - patternsAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) + patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter - linesAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) + linesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) linesSpinner.adapter = linesAdapter - + if (linesSpinner.onItemSelectedListener != null){ + Log.d(DEBUG_TAG, "linesSpinner listener != null") + } linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { val selRoute = currentRoutes.get(pos) routeDescriptionTextView.text = selRoute.longName val oldRoute = viewModel.getRouteIDQueried() val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim()) - Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner") + Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner, oldRoute: $oldRoute") //launch query for this gtfsID viewModel.setRouteIDQuery(selRoute.gtfsId) //reset spinner position if(resetSpinner) patternsSpinner.setSelection(0) } override fun onNothingSelected(p0: AdapterView<*>?) { } } patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) // setPatternAndReqStops(patternWithStops) } override fun onNothingSelected(p0: AdapterView<*>?) { } } } return rootView } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener) mListener = context else throw RuntimeException(context.toString() + " must implement CommonFragmentListener") } override fun onResume() { super.onResume() mListener?.readyGUIfor(FragmentKind.LINES) + + Log.d(DEBUG_TAG, "Resuming lines fragment") + //Log.d(DEBUG_TAG, "OnResume, selected line spinner pos: ${linesSpinner.selectedItemPosition}") + //Log.d(DEBUG_TAG, "OnResume, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) - - //val lines = viewModel.(); - viewModel.routesGTTLiveData.observe(this) { - setRoutes(it) - } - - viewModel.patternsWithStopsByRouteLiveData.observe(this){ - patterns -> - run { - currentPatterns = patterns.sortedBy { p-> p.pattern.code } - //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } - patternsAdapter?.let { - it.clear() - it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) - it.notifyDataSetChanged() - } - - val pos = patternsSpinner.selectedItemPosition - //might be possible that the selectedItem is different (larger than list size) - if(pos!= INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ - setPatternAndReqStops(currentPatterns[pos]) - } - - } - } - - viewModel.stopsForPatternLiveData.observe(this){stops-> - Log.d("BusTO-LinesFragment", "Setting stops from DB") - setCurrentStops(stops) - } + Log.d(DEBUG_TAG, "Fragment onCreate") } override fun getBaseViewForSnackBar(): View? { return null } private fun setRoutes(routes: List){ + Log.d(DEBUG_TAG, "Resetting routes") currentRoutes = routes.sortedWith(linesComparator) + if (linesAdapter!=null){ + + var selGtfsRoute = viewModel.getRouteIDQueried() + var selPatternIndex = 0 + if(selGtfsRoute == null){ + selGtfsRoute ="" + } + val adapter = linesAdapter!! + + if (adapter.isEmpty) { + Log.d(DEBUG_TAG, "Lines adapter is empty") + } + else{ + adapter.clear() + + } + adapter.addAll(currentRoutes.map { r -> r.shortName }) + adapter.notifyDataSetChanged() + for(j in 0 until currentRoutes.size){ + val route = currentRoutes[j] + if (route.gtfsId == selGtfsRoute) { + selPatternIndex = j + Log.d(DEBUG_TAG, "Route $selGtfsRoute has index $j") + } + } + linesSpinner.setSelection(selPatternIndex) + // + } + /* linesAdapter?.clear() linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) linesAdapter?.notifyDataSetChanged() + */ } private fun setCurrentStops(stops: List){ val orderBy = currentPatternStops.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") - var setNewAdapter = true; + var setNewAdapter = true if(stopsRecyclerView.adapter is StopRecyclerAdapter){ val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) ){ Log.d(DEBUG_TAG, "Found same stops on recyclerview") setNewAdapter = false } /*else { Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") adapter.stops = stopsSorted adapter.notifyDataSetChanged() }*/ } if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.requestStopsForPatternWithStops(patternWithStops) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) Log.d("BusTO-LinesFragment", "Creating context menu ") if (v.id == R.id.patternStopsRecyclerView) { // if we aren't attached to activity, return null if (activity == null) return - val inflater = activity!!.menuInflater + val inflater = requireActivity().menuInflater inflater.inflate(R.menu.menu_line_item, menu) - } } override fun onContextItemSelected(item: MenuItem): Boolean { if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter val stop = adapter.stops.get(adapter.getPosition()) val acId = item.itemId if(acId == R.id.action_view_on_map){ // view on the map if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() return true } mListener!!.showMapCenteredOnStop(stop) return true } else if (acId == R.id.action_show_arrivals){ - mListener?.requestArrivalsForStopID(stop.ID); + mListener?.requestArrivalsForStopID(stop.ID) return true } return false } + override fun onStop() { + super.onStop() + Log.d(DEBUG_TAG, "Fragment stopped") + + recyclerViewState = stopsRecyclerView.layoutManager?.onSaveInstanceState() + patternsSpinnerState = patternsSpinner.onSaveInstanceState() + } + + + override fun onStart() { + super.onStart() + + Log.d(DEBUG_TAG, "OnStart, selected line spinner pos: ${linesSpinner.selectedItemPosition}") + Log.d(DEBUG_TAG, "OnStart, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") + + if (recyclerViewState!=null){ + stopsRecyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) + } + if(patternsSpinnerState!=null){ + patternsSpinner.onRestoreInstanceState(patternsSpinnerState) + } + } + /* + override fun onDestroyView() { + super.onDestroyView() + Log.d(DEBUG_TAG, "Fragment view destroyed") + + } + + override fun onDestroy() { + super.onDestroy() + Log.d(DEBUG_TAG, "Fragment destroyed") + } + */ + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + Log.d(DEBUG_TAG, "OnViewStateRes, bundled saveinstancestate: $savedInstanceState") + Log.d(DEBUG_TAG, "OnViewStateRes, selected line spinner pos: ${linesSpinner.selectedItemPosition}") + Log.d(DEBUG_TAG, "OnViewStateRes, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") + } + } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt index 2ec7a5e..2bab099 100644 --- a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt +++ b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt @@ -1,82 +1,88 @@ package it.reyboz.bustorino.fragments import android.app.Application import android.util.Log import androidx.lifecycle.* import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() + private var lastShownPatternStops = ArrayList() + val stopsForPatternLiveData = MutableLiveData>() val executor = Executors.newFixedThreadPool(2) init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) } val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } - val routesName: LiveData> = Transformations.map(routesGTTLiveData) { - it.map { route -> route.longName } - } fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } fun getRouteIDQueried(): String?{ return routeIDToSearch.value } - var shouldShowMessage = true; + var shouldShowMessage = true - fun requestStopsForGTFSIDs(gtfsIDs: List){ + private fun requestStopsForGTFSIDs(gtfsIDs: List){ + if (gtfsIDs.equals(lastShownPatternStops)){ + //nothing to do + return + } oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } + lastShownPatternStops.clear() + for(id in gtfsIDs) + lastShownPatternStops.add(id) } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java index 8d8681c..b7937e2 100644 --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,168 +1,182 @@ /* BusTO - Fragments components Copyright (C) 2020 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.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.*; import androidx.room.Database; import it.reyboz.bustorino.R; import it.reyboz.bustorino.data.DatabaseUpdate; import java.lang.ref.WeakReference; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; + public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show"; + private boolean setSummaryStartupPref = false; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ - //ListPreference preference = findPreference(R.string.arrival_times) + ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN); + if(startupScreenPref !=null){ + if (startupScreenPref.getValue()==null){ + startupScreenPref.setSummary(getString(R.string.nav_arrivals_text)); + setSummaryStartupPref = true; + } + } + + //Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue()); Preference dbUpdateNow = findPreference("pref_db_update_now"); if (dbUpdateNow!=null) dbUpdateNow.setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull Preference preference) { //trigger update if(getContext()!=null) { DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true); Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show(); return true; } return false; } } ); else { Log.e("BusTO-Preferences", "Cannot find db update preference"); } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); //sometimes this happens if(getContext()==null) return; + if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){ + ListPreference listPref = (ListPreference) pref; + pref.setSummary(listPref.getEntry()); + } /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; - SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } }