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