diff --git a/build.gradle b/build.gradle index b70762a..c9731ec 100644 --- a/build.gradle +++ b/build.gradle @@ -1,60 +1,58 @@ buildscript { repositories { mavenCentral() - jcenter() + jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' } } apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion '26.0.1' compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } - buildTypes{ - debug{ + buildTypes { + debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { mavenCentral() mavenLocal() } dependencies { compile 'com.android.support:support-v4:25.3.1' compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:design:25.3.1' compile 'org.jsoup:jsoup:1.7.3' compile 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' } } -dependencies { -} diff --git a/res/layout/fragment_nearby_stops.xml b/res/layout/fragment_nearby_stops.xml new file mode 100644 index 0000000..3119b0c --- /dev/null +++ b/res/layout/fragment_nearby_stops.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/res/layout/square_stop_element.xml b/res/layout/square_stop_element.xml new file mode 100644 index 0000000..fd2be01 --- /dev/null +++ b/res/layout/square_stop_element.xml @@ -0,0 +1,39 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f55a45a..2386fcd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,91 +1,93 @@ BusTO You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Lines: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About More about News releases Bug submission Source code Licence Meet the author Bus stop is now in your favorites Favorites Favorites Press on a bus stop name to populate this list Delete Rename Rename the bus stop Reset About Tap the bus stop name to add it to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable GOT IT! Welcome!

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

Why use this app?

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

How does it work?

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

Who worked on BusTO:
- Valerio Bozzolan main developer.
- Marco Gagino main contributor and icon creator.
- Paolo Alloatti original concept of the icon.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

The app and the related source code are released by Valerio Bozzolan under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.

Notes

This app has been developed hoping to be useful to everyone but without ANY warranty.

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

Get involved! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map ListFragment - BusTO it.reyboz.bustorino.preferences + + Nearby stops
diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index c6b3840..aecdb02 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,594 +1,607 @@ /* 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.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.middleware.*; -public class ActivityMain extends GeneralActivity implements ResultListFragment.ResultFragmentListener { +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; private GPSLocationAdapter locationHandler; ///////////////////////////////// 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); 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()); if(getOption(LOCATION_PERMISSION_GIVEN,false)==false){ assertLocationPermissions(); } - locationHandler = new GPSLocationAdapter(getApplicationContext()); - LocationManager locationManager = (LocationManager) - getSystemService(Context.LOCATION_SERVICE); - try { - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, locationHandler); - } catch (SecurityException ec){ - //ignored - Log.w("Busto","Position request failed even though user accepted permission: "+ec.getMessage()); - } + //locationHandler = new GPSLocationAdapter(getApplicationContext()); + + 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 - + NearbyStopsFragment fr = NearbyStopsFragment.newInstance(); + FragmentTransaction ft = framan.beginTransaction(); + ft.add(R.id.resultFrame,fr,"Location"); + ft.commitNow(); + swipeRefreshLayout.setVisibility(View.VISIBLE); + swipeRefreshLayout.setEnabled(false); } } @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); } 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); locationHandler.startRequestingPosition(); } 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); } /** * 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) { hideKeyboard(); if(fragmentType==null) Log.e("ActivityMain","Problem with fragmentType"); else switch (fragmentType){ case ResultListFragment.TYPE_LINES: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case ResultListFragment.TYPE_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/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java index 99dbb01..849b3f7 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,384 +1,385 @@ /* BusTO - Backend components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.support.annotation.Nullable; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class FiveTAPIFetcher implements ArrivalsFetcher{ private static final String SECRET_KEY="759C97DC7D115966C30FD9169BB200D9"; private static final String DEBUG_NAME = "FiveTAPIFetcher"; final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab")); @Override public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { //set the date for the request as now Palina p = new Palina(stopID); //request parameters String response = performAPIRequest(QueryType.ARRIVALS,stopID,res); if(response==null) { if(res.get()==result.SERVER_ERROR_404) { Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the hack is not working anymore"); res.set(result.EMPTY_RESULT_SET); }; return p; } /* Slight problem: "longName": ==> DESCRIPTION "name": "13N", "departures": [ { "arrivalTimeInt": 1272, "time": "21:12", "rt": false }] "lineType": "URBANO" ==> URBANO can be either bus or tram or METRO */ JSONArray arr; try{ arr = new JSONArray(response); String type; Route.Type routetype; for(int i =0; i getDirectionsForStop(String stopID, AtomicReference res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); if(response == null) return null; ArrayList routes = new ArrayList<>(10); try { JSONArray lines =new JSONArray(response); for(int i=0; i 1) { String secondo = exploded[exploded.length-2]; if (secondo.contains("festivo")) { festivo = Route.FestiveInfo.FESTIVO; } else if (secondo.contains("feriale")) { festivo = Route.FestiveInfo.FERIALE; } else if(secondo.contains("lun. - ven")) { serviceDays = Route.reduced_week; } else if(secondo.contains("sab - fest.")){ serviceDays = Route.weekend; festivo = Route.FestiveInfo.FESTIVO; } else { Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+ "Couldn't find a the service days\n"+ "Description: "+secondo+","+description ); } if(exploded.length>2){ switch (exploded[exploded.length-3].trim()) { case "bus": t = Route.Type.BUS; break; case "tram": //never happened, but if it could happen you can get it t = Route.Type.TRAM; break; default: //nothing } } } else //only one piece if(description.contains("festivo")){ festivo = Route.FestiveInfo.FESTIVO; } else if(description.contains("feriale")){ festivo = Route.FestiveInfo.FERIALE; } if(lineName.trim().equals("10")|| lineName.trim().equals("15")) t= Route.Type.TRAM; if(direction.contains("-")){ // direction = direction.split("-")[1]; } Route r = new Route(lineName.trim(),direction.trim(),t,null); if(serviceDays.length>0) r.serviceDays = serviceDays; r.festivo = festivo; r.branchid = branchid; r.description = description.trim(); r.setStopsList(Arrays.asList(stops.split(","))); routes.add(r); } res.set(result.OK); } catch (JSONException e) { res.set(result.PARSER_ERROR); e.printStackTrace(); return null; } return routes; } public ArrayList getAllStopsFromGTT(AtomicReference res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); if(response==null) return null; ArrayList stopslist = null; try{ JSONObject responseJSON = new JSONObject(response); JSONArray stops = responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i getAllLinesFromGTT(AtomicReference res){ String resp = performAPIRequest(QueryType.LINES,null,res); if(resp==null) { return null; } ArrayList routes = null; try { JSONArray lines = new JSONArray(resp); routes = new ArrayList<>(lines.length()); for(int i = 0; i res){ Date d = new Date(); URL u; Hashtable param = new Hashtable<>(); try { String address = getURLForOperation(t,stopID); //Log.d(DEBUG_NAME,"The address to query is: "+address); param.put("TOKEN",getAccessToken(address,d)); param.put("TIMESTAMP",String.valueOf(d.getTime())); param.put("Accept-Encoding","gzip"); param.put("Connection","Keep-Alive"); u = new URL(address); } catch (UnsupportedEncodingException | NoSuchAlgorithmException |MalformedURLException e) { e.printStackTrace(); res.set(result.PARSER_ERROR); return null; } String response = networkTools.queryURL(u,res,param); return response; } /** * Get the Token needed to access the API * @param URL the URL of the request * @return token * @throws NoSuchAlgorithmException if the system doesn't support MD5 * @throws UnsupportedEncodingException if we made mistakes in writing utf-8 */ private static String getAccessToken(String URL,Date d) throws NoSuchAlgorithmException,UnsupportedEncodingException{ MessageDigest md = MessageDigest.getInstance("MD5"); String strippedQuery = URL.replace("http://www.5t.torino.it/proxyws",""); //return the time in milliseconds long timeMilli = d.getTime(); StringBuilder sb = new StringBuilder(); sb.append(strippedQuery); sb.append(timeMilli); sb.append(SECRET_KEY); String stringToBeHashed = sb.toString(); //Log.d(DEBUG_NAME,"Hashing string: "+stringToBeHashed); md.reset(); byte[] data = md.digest(stringToBeHashed.getBytes("UTF-8")); sb = new StringBuilder(); for (byte b : data){ sb.append(String.format("%02x",b)); } String result = sb.toString(); //Log.d(DEBUG_NAME,"getting token:\n\treduced URL: "+strippedQuery+"\n\ttimestamp: "+timeMilli+"\nTOKEN:"+result.toLowerCase()); return result.toLowerCase(); } /** * Get the right url for the operation you are doing, to be fed into the queryURL method * @param t type of operation * @param stopID stop on which you are working on * @return the Url to go to * @throws UnsupportedEncodingException if it cannot be converted to utf-8 */ private static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { final StringBuilder sb = new StringBuilder(); sb.append("http://www.5t.torino.it/proxyws/ws2.1/rest/"); if(t!=QueryType.LINES) sb.append("stops/"); switch (t){ case ARRIVALS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/departures"); break; case DETAILS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/branches/details"); break; case STOPS_ALL: sb.append("all"); break; case STOPS_VERSION: sb.append("version"); break; case LINES: sb.append("lines/all"); break; } return sb.toString(); } public enum QueryType { ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 5421703..b16c07e 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,289 +1,290 @@ /* 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 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); } public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); return routes.size(); } int count=0; for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; while (!correct) { 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); Calendar c = Calendar.getInstance(); int to = c.get(Calendar.DAY_OF_WEEK); if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check it is in service for (int d : selected.serviceDays) { if (d == to) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (to <= 7 && to > 1) correct = true; break; case FESTIVO: if (to == 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 == false || selected == null) continue; //we didn't find any match, should we point it out? //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 0749f29..89711a0 100644 --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -1,278 +1,286 @@ /* 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.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; 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(); 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/backend/StopSorterByDistance.java b/src/it/reyboz/bustorino/backend/StopSorterByDistance.java new file mode 100644 index 0000000..d6617f1 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/StopSorterByDistance.java @@ -0,0 +1,18 @@ +package it.reyboz.bustorino.backend; + +import android.location.Location; + +import java.util.Comparator; + +public class StopSorterByDistance implements Comparator { + private final Location locToCompare; + + public StopSorterByDistance(Location locToCompare) { + this.locToCompare = locToCompare; + } + + @Override + public int compare(Stop o1, Stop o2) { + return (int) (o1.getDistanceFromLocation(locToCompare)-o2.getDistanceFromLocation(locToCompare)); + } +} diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java new file mode 100644 index 0000000..171fb7e --- /dev/null +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -0,0 +1,19 @@ +package it.reyboz.bustorino.backend; + +public abstract class utils { + private static final double EarthRadius = 6371e3; + public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ + final double phi1 = Math.toRadians(lat1); + final double phi2 = Math.toRadians(lat2); + + final double deltaPhi = Math.toRadians(lat2-lat1); + final double deltaTheta = Math.toRadians(long2-long1); + + final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ + Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); + final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); + + return EarthRadius*c; + + } +} diff --git a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java new file mode 100644 index 0000000..6b60bc5 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java @@ -0,0 +1,76 @@ +/* + 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.util.Log; +import android.widget.AbsListView; + +import java.lang.ref.WeakReference; + +public class CommonScrollListener implements AbsListView.OnScrollListener{ + + WeakReference listenerWeakReference; + //enable swipeRefreshLayout when scrolling down or not + boolean enableRefreshLayout; + int lastvisibleitem; + + public CommonScrollListener(FragmentListener lis,boolean enableRefreshLayout){ + listenerWeakReference = new WeakReference<>(lis); + this.enableRefreshLayout = enableRefreshLayout; + } + @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) { + FragmentListener listener = listenerWeakReference.get(); + if(listener==null){ + //can't do anything, sorry + Log.i(this.getClass().getName(),"called onScroll but FragmentListener is null"); + return; + } + if (firstVisibleItem!=0) { + if (lastvisibleitem < firstVisibleItem) { + Log.i("Busto", "Scrolling DOWN"); + listener.showFloatingActionButton(false); + //lastScrollUp = true; + } else if (lastvisibleitem > firstVisibleItem) { + Log.i("Busto", "Scrolling UP"); + listener.showFloatingActionButton(true); + //lastScrollUp = false; + } + lastvisibleitem = firstVisibleItem; + } + if(enableRefreshLayout){ + 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; + } + listener.enableRefreshLayout(enable); + //Log.d(getString(R.string.list_fragment_debug),"onScroll active, first item visible: "+firstVisibleItem+", refreshlayout enabled: "+enable); + }} +} diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index 9fea295..c03bea8 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,228 +1,228 @@ /* 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; 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()); } 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; 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 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); } else { displayStuff = p.ID; } listFragment = ResultListFragment.newInstance(ResultListFragment.TYPE_LINES,displayStuff); attachFragmentToContainer(fm,listFragment,true,ResultListFragment.getFragmentTag(p)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); listFragment = (ResultListFragment) fm.findFragmentById(R.id.resultFrame); } listFragment.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); attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query); if (act instanceof FragmentListener) ((FragmentListener) act).readyGUIfor(ResultListFragment.TYPE_STOPS); listfragment.setListAdapter(new StopAdapter(act.getApplicationContext(), 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 */ - private void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){ + 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); 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 69ee268..3b8b955 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentListener.java +++ b/src/it/reyboz/bustorino/fragments/FragmentListener.java @@ -1,13 +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); + /** + * 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 new file mode 100644 index 0000000..1c28c62 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -0,0 +1,261 @@ +/* + 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}; + //needed for the @SimpleCursorAdapter + private final String[] bindFrom = {StopsTable.COL_ID,StopsTable.COL_NAME,StopsTable.COL_LINES_STOPPING}; + private final int[] bindTo = {R.id.busStopIDView,R.id.stopNameView,R.id.routesStoppingTextView}; + //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); + } + + @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 + } + } + + @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 570c03e..1ad5c73 100644 --- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -1,358 +1,345 @@ /* 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.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 { // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private static final String LIST_TYPE = "list-type"; private static final String STOP_TITLE = "messageExtra"; 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 boolean adapterSet = false; - private ResultFragmentListener mListener; + private FragmentListener mListener; private TextView messageTextView; FloatingActionButton fabutton; private ListView resultsListView; ListAdapter mListAdapter = null; boolean listShown; 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) { ResultListFragment fragment = new ResultListFragment(); Bundle args = new Bundle(); args.putString(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); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { adapterType = getArguments().getString(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) { 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; } 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 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 static String getFragmentTag(Palina p){ return p.ID; } @Override public void onResume() { super.onResume(); //Log.d(getString(R.string.list_fragment_debug),"Fragment restored, saved listAdapter is "+(mListAdapter)); if(mListAdapter!=null){ ListAdapter adapter = mListAdapter; mListAdapter = null; setListAdapter(adapter); } mListener.readyGUIfor(adapterType); } @Override public void onPause() { if(adapterType.equals(TYPE_LINES)) { 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 ResultFragmentListener) { - mListener = (ResultFragmentListener) context; + if (context instanceof FragmentListener) { + mListener = (FragmentListener) context; fabutton = (FloatingActionButton) getActivity().findViewById(R.id.floatingActionButton); } else { throw new RuntimeException(context.toString() + " must implement ResultFragmentListener"); } } @Override public void onDetach() { mListener = null; if(fabutton!=null) fabutton.show(); super.onDetach(); } @Override public void onDestroyView() { resultsListView = null; //Log.d(getString(R.string.list_fragment_debug), "called onDestroyView"); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); super.onDestroyView(); } 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 */ /* 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){ messageTextView.setText(message); switch (adapterType){ case TYPE_LINES: 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: 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 */ - /** - * This interface is useful for communicating with the activity - * The name has been automatically generated (do not blame me) - */ - public interface ResultFragmentListener extends FragmentListener { - /** - * Houston, we need another fragment! - * - * @param ID the Stop ID - */ - void createFragmentForStop(String ID); - - void addLastStopToFavorites(); - } + /** * 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/middleware/AppDataProvider.java b/src/it/reyboz/bustorino/middleware/AppDataProvider.java index 06b551b..d64aa34 100644 --- a/src/it/reyboz/bustorino/middleware/AppDataProvider.java +++ b/src/it/reyboz/bustorino/middleware/AppDataProvider.java @@ -1,227 +1,234 @@ /* 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 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); + 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); } @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()); 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()>=5 && "location".equals(parts.get(2))){ - Double latitude = Double.parseDouble(parts.get(3)); - Double longitude = Double.parseDouble(parts.get(4)); + 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()>=6 ? Float.parseFloat(parts.get(5))/1000 : 0.1f; - + 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)+" km"); Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres - String[] cols = {StopsTable.COL_ID,StopsTable.COL_LAT,StopsTable.COL_LONG, - StopsTable.COL_NAME,StopsTable.COL_TYPE,StopsTable.COL_LINES_STOPPING}; - String whereClause = "WHERE "+StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND " + + 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,cols,whereClause,null,null,null,null); + return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null); //return getStopsNearby(latitude,longitude,distance); } - else throw new IllegalArgumentException("You must provide latitude and longitude"); + 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; + } 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); } diff --git a/src/it/reyboz/bustorino/middleware/SquareStopAdapter.java b/src/it/reyboz/bustorino/middleware/SquareStopAdapter.java new file mode 100644 index 0000000..ce61d2f --- /dev/null +++ b/src/it/reyboz/bustorino/middleware/SquareStopAdapter.java @@ -0,0 +1,121 @@ +/* + BusTO - UI components + Copyright (C) 2017 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.Context; +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.TextView; +import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.fragments.FragmentListener; + +import java.util.Comparator; +import java.util.List; + +public class SquareStopAdapter extends ArrayAdapter { + private static int layoutRes = R.layout.square_stop_element; + //private List stops; + private Context context; + private @Nullable Location userPosition; + private FragmentListener listener; + + public SquareStopAdapter(List objects, Context con,FragmentListener fragmentListener,@Nullable Location pos) { + super(con,layoutRes,objects);//stops = objects; + context = con; + listener = fragmentListener; + userPosition = pos; + } + + @Override + public void sort(@NonNull Comparator comparator) { + super.sort(comparator); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + SquareViewHolder vh; + if(convertView == null) { + //expand the view + convertView = inflater.inflate(layoutRes, parent,false); + vh = new SquareViewHolder(); + vh.stopIDView = (TextView) convertView.findViewById(R.id.busStopIDView); + vh.stopNameView = (TextView) convertView.findViewById(R.id.stopNameView); + vh.routesView = (TextView) convertView.findViewById(R.id.routesStoppingTextView); + vh.distancetextView = (TextView) convertView.findViewById(R.id.distanceTextView); + + } else { + vh = (SquareViewHolder) convertView.getTag(); + } + + + final Stop stop = getItem(position); + if(stop!=null){ + if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ + Double distance = stop.getDistanceFromLocation(userPosition); + vh.distancetextView.setText(distance.intValue()+" m"); + } else { + vh.distancetextView.setVisibility(View.GONE); + } + vh.stopNameView.setText(stop.getStopDisplayName()); + vh.stopIDView.setText(stop.ID); + String whatStopsHere = stop.routesThatStopHereToString(); + if(whatStopsHere == null) { + vh.routesView.setVisibility(View.GONE); + } else { + vh.routesView.setText(whatStopsHere); + vh.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern + } + convertView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.createFragmentForStop(stop.ID); + } + }); + } else { + Log.w("SquareStopAdapter","!! The selected stop is null !!"); + } + + + //Log.d("SquareStopAdapter","Stop: "+ vh.stopIDView.getText()+" "+ vh.stopNameView.getText()); + convertView.setTag(vh); + return convertView; + + } + private static class SquareViewHolder{ + TextView stopIDView; + TextView stopNameView; + TextView routesView; + TextView distancetextView; + } + /* + @Override + public Stop getItem(int position) { + return stops.get(position); + } + */ +}