diff --git a/res/layout/fragment_arrivals.xml b/res/layout/fragment_arrivals.xml index ce184fe..d5bf8a2 100644 --- a/res/layout/fragment_arrivals.xml +++ b/res/layout/fragment_arrivals.xml @@ -1,98 +1,98 @@ \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index e52be70..29e036d 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,126 +1,128 @@ 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 Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code Preferiti Aiuto Informazioni Più informazioni News Invia bug Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh! Schiaccia 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. 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.
- Ludovico Pavesi ex rockstar developer anziano.
- 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… Numero di fermate Impostazioni Impostazioni Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Abilitare il GPS Raggio di ricerca Funzionalità sperimentali 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 + Cambiamento fonte orari... +
diff --git a/res/values/strings.xml b/res/values/strings.xml index 525f6c2..b2bf7ab 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,136 +1,138 @@ BusTO 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 Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Lines: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About More about News releases Bug submission Source code Licence 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 GOT IT! 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.
- Ludovico Pavesi previous senior rockstar developer.
- 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 Finding the position… No stops nearby Number of stops Preferences Settings Settings Experimental features Search radius Recent stops General settings Database management Launch manual database update Please enable GPS Database update in progress… 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 + Changing arrival times source... +
diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 3ba0a3d..de4e21c 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,906 +1,908 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan 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.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.NavUtils; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.*; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import android.support.design.widget.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class ActivityMain extends GeneralActivity implements FragmentListener { /* * Layout elements */ private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private SwipeRefreshLayout swipeRefreshLayout; private FloatingActionButton floatingActionButton; private FragmentManager framan; private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; private ImageButton addToFavorites; /* * Options */ private final String OPTION_SHOW_LEGEND = "show_legend"; private final String LOCATION_PERMISSION_GIVEN = "loc_permission"; /* * Status */ private DBStatusManager prefsManager; private DBStatusManager.OnDBUpdateStatusChangeListener updatelistener; private static final String DEBUG_TAG = "BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference res) { SystemClock.sleep(5000); res.set(result.SERVER_ERROR); return new Palina(); } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ - private RecursionHelper ArrivalFetchersRecursionHelper = new RecursionHelper<>(new ArrivalsFetcher[]{new GTTJSONFetcher(), new FiveTScraperFetcher()}); - private RecursionHelper StopsFindersByNameRecursionHelper = new RecursionHelper<>(new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); + private ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + private StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; /* * Position */ //Fine location criteria private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; /* * Database Access */ private StopsDB stopsDB; private UserDB userDB; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// /* * @see swipeRefreshLayout */ private Handler handler = new Handler(); private final Runnable refreshing = new Runnable() { public void run() { if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); String stopName = fragment.getStopID(); - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(stopName); - } else - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(); + + new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray()).execute(stopName); + } else //we create a new fragment, which is WRONG + new AsyncDataDownload(fh, arrivalsFetchers).execute(); } }; //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); this.stopsDB = new StopsDB(getApplicationContext()); this.userDB = new UserDB(getApplicationContext()); setContentView(R.layout.activity_main); busStopSearchByIDEditText = (EditText) findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = (EditText) findViewById(R.id.busStopSearchByNameEditText); progressBar = (ProgressBar) findViewById(R.id.progressBar); howDoesItWorkTextView = (TextView) findViewById(R.id.howDoesItWorkTextView); hideHintButton = (Button) findViewById(R.id.hideHintButton); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.listRefreshLayout); floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton); framan.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { @Override public void onBackStackChanged() { Log.d("MainActivity, BusTO", "BACK STACK CHANGED"); } }); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); busStopSearchByNameEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); // Called when the layout is pulled down swipeRefreshLayout .setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { handler.post(refreshing); } }); /** * @author Marco Gagino!!! */ //swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); fh = new FragmentHelper(this, R.id.listRefreshLayout, R.id.resultFrame); setSearchModeBusStopID(); //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; String busStopDisplayName = 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"); busStopDisplayName = b.getString("bus-stop-display-name"); /** * 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 - ArrivalFetchersRecursionHelper.reset(); 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); /* //THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS // STOP IS ADDED AUTOMATICALLY Stop nextStop = new Stop(busStopID); // forcing it as user name even though it could be standard name, it doesn't really matter nextStop.setStopUserName(busStopDisplayName); //set stop as last succe fh.setLastSuccessfullySearchedBusStop(nextStop); */ createFragmentForStop(busStopID); } //Try (hopefully) database update //TODO: Start the service in foreground, check last time it ran before DatabaseUpdateService.startDBUpdate(getApplicationContext()); /* Set database update */ updatelistener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public boolean defaultStatusValue() { return true; } @Override public void onDBStatusChanged(boolean updating) { if (updating) { createDefaultSnackbar(); } else if (snackbar != null) { snackbar.dismiss(); snackbar = null; } } }; prefsManager = new DBStatusManager(getApplicationContext(), updatelistener); prefsManager.registerListener(); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// //SETUP LOCATION locmgr = (LocationManager) getSystemService(LOCATION_SERVICE); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); //We want the nearby bus stops! handler.post(new NearbyStopsRequester()); //If there are no providers available, then, wait for them Log.d("MainActivity", "Created"); } /** * Reload bus stop timetable when it's fulled resumed from background. */ /** * @Override protected void onPostResume() { * super.onPostResume(); * Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop()); * if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) { * setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID); - * //new asyncWgetBusStopFromBusStopID(lastSuccessfullySearchedBusStop.ID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); * new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); * } else { * //we have new activity or we don't have a new searched stop. * //Let's search stops nearby * LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); * Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.resultFrame); *

*

* } * //show the FAB since it remains hidden * floatingActionButton.show(); *

* } **/ @Override protected void onPause() { super.onPause(); fh.stopLastRequestIfNeeded(); fh.setBlockAllActivities(true); if (updatelistener != null && prefsManager != null) prefsManager.unregisterListener(); locmgr.removeUpdates(locListener); } @Override protected void onResume() { super.onResume(); fh.setBlockAllActivities(false); if (updatelistener != null && prefsManager != null) { prefsManager.registerListener(); if (prefsManager.isDBUpdating(true)) { createDefaultSnackbar(); } } if (pendingNearbyStopsRequest) handler.post(new NearbyStopsRequester()); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); actionHelpMenuItem = menu.findItem(R.id.action_help); return true; } /** * Callback fired when a MenuItem is selected * * @param item * @return */ @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. switch (item.getItemId()) { case android.R.id.home: // Respond to the action bar's Up/Home button NavUtils.navigateUpFromSameTask(this); return true; case R.id.action_help: showHints(); return true; case R.id.action_favorites: startActivity(new Intent(ActivityMain.this, ActivityFavorites.class)); return true; case R.id.action_map: startActivity(new Intent(ActivityMain.this, ActivityMap.class)); return true; case R.id.action_about: startActivity(new Intent(ActivityMain.this, ActivityAbout.class)); return true; case R.id.action_news: openIceweasel("https://gitpull.it/w/librebusto/#how-to-get-news"); return true; case R.id.action_bugs: openIceweasel("https://gitpull.it/w/librebusto/#how-to-create-a-bug-feature"); return true; case R.id.action_source: openIceweasel("https://gitpull.it/w/librebusto/#how-to-hack-busto"); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html"); return true; case R.id.action_settings: Log.d("MAINBusTO", "Pressed button preferences"); startActivity(new Intent(ActivityMain.this, ActivitySettings.class)); } return super.onOptionsItemSelected(item); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); - //OLD ASYNCTASK - //new asyncWgetBusStopFromBusStopID(busStopID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); - if (busStopID == null || busStopID.length() <= 0) { - showMessage(R.string.insert_bus_stop_number_error); - toggleSpinner(false); - } else { - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(busStopID); - Log.d("MainActiv", "Started search for arrivals of stop " + busStopID); - } + createFragmentForStop(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); - new AsyncDataDownload(AsyncDataDownload.RequestType.STOPS, fh).execute(query); + new AsyncDataDownload(fh, stopsFinderByNames).execute(query); } } /** * PERMISSION STUFF **/ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case PERMISSION_REQUEST_POSITION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { setOption(LOCATION_PERMISSION_GIVEN, true); //if we sent a request for a new NearbyStopsFragment if (pendingNearbyStopsRequest) { pendingNearbyStopsRequest = false; handler.post(new NearbyStopsRequester()); } } else { //permission denied setOption(LOCATION_PERMISSION_GIVEN, false); } //add other cases for permissions } } @Override public void createFragmentForStop(String ID) { - //new asyncWgetBusStopFromBusStopID(ID, ArrivalFetchersRecursionHelper,lastSuccessfullySearchedBusStop); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showMessage(R.string.insert_bus_stop_number_error); toggleSpinner(false); - } else { - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(ID); + } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { + ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); + if (fragment.getStopID() != null && fragment.getStopID().equals(ID)){ + // Run with previous fetchers + //fragment.getCurrentFetchers().toArray() + new AsyncDataDownload(fh,fragment.getCurrentFetchersAsArray()).execute(ID); + } else{ + new AsyncDataDownload(fh, arrivalsFetchers).execute(ID); + } + } + else { + new AsyncDataDownload(fh,arrivalsFetchers).execute(ID); Log.d("MainActiv", "Started search for arrivals of stop " + ID); } } /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(this); integrator.initiateScan(); } /** * Receive the Barcode Scanner Intent */ public void onActivityResult(int requestCode, int resultCode, Intent intent) { IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); Uri uri; try { uri = Uri.parse(scanResult != null ? scanResult.getContents() : null); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { Toast.makeText(getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); createFragmentForStop(busStopID); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } private void createDefaultSnackbar() { if (snackbar == null) { snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); } snackbar.show(); } ///////////////////////////////// POSITION STUFF////////////////////////////////////////////// private void resolveStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { pendingNearbyStopsRequest = false; handler.post(new NearbyStopsRequester()); } } final LocationListener locListener = new LocationListener() { @Override public void onLocationChanged(Location location) { Log.d(DEBUG_TAG, "Location changed"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d(DEBUG_TAG, "Location provider status: " + status); if (status == LocationProvider.AVAILABLE) { resolveStopRequest(provider); } } @Override public void onProviderEnabled(String provider) { resolveStopRequest(provider); } @Override public void onProviderDisabled(String provider) { } }; class NearbyStopsRequester implements Runnable { @SuppressLint("MissingPermission") @Override public void run() { final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); if (!canRunPosition) { pendingNearbyStopsRequest = true; assertLocationPermissions(); return; } else setOption(LOCATION_PERMISSION_GIVEN, true); LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); if (locManager == null) { Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); return; } if (anyLocationProviderMatchesCriteria(locManager, cr, true) && fh.getLastSuccessfullySearchedBusStop() == null) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = framan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = framan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(R.id.resultFrame, fragment, "nearbyStop_correct"); ft.commit(); framan.executePendingTransactions(); pendingNearbyStopsRequest = false; } else if (!anyLocationProviderMatchesCriteria(locManager, cr, true)) { //Wait for the providers Log.d(DEBUG_TAG, "Queuing position request"); pendingNearbyStopsRequest = true; locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, locListener); } } } private boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) { List providers = mng.getProviders(cr, enabled); Log.d(DEBUG_TAG, "Getting enabled location providers: "); for (String s : providers) { Log.d(DEBUG_TAG, "Provider " + s); } return providers.size() > 0; } ///////////////////////////////// OTHER STUFF ////////////////////////////////////////////////// /** * Get the last successfully searched bus stop or NULL * * @return */ @Override public Stop getLastSuccessfullySearchedBusStop() { return fh.getLastSuccessfullySearchedBusStop(); } /** * Get the last successfully searched bus stop ID or NULL * * @return */ @Override public String getLastSuccessfullySearchedBusStopID() { Stop stop = getLastSuccessfullySearchedBusStop(); return stop == null ? null : stop.ID; } /** * Update the star "Add to favorite" icon */ @Override public void updateStarIconFromLastBusStop() { // no favorites no party! addToFavorites = (ImageButton) findViewById(R.id.addToFavorites); if (addToFavorites == null) { Log.d("MainActivity", "Why the fuck the star is not here?!"); return; } // check if there is a last Stop String stopID = getLastSuccessfullySearchedBusStopID(); 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); } } /** * Check if the last Bus Stop is in the favorites * * @return */ public boolean isStopInFavorites(String busStopId) { boolean found = false; // no stop no party if (busStopId != null) { SQLiteDatabase userDB = new UserDB(getApplicationContext()).getReadableDatabase(); found = UserDB.isStopInFavorites(userDB, busStopId); } return found; } /** * Add the last Stop to favorites */ @Override public void toggleLastStopToFavorites() { Stop stop = getLastSuccessfullySearchedBusStop(); if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE) { /** * Callback fired when the Stop is saved in the favorites * @param result */ @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); // update the star icon updateStarIconFromLastBusStop(); } }.execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(); } } @Override public void showFloatingActionButton(boolean yes) { if (yes) floatingActionButton.show(); else floatingActionButton.hide(); } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// @Override public void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } @Override public void showMessage(int messageID) { Toast.makeText(getApplicationContext(), messageID, Toast.LENGTH_SHORT).show(); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); actionHelpMenuItem.setVisible(true); } //TODO: toggle spinner from mainActivity @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(false); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) { locmgr.removeUpdates(locListener); pendingNearbyStopsRequest = false; } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment"); return; } // Shows hints } /** * Open an URL in the default browser. * * @param url URL */ public void openIceweasel(String url) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(browserIntent1); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } public void changeStarType(String stopID) { if (isStopInFavorites(stopID)) { changeStarFilled(); } else { changeStarOutline(); } } public void changeStarFilled() { addToFavorites.setImageResource(R.drawable.ic_star_filled); } public void changeStarOutline() { addToFavorites.setImageResource(R.drawable.ic_star_outline); } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java index 4d5887a..9844d47 100644 --- a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java +++ b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java @@ -1,52 +1,58 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; // "arrivals" è più usato di "transit" o simili, e chi sono io per mettermi a dibattere con gli inglesi? import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; public interface ArrivalsFetcher extends Fetcher { // /** // * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. // * Don't call this in UI thread! // * // * @param stopID stop ID, in normalized form. // * @param routeID route ID, in normalized form. // * @param res result code (will be set by this method) // * @return arrival times // * @see it.reyboz.bustorino.backend.Fetcher.result // * @see FiveTNormalizer // */ // Palina ReadArrivalTimesRoute(String stopID, String routeID, AtomicReference res); /** * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. * Don't call this in UI thread! * * @param stopID stop ID, in normalized form. * @param res result code (will be set by this method) * @return arrival times * @see it.reyboz.bustorino.backend.Fetcher.result * @see FiveTNormalizer */ Palina ReadArrivalTimesAll(String stopID, AtomicReference res); + + /** + * Get the determined source for the Fetcher + * @return the source of the arrival times + */ + Passaggio.Source getSourceForFetcher(); } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java index 5b8baf5..87bd066 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,392 +1,397 @@ /* BusTO - Backend 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.backend; import android.support.annotation.Nullable; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class FiveTAPIFetcher implements ArrivalsFetcher{ private static final String DEBUG_NAME = "FiveTAPIFetcher"; private final Map defaultHeaders = getDefaultHeaders(); final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab")); @Override public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { //set the date for the request as now Palina p = new Palina(stopID); //request parameters String response = performAPIRequest(QueryType.ARRIVALS,stopID,res); if(response==null) { if(res.get()==result.SERVER_ERROR_404) { Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the hack is not working anymore"); res.set(result.EMPTY_RESULT_SET); } return p; } try { List routes = parseArrivalsServerResponse(response, res); for(Route r: routes){ p.addRoute(r); } } catch (JSONException ex){ res.set(result.PARSER_ERROR); return null; } res.set(result.OK); p.sortRoutes(); return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.FiveTAPI; + } + List parseArrivalsServerResponse(String JSONresponse, AtomicReference res) throws JSONException{ ArrayList routes = new ArrayList<>(3); /* Slight problem: "longName": ==> DESCRIPTION "name": "13N", "departures": [ { "arrivalTimeInt": 1272, "time": "21:12", "rt": false }] "lineType": "URBANO" ==> URBANO can be either bus or tram or METRO */ JSONArray arr; try{ arr = new JSONArray(JSONresponse); String type; Route.Type routetype; for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{ if(response == null || response.length()==0) throw new IllegalArgumentException("Response string is null or void"); ArrayList routes = new ArrayList<>(10); JSONArray lines =new JSONArray(response); for(int i=0; i 1) { String secondo = exploded[exploded.length-2]; if (secondo.contains("festivo")) { festivo = Route.FestiveInfo.FESTIVO; } else if (secondo.contains("feriale")) { festivo = Route.FestiveInfo.FERIALE; } else if(secondo.contains("lun. - ven")) { serviceDays = Route.reduced_week; } else if(secondo.contains("sab - fest.")){ serviceDays = Route.weekend; festivo = Route.FestiveInfo.FESTIVO; } else { /* Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+ "Couldn't find a the service days\n"+ "Description: "+secondo+","+description ); */ } if(exploded.length>2){ switch (exploded[exploded.length-3].trim()) { case "bus": t = Route.Type.BUS; break; case "tram": //never happened, but if it could happen you can get it t = Route.Type.TRAM; break; default: //nothing } } } else //only one piece if(description.contains("festivo")){ festivo = Route.FestiveInfo.FESTIVO; } else if(description.contains("feriale")){ festivo = Route.FestiveInfo.FERIALE; } if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM; if(direction.contains("-")){ //Sometimes the actual filtered direction still remains the full line (including both extremes) direction = direction.split("-")[1]; } Route r = new Route(lineName.trim(),direction.trim(),t,new ArrayList<>()); if(serviceDays.length>0) r.serviceDays = serviceDays; r.festivo = festivo; r.branchid = branchid; r.description = description.trim(); r.setStopsList(Arrays.asList(stops.split(","))); routes.add(r); } return routes; } public List getDirectionsForStop(String stopID, AtomicReference res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); List routes; try{ routes = parseDirectionsFromResponse(response); res.set(result.OK); } catch (JSONException | IllegalArgumentException e) { e.printStackTrace(); res.set(result.PARSER_ERROR); routes = null; } return routes; } public ArrayList getAllStopsFromGTT(AtomicReference res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); if(response==null) return null; ArrayList stopslist; try{ JSONObject responseJSON = new JSONObject(response); JSONArray stops = responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i getAllLinesFromGTT(AtomicReference res){ String resp = performAPIRequest(QueryType.LINES,null,res); if(resp==null) { return null; } ArrayList routes = null; try { JSONArray lines = new JSONArray(resp); routes = new ArrayList<>(lines.length()); for(int i = 0; i getDefaultHeaders(){ HashMap param = new HashMap<>(); param.put("Host","www.5t.torino.it"); param.put("Connection","Keep-Alive"); param.put("Accept-Encoding", "gzip"); return param; } /** * Create and perform the network request. This method adds parameters and returns the result * @param t type of request to be performed * @param stopID optional parameter, stop ID which you need for passages and branches * @param res result container * @return a String which contains the result of the query, to be parsed */ @Nullable public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference res){ URL u; Map param; try { String address = getURLForOperation(t,stopID); //Log.d(DEBUG_NAME,"The address to query is: "+address); param = getDefaultHeaders(); u = new URL(address); } catch (UnsupportedEncodingException |MalformedURLException e) { e.printStackTrace(); res.set(result.PARSER_ERROR); return null; } String response = networkTools.queryURL(u,res,param); return response; } /** * Get the right url for the operation you are doing, to be fed into the queryURL method * @param t type of operation * @param stopID stop on which you are working on * @return the Url to go to * @throws UnsupportedEncodingException if it cannot be converted to utf-8 */ public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { final StringBuilder sb = new StringBuilder(); sb.append("http://www.5t.torino.it/ws2.1/rest/"); if(t!=QueryType.LINES) sb.append("stops/"); switch (t){ case ARRIVALS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/departures"); break; case DETAILS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/branches/details"); break; case STOPS_ALL: sb.append("all"); break; case STOPS_VERSION: sb.append("version"); break; case LINES: sb.append("lines/all"); break; } return sb.toString(); } public enum QueryType { ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java index d5da78f..5c4152f 100644 --- a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java @@ -1,206 +1,211 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan 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; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.net.URL; import java.net.URLEncoder; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; //import android.util.Log; /** * Contains large chunks of code taken from the old GTTSiteSucker, AsyncWget and AsyncWgetBusStopFromBusStopID classes.
*


* «BusTO, because sucking happens»
*
* @author Valerio Bozzolan */ public class FiveTScraperFetcher implements ArrivalsFetcher { /** * Execute regexes. * * @param needle Regex * @param haystack Entire string * @return Matched string */ private static String grep(String needle, String haystack) { String matched = null; Matcher matcher = Pattern.compile( needle).matcher(haystack); if (matcher.find()) { matched = matcher.group(1); } return matched; } @Override public Palina ReadArrivalTimesAll(final String stopID, final AtomicReference res) { Palina p = new Palina(stopID); int routeIndex; String responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = null; try { responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = networkTools.getDOM(new URL("http://www.5t.torino.it/5t/trasporto/arrival-times-byline.jsp?action=getTransitsByLine&shortName=" + URLEncoder.encode(stopID, "utf-8")), res); } catch (Exception e) { res.set(result.PARSER_ERROR); } if(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas == null) { // result already set in getDOM() return p; } Document doc = Jsoup.parse(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas); // Tried in rete Edisu (it does Man In The Middle... asd) Element span = doc.select("span").first(); if(span == null) { res.set(result.SERVER_ERROR); return p; } String busStopID = grep("^(.+) ", span.html()); if (busStopID == null) { //Log.e("BusStop", "Empty busStopID from " + span.html()); res.set(result.EMPTY_RESULT_SET); return p; } // this also appears when no stops are found, but that case has already been handled above Element error = doc.select("p.errore").first(); if (error != null) { res.set(result.SERVER_ERROR); return p; } String busStopName = grep("^.+ (.+)", span.html()); // The first "dot" is the single strange space character in the middle of "39{HERE→} {←HERE}PORTA NUOVA" if (busStopName == null) { //Log.e("BusStop", "Empty busStopName from " + span.html()); res.set(result.SERVER_ERROR); return p; } p.setStopName(busStopName.trim()); // Every table row is a busLine Elements trs = doc.select("table tr"); for (Element tr : trs) { Element line = tr.select("td.line a").first(); if (!line.hasText()) { res.set(result.SERVER_ERROR); return p; } String busLineName = line.text(); // this is yet another ID, that has no known use so we can safely ignore it // Integer busLineID = string2Integer( // grep( // "([0-9]+)$", // line.attr("href") // ) // ); if (busLineName == null) { res.set(result.SERVER_ERROR); return p; } // this fetcher doesn't support railways and probably they've removed METRO too, but anyway... if(busLineName.equals("METRO")) { routeIndex = p.addRoute(busLineName, "", Route.Type.METRO); } else { if(busLineName.length() >= 4) { boolean isExtraurbano = true; for(int ch = 0; ch < busLineName.length(); ch++) { if(!Character.isDigit(busLineName.charAt(ch))) { isExtraurbano = false; break; } } if(isExtraurbano) { routeIndex = p.addRoute(busLineName, "", Route.Type.LONG_DISTANCE_BUS); } else { routeIndex = p.addRoute(busLineName, "", Route.Type.BUS); } } else { routeIndex = p.addRoute(busLineName, "", Route.Type.BUS); } } // Every busLine have passages Elements tds = tr.select("td:not(.line)"); for (Element td : tds) { //boolean isInRealTime = td.select("i").size() > 0; //td.select("i").remove(); // Stripping "*" String time = td.text().trim(); if (time.equals("")) { // Yes... Sometimes there is an EMPTY td ._. continue; } p.addPassaggio(time, Passaggio.Source.FiveTScraper, routeIndex); } } p.sortRoutes(); res.set(result.OK); return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.FiveTScraper; + } + // preserved for future generations: // /* // * I've sent many emails to the public email info@5t.torino.it to write down something like: // * «YOUR SITE EXPLODE IF I USE **YOUR** BUS LINE IDs STARTING WITH ZERO!!!!!» // * So, waiting for a response, I must purge the busStopID from "0"s .__. // * IN YOUR FACE 5T/GTT. IN YOUR FACE. // * // * @param busStopID // * @return parseInt(busStopID) // * @antifeatured yep // * @notabug yep // * @wontfix yep // */ // protected final String getFilteredBusStopID(String busStopID) { // /* // * OK leds me ezplain why 'm dong this shot of shittt. OK zo swhy? // * Bhumm thads because the GTT/5T site-"developer" ids obviusli drunk. // */ // String enableGTTDeveloperSimulator = "on"; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // final char ZZZZZZZEEEEROOOOOO = '0'; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // char[] cinquettiBarraGtt = busStopID.toCharArray(); // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // int merda = 0; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // while (merda < cinquettiBarraGtt.length && cinquettiBarraGtt[merda] == ZZZZZZZEEEEROOOOOO) { // // COMPLETELELELLELEEELY DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // Log.i("AsyncWgetBusStop", "scimmie ubriache assunte per tirar su il sito 5T/GTT"); // DR // merda++; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // } // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // String trenoDiMerda = ""; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // for (; merda < cinquettiBarraGtt.length; merda++) { // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // trenoDiMerda += cinquettiBarraGtt[merda]; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // } // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // enableGTTDeveloperSimulator = "off"; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // // return trenoDiMerda; // } } diff --git a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java index 2afce45..ebd272e 100644 --- a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java @@ -1,111 +1,116 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; import android.support.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.net.URLEncoder; import java.util.concurrent.atomic.AtomicReference; public class GTTJSONFetcher implements ArrivalsFetcher { @Override @NonNull public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { URL url; Palina p = new Palina(stopID); String routename; String bacino; String content; JSONArray json; int howManyRoutes, howManyPassaggi, i, j, pos; // il misto inglese-italiano è un po' ridicolo ma tanto vale... JSONObject thisroute; JSONArray passaggi; try { url = new URL("http://www.gtt.to.it/cms/index.php?option=com_gtt&task=palina.getTransitiOld&palina=" + URLEncoder.encode(stopID, "utf-8") + "&realtime=true"); } catch (Exception e) { res.set(result.PARSER_ERROR); return p; } content = networkTools.queryURL(url, res); if(content == null) { return p; } try { json = new JSONArray(content); } catch(JSONException e) { res.set(result.PARSER_ERROR); return p; } try { // returns [{"PassaggiRT":[],"Passaggi":[]}] for non existing stops! json.getJSONObject(0).getString("Linea"); // if we can get this, then there's something useful in the array. } catch(JSONException e) { res.set(result.EMPTY_RESULT_SET); return p; } howManyRoutes = json.length(); if(howManyRoutes == 0) { res.set(result.EMPTY_RESULT_SET); return p; } try { for(i = 0; i < howManyRoutes; i++) { thisroute = json.getJSONObject(i); routename = thisroute.getString("Linea"); try { bacino = thisroute.getString("Bacino"); } catch (JSONException ignored) { // if "Bacino" gets removed... bacino = "U"; } pos = p.addRoute(routename, thisroute.getString("Direzione"), FiveTNormalizer.decodeType(routename, bacino)); passaggi = thisroute.getJSONArray("PassaggiRT"); howManyPassaggi = passaggi.length(); for(j = 0; j < howManyPassaggi; j++) { p.addPassaggio(passaggi.getString(j).concat("*"), Passaggio.Source.GTTJSON, pos); } passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones howManyPassaggi = passaggi.length(); for(j = 0; j < howManyPassaggi; j++) { p.addPassaggio(passaggi.getString(j), Passaggio.Source.GTTJSON, pos); } } } catch (JSONException e) { res.set(result.PARSER_ERROR); return p; } p.sortRoutes(); res.set(result.OK); return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.GTTJSON; + } + } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index a67c86c..4e3c065 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,349 +1,349 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; import android.util.Log; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude()); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { this.routes.add(new Route(routeID, destinazione, type, new ArrayList<>(6))); routesModified = true; return this.routes.size() - 1; // last inserted element and pray that direct access to ArrayList elements really is direct } public int addRoute(Route r){ this.routes.add(r); routesModified = true; return this.routes.size()-1; } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } // /** // * Clears a route timetable (or creates an empty route) and returns its index // * // * @param routeID name // * @param destinazione end of line\terminus // * @return array index for this route // */ // public int updateRoute(String routeID, String destinazione) { // int s = this.routes.size(); // RouteInternal r; // // for(int i = 0; i < s; i++) { // r = routes.get(i); // if(r.name.compareTo(routeID) == 0 && r.destinazione.compareTo(destinazione) == 0) { // // capire se è possibile che ci siano stessa linea e stessa destinazione su 2 righe diverse del sito e qui una sovrascrive l'altra (probabilmente no) // r.updateFlag(); // r.deletePassaggio(); // return i; // } // } // // return this.addRoute(routeID, destinazione); // } // // /** // * Deletes routes marked as "not updated" (= disappeared from the GTT website\API\whatever). // * Sets all remaining routes to "not updated" because that's how this contraption works. // */ // public void finishUpdatingRoutes() { // RouteInternal r; // // for(Iterator itr = this.routes.iterator(); itr.hasNext(); ) { // r = itr.next(); // if(r.unupdateFlag()) { // itr.remove(); // } // } // } // /** // * Gets the current timetable for a route. Returns null if the route doesn't exist. // * This is slower than queryRouteByIndex. // * // * @return timetable (passaggi) // */ // public List queryRoute(String routeID) { // for(Route r : this.routes) { // if(routeID.equals(r.name)) { // return r.getPassaggi(); // } // } // // return null; // } // // /** // * Gets the current timetable for this route, from its index in the array. // * // * @return timetable (passaggi) // */ // public List queryRouteByIndex(int index) { // return this.routes.get(index).getPassaggi(); // } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); - mSource = Passaggio.Source.UNKNOWN; + mSource = Passaggio.Source.UNDETERMINED; break; } } - if(mSource == Passaggio.Source.UNKNOWN) + if(mSource == Passaggio.Source.UNDETERMINED) break; } //finished with the check, setting flags routesModified = false; allSource = mSource; } public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java index e09901d..6c1e338 100644 --- a/src/it/reyboz/bustorino/backend/Passaggio.java +++ b/src/it/reyboz/bustorino/backend/Passaggio.java @@ -1,159 +1,159 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; import android.support.annotation.NonNull; import android.util.Log; public final class Passaggio implements Comparable { private static final int UNKNOWN_TIME = -3; private static final String DEBUG_TAG = "BusTO-Passaggio"; private final String passaggioGTT; public final int hh,mm; public final boolean isInRealTime; public final Source source; /** * Useless constructor. * * //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. */ // public Passaggio(@NonNull String TimeGTT) { // this.passaggio = TimeGTT; // } @Override public String toString() { return this.passaggioGTT; } /** * Constructs a time (passaggio) for the timetable. * * @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. * @throws IllegalArgumentException if nothing reasonable can be extracted from the string */ public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) { passaggioGTT = TimeGTT; source = sorgente; String[] parts = TimeGTT.split(":"); String hh,mm; boolean realtime; if(parts.length != 2) { //throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); this.hh = UNKNOWN_TIME; this.mm = UNKNOWN_TIME; this.isInRealTime = false; return; } hh = parts[0]; if(parts[1].endsWith("*")) { mm = parts[1].substring(0, parts[1].length() - 1); realtime = true; } else { mm = parts[1]; realtime = false; } int hour=-3,min=-3; try { hour = Integer.parseInt(hh); min = Integer.parseInt(mm); } catch (NumberFormatException ex){ Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes"); hour = UNKNOWN_TIME; min = UNKNOWN_TIME; realtime = false; } finally { this.hh = hour; this.mm = min; this.isInRealTime = realtime; } } public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){ this.hh = hour; this.mm = minutes; this.isInRealTime = realtime; this.source = sorgente; //Build the passaggio string StringBuilder sb = new StringBuilder(); sb.append(hour).append(":").append(minutes); if(realtime) sb.append("*"); this.passaggioGTT = sb.toString(); } public static String createPassaggioGTT(String timeInput, boolean realtime){ final String time = timeInput.trim(); if(time.contains("*")){ if(realtime) return time; else return time.substring(0,time.length()-1); } else{ if(realtime) return time.concat("*"); else return time; } } @Override public int compareTo(@NonNull Passaggio other) { if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME) return 0; else { int diff = this.hh - other.hh; // an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01) if (diff > 12) { // untested diff -= 24; } else if (diff < -12) { diff += 24; } diff *= 60; diff += this.mm - other.mm; // we should take into account if one is in real time and the other isn't, shouldn't we? if (other.isInRealTime) { ++diff; } if (this.isInRealTime) { --diff; } //TODO: separate Realtime and Non-Realtime, especially for the GTTJSONFetcher return diff; } } // // @Override // public String toString() { // String resultString = (this.hh).concat(":").concat(this.mm); // if(this.isInRealTime) { // return resultString.concat("*"); // } else { // return resultString; // } // } public enum Source{ - FiveTAPI,GTTJSON,FiveTScraper, UNKNOWN + FiveTAPI,GTTJSON,FiveTScraper, UNDETERMINED } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index 4d9e101..a77bd7e 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,32 +1,31 @@ package it.reyboz.bustorino.backend; import android.content.Context; -import android.support.v7.widget.RecyclerView; import android.view.View; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } } diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java index 3b31b66..0a68863 100644 --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,342 +1,387 @@ /* 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.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; +import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; +import it.reyboz.bustorino.backend.Fetcher; +import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; +import it.reyboz.bustorino.backend.FiveTScraperFetcher; +import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; -import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.middleware.AppDataProvider; import it.reyboz.bustorino.middleware.NextGenDB; import it.reyboz.bustorino.middleware.UserDB; public class ArrivalsFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks { private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG = "BUSTOArrivalsFragment"; private final static int loaderFavId = 2; private final static int loaderStopId = 1; + private final static ArrivalsFetcher[] defaultFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; static final String STOP_TITLE = "messageExtra"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; + private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); + public static ArrivalsFragment newInstance(String stopID){ + return newInstance(stopID, null); + } + + public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ + ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); - ArrivalsFragment fragment = new ArrivalsFragment(); //parameter for ResultListFragment args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); - fragment.setArguments(args); - return fragment; - } - public static ArrivalsFragment newInstance(String stopID,String stopName){ - ArrivalsFragment fragment = newInstance(stopID); - Bundle args = fragment.getArguments(); - args.putString(KEY_STOP_NAME,stopName); + if (stopName != null){ + args.putString(KEY_STOP_NAME,stopName); + } fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); resultsListView = (ListView) root.findViewById(R.id.resultsListView); timesSourceTextView = (TextView) root.findViewById(R.id.timesSourceTextView); + timesSourceTextView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + rotateFetchers(); + timesSourceTextView.setText(R.string.arrival_source_changing); + mListener.createFragmentForStop(stopID); + return true; + } + }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites mListener.toggleLastStopToFavorites(); }); resultsListView.setOnItemClickListener((parent, view, position, id) -> { String routeName; Route r = (Route) parent.getItemAtPosition(position); routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay()); if (routeName == null) { routeName = r.getNameForDisplay(); } if (r.destinazione == null || r.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show(); } }); String displayName = getArguments().getString(STOP_TITLE); setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().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); } return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); if(stopID!=null){ //refresh the arrivals if(!justCreated) mListener.createFragmentForStop(stopID); else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); } } @Nullable public String getStopID() { return stopID; } + /** + * Give the fetchers + * @return the list of the fetchers + */ + public ArrayList getCurrentFetchers(){ + ArrayList v = new ArrayList(); + for (ArrivalsFetcher fetcher: fetchers){ + v.add(fetcher); + } + return v; + } + public Fetcher[] getCurrentFetchersAsArray(){ + Fetcher[] arr = new Fetcher[fetchers.size()]; + fetchers.toArray(arr); + return arr; + } + + private void rotateFetchers(){ + Collections.rotate(fetchers, -1); + } + + /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ 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 { final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina); showArrivalsSources(lastUpdatedPalina); super.resetListAdapter(adapter); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; - case UNKNOWN: + case UNDETERMINED: //Don't show the view timesSourceTextView.setVisibility(View.GONE); return; default: throw new IllegalStateException("Unexpected value: " + source); } final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setVisibility(View.VISIBLE); timesSourceTextView.setText(base_message); } @Override public void setNewListAdapter(ListAdapter adapter) { throw new UnsupportedOperationException(); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && stopName.length() > 0) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon mListener.updateStarIconFromLastBusStop(); } @Override public Loader onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ data.moveToFirst(); final String probableName = data.getString(colUserName); if(probableName!=null && !probableName.isEmpty()){ stopName = probableName; updateMessage(); } } if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); stopName = data.getString(data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME )); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index 5dfc492..ed48aa1 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,232 +1,230 @@ /* BusTO (fragments) 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.ContentResolver; import android.content.ContentValues; import android.database.sqlite.SQLiteException; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { GeneralActivity act; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private int primaryFrameLayout,secondaryFrameLayout, swipeRefID; public static final int NO_FRAME = -3; private WeakReference lastTaskRef; private NextGenDB newDBHelper; private boolean shouldHaltAllActivities=false; public FragmentHelper(GeneralActivity act, int swipeRefID, int mainFrame) { this(act,swipeRefID,mainFrame,NO_FRAME); } public FragmentHelper(GeneralActivity act, int swipeRefID, int primaryFrameLayout, int secondaryFrameLayout) { this.act = act; this.swipeRefID = swipeRefID; this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; newDBHelper = new NextGenDB(act.getApplicationContext()); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } public void setLastTaskRef(WeakReference lastTaskRef) { this.lastTaskRef = lastTaskRef; } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ public void createOrUpdateStopFragment(Palina p){ boolean sameFragment; ArrivalsFragment arrivalsFragment; if(act==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG return; } FragmentManager fm = act.getSupportFragmentManager(); if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else sameFragment = false; setLastSuccessfullySearchedBusStop(p); if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); String displayStuff; if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } attachFragmentToContainer(fm,arrivalsFragment,true,ResultListFragment.getFragmentTag(p)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); } - - final PalinaAdapter adapter = new PalinaAdapter(act.getApplicationContext(), p); // DO NOT CALL `setListAdapter` ever on arrivals fragment arrivalsFragment.updateFragmentData(p); act.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createFragmentFor(List resultList,String query){ act.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query); listfragment.setStopList(resultList); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ if (act instanceof FragmentListener) ((FragmentListener) act).toggleSpinner(on); else { SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID); srl.setRefreshing(false); } } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param sendToSecondaryFrame needs to be displayed in secondary frame or not * @param tag tag for the fragment */ public void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){ FragmentTransaction ft = fm.beginTransaction(); if(sendToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) ft.replace(secondaryFrameLayout,fragment,tag); else ft.replace(primaryFrameLayout,fragment,tag); ft.addToBackStack("state_"+tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); ft.commit(); //fm.executePendingTransactions(); } synchronized public int insertBatchDataInNextGenDB(ContentValues[] valuesArr,String tableName){ if(newDBHelper !=null) try { return newDBHelper.insertBatchContent(valuesArr, tableName); } catch (SQLiteException exc){ Log.w("DB Batch inserting: ","ERROR Inserting the data batch: ",exc.fillInStackTrace()); return -2; } else return -1; } synchronized public ContentResolver getContentResolver(){ return act.getContentResolver(); } public void setBlockAllActivities(boolean shouldI) { this.shouldHaltAllActivities = shouldI; } public void stopLastRequestIfNeeded(){ if(lastTaskRef == null) return; AsyncDataDownload task = lastTaskRef.get(); if(task!=null){ task.cancel(true); } } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.result res){ //TODO: implement a common set of errors for all fragments switch (res){ case OK: break; case CLIENT_OFFLINE: act.showMessage(R.string.network_error); break; case SERVER_ERROR: if (act.isConnected()) { act.showMessage(R.string.parsing_error); } else { act.showMessage(R.string.network_error); } case PARSER_ERROR: default: act.showMessage(R.string.internal_error); break; case QUERY_TOO_SHORT: act.showMessage(R.string.query_too_short); break; case EMPTY_RESULT_SET: act.showMessage(R.string.no_bus_stop_have_this_name); break; } } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index fed7457..6255fe3 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,318 +1,321 @@ /* BusTO (middleware) 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.middleware; import android.content.ContentResolver; import android.content.ContentValues; import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; -import android.support.v7.app.AppCompatActivity; +import android.support.annotation.NonNull; import android.util.Log; -import it.reyboz.bustorino.R; + import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.middleware.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.Calendar; /** * This should be used to download data, but not to display it */ public class AsyncDataDownload extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; private boolean failedAll = false; - private AtomicReference res; - private RequestType t; + private final AtomicReference res; + private final RequestType t; private String query; WeakReference helperRef; - private ArrayList otherActivities = new ArrayList<>(); + private final ArrayList otherActivities = new ArrayList<>(); + private final Fetcher[] theFetchers; - public AsyncDataDownload(RequestType type,FragmentHelper fh) { - t = type; + public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers) { + RequestType type; helperRef = new WeakReference<>(fh); fh.setLastTaskRef(new WeakReference<>(this)); res = new AtomicReference<>(); + + theFetchers = fetchers; + if (theFetchers.length < 1){ + throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); + } + if (theFetchers[0] instanceof ArrivalsFetcher){ + type = RequestType.ARRIVALS; + } else if (theFetchers[0] instanceof StopsFinderByName){ + type = RequestType.STOPS; + } else{ + type = null; + } + t = type; + } @Override protected Object doInBackground(String... params) { - RecursionHelper r; + RecursionHelper r = new RecursionHelper<>(theFetchers); boolean success=false; Object result; - switch (t){ - case ARRIVALS: - r = new RecursionHelper<>(new ArrivalsFetcher[] {new FiveTAPIFetcher(),new GTTJSONFetcher(), new FiveTScraperFetcher()}); - break; - case STOPS: - r = new RecursionHelper<>(new StopsFinderByName[] {new GTTStopsFetcher(), new FiveTStopsFetcher()}); - break; - default: - //TODO put error message - return null; - } FragmentHelper fh = helperRef.get(); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ return null; } //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); while(r.valid()) { if(this.isCancelled()) { return null; } //get the data from the fetcher switch (t){ case ARRIVALS: ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); Palina p; String stopID; if(params.length>0) stopID=params[0]; //(it's a Palina) else if(lastSearchedBusStop!=null) stopID = lastSearchedBusStop.ID; //(it's a Palina) else { publishProgress(Fetcher.result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times if(f instanceof FiveTAPIFetcher && Integer.parseInt(stopID)>= 8200) continue; p= f.ReadArrivalTimesAll(stopID,res); publishProgress(res.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); if(gres.get() == Fetcher.result.OK){ p.addInfoFromRoutes(branches); Thread t = new Thread(new BranchInserter(branches,fh,stopID)); t.start(); otherActivities.add(t); } //put updated values into Database } if(lastSearchedBusStop != null && res.get()== Fetcher.result.OK) { // check that we don't have the same stop if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } } result = p; //TODO: find a way to avoid overloading the user with toasts break; case STOPS: StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward(); List resultList= finder.FindByName(params[0], this.res); //it's a List Log.d(TAG,"Using the StopFinderByName: "+finder.getClass()); query =params[0]; result = resultList; //dummy result break; default: result = null; } //find if it went well if(res.get()== Fetcher.result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return result; } } //at this point, we are sure that the result has been negative failedAll=true; return null; } @Override protected void onProgressUpdate(Fetcher.result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.result r : values){ //TODO: make Toast fh.showErrorMessage(r); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Object o) { FragmentHelper fh = helperRef.get(); if(failedAll || o == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; switch (t){ case ARRIVALS: Palina palina = (Palina) o; fh.createOrUpdateStopFragment(palina); break; case STOPS: //this should never be a problem List stopList = (List) o; if(query!=null && !isCancelled()) { fh.createFragmentFor(stopList,query); } else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT"); break; case DBUPDATE: break; } } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } public enum RequestType { ARRIVALS,STOPS,DBUPDATE } public class BranchInserter implements Runnable{ private final List routesToInsert; private String stopID; private final FragmentHelper fragmentHelper; public BranchInserter(List routesToInsert,FragmentHelper fh,String stopID) { this.routesToInsert = routesToInsert; this.stopID = stopID; this.fragmentHelper = fh; } @Override public void run() { ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); values[routesToInsert.indexOf(r)] = cv; for(int i=0; i