diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java
index 98406a1..ae7ee18 100644
--- a/src/it/reyboz/bustorino/ActivityMain.java
+++ b/src/it/reyboz/bustorino/ActivityMain.java
@@ -1,796 +1,798 @@
/*
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.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.List;
import static android.location.Criteria.NO_REQUIREMENT;
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 (bug #1512948)
private int searchMode;
/*
* 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()});
/*
* 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();
}
};
//// 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;
}
@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_about:
startActivity(new Intent(ActivityMain.this, ActivityAbout.class));
return true;
case R.id.action_news:
openIceweasel("https://blog.reyboz.it/tag/busto/");
return true;
case R.id.action_bugs:
openIceweasel("https://bugs.launchpad.net/bus-torino");
return true;
case R.id.action_source:
openIceweasel("https://code.launchpad.net/bus-torino");
return true;
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html");
return true;
case R.id.action_author:
// TODO: a neutral website with a cute landing page and credits to everyone? -- boz, Thu Jan 17 02:17:45 CET 2019
openIceweasel("https://boz.reyboz.it/?lovebusto");
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);
}
} else { // searchMode == SEARCH_BY_NAME
String query = busStopSearchByNameEditText.getText().toString();
//new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper);
new AsyncDataDownload(AsyncDataDownload.RequestType.STOPS,fh).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);
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 //////////////////////////////////////////////////
@Override
public void addLastStopToFavorites() {
if(fh.getLastSuccessfullySearchedBusStop() != null) {
new AsyncAddToFavorites(getApplicationContext()).execute(fh.getLastSuccessfullySearchedBusStop());
}
}
@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;
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
index f16bade..260a1fb 100644
--- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
+++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
@@ -1,144 +1,146 @@
/*
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.adapters;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
/**
* This once was a ListView Adapter for BusLine[].
*
* Thanks to Framentos developers for the guide:
* http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/#
*
* @author Valerio Bozzolan
* @author Ludovico Pavesi
*/
public class PalinaAdapter extends ArrayAdapter {
private LayoutInflater li;
private static int row_layout = R.layout.entry_bus_line_passage;
private static final int metroBg = R.drawable.route_background_metro;
private static final int busBg = R.drawable.route_background_bus;
private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance;
private static final int busIcon = R.drawable.bus;
private static final int trainIcon = R.drawable.subway;
private static final int tramIcon = R.drawable.tram;
//private static final int cityIcon = R.drawable.city;
// hey look, a pattern!
private static class ViewHolder {
TextView rowStopIcon;
TextView rowRouteDestination;
TextView rowRouteTimetable;
}
public PalinaAdapter(Context context, Palina p) {
super(context, row_layout, p.queryAllRoutes());
li = LayoutInflater.from(context);
}
/**
* Some parts taken from the AdapterBusLines class.
* Some parts inspired by these enlightening tutorials:
* http://www.simplesoft.it/android/guida-agli-adapter-e-le-listview-in-android.html
* https://www.codeofaninja.com/2013/09/android-viewholder-pattern-example.html
* And some other bits and bobs TIRATI FUORI DAL NULLA CON L'INTUIZIONE INTELLETTUALE PERCHÉ
* SEMBRA CHE NESSUNO ABBIA LA MINIMA IDEA DI COME FUNZIONA UN ADAPTER SU ANDROID.
*/
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
ViewHolder vh;
if(convertView == null) {
// INFLATE!
// setting a parent here is not supported and causes a fatal exception, apparently.
convertView = li.inflate(row_layout, null);
// STORE TEXTVIEWS!
vh = new ViewHolder();
vh.rowStopIcon = (TextView) convertView.findViewById(R.id.routeID);
vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination);
vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere);
// STORE VIEWHOLDER IN\ON\OVER\UNDER\ABOVE\BESIDE THE VIEW!
convertView.setTag(vh);
} else {
// RECOVER THIS STUFF!
vh = (ViewHolder) convertView.getTag();
}
Route route = getItem(position);
vh.rowStopIcon.setText(route.getNameForDisplay());
if(route.destinazione==null || route.destinazione.length() == 0) {
vh.rowRouteDestination.setVisibility(View.GONE);
} else {
// View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible.
vh.rowRouteDestination.setVisibility(View.VISIBLE);
vh.rowRouteDestination.setText(route.destinazione);
}
switch (route.type) {
+ //UNKNOWN = BUS for the moment
+ case UNKNOWN:
case BUS:
default:
// convertView could contain another background, reset it
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
break;
case LONG_DISTANCE_BUS:
vh.rowStopIcon.setBackgroundResource(extraurbanoBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
break;
case METRO:
vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
vh.rowStopIcon.setBackgroundResource(metroBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
break;
case RAILWAY:
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
break;
case TRAM: // never used but whatever.
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0);
break;
}
List passaggi = route.passaggi;
if(passaggi.size() == 0) {
vh.rowRouteTimetable.setText(R.string.no_passages);
} else {
vh.rowRouteTimetable.setText(route.getPassaggiToString());
}
return convertView;
}
}
diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
index 586df23..3d36871 100644
--- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
+++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
@@ -1,426 +1,428 @@
/*
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 SECRET_KEY="759C97DC7D115966C30FD9169BB200D9";
private static final String DEBUG_NAME = "FiveTAPIFetcher";
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;
}
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 == null &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM;
+ 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 = 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 getHeadersForRequest(String url) throws UnsupportedEncodingException, NoSuchAlgorithmException {
final Date d = new Date();
HashMap param = new HashMap<>();
param.put("TOKEN",getAccessToken(url,d));
param.put("TIMESTAMP",String.valueOf(d.getTime()));
param.put("Accept-Encoding","gzip");
param.put("Connection","Keep-Alive");
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 = getHeadersForRequest(address);
u = new URL(address);
} catch (UnsupportedEncodingException | NoSuchAlgorithmException |MalformedURLException e) {
e.printStackTrace();
res.set(result.PARSER_ERROR);
return null;
}
String response = networkTools.queryURL(u,res,param);
return response;
}
/**
* Get the Token needed to access the API
* @param URL the URL of the request
* @return token
* @throws NoSuchAlgorithmException if the system doesn't support MD5
* @throws UnsupportedEncodingException if we made mistakes in writing utf-8
*/
private static String getAccessToken(String URL,Date d) throws NoSuchAlgorithmException,UnsupportedEncodingException{
MessageDigest md = MessageDigest.getInstance("MD5");
String strippedQuery = URL.replace("http://www.5t.torino.it/proxyws","");
//return the time in milliseconds
long timeMilli = d.getTime();
StringBuilder sb = new StringBuilder();
sb.append(strippedQuery);
sb.append(timeMilli);
sb.append(SECRET_KEY);
String stringToBeHashed = sb.toString();
//Log.d(DEBUG_NAME,"Hashing string: "+stringToBeHashed);
md.reset();
byte[] data = md.digest(stringToBeHashed.getBytes("UTF-8"));
sb = new StringBuilder();
for (byte b : data){
sb.append(String.format("%02x",b));
}
String result = sb.toString();
//Log.d(DEBUG_NAME,"getting token:\n\treduced URL: "+strippedQuery+"\n\ttimestamp: "+timeMilli+"\nTOKEN:"+result.toLowerCase());
return result.toLowerCase();
}
/**
* 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/proxyws/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/Route.java b/src/it/reyboz/bustorino/backend/Route.java
index dae1d80..f805f51 100644
--- a/src/it/reyboz/bustorino/backend/Route.java
+++ b/src/it/reyboz/bustorino/backend/Route.java
@@ -1,356 +1,356 @@
/*
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.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
public class Route implements Comparable {
final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY};
final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY};
private final static int BRANCHID_MISSING = -1;
private final String name;
public String destinazione;
public final List passaggi;
//create a copy of the list, so that
private List sortedPassaggi;
public final Type type;
public String description;
//ordered list of stops, from beginning to end of line
private List stopsList = null;
public int branchid = BRANCHID_MISSING;
public int[] serviceDays ={};
//0=>feriale, 1=>festivo -2=>unknown
public FestiveInfo festivo = FestiveInfo.UNKNOWN;
public enum Type { // "long distance" sono gli extraurbani.
- BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5);
+ BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2);
//TODO: decide to give some special parameter to each field
private int code;
Type(int code){
this.code = code;
}
public int getCode(){
return this.code;
}
@Nullable
public static Type fromCode(int i){
switch (i){
case 1:
return BUS;
case 2:
return LONG_DISTANCE_BUS;
case 3:
return METRO;
case 4:
return RAILWAY;
case 5:
return TRAM;
+ case -2:
+ return UNKNOWN;
default:
return null;
}
}
}
public enum FestiveInfo{
FESTIVO(1),FERIALE(0),UNKNOWN(-2);
private int code;
FestiveInfo(int code){
this.code = code;
}
public int getCode() {
return code;
}
public static FestiveInfo fromCode(int i){
switch (i){
case -2:
return UNKNOWN;
case 0:
return FERIALE;
case 1:
return FESTIVO;
default:
return UNKNOWN;
}
}
}
/**
* Constructor.
*
* @param name route ID
* @param destinazione terminus\end of line
* @param type bus, long distance bus, underground, and so on
* @param passaggi timetable, a good choice is an ArrayList of size 6
* @param description the description of the line, usually given by the FiveTAPIFetcher
* @see Palina Palina.addRoute() method
*/
public Route(String name, String destinazione, List passaggi, Type type, String description) {
this.name = name;
this.destinazione = parseDestinazione(destinazione);
this.passaggi = passaggi;
this.type = type;
this.description = description;
}
/**
* Constructor used in GTTJSONFetcher, see above
*/
public Route(String name, String destinazione, Type type, List passaggi) {
this(name,destinazione,passaggi,type,null);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,Type t,String description){
this(name,null,new ArrayList<>(),t,description);
}
/**
* Exactly what it says on the tin.
*
* @return times from the timetable
*/
public List getPassaggi() {
return this.passaggi;
}
public void setStopsList(List stopsList) {
this.stopsList = Collections.unmodifiableList(stopsList);
}
public List getStopsList(){
return this.stopsList;
}
/**
* 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, Passaggio.Source source) {
this.passaggi.add(new Passaggio(TimeGTT, source));
}
//Overloaded
public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
this.passaggi.add(new Passaggio(hour, minutes, realtime, source));
}
private String parseDestinazione(String direzione){
if(direzione==null) return null;
//trial to add space to the parenthesis
String[] exploded = direzione.split("\\(");
if(exploded.length>1){
StringBuilder sb = new StringBuilder();
sb.append(exploded[0]);
for(int i=1; i arrivals;
int max;
if(sort){
if(sortedPassaggi==null){
sortedPassaggi = new ArrayList<>(passaggi.size());
sortedPassaggi.addAll(passaggi);
Collections.sort(sortedPassaggi);
}
arrivals = sortedPassaggi;
} else arrivals = passaggi;
- if(start_idx+number>arrivals.size())
- max = arrivals.size();
- else max = start_idx+number;
+ max = Math.min(start_idx + number, arrivals.size());
for(int j= start_idx; j0){
this.passaggi.addAll(other.passaggi);
}
if(this.destinazione == null && other.destinazione!=null) {
this.destinazione = other.destinazione;
adjusted = true;
}
if(!this.isBranchIdValid() && other.isBranchIdValid()) {
this.branchid = other.branchid;
adjusted = true;
}
if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){
this.festivo = other.festivo;
adjusted = true;
}
if(other.description!=null && (this.description==null ||
(this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) ||
(this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) {
this.description = other.description;
}
return adjusted;
}
}
diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
index 314199b..02df227 100644
--- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
@@ -1,194 +1,201 @@
/*
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.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.widget.TextView;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.DBStatusManager;
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 @Nullable String stopID,stopName;
private TextView messageTextView;
private DBStatusManager prefs;
private DBStatusManager.OnDBUpdateStatusChangeListener listener;
+ private boolean justCreated = false;
public static ArrivalsFragment newInstance(String stopID){
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);
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 f = this;
listener = new DBStatusManager.OnDBUpdateStatusChangeListener() {
@Override
public void onDBStatusChanged(boolean updating) {
if(!updating){
getLoaderManager().restartLoader(loaderFavId,getArguments(),f);
} 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 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();
}
}
@Nullable
public String getStopID() {
return stopID;
}
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));
}
@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.d("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL");
+ 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/ResultListFragment.java b/src/it/reyboz/bustorino/fragments/ResultListFragment.java
index 6d42a9e..382fe30 100644
--- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java
+++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java
@@ -1,293 +1,293 @@
/*
BusTO - Fragments components
Copyright (C) 2016 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import android.support.design.widget.FloatingActionButton;
import it.reyboz.bustorino.ActivityMain;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
/**
* This is a generalized fragment that can be used both for
*
*
*/
public class ResultListFragment extends Fragment{
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
static final String LIST_TYPE = "list-type";
private static final String STOP_TITLE = "messageExtra";
protected static final String LIST_STATE = "list_state";
private static final String MESSAGE_TEXT_VIEW = "message_text_view";
private FragmentKind adapterKind;
private boolean adapterSet = false;
- private FragmentListener mListener;
+ protected FragmentListener mListener;
private TextView messageTextView;
private FloatingActionButton fabutton;
private ListView resultsListView;
private ListAdapter mListAdapter = null;
boolean listShown;
private Parcelable mListInstanceState = null;
public ResultListFragment() {
// Required empty public constructor
}
public ListView getResultsListView() {
return resultsListView;
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param listType whether the list is used for STOPS or LINES (Orari)
* @return A new instance of fragment ResultListFragment.
*/
public static ResultListFragment newInstance(FragmentKind listType, String eventualStopTitle) {
ResultListFragment fragment = new ResultListFragment();
Bundle args = new Bundle();
args.putSerializable(LIST_TYPE, listType);
if (eventualStopTitle != null)
args.putString(STOP_TITLE, eventualStopTitle);
fragment.setArguments(args);
return fragment;
}
public static ResultListFragment newInstance(FragmentKind listType) {
return newInstance(listType, null);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
adapterKind = (FragmentKind) getArguments().getSerializable(LIST_TYPE);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_list_view, container, false);
messageTextView = (TextView) root.findViewById(R.id.messageTextView);
if (adapterKind != null) {
resultsListView = (ListView) root.findViewById(R.id.resultsListView);
switch (adapterKind) {
case STOPS:
resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
/*
* Casting because of Javamerda
* @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener
*/
Stop busStop = (Stop) parent.getItemAtPosition(position);
mListener.createFragmentForStop(busStop.ID);
}
});
//set the textviewMessage
setTextViewMessage(getString(R.string.results));
break;
case ARRIVALS:
resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long 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));
break;
default:
throw new IllegalStateException("Argument passed was not of a supported type");
}
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);
}
} else
Log.d(getString(R.string.list_fragment_debug), "No content root for fragment");
return root;
}
public boolean isFragmentForTheSameStop(Palina p) {
return adapterKind.equals(FragmentKind.ARRIVALS) && getTag().equals(getFragmentTag(p));
}
public static String getFragmentTag(Palina p) {
return p.ID;
}
@Override
public void onResume() {
super.onResume();
//Log.d(getString(R.string.list_fragment_debug),"Fragment restored, saved listAdapter is "+(mListAdapter));
if (mListAdapter != null) {
ListAdapter adapter = mListAdapter;
mListAdapter = null;
setListAdapter(adapter);
}
if (mListInstanceState != null) {
Log.d("resultsListView", "trying to restore instance state");
resultsListView.onRestoreInstanceState(mListInstanceState);
}
switch (adapterKind) {
case ARRIVALS:
resultsListView.setOnScrollListener(new CommonScrollListener(mListener, true));
fabutton.show();
break;
case STOPS:
resultsListView.setOnScrollListener(new CommonScrollListener(mListener, false));
break;
default:
//NONE
}
mListener.readyGUIfor(adapterKind);
}
@Override
public void onPause() {
if (adapterKind.equals(FragmentKind.ARRIVALS)) {
SwipeRefreshLayout reflay = (SwipeRefreshLayout) getActivity().findViewById(R.id.listRefreshLayout);
reflay.setEnabled(false);
Log.d("BusTO Fragment " + this.getTag(), "RefreshLayout disabled");
}
super.onPause();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof FragmentListener) {
mListener = (FragmentListener) context;
fabutton = (FloatingActionButton) getActivity().findViewById(R.id.floatingActionButton);
} else {
throw new RuntimeException(context.toString()
+ " must implement ResultFragmentListener");
}
}
@Override
public void onDetach() {
mListener = null;
if (fabutton != null)
fabutton.show();
super.onDetach();
}
@Override
public void onDestroyView() {
resultsListView = null;
//Log.d(getString(R.string.list_fragment_debug), "called onDestroyView");
getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString());
super.onDestroyView();
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
Log.d("ResultListFragment", "onViewStateRestored");
if (savedInstanceState != null) {
mListInstanceState = savedInstanceState.getParcelable(LIST_STATE);
Log.d("ResultListFragment", "listInstanceStatePresent :" + mListInstanceState);
}
}
public void setListAdapter(ListAdapter adapter) {
boolean hadAdapter = mListAdapter != null;
mListAdapter = adapter;
if (resultsListView != null) {
resultsListView.setAdapter(adapter);
resultsListView.setVisibility(View.VISIBLE);
}
}
/**
* Set the message textView
* @param message the whole message to write in the textView
*/
public void setTextViewMessage(String message) {
messageTextView.setText(message);
switch (adapterKind) {
case ARRIVALS:
final ActivityMain activ = (ActivityMain) getActivity();
messageTextView.setClickable(true);
messageTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mListener.addLastStopToFavorites();
}
});
break;
case STOPS:
messageTextView.setClickable(false);
break;
}
messageTextView.setVisibility(View.VISIBLE);
}
}