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";
}
}
}