diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 23acf97..2d4a85b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,102 +1,102 @@ diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index ec27ed9..7d0909a 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,557 +1,559 @@ /* 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.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; 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.Stop; import it.reyboz.bustorino.backend.StopsFinderByName; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.fragments.ResultListFragment; import it.reyboz.bustorino.middleware.*; public class ActivityMain extends GeneralActivity implements ResultListFragment.ResultFragmentListener { /* * 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"; /* // 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); 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()); 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(); } } @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 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()); } } ////////////////////////////////////// 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 bdae276..99dbb01 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,333 +1,384 @@ /* 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 List getAllStopsFromGTT(AtomicReference res){ + public ArrayList getAllStopsFromGTT(AtomicReference res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); + if(response==null) return null; ArrayList stopslist = null; try{ - JSONArray stops = new JSONArray(response); + 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 */ - public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { + 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/stops/"); + 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 + ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/FiveTNormalizer.java b/src/it/reyboz/bustorino/backend/FiveTNormalizer.java index a1f0e23..0936b46 100644 --- a/src/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/src/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -1,286 +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; /** * 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; } } //TODO: DO THE OPPOSITE FOR FIVETAPIFETCHER /** * 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 "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){ - if(displayName.trim().charAt(displayName.length()-1)=='/'){ + String name = displayName.trim(); + if(name.charAt(displayName.length()-1)=='/'){ return displayName.replace(" ","").replace("/","B"); } - switch (displayName.trim().toLowerCase()){ + switch (name.toLowerCase()){ 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(); + // 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"); + else return name; + } catch (NumberFormatException e){ + //It's not "# navetta" + return name; } } } diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java index cfbd774..05b9b7f 100644 --- a/src/it/reyboz/bustorino/backend/Route.java +++ b/src/it/reyboz/bustorino/backend/Route.java @@ -1,262 +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.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; public class Route implements Comparable { final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY}; final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY}; final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY}; final static int BRANCHID_MISSING = -1; public final String name; public String destinazione; public final List passaggi; public final Type type; public String description; //ordered list of stops, from beginning to end of line private List stopsList = null; public int branchid = BRANCHID_MISSING; public int[] serviceDays ={}; //0=>feriale, 1=>festivo -2=>unknown public FestiveInfo festivo = FestiveInfo.UNKNOWN; public enum Type { // "long distance" sono gli extraurbani. - BUS, LONG_DISTANCE_BUS, METRO, RAILWAY, TRAM + BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5); + //TODO: decide to give some special parameter to each field + private int code; + Type(int code){ + this.code = code; + } + public int getCode(){ + return this.code; + } + @Nullable + public static Type fromCode(int i){ + switch (i){ + case 1: + return BUS; + case 2: + return LONG_DISTANCE_BUS; + case 3: + return METRO; + case 4: + return RAILWAY; + case 5: + return TRAM; + default: + return null; + } + } } public enum FestiveInfo{ FESTIVO(1),FERIALE(0),UNKNOWN(-2); private int code; FestiveInfo(int code){ this.code = code; } public int getCode() { return code; } - public static FestiveInfo returnTypefromCode(int i){ + public static FestiveInfo fromCode(int i){ switch (i){ case -2: return UNKNOWN; case 0: return FERIALE; case 1: return FESTIVO; default: return UNKNOWN; } } } /** * Constructor. * * @param name route ID * @param destinazione terminus\end of line * @param type bus, long distance bus, underground, and so on * @param passaggi timetable, a good choice is an ArrayList of size 6 * @see Palina Palina.addRoute() method */ public Route(String name, String destinazione, Type type, List passaggi) { this.name = name; this.destinazione = destinazione; this.passaggi = passaggi; this.type = type; this.description = null; } /** * Constructor used by the new Api * @param name stop Name * @param t optional type * @param description line rough description */ public Route(String name,Type t,String description){ this.name = name; this.type = t; this.passaggi = new ArrayList<>(); this.destinazione = null; this.description = description; } /** * Exactly what it says on the tin. * * @return times from the timetable */ public List getPassaggi() { return this.passaggi; } public void setStopsList(List stopsList) { this.stopsList = Collections.unmodifiableList(stopsList); } public List getStopsList(){ return this.stopsList; } /** * Adds a time (passaggio) to the timetable for this route * * @param TimeGTT time in GTT format (e.g. "11:22*") */ public void addPassaggio(String TimeGTT) { this.passaggi.add(new Passaggio(TimeGTT)); } public static String getPassageString(String input,boolean realtime){ String time = input.trim(); if(time.contains("*")){ if(realtime) return time; else return time.substring(0,time.length()-1); } else{ if(realtime) return time.concat("*"); else return time; } } @Override public int compareTo(@NonNull Route other) { int res; int thisAsInt, otherAsInt; // sorting by numbers alone yields a far more "natural" result (36N goes before 2024, 95B next to 95, and the like) thisAsInt = networkTools.failsafeParseInt(this.name.replaceAll("[^0-9]", "")); otherAsInt = networkTools.failsafeParseInt(other.name.replaceAll("[^0-9]", "")); // compare. // numeric route IDs (names) if(thisAsInt != 0 && otherAsInt != 0) { res = thisAsInt - otherAsInt; if(res != 0) { return res; } } else { // non-numeric res = this.name.compareTo(other.name); if (res != 0) { return res; } } // try comparing their destination if(this.destinazione!=null){ res = this.destinazione.compareTo(other.destinazione); if(res != 0) { return res; } } //compare the lines if(this.stopsList!=null && other.stopsList!=null){ int d = this.stopsList.size()-other.stopsList.size(); if(d!=0) return d; else { //the two have the same number of stops } } // probably useless, but... last attempt. if(this.type != other.type) { // ordinal() is evil or whatever, who cares. return this.type.ordinal() - other.type.ordinal(); } return 0; } public boolean isBranchIdValid(){ return branchid!=BRANCHID_MISSING; } @Override public boolean equals(Object obj) { if(obj instanceof Route){ Route r = (Route) obj; boolean result = false; if(this.name.equals(r.name) && this.branchid == r.branchid){ if(this.stopsList!=null && r.stopsList!=null){ int d = this.stopsList.size()-r.stopsList.size(); if(d!=0) { result = false; } else { result = true; for(int j=0; j. */ package it.reyboz.bustorino.backend; 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; + } } diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java index 6109b60..54642c0 100644 --- a/src/it/reyboz/bustorino/backend/networkTools.java +++ b/src/it/reyboz/bustorino/backend/networkTools.java @@ -1,163 +1,173 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.support.annotation.Nullable; +import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Map; import java.util.Scanner; import java.util.concurrent.atomic.AtomicReference; public abstract class networkTools { static String getDOM(final URL url, final AtomicReference res) { //Log.d("asyncwget", "Catching URL in background: " + uri[0]); HttpURLConnection urlConnection; StringBuilder result = null; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { res.set(Fetcher.result.SERVER_ERROR); return null; } try { InputStream in = new BufferedInputStream( urlConnection.getInputStream()); BufferedReader reader = new BufferedReader( new InputStreamReader(in)); result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line); } } catch (Exception e) { //Log.e("asyncwget", e.getMessage()); } finally { if (urlConnection != null) { urlConnection.disconnect(); } } if (result == null) { res.set(Fetcher.result.SERVER_ERROR); return null; } res.set(Fetcher.result.PARSER_ERROR); // will be set to "OK" later, this is a safety net in case StringBuilder returns null, the website returns an HTTP 204 or something like that. return result.toString(); } @Nullable static String queryURL(URL url, AtomicReference res){ return queryURL(url,res,null); } @Nullable static String queryURL(URL url, AtomicReference res, Map headers) { HttpURLConnection urlConnection; InputStream in; String s; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { res.set(Fetcher.result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. return null; } // TODO: make this configurable? urlConnection.setConnectTimeout(5000); urlConnection.setReadTimeout(10000); if(headers!= null){ for(String key : headers.keySet()){ urlConnection.setRequestProperty(key,headers.get(key)); } } res.set(Fetcher.result.SERVER_ERROR); // will be set to OK later try { in = urlConnection.getInputStream(); } catch (Exception e) { try { if(urlConnection.getResponseCode()==404) res.set(Fetcher.result.SERVER_ERROR_404); } catch (IOException e2) { e2.printStackTrace(); } return null; } + //s = streamToString(in); + try { + final long startTime = System.currentTimeMillis(); + s = parseStreamToString(in); + final long endtime = System.currentTimeMillis(); + Log.d("NetworkTools-queryURL","reading response took "+(endtime-startTime)+" millisec"); + } catch (IOException e) { + e.printStackTrace(); + return null; + } - s = streamToString(in); try { in.close(); } catch(Exception ignored) {} try { urlConnection.disconnect(); } catch(Exception ignored) {} if(s.length() == 0) { return null; } else { return s; } } // https://stackoverflow.com/a/5445161 static String streamToString(InputStream is) { Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } /** * New method, maybe faster, to read inputStream * also see https://stackoverflow.com/a/5445161 * @param is what to read * @return the String Read * @throws IOException from the InputStreamReader */ static String parseStreamToString(InputStream is) throws IOException{ final int bufferSize = 1024; final char[] buffer = new char[bufferSize]; final StringBuilder out = new StringBuilder(); InputStreamReader in = new InputStreamReader(is, "UTF-8"); int rsz= in.read(buffer, 0, buffer.length); while( rsz >0) { out.append(buffer, 0, rsz); rsz = in.read(buffer, 0, buffer.length); } return out.toString(); } static int failsafeParseInt(String str) { try { return Integer.parseInt(str); } catch(NumberFormatException e) { return 0; } } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index aae64c4..9fea295 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,212 +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){ 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/middleware/AppDataProvider.java b/src/it/reyboz/bustorino/middleware/AppDataProvider.java index 9eca718..5f78f96 100644 --- a/src/it/reyboz/bustorino/middleware/AppDataProvider.java +++ b/src/it/reyboz/bustorino/middleware/AppDataProvider.java @@ -1,134 +1,187 @@ /* 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 java.net.URI; +import it.reyboz.bustorino.middleware.NextGenDB.Contract.*; 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 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); /* * 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,"updatebranches/",ADD_UPDATE_BRANCHES); + 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. - throw new UnsupportedOperationException("Not yet implemented"); + 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. throw new UnsupportedOperationException("Not yet implemented"); } @Override public Uri insert(Uri uri, ContentValues values) { // TODO: Implement this to handle requests to insert a new row. //throw new UnsupportedOperationException("Not yet implemented"); db = appDBHelper.getWritableDatabase(); Uri finalUri = null; - long last_rowid; + 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"); - values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME); - Cursor c = db.query(NextGenDB.Contract.LinesTable.TABLE_NAME, - new String[]{NextGenDB.Contract.LinesTable._ID},NextGenDB.Contract.LinesTable.COLUMN_NAME+"like ?s", + 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); - long lineid = c.getInt(0); - c.close(); - values.put(NextGenDB.Contract.BranchesTable.COL_LINE,lineid); - long rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE); - finalUri= Uri.parse("content://"+AUTHORITY+"/branches/"+rowid); + 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: - last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values); - finalUri = ContentUris.withAppendedId(uri,last_rowid); + //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 false; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // TODO: Implement this to handle query requests from clients. 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"); } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index c3d1e9b..488ae64 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,268 +1,367 @@ /* 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(); 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 { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn == null) { // something really bad happened, start from scratch //getNameOrGetRekt(); //TODO } else { // "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; Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); //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) { + 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); 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"); + 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.app.IntentService; -import android.content.Intent; -import android.content.Context; -import android.content.SharedPreferences; +import android.content.*; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.util.Log; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; +import it.reyboz.bustorino.backend.Route; +import it.reyboz.bustorino.backend.Stop; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; import java.util.concurrent.atomic.AtomicReference; +import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*; + /** * An {@link IntentService} subclass for handling asynchronous task requests in * a service on a separate handler thread. */ public class DatabaseUpdateService extends IntentService { // IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS private static final String ACTION_UPDATE = "it.reyboz.bustorino.middleware.action.UPDATE_DB"; private static final String DB_VERSION = "NextGenDB.GTTVersion"; private static final String DEBUG_TAG = "DatabaseService_BusTO"; // TODO: Rename parameters private static final String TRIAL = "it.reyboz.bustorino.middleware.extra.TRIAL"; private static final int MAX_TRIALS = 5; public DatabaseUpdateService() { super("DatabaseUpdateService"); } + private int updateTrial; /** * Starts this service to perform action Foo with the given parameters. If * the service is already performing a task this action will be queued. * * @see IntentService */ public static void startDBUpdate(Context context) { startDBUpdate(context,0); } public static void startDBUpdate(Context con, int trial){ Intent intent = new Intent(con, DatabaseUpdateService.class); intent.setAction(ACTION_UPDATE); intent.putExtra(TRIAL,trial); con.startService(intent); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { final String action = intent.getAction(); if (ACTION_UPDATE.equals(action)) { + Log.d(DEBUG_TAG,"Started action update"); + SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE); + int versionDB = shPr.getInt(DB_VERSION,-1); final int trial = intent.getIntExtra(TRIAL,-1); - if(!isUpdateNeeded(trial)) return; + updateTrial = trial; + + int newVersion = getNewVersion(trial); + Log.d(DEBUG_TAG,"newDBVersion: "+newVersion+" oldVersion: "+versionDB); + if(versionDB==-1 || newVersion>versionDB){ + Log.d(DEBUG_TAG,"Downloading the bus stops info"); + final AtomicReference gres = new AtomicReference<>(); + getContentResolver().delete(Uri.parse("content://"+AppDataProvider.AUTHORITY+"/stops"),null,null); + if(!performDBUpdate(gres)) restartDBUpdateifPossible(trial,gres); + /*switch (gres.get()){ + case SERVER_ERROR: + restartDBUpdateifPossible(trial); + break; + case PARSER_ERROR: + + break; + case EMPTY_RESULT_SET: + break; + case QUERY_TOO_SHORT: + break; + case SERVER_ERROR_404: + break; + }*/ + else { + SharedPreferences.Editor ed = shPr.edit(); + ed.putInt(DB_VERSION,newVersion); + // BY COMMENTING THIS, THE APP WILL CONTINUOUSLY UPDATE THE DATABASE + ed.apply(); + } + } else { + Log.d(DEBUG_TAG,"No update needed"); + } + + + Log.d(DEBUG_TAG,"Finished update"); } } } - public void performDBUpdate(){ + public boolean performDBUpdate(AtomicReference gres){ + + final FiveTAPIFetcher f = new FiveTAPIFetcher(); + final ArrayList stops = f.getAllStopsFromGTT(gres); + //final ArrayList cpOp = new ArrayList<>(); + + if(gres.get()!= Fetcher.result.OK){ + Log.w(DEBUG_TAG,"Something went wrong downloading"); + return false; + + } + final NextGenDB dbHelp = new NextGenDB(getApplicationContext()); + final SQLiteDatabase db = dbHelp.getWritableDatabase(); + //Empty the needed tables + db.beginTransaction(); + db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); + db.delete(LinesTable.TABLE_NAME,null,null); + + //put new data + long startTime = System.currentTimeMillis(); + + Log.d(DEBUG_TAG,"Inserting "+stops.size()+" stops"); + for (final Stop s : stops) { + final ContentValues cv = new ContentValues(); + + cv.put(StopsTable.COL_ID, s.ID); + cv.put(StopsTable.COL_NAME, s.getStopDefaultName()); + if (s.location != null) + cv.put(StopsTable.COL_LOCATION, s.location); + cv.put(StopsTable.COL_LAT, s.getLatitude()); + cv.put(StopsTable.COL_LONG, s.getLongitude()); + if (s.getAbsurdGTTPlaceName() != null) cv.put(StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName()); + cv.put(StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString()); + if (s.type != null) cv.put(StopsTable.COL_TYPE, s.type.getCode()); + + //Log.d(DEBUG_TAG,cv.toString()); + //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); + //valuesArr[i] = cv; + db.insert(StopsTable.TABLE_NAME, null, cv); + + } + db.setTransactionSuccessful(); + db.endTransaction(); + long endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG,"Inserting stops took: "+((double) (endTime-startTime)/1000)+" s"); + + final ArrayList routes = f.getAllLinesFromGTT(gres); + + if(routes==null){ + Log.w(DEBUG_TAG,"Something went wrong downloading the lines"); + return false; + + } + + db.beginTransaction(); + startTime = System.currentTimeMillis(); + for (Route r: routes){ + final ContentValues cv = new ContentValues(); + cv.put(LinesTable.COLUMN_NAME,r.name); + switch (r.type){ + case BUS: + cv.put(LinesTable.COLUMN_TYPE,"URBANO"); + break; + case RAILWAY: + cv.put(LinesTable.COLUMN_TYPE,"FERROVIA"); + break; + case LONG_DISTANCE_BUS: + cv.put(LinesTable.COLUMN_TYPE,"EXTRA"); + break; + } + cv.put(LinesTable.COLUMN_DESCRIPTION,r.description); + db.insert(LinesTable.TABLE_NAME,null,cv); + } + db.setTransactionSuccessful(); + db.endTransaction(); + endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG,"Inserting lines took: "+((double) (endTime-startTime)/1000)+" s"); + + return true; } - private boolean isUpdateNeeded(int trial){ - SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE); - int versionDB = shPr.getInt(DB_VERSION,-1); - if(versionDB==-1) return true; + private int getNewVersion(int trial){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ - if(gres.get()!= Fetcher.result.PARSER_ERROR){ - restartDBUpdateifPossible(trial); - } - return false; + restartDBUpdateifPossible(trial,gres); + return -2; } boolean needed; try { JSONObject resp = new JSONObject(networkRequest); - int ver = resp.getInt("id"); - if(ver>versionDB) { - SharedPreferences.Editor editor = shPr.edit(); - editor.putInt(DB_VERSION, ver); - //TODO: add the version date maybe? - editor.apply(); - needed = true; - } else { - needed = false; - } - + return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); - restartDBUpdateifPossible(trial); - needed = false; + Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); + return -2; } - return needed; } - private void restartDBUpdateifPossible(int currentTrial){ - if (currentTrial res){ + if (currentTrial. */ package it.reyboz.bustorino.middleware; +import android.content.ContentValues; import android.content.Context; +import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.LinearGradient; import android.provider.BaseColumns; +import android.util.Log; -import static it.reyboz.bustorino.middleware.NextGenDB.Contract.BranchesTable.*; +import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; public static final int DATABASE_VERSION = 1; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ - Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME+" TEXT, "+ - Contract.LinesTable.COLUMN_FAKE_DESCRIPTION+" TEXT, "+Contract.LinesTable.COLUMN_BACINO+" TEXT )"; + Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ + Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ + "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; + private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ - Contract.BranchesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER, "+ + Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ - Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ + Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF - COL_LUN+" INTEGER, "+COL_MAR+" INTEGER, "+COL_MER+" INTEGER, "+COL_GIO+" INTEGER, "+COL_VEN+" INTEGER, "+ - COL_SAB+" INTEGER, "+COL_DOM+" INTEGER, "+ + BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ + BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ - "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + ") ,"+ - "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +") ,"+ + "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ + "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ - Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" TEXT, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ + Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public NextGenDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { + Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ + SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); + db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } + /** + * Insert batch content, already prepared as + * @param content ContentValues array + * @return number of lines inserted + */ + public int insertBatchContent(ContentValues[] content,String tableName){ + final SQLiteDatabase db = this.getWritableDatabase(); + int success = 0; + + db.beginTransaction(); + + for (final ContentValues cv : content) { + try { + db.replaceOrThrow(tableName, null, cv); + success++; + } catch (SQLiteConstraintException d){ + Log.w("NextGenDB_Insert","Failed insert with FOREIGN KEY... \n"+d.getMessage()); + + } catch (Exception e) { + Log.w("NextGenDB_Insert", e); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + return success; + } + public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public static final class LinesTable implements BaseColumns{ //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "name"; - public static final String COLUMN_FAKE_DESCRIPTION = "line_description"; - public static final String COLUMN_BACINO = "bacino"; + public static final String COLUMN_DESCRIPTION = "line_description"; + public static final String COLUMN_TYPE = "bacino"; } public static final class BranchesTable implements BaseColumns{ public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "direzione"; public static final String COL_FESTIVO = "festivo"; + public static final String COL_TYPE = "type"; public static final String COL_LUN="lun"; public static final String COL_MAR="mar"; public static final String COL_MER="mer"; public static final String COL_GIO="gio"; public static final String COL_VEN="ven"; public static final String COL_SAB="sab"; public static final String COL_DOM="dom"; } public static final class ConnectionsTable { public static final String TABLE_NAME = "connections"; - public static final String COLUMN_BRANCH = "branchid"; + public static final String COLUMN_BRANCH = "branch"; public static final String COLUMN_STOP_ID = "stopid"; - static final String COLUMN_ORDER = "order"; + static final String COLUMN_ORDER = "ordine"; } public static final class StopsTable { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "id"; //integer public static final String COL_TYPE = "type"; public static final String COL_NAME = "name"; public static final String COL_LAT = "lat"; public static final String COL_LONG = "longitude"; - public static final String COL_LOCATION = "type"; + public static final String COL_LOCATION = "location"; public static final String COL_PLACE = "placeName"; public static final String COL_LINES_STOPPING = "lines"; } } }