diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 24ba670..13fe320 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,105 +1,108 @@ + + diff --git a/res/layout/entry_bus_stop.xml b/res/layout/entry_bus_stop.xml index 53c2593..ee45178 100644 --- a/res/layout/entry_bus_stop.xml +++ b/res/layout/entry_bus_stop.xml @@ -1,70 +1,73 @@ \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 9baee1f..653aff0 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,633 +1,630 @@ /* 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.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.LocationManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; 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 com.melnykov.fab.FloatingActionButton; import android.support.design.widget.FloatingActionButton; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.FiveTStopsFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.GTTStopsFetcher; import it.reyboz.bustorino.backend.StopsFinderByName; -import it.reyboz.bustorino.fragments.FragmentHelper; -import it.reyboz.bustorino.fragments.FragmentListener; -import it.reyboz.bustorino.fragments.NearbyStopsFragment; -import it.reyboz.bustorino.fragments.ResultListFragment; +import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; 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; /* * 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"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference res) { SystemClock.sleep(5000); res.set(result.SERVER_ERROR); return new Palina(); } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ private RecursionHelper ArrivalFetchersRecursionHelper = new RecursionHelper<>(new ArrivalsFetcher[] {new GTTJSONFetcher(), new FiveTScraperFetcher()}); private RecursionHelper StopsFindersByNameRecursionHelper = new RecursionHelper<>(new StopsFinderByName[] {new GTTStopsFetcher(), new FiveTStopsFetcher()}); private 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() { 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,FragmentHelper.NO_FRAME); + 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 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()); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// //Let's search stops nearby LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); if(locManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && fh.getLastSuccessfullySearchedBusStop()==null && getOption(LOCATION_PERMISSION_GIVEN,false)) { NearbyStopsFragment fr = NearbyStopsFragment.newInstance(); FragmentTransaction ft = framan.beginTransaction(); ft.add(R.id.resultFrame, fr, "Location"); ft.commitNow(); swipeRefreshLayout.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); } else if(!getOption(LOCATION_PERMISSION_GIVEN, false)){ assertLocationPermissions(); } 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 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("http://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("http://www.gnu.org/licenses/gpl-3.0.html"); return true; case R.id.action_author: openIceweasel("http://boz.reyboz.it?lovebusto"); return true; } 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); } } @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); handler.post(new Runnable() { @Override public void run() { Log.d("mainActivity","Recreating stop fragment"); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(); 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(); } }); } 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(); } } } ///////////////////////////////// OTHER STUFF ////////////////////////////////////////////////// @Override public void addLastStopToFavorites() { if(fh.getLastSuccessfullySearchedBusStop() != null) { new AsyncAddToFavorites(this).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(String fragmentType) { + public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); if(fragmentType==null) Log.e("ActivityMain","Problem with fragmentType"); else switch (fragmentType){ - case ResultListFragment.TYPE_LINES: + case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; - case ResultListFragment.TYPE_STOPS: + 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/backend/FiveTNormalizer.java b/src/it/reyboz/bustorino/backend/FiveTNormalizer.java index 2c42045..ddab8be 100644 --- a/src/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/src/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -1,317 +1,319 @@ /* 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; /** * Converts some weird stop IDs found on the 5T website to the form used everywhere else (including GTT website). *

* A stop ID in normalized form is:
* - a string containing a number without leading zeros
* - a string beginning with "ST" and then a number
* - whatever the GTT website uses.
*

* A bus route ID in normalized form is:
* - a string containing a number and optionally: "B", "CS", "CD", "N", "S", "C" at the end
* - the string "METRO"
* - "ST1" or "ST2" (Star 1 and Star 2)
* - a string beginning with E, N, S or W, followed by two digits (with a leading zero)
* - "RV2", "OB1"
* - "CAN" or "FTC" (railway lines)
* - ...screw it, let's just hope all the websites and APIs return something sane as route IDs.
*

* This class exists because Java doesn't support traits.
*
* Note: this class also just became useless, as 5T now uses the same format as GTT website. */ public abstract class FiveTNormalizer { public static String FiveTNormalizeRoute(String RouteID) { while (RouteID.startsWith("0")) { RouteID = RouteID.substring(1); } return RouteID; } // public static String FiveTNormalizeStop(String StopID) { // StopID = FiveTNormalizeRoute(StopID); // // is this faster than a regex? // if (StopID.length() == 5 && StopID.startsWith("ST") && Character.isLetter(StopID.charAt(2)) && Character.isLetter(StopID.charAt(3)) && Character.isLetter(StopID.charAt(4))) { // switch (StopID) { // case "STFER": // return "8210"; // case "STPAR": // return "8211"; // case "STMAR": // return "8212"; // case "STMAS": // return "8213"; // case "STPOS": // return "8214"; // case "STMGR": // return "8215"; // case "STRIV": // return "8216"; // case "STRAC": // return "8217"; // case "STBER": // return "8218"; // case "STPDA": // return "8219"; // case "STDOD": // return "8220"; // case "STPSU": // return "8221"; // case "STVIN": // return "8222"; // case "STREU": // return "8223"; // case "STPNU": // return "8224"; // case "STMCI": // return "8225"; // case "STNIZ": // return "8226"; // case "STDAN": // return "8227"; // case "STCAR": // return "8228"; // case "STSPE": // return "8229"; // case "STLGO": // return "8230"; // } // } // return StopID; // } // // public static String NormalizedToFiveT(final String StopID) { // if(StopID.startsWith("82") && StopID.length() == 4) { // switch (StopID) { // case "8230": // return "STLGO"; // case "8229": // return "STSPE"; // case "8228": // return "STCAR"; // case "8227": // return "STDAN"; // case "8226": // return "STNIZ"; // case "8225": // return "STMCI"; // case "8224": // return "STPNU"; // case "8223": // return "STREU"; // case "8222": // return "STVIN"; // case "8221": // return "STPSU"; // case "8220": // return "STDOD"; // case "8219": // return "STPDA"; // case "8218": // return "STBER"; // case "8217": // return "STRAC"; // case "8216": // return "STRIV"; // case "8215": // return "STMGR"; // case "8214": // return "STPOS"; // case "8213": // return "STMAS"; // case "8212": // return "STMAR"; // case "8211": // return "STPAR"; // case "8210": // return "STFER"; // } // } // // return StopID; // } public static Route.Type decodeType(final String routename, final String bacino) { if(routename.equals("METRO")) { return Route.Type.METRO; } else if(routename.equals("79")) { return Route.Type.RAILWAY; } switch (bacino) { case "U": return Route.Type.BUS; case "F": return Route.Type.RAILWAY; case "E": return Route.Type.LONG_DISTANCE_BUS; default: return Route.Type.BUS; } } /** * Converts a route ID from internal format to display format, returns null if it has the same name. * * @param routeID ID in "internal" and normalized format * @return string with display name, null if unchanged */ public static String routeInternalToDisplay(final String routeID) { if(routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0,2).concat("/"); } switch(routeID) { case "1C": return "1 Chieri"; case "1N": return "1 Nichelino"; case "OB1": return "1 Orbassano"; case "2C": return "2 Chieri"; case "RV2": return "2 Rivalta"; case "CO1": return "Circolare Collegno"; case "SE1": // I wonder why GTT calls this "SE1" while other absurd names have a human readable name too. return "1 Settimo"; case "16CD": return "16 Circolare Destra"; case "16CS": return "16 Circolare Sinistra"; case "79": return "Cremagliera Sassi-Superga"; case "W01": return "Night Buster 1 Arancio"; case "N10": return "Night Buster 10 Gialla"; case "W15": return "Night Buster 15 Rosa"; case "S18": return "Night Buster 18 Blu"; case "S04": return "Night Buster 4 Azzurra"; case "N4": return "Night Buster 4 Rossa"; case "N57": return "Night Buster 57 Oro"; case "W60": return "Night Buster 60 Argento"; case "E68": return "Night Buster 68 Verde"; case "S05": return "Night Buster 5 Viola"; case "ST1": return "Star 1"; case "ST2": return "Star 2"; case "4N": return "4 Navetta"; case "10N": return "10 Navetta"; case "13N": return "13 Navetta"; case "35N": return "35 Navetta"; case "36N": return "36 Navetta"; case "36S": return "36 Speciale"; case "38S": return "38 Speciale"; case "44S": return "44 Scolastico"; case "46N": return "46 Navetta"; default: return null; } } public static String routeDisplayToInternal(String displayName){ String name = displayName.trim(); if(name.charAt(displayName.length()-1)=='/'){ return displayName.replace(" ","").replace("/","B"); } switch (name.toLowerCase()){ //DEFAULT CASES case "star 1": return "ST1"; case "star 2": return "ST2"; case "night buster 1 arancio": return "W01"; case "night buster 10 gialla": return "N10"; case "night buster 15 rosa": return "W15"; case "night buster 18 blu": return "S18"; case "night buster 4 azzurra": return "S04"; case "night buster 4 rossa": return "N4"; case "night buster 57 oro": return "N57"; case "night buster 60 argento": return "W60"; case "night buster 68 verde": return "E68"; case "night buster 5 viola": return "S05"; case "1 nichelino": return "1N"; case "1 chieri": return "1C"; case "1 orbassano": return "OB1"; case "2 chieri": return "2C"; case "2 rivalta": return "RV2"; default: // return displayName.trim(); } String[] arr = name.toLowerCase().split("\\s+"); try { if (arr.length == 2 && arr[1].trim().equals("navetta") && Integer.decode(arr[0]) > 0) return arr[0].trim().concat("N"); } catch (NumberFormatException e){ //It's not "# navetta" Log.w("FivetNorm","checking number when it's not"); } if(name.toLowerCase().contains("night buster")){ if(name.toLowerCase().contains("viola")) return "S05"; + else if(name.toLowerCase().contains("verde")) + return "E68"; } //Everything failed, let's at least compact the the (probable) code return name.replace(" ",""); } } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 7da3540..599b866 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,303 +1,305 @@ /* 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<>(); public Palina(String stopID) { super(stopID); } /** * 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))); 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); return this.routes.size()-1; } /** * 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, int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT); } /** * 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; } // /** // * 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(); // } /** * 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.name.equals(additionalRoutes.get(j).name)) { 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.name); 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/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java index 89711a0..1c91b7a 100644 --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -1,286 +1,288 @@ /* 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.location.Location; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.net.URLEncoder; +import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; - public final @Nullable Route.Type type; + public @Nullable Route.Type type; private @Nullable List routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * Constructor that sets EVERYTHING. */ public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon) { this.ID = ID; this.name = name; this.username = userName; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } private @Nullable String buildString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); + Collections.sort(routesThatStopHere); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
* * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.
* Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } public Double getDistanceFromLocation(Location loc){ if(this.lat!=null && this.lon !=null) return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude()); else return Double.POSITIVE_INFINITY; } } diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java new file mode 100644 index 0000000..b9026fd --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -0,0 +1,154 @@ +/* + 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.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 int loaderFavId = 2; + private final static int loaderStopId = 1; + private @Nullable String stopID,stopName; + private TextView messageTextView; + + 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); + } + @Override + public void onResume() { + super.onResume(); + LoaderManager loaderManager = getLoaderManager(); + if(stopID!=null){ + //start the loader + loaderManager.restartLoader(loaderFavId,getArguments(),this); + updateMessage(); + } + } + + 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.getAlmostFinishedBuilder(); + 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"); + } + } + + } + + @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 c03bea8..8924293 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,228 +1,200 @@ /* 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.SQLiteDatabase; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; -import android.support.v4.content.ContentResolverCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import it.reyboz.bustorino.R; 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.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; - UserDB userDB; - StopsDB stopsDB; - private NextGenDB newDB; + + private NextGenDB newDBHelper; 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; - stopsDB = new StopsDB(act); - userDB = new UserDB(act); - newDB = new NextGenDB(act.getApplicationContext()); + newDBHelper = new NextGenDB(act.getApplicationContext()); } public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } /** * 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 refreshing; - ResultListFragment listFragment; + ArrivalsFragment arrivalsFragment; if(act==null) { //SOMETHING WENT VERY WRONG return; } SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID); FragmentManager fm = act.getSupportFragmentManager(); if(srl.isRefreshing()) refreshing=true; - else if(fm.findFragmentById(R.id.resultFrame) instanceof ResultListFragment) { - listFragment = (ResultListFragment) fm.findFragmentById(R.id.resultFrame); - refreshing = listFragment.isFragmentForTheSameStop(p); + else if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { + arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); + refreshing = arrivalsFragment.isFragmentForTheSameStop(p); } else refreshing = false; setLastSuccessfullySearchedBusStop(p); if(!refreshing) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); String displayStuff; if (displayName != null && displayName.length() > 0) { - displayStuff = p.ID.concat(" - ").concat(displayName); + arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { - displayStuff = p.ID; + arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } - listFragment = ResultListFragment.newInstance(ResultListFragment.TYPE_LINES,displayStuff); - attachFragmentToContainer(fm,listFragment,true,ResultListFragment.getFragmentTag(p)); + attachFragmentToContainer(fm,arrivalsFragment,true,ResultListFragment.getFragmentTag(p)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); - listFragment = (ResultListFragment) fm.findFragmentById(R.id.resultFrame); + arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); } - listFragment.setListAdapter(new PalinaAdapter(act.getApplicationContext(),p)); + arrivalsFragment.setListAdapter(new PalinaAdapter(act.getApplicationContext(),p)); act.hideKeyboard(); - if (act instanceof FragmentListener) ((FragmentListener) act).readyGUIfor(ResultListFragment.TYPE_LINES); - 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(); - ResultListFragment listfragment = ResultListFragment.newInstance(ResultListFragment.TYPE_STOPS); + StopListFragment listfragment = StopListFragment.newInstance(query); attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query); - if (act instanceof FragmentListener) ((FragmentListener) act).readyGUIfor(ResultListFragment.TYPE_STOPS); - listfragment.setListAdapter(new StopAdapter(act.getApplicationContext(), resultList)); + 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(); } - /* - Set of methods to "wrap" database operations - */ - //Find a way to open databases - public void openStopsDB(){ - stopsDB.openIfNeeded(); - } - public String getLocationFromDB(Stop p){ - return stopsDB.getLocationFromID(p.ID); - } - public List getStopRoutesFromDB(String stopID){ - return stopsDB.getRoutesByStop(stopID); - } - public void closeDBIfNeeded(){ - stopsDB.closeIfNeeded(); - } - public String getStopNamefromDB(String stopID){ - return stopsDB.getNameFromID(stopID); - } synchronized public int insertBatchDataInNextGenDB(ContentValues[] valuesArr,String tableName){ - if(newDB!=null) - return newDB.insertBatchContent(valuesArr,tableName); + if(newDBHelper !=null) + return newDBHelper.insertBatchContent(valuesArr,tableName); else return -1; } synchronized public ContentResolver getContentResolver(){ return act.getContentResolver(); } /** * 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/fragments/FragmentListener.java b/src/it/reyboz/bustorino/fragments/FragmentListener.java index 3b8b955..8830d71 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentListener.java +++ b/src/it/reyboz/bustorino/fragments/FragmentListener.java @@ -1,50 +1,50 @@ /* 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 it.reyboz.bustorino.backend.Palina; public interface FragmentListener { void toggleSpinner(boolean state); /** * Sends the message to the activity to adapt the GUI * to the fragment that has been attached * @param fragmentType the type of fragment attached */ - void readyGUIfor(String fragmentType); + void readyGUIfor(FragmentKind fragmentType); /** * Houston, we need another fragment! * * @param ID the Stop ID */ void createFragmentForStop(String ID); void addLastStopToFavorites(); /** * Tell the activity that we need to disable/enable its floatingActionButton * @param yes or no */ void showFloatingActionButton(boolean yes); /** * Tell activity that we need to enable/disable the refreshLayout * @param yes or no */ void enableRefreshLayout(boolean yes); } diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 0024af7..0932dbf 100644 --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,269 +1,269 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.database.Cursor; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.GridView; import android.widget.ProgressBar; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopSorterByDistance; import it.reyboz.bustorino.middleware.AppDataProvider; import it.reyboz.bustorino.middleware.NextGenDB.Contract.*; import it.reyboz.bustorino.middleware.SquareStopAdapter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { private FragmentListener mListener; private LocationManager locManager; private FragmentLocationListener locationListener; private final String[] PROJECTION = {StopsTable.COL_ID,StopsTable.COL_LAT,StopsTable.COL_LONG, StopsTable.COL_NAME,StopsTable.COL_TYPE,StopsTable.COL_LINES_STOPPING}; private final static String DEBUG_TAG = "NearbyStopsFragment"; //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private GridView gV; private SquareStopAdapter madapter; boolean canStartUpdate = true; private Location lastReceivedLocation = null; private ProgressBar loadingProgressBar; private int distance; public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ - // TODO: Rename and change types and number of parameters public static NearbyStopsFragment newInstance() { NearbyStopsFragment fragment = new NearbyStopsFragment(); Bundle args = new Bundle(); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { /* mParam1 = getArguments().getString(ARG_PARAM1); mParam2 = getArguments().getString(ARG_PARAM2); */ } locManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); locationListener = new FragmentLocationListener(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gV = (GridView) root.findViewById(R.id.stopGridNearby); loadingProgressBar = (ProgressBar) root.findViewById(R.id.loadingBar); return root; } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof FragmentListener) { mListener = (FragmentListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onPause() { super.onPause(); canStartUpdate = false; locManager.removeUpdates(locationListener); gV.setAdapter(null); Log.d(DEBUG_TAG,"On paused called"); } @Override public void onResume() { super.onResume(); canStartUpdate = true; try{ locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,2000,10,locationListener); } catch (SecurityException ex){ //ignored //try another location provider } if(madapter!=null){ gV.setAdapter(madapter); loadingProgressBar.setVisibility(View.GONE); } Log.d(DEBUG_TAG,"OnResume called"); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); gV.setVisibility(View.INVISIBLE); gV.setOnScrollListener(new CommonScrollListener(mListener,false)); } @Override public void onDetach() { super.onDetach(); mListener = null; } @Override public Loader onCreateLoader(int id, Bundle args) { //BUILD URI lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) .appendPath(String.valueOf(distance)); //distance CursorLoader cl = new CursorLoader(getContext(),builder.build(),PROJECTION,null,null,null); cl.setUpdateThrottle(1000); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { ArrayList stopList = new ArrayList<>(); data.moveToFirst(); if(data.getCount()<4){ distance = distance*2; Bundle d = new Bundle(); d.putParcelable(BUNDLE_LOCATION,lastReceivedLocation); getLoaderManager().restartLoader(LOADER_ID,d,this); return; } Log.d("LoadFromCursor","Number of nearby stops: "+data.getCount()); final int col_id = data.getColumnIndex(StopsTable.COL_ID); final int latInd = data.getColumnIndex(StopsTable.COL_LAT); final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); for(int i=0; i loader) { } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements LocationListener{ LoaderManager.LoaderCallbacks callbacks; public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { this.callbacks = callbacks; } @Override public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); if(accuracy<60 && canStartUpdate) { distance = 100; final Bundle setting = new Bundle(); setting.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,setting,callbacks); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } } } diff --git a/src/it/reyboz/bustorino/fragments/ResultListFragment.java b/src/it/reyboz/bustorino/fragments/ResultListFragment.java index 8d02fdb..b86cf87 100644 --- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -1,370 +1,291 @@ /* 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; -import it.reyboz.bustorino.middleware.AsyncAddToFavorites; -import it.reyboz.bustorino.middleware.PalinaAdapter; /** * This is a generalized fragment that can be used both for * * */ -public class ResultListFragment extends Fragment { +public class ResultListFragment extends Fragment{ // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String LIST_TYPE = "list-type"; + static final String LIST_TYPE = "list-type"; private static final String STOP_TITLE = "messageExtra"; - private static final String LIST_STATE = "list_state"; + protected static final String LIST_STATE = "list_state"; - public static final String TYPE_LINES ="lines"; - public static final String TYPE_STOPS = "fermate"; - private static final String MESSAGE_TEXT_VIEW ="message_text_view"; - private String adapterType; + private static final String MESSAGE_TEXT_VIEW = "message_text_view"; + private FragmentKind adapterKind; private boolean adapterSet = false; private FragmentListener mListener; private TextView messageTextView; - FloatingActionButton fabutton; + 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(String listType,String eventualStopTitle) { + public static ResultListFragment newInstance(FragmentKind listType, String eventualStopTitle) { ResultListFragment fragment = new ResultListFragment(); Bundle args = new Bundle(); - args.putString(LIST_TYPE, listType); - if(eventualStopTitle!=null) - args.putString(STOP_TITLE,eventualStopTitle); + args.putSerializable(LIST_TYPE, listType); + if (eventualStopTitle != null) + args.putString(STOP_TITLE, eventualStopTitle); fragment.setArguments(args); return fragment; } - public static ResultListFragment newInstance(String listType){ - return newInstance(listType,null); + + public static ResultListFragment newInstance(FragmentKind listType) { + return newInstance(listType, null); } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { - adapterType = getArguments().getString(LIST_TYPE); + adapterKind = (FragmentKind) getArguments().getSerializable(LIST_TYPE); } } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - hideMyFAB mInterface = new hideMyFAB() { - @Override - public void showFloatingActionButton(boolean show) { - if (fabutton!=null){ - if(show) fabutton.show(); - else fabutton.hide(); - } - } - }; View root = inflater.inflate(R.layout.fragment_list_view, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); - if(adapterType!=null) { + if (adapterKind != null) { resultsListView = (ListView) root.findViewById(R.id.resultsListView); - if(adapterType.equals(TYPE_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); - } - }); - resultsListView.setOnScrollListener(new ListFragmentScrollListener(false,mInterface)); - - //set the textviewMessage - setTextViewMessage(getString(R.string.results)); - } - else if(adapterType.equals(TYPE_LINES)) { - 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.name); - if (routeName == null) { - routeName = r.name; + 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); } - 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(); + }); + + //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.name); + if (routeName == null) { + routeName = r.name; + } + 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)); - resultsListView.setOnScrollListener(new ListFragmentScrollListener(true,mInterface)); + }); + 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) { + + 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 adapterType.equals(TYPE_LINES) && getTag().equals(getFragmentTag(p)); + + public boolean isFragmentForTheSameStop(Palina p) { + return adapterKind.equals(FragmentKind.ARRIVALS) && getTag().equals(getFragmentTag(p)); } - public static String getFragmentTag(Palina 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){ + if (mListAdapter != null) { ListAdapter adapter = mListAdapter; mListAdapter = null; setListAdapter(adapter); } - if(mListInstanceState!=null){ - Log.d("resultsListView","trying to restore instance state"); + if (mListInstanceState != null) { + Log.d("resultsListView", "trying to restore instance state"); resultsListView.onRestoreInstanceState(mListInstanceState); } - mListener.readyGUIfor(adapterType); + switch (adapterKind) { + case ARRIVALS: + resultsListView.setOnScrollListener(new CommonScrollListener(mListener, true)); + break; + case STOPS: + resultsListView.setOnScrollListener(new CommonScrollListener(mListener, false)); + break; + default: + //NONE + } + mListener.readyGUIfor(adapterKind); } @Override public void onPause() { - if(adapterType.equals(TYPE_LINES)) { + 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) + 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 onSaveInstanceState(Bundle outState) { - outState.putParcelable(LIST_STATE,resultsListView.onSaveInstanceState()); - Log.d("ResultListFragment","onSaveInstanceState"); - super.onSaveInstanceState(outState); - } @Override public void onViewStateRestored(@Nullable Bundle savedInstanceState) { super.onViewStateRestored(savedInstanceState); - Log.d("ResultListFragment","onViewStateRestored"); - if(savedInstanceState!=null){ + Log.d("ResultListFragment", "onViewStateRestored"); + if (savedInstanceState != null) { mListInstanceState = savedInstanceState.getParcelable(LIST_STATE); - Log.d("ResultListFragment","listInstanceStatePresent :"+mListInstanceState); + 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); } } /** - * This method attaches the FloatingActionButton to the current ListView + * Set the message textView + * @param message the whole message to write in the textView */ - /* TODO REWORKING - public void attachFABToListView(){ - if(resultsListView!=null) - switch(adapterType){ - case TYPE_LINES: - fabutton.attachToListView(resultsListView, null, new ListFragmentScrollListener()); - break; - default: - fabutton.attachToListView(resultsListView); - break; - } - } - */ - public void setTextViewMessage(String message){ + public void setTextViewMessage(String message) { messageTextView.setText(message); - switch (adapterType){ - case TYPE_LINES: + 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 TYPE_STOPS: + case STOPS: messageTextView.setClickable(false); break; } messageTextView.setVisibility(View.VISIBLE); } - - /** - * This methods sets both the adapter and the textviewMessage - * Do not call when the fragment shows a list of stops - * @param p the palina to get the info from - * TODO: overload it so that it can be used also by the STOPS - */ - - - - - /** - * Classe per gestire gli scroll - * - */ - class ListFragmentScrollListener implements AbsListView.OnScrollListener{ - boolean enableRefreshLayout,lastScrollUp=false; - int mLastFirstVisibleItem; - hideMyFAB fabInterface; - public ListFragmentScrollListener(boolean enableRefreshLayout, hideMyFAB iface){ - this.enableRefreshLayout = enableRefreshLayout; - this.fabInterface = iface; - } - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - /* - * This seems to be a totally useless method - */ - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - if (firstVisibleItem!=0) { - if (mLastFirstVisibleItem < firstVisibleItem) { - Log.i("Busto", "Scrolling DOWN"); - fabInterface.showFloatingActionButton(false); - //lastScrollUp = true; - } else if (mLastFirstVisibleItem > firstVisibleItem) { - Log.i("Busto", "Scrolling UP"); - fabInterface.showFloatingActionButton(true); - //lastScrollUp = false; - } - mLastFirstVisibleItem = firstVisibleItem; - } - if(enableRefreshLayout){ - SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) getActivity().findViewById(R.id.listRefreshLayout); - boolean enable = false; - if(view != null && view.getChildCount() > 0){ - // check if the first item of the list is visible - boolean firstItemVisible = view.getFirstVisiblePosition() == 0; - // check if the top of the first item is visible - boolean topOfFirstItemVisible = view.getChildAt(0).getTop() == 0; - // enabling or disabling the refresh layout - enable = firstItemVisible && topOfFirstItemVisible; - } - refreshLayout.setEnabled(enable); - //Log.d(getString(R.string.list_fragment_debug),"onScroll active, first item visible: "+firstVisibleItem+", refreshlayout enabled: "+enable); - }} - } - -} -interface hideMyFAB { - void showFloatingActionButton(boolean show); } diff --git a/src/it/reyboz/bustorino/fragments/StopListFragment.java b/src/it/reyboz/bustorino/fragments/StopListFragment.java new file mode 100644 index 0000000..a6f7e59 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/StopListFragment.java @@ -0,0 +1,145 @@ +/* + 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.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.Log; +import it.reyboz.bustorino.backend.Route; +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.middleware.AppDataProvider; +import it.reyboz.bustorino.middleware.NextGenDB.Contract.StopsTable; +import it.reyboz.bustorino.middleware.StopAdapter; + +import java.util.Arrays; +import java.util.List; + +public class StopListFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks { + + private List stopList; + private StopAdapter mListAdapter; + private static final String[] dataProjection={StopsTable.COL_LINES_STOPPING,StopsTable.COL_PLACE,StopsTable.COL_TYPE,StopsTable.COL_LOCATION}; + private static final String KEY_STOP_ID = "stopID"; + private static final String WORDS_SEARCHED= "query"; + private static final int EXTRA_ID=160; + + private String searchedWords; + public StopListFragment(){ + //required empty constructor + } + + public static StopListFragment newInstance(String searchQuery) { + + Bundle args = new Bundle(); + //TODO: search stops inside the DB + args.putString(WORDS_SEARCHED,searchQuery); + StopListFragment fragment = new StopListFragment(); + args.putSerializable(LIST_TYPE,FragmentKind.STOPS); + fragment.setArguments(args); + return fragment; + } + + public void setStopList(List stopList){ + this.stopList = stopList; + + } + + + @Override + public void onResume() { + super.onResume(); + LoaderManager loaderManager = getLoaderManager(); + if(stopList!=null) { + mListAdapter = new StopAdapter(getContext(),stopList); + setListAdapter(mListAdapter); + for (int i = 0; i < stopList.size(); i++) { + final Bundle b = new Bundle(); + b.putString(KEY_STOP_ID, stopList.get(i).ID); + loaderManager.restartLoader(i, b, this); + } + + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + searchedWords = getArguments().getString(WORDS_SEARCHED); + + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + //The id will be the position of the element in the list + + Uri.Builder builder = new Uri.Builder(); + String stopID = args.getString(KEY_STOP_ID); + //Log.d("StopListLoader","Creating loader for stop "+stopID+" in position: "+id); + if(stopID!=null) { + builder.scheme("content").authority(AppDataProvider.AUTHORITY) + .appendPath("stop").appendPath(stopID); + CursorLoader cursorLoader = new CursorLoader(getContext(),builder.build(),dataProjection,null,null,null); + return cursorLoader; + } else return null; + + } + + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + //check that we have valid data + if(data==null) return; + final int numRows = data.getCount(); + final int elementIdx = loader.getId(); + + if (numRows==0) { + Log.w(this.getClass().getName(),"No info for stop in position "+elementIdx); + return; + } else if(numRows>1){ + Log.d("StopLoading","we have "+numRows+" rows, should only have 1. Taking the first..."); + } + final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); + data.moveToFirst(); + Stop stopToModify = stopList.get(elementIdx); + final String linesStopping = data.getString(linesIndex); + stopToModify.setRoutesThatStopHere(Arrays.asList(linesStopping.split(","))); + try { + final String possibleLocation = data.getString(data.getColumnIndexOrThrow(StopsTable.COL_LOCATION)); + + if (stopToModify.location == null && possibleLocation != null && !possibleLocation.isEmpty() && !possibleLocation.equals("_")) { + stopToModify.location = possibleLocation; + } + if (stopToModify.type == null) { + stopToModify.type = Route.Type.fromCode(data.getInt(data.getColumnIndex(StopsTable.COL_TYPE))); + } + }catch (IllegalArgumentException arg){ + if(arg.getMessage().contains("'location' does not exist")) Log.w("StopLoading","stop with no location found"); + } + //Log.d("StopListFragmentLoader","Finished parsing data for stop in position "+elementIdx); + mListAdapter.notifyDataSetChanged(); + } + + @Override + public void onLoaderReset(Loader loader) { + loader.abandon(); + } +} diff --git a/src/it/reyboz/bustorino/middleware/AppDataProvider.java b/src/it/reyboz/bustorino/middleware/AppDataProvider.java index 878c5c3..6b85724 100644 --- a/src/it/reyboz/bustorino/middleware/AppDataProvider.java +++ b/src/it/reyboz/bustorino/middleware/AppDataProvider.java @@ -1,239 +1,253 @@ /* 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.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.Log; import it.reyboz.bustorino.middleware.NextGenDB.Contract.*; import java.util.List; public class AppDataProvider extends ContentProvider { public static final String AUTHORITY = "it.reyboz.bustorino.provider"; private static final int STOP_OP = 1; private static final int LINE_OP = 2; private static final int BRANCH_OP = 3; private static final int FAVORITES_OP =4; private static final int MANY_STOPS = 5; private static final int ADD_UPDATE_BRANCHES = 6; private static final int LINE_INSERT_OP = 7; private static final int CONNECTIONS = 8; private static final int LOCATION_SEARCH = 9; private static final String DEBUG_TAG="AppDataProvider"; private NextGenDB appDBHelper; + private UserDB udbhelper; private SQLiteDatabase db; public AppDataProvider() { } private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. */ sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP); sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS); sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP); sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP); sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP); sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES); sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS); - sUriMatcher.addURI(AUTHORITY,"favorites",FAVORITES_OP); + sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Implement this to handle requests to delete one or more rows. db = appDBHelper.getWritableDatabase(); int rows; switch (sUriMatcher.match(uri)){ case MANY_STOPS: rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null); break; default: throw new UnsupportedOperationException("Not yet implemented"); } return rows; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. int match = sUriMatcher.match(uri); String baseTypedir = "vnd.android.cursor.dir/"; String baseTypeitem = "vnd.android.cursor.item/"; switch (match){ case LOCATION_SEARCH: return baseTypedir+"stop"; case LINE_OP: return baseTypeitem+"line"; case CONNECTIONS: return baseTypedir+"stops"; } return baseTypedir+"/item"; } @Override public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{ db = appDBHelper.getWritableDatabase(); Uri finalUri = null; long last_rowid = -1; switch (sUriMatcher.match(uri)){ case ADD_UPDATE_BRANCHES: Log.d("InsBranchWithProvider","new Insert request"); String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME); if(line_name==null) throw new IllegalArgumentException("No line name given"); long lineid = -1; Cursor c = db.query(LinesTable.TABLE_NAME, new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?", new String[]{line_name},null,null,null); Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches"); if(c.getCount() == 0){ //There are no lines, insert? //NOPE /* c.close(); ContentValues cv = new ContentValues(); cv.put(LinesTable.COLUMN_NAME,line_name); lineid = db.insert(LinesTable.TABLE_NAME,null,cv); */ break; }else { c.moveToFirst(); /* while(c.moveToNext()){ Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n" +c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION))); }*/ lineid = c.getInt(c.getColumnIndex(NextGenDB.Contract.LinesTable._ID)); c.close(); } values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME); values.put(BranchesTable.COL_LINE,lineid); last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE); break; case MANY_STOPS: //Log.d("AppDataProvider_busTO","New stop insert request"); try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; case CONNECTIONS: try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; default: throw new IllegalArgumentException("Invalid parameters"); } finalUri = ContentUris.withAppendedId(uri,last_rowid); return finalUri; } @Override public boolean onCreate() { - // TODO: Implement this to initialize your content provider on startup. appDBHelper = new NextGenDB(getContext()); + udbhelper = new UserDB(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException{ // TODO: Implement this to handle query requests from clients. SQLiteDatabase db = appDBHelper.getReadableDatabase(); List parts = uri.getPathSegments(); switch (sUriMatcher.match(uri)){ case LOCATION_SEARCH: //authority/stops/location/"Lat"/"Lon"/"distance" //distance in metres (integer) if(parts.size()>=4 && "location".equals(parts.get(1))){ Double latitude = Double.parseDouble(parts.get(2)); Double longitude = Double.parseDouble(parts.get(3)); //converting distance to a float to not lose precision Float distance = parts.size()>=5 ? Float.parseFloat(parts.get(4))/1000 : 0.1f; if(parts.size()>=5) Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m"); Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres String whereClause = StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND " +StopsTable.COL_LAT +" > "+(latitude-distasAngle)+" AND "+ StopsTable.COL_LONG+" < "+(longitude+distasAngle)+" AND "+StopsTable.COL_LONG+" > "+(longitude-distasAngle); Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause); return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null); //return getStopsNearby(latitude,longitude,distance); } else { Log.w(DEBUG_TAG,"Not enough parameters"); if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s); return null; } case FAVORITES_OP: - - break; + final String stopFavSelection = UserDB.getFavoritesColumnNamesAsArray[0]+" = ?"; + db = udbhelper.getReadableDatabase(); + Log.d(DEBUG_TAG,"Asked information on Favorites about stop with id "+uri.getLastPathSegment()); + return db.query(UserDB.TABLE_NAME,projection,stopFavSelection,new String[]{uri.getLastPathSegment()},null,null,sortOrder); + case STOP_OP: + //Let's try this plain and simple + final String[] selectionValues = {uri.getLastPathSegment()}; + final String stopSelection = StopsTable.COL_ID+" = ?"; + Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]); + return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder); default: Log.d("DataProvider","got request "+uri.getPath()+" which doesn't match anything"); } throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } // public static Uri getBaseUriGivenOp(int operationType); + public static Uri.Builder getAlmostFinishedBuilder(){ + final Uri.Builder b = new Uri.Builder(); + b.scheme("content").authority(AUTHORITY); + return b; + } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index 4c725f6..d0fc3b0 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,369 +1,344 @@ /* 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.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; import android.support.v7.app.AppCompatActivity; 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 String query; WeakReference helperRef; private ArrayList otherActivities = new ArrayList<>(); public AsyncDataDownload(RequestType type,FragmentHelper fh) { t = type; helperRef = new WeakReference<>(fh); res = new AtomicReference<>(); } @Override protected Object doInBackground(String... params) { RecursionHelper r; 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; } 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 } - //TODO: use ContentProvider when ready if(lastSearchedBusStop != null && res.get()== Fetcher.result.OK) { // check that we don't have the same stop - if(!lastSearchedBusStop.ID.equals(p.ID)) { - // remove it, get new name - //getNameOrGetRekt(); - //TODO - } else { + if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); - if(sn == null) { - // something really bad happened, start from scratch - //getNameOrGetRekt(); - //TODO - } else { + if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } - } else if(res.get()== Fetcher.result.OK) { - // we haven't searched anything yet - //getNameOrGetRekt(); - } - - //Try to find the name of the stop inside StopsDB - - if(p.getStopDisplayName() == null){ - fh.openStopsDB(); - p.setStopName(fh.getStopNamefromDB(p.ID)); - fh.closeDBIfNeeded(); } 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 - fh.openStopsDB(); - for (Stop stop : resultList){ - if(stop.location == null) stop.location = fh.getLocationFromDB(stop); - stop.setRoutesThatStopHere(fh.getStopRoutesFromDB(stop.ID)); - } - fh.closeDBIfNeeded(); 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; } 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) { 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 } /** * Run this in a background thread.
* Sets a stop name for this.palina, guaranteed not to be null! **/ //TODO:Implement this /* private void getNameOrGetRekt(Palina p) { String nameMaybe; SQLiteDatabase udb = uDB.getReadableDatabase(); // does it already have a name (for fetchers that support it, or already got from favorites)? nameMaybe = p.getStopDisplayName(); if(nameMaybe != null && nameMaybe.length() > 0) { return; } // ok, let's search favorites. String usernameMaybe = UserDB.getStopUserName(udb, this.p.ID); if(usernameMaybe != null && usernameMaybe.length() > 0) { p.setStopUserName(usernameMaybe); return; } // let's try StopsDB, then. db.openIfNeeded(); nameMaybe = db.getNameFromID(this.p.ID); db.closeIfNeeded(); if(nameMaybe != null && nameMaybe.length() > 0) { p.setStopName(nameMaybe); return; } // no name to be found anywhere, don't bother searching it next time p.setStopName(""); }*/ public class BranchInserter implements Runnable{ private List routesToInsert; private String stopID; private 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.name); 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. */ package it.reyboz.bustorino.middleware; import android.content.Context; import android.support.annotation.NonNull; 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.Stop; /** * @see PalinaAdapter */ public class StopAdapter extends ArrayAdapter { private LayoutInflater li; private static int row_layout = R.layout.entry_bus_stop; 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; + private static class ViewHolder { TextView busStopIDTextView; TextView busStopNameTextView; //TextView busLineVehicleIcon; TextView busStopLinesTextView; TextView busStopLocaLityTextView; } public StopAdapter(Context context, List stops) { super(context, row_layout, stops); li = LayoutInflater.from(context); } @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { ViewHolder vh; if(convertView == null) { convertView = li.inflate(row_layout, null); vh = new ViewHolder(); vh.busStopIDTextView = (TextView) convertView.findViewById(R.id.busStopID); vh.busStopNameTextView = (TextView) convertView.findViewById(R.id.busStopName); vh.busStopLinesTextView = (TextView) convertView.findViewById(R.id.routesThatStopHere); vh.busStopLocaLityTextView = (TextView) convertView.findViewById(R.id.busStopLocality); convertView.setTag(vh); } else { vh = (ViewHolder) convertView.getTag(); } Stop stop = getItem(position); vh.busStopIDTextView.setText(stop.ID); // NOTE: intentionally ignoring stop username in search results: if it's in the favorites, why are you searching for it? vh.busStopNameTextView.setText(stop.getStopDisplayName()); - String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { vh.busStopLinesTextView.setVisibility(View.GONE); } else { vh.busStopLinesTextView.setText(whatStopsHere); vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } if(stop.type == null) { vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); } else { switch(stop.type) { case BUS: default: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: case RAILWAY: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: // è l'opposto della città ma va beh, dettagli. vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0); } } if (stop.location == null) { vh.busStopLocaLityTextView.setVisibility(View.GONE); } else { vh.busStopLocaLityTextView.setText(stop.location); vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } return convertView; } } diff --git a/src/it/reyboz/bustorino/middleware/UserDB.java b/src/it/reyboz/bustorino/middleware/UserDB.java index eb9b87f..8874f91 100644 --- a/src/it/reyboz/bustorino/middleware/UserDB.java +++ b/src/it/reyboz/bustorino/middleware/UserDB.java @@ -1,248 +1,248 @@ /* 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.middleware; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.content.Context; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsDBInterface; public class UserDB extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "user.db"; - private static final String TABLE_NAME = "favorites"; + static final String TABLE_NAME = "favorites"; private final Context c; // needed during upgrade - private static String[] usernameColumnNameAsArray = {"username"}; - private static String[] getFavoritesColumnNamesAsArray = {"ID", "username"}; + private final static String[] usernameColumnNameAsArray = {"username"}; + public final static String[] getFavoritesColumnNamesAsArray = {"ID", "username"}; public UserDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.c = context; } @Override public void onCreate(SQLiteDatabase db) { // exception intentionally left unhandled db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)"); if(OldDB.doesItExist(this.c)) { upgradeFromOldDatabase(db); } } private void upgradeFromOldDatabase(SQLiteDatabase newdb) { OldDB old; try { old = new OldDB(this.c); } catch(IllegalStateException e) { // can't create database => it doesn't really exist, no matter what doesItExist() says return; } int ver = old.getOldVersion(); /* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app * has crashed midway through the upgrade and the user is retrying, that should never show * up here. And if it does, try to recover favorites anyway. * Versions < 8 already got dropped during the update process, so let's do the same. * * Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it * more complicated. Workaround added in OldDB. */ if(ver >= 8) { ArrayList ID = new ArrayList<>(); ArrayList username = new ArrayList<>(); int len; int len2; try { Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {}); int zero = c.getColumnIndex("busstop_ID"); int one = c.getColumnIndex("busstop_username"); while(c.moveToNext()) { try { ID.add(c.getString(zero)); } catch(Exception e) { // no ID = can't add this continue; } if(c.getString(one) == null || c.getString(one).length() <= 0) { username.add(null); } else { username.add(c.getString(one)); } } c.close(); old.close(); } catch(Exception ignored) { // there's no hope, go ahead and nuke old database. } len = ID.size(); len2 = username.size(); if(len2 < len) { len = len2; } if (len > 0) { try { Stop stopStopStopStopStop; for (int i = 0; i < len; i++) { stopStopStopStopStop = new Stop(ID.get(i)); stopStopStopStopStop.setStopUserName(username.get(i)); addOrUpdateStop(stopStopStopStopStop, newdb); } } catch(Exception ignored) { // partial data is better than no data at all, no transactions here } } } if(!OldDB.destroy(this.c)) { // TODO: notify user somehow? Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user."); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } /** * Gets stop name set by the user. * * @param db readable database * @param stopID stop ID * @return name set by user, or null if not set\not found */ public static String getStopUserName(SQLiteDatabase db, String stopID) { String username = null; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null); if(c.moveToNext()) { username = c.getString(c.getColumnIndex("username")); } c.close(); } catch(SQLiteException ignored) {} return username; } public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) { List l = new ArrayList<>(); Stop s; String stopID, stopUserName; try { Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null); int colID = c.getColumnIndex("ID"); int colUser = c.getColumnIndex("username"); while(c.moveToNext()) { stopUserName = c.getString(colUser); stopID = c.getString(colID); s = dbi.getAllFromID(stopID); if(s == null) { // can't find it in database l.add(new Stop(stopUserName, stopID, null, null, null)); } else { // setStopName() already does sanity checks s.setStopUserName(stopUserName); l.add(s); } } c.close(); } catch(SQLiteException ignored) {} // comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database Collections.sort(l); return l; } public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { ContentValues cv = new ContentValues(); long result = -1; String un = s.getStopUserName(); cv.put("ID", s.ID); // is there an username? if(un == null) { // no: see if it's in the database cv.put("username", getStopUserName(db, s.ID)); } else { // yes: use it cv.put("username", un); } try { result = db.insert(TABLE_NAME, null, cv); } catch (SQLiteException ignored) {} // Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return return (result != -1) || updateStop(s, db); } public static boolean updateStop(Stop s, SQLiteDatabase db) { try { ContentValues cv = new ContentValues(); cv.put("username", s.getStopUserName()); db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean deleteStop(Stop s, SQLiteDatabase db) { try { db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } }