diff --git a/src/it/reyboz/bustorino/ActivityMap.java b/src/it/reyboz/bustorino/ActivityMap.java
index 1ef2806..0b87001 100644
--- a/src/it/reyboz/bustorino/ActivityMap.java
+++ b/src/it/reyboz/bustorino/ActivityMap.java
@@ -1,387 +1,393 @@
/*
BusTO Activities
Copyright (C) 2020 Andrea Ugo e 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;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.support.annotation.RequiresApi;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceManager;
import android.widget.Toast;
+import it.reyboz.bustorino.middleware.NextGenDB;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
import java.util.*;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.map.CustomInfoWindow;
import it.reyboz.bustorino.middleware.StopsDB;
public class ActivityMap extends AppCompatActivity {
private static final String TAG = "Busto-MapActivity";
private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom";
private static final String MAP_CENTER_LAT_KEY = "map-center-lat";
private static final String MAP_CENTER_LON_KEY = "map-center-lon";
public static final String BUNDLE_LATIT = "lat";
public static final String BUNDLE_LONGIT = "lon";
public static final String BUNDLE_NAME = "name";
public static final String BUNDLE_ID = "ID";
private static final double DEFAULT_CENTER_LAT = 45.0708;
private static final double DEFAULT_CENTER_LON = 7.6858;
private static final double POSITION_FOUND_ZOOM = 18.3;
private HashSet shownStops = null;
private MapView map = null;
public Context ctx;
private MyLocationNewOverlay mLocationOverlay = null;
private FolderOverlay stopsFolderOverlay = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//handle permissions first, before map is created. not depicted here
//load/initialize the osmdroid configuration, this can be done
ctx = getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
//setting this before the layout is inflated is a good idea
//it 'should' ensure that the map has a writable location for the map cache, even without permissions
//if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath
//see also StorageUtils
//note, the load method also sets the HTTP User Agent to your application's package name, abusing osm's tile servers will get you banned based on this string
//inflate and create the map
setContentView(R.layout.activity_map);
map = (MapView) findViewById(R.id.map);
map.setTileSource(TileSourceFactory.MAPNIK);
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true);
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true);
btCenterMap = (ImageButton) findViewById(R.id.ic_center_map);
btFollowMe = (ImageButton) findViewById(R.id.ic_follow_me);
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
// take the parameters if it's called from other Activities
Bundle b = getIntent().getExtras();
startMap(b, savedInstanceState);
// on drag and zoom reload the markers
map.addMapListener(new DelayedMapListener(new MapListener() {
@Override
public boolean onScroll(ScrollEvent paramScrollEvent) {
loadMarkers();
return true;
}
@Override
public boolean onZoom(ZoomEvent event) {
loadMarkers();
return true;
}
}));
btCenterMap.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "centerMap clicked ");
final GeoPoint myPosition = mLocationOverlay.getMyLocation();
map.getController().animateTo(myPosition);
}
});
btFollowMe.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "btFollowMe clicked ");
if (!mLocationOverlay.isFollowLocationEnabled()) {
mLocationOverlay.enableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
} else {
mLocationOverlay.disableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me);
}
}
});
}
public void startMap(Bundle incoming, Bundle savedInstanceState) {
//parse incoming bundle
GeoPoint marker = null;
String name = null;
String ID = null;
if(incoming != null) {
double lat = incoming.getDouble(BUNDLE_LATIT);
double lon = incoming.getDouble(BUNDLE_LONGIT);
marker = new GeoPoint(lat, lon);
name = incoming.getString(BUNDLE_NAME);
ID = incoming.getString(BUNDLE_ID);
}
shownStops = new HashSet<>();
// move the map on the marker position or on a default view point: Turin, Piazza Castello
// and set the start zoom
IMapController mapController = map.getController();
GeoPoint startPoint = null;
if (marker != null) {
startPoint = marker;
mapController.setZoom(POSITION_FOUND_ZOOM);
}
else if (savedInstanceState != null) {
mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY));
mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY),
savedInstanceState.getDouble(MAP_CENTER_LON_KEY)));
} else {
boolean found = false;
LocationManager locationManager =
(LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if(locationManager!=null) {
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
mapController.setZoom(POSITION_FOUND_ZOOM);
startPoint = new GeoPoint(userLocation);
found = true;
}
}
if(!found){
startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON);
mapController.setZoom(16.0);
}
}
// set the minimum zoom level
map.setMinZoomLevel(15.0);
//add contingency check (shouldn't happen..., but)
if (startPoint != null) {
mapController.setCenter(startPoint);
}
// Location Overlay
// from OpenBikeSharing (THANK GOD)
GpsMyLocationProvider imlp = new GpsMyLocationProvider(this.getBaseContext());
imlp.setLocationUpdateMinDistance(5);
imlp.setLocationUpdateMinTime(2000);
this.mLocationOverlay = new MyLocationNewOverlay(imlp,map);
mLocationOverlay.enableMyLocation();
mLocationOverlay.enableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
mLocationOverlay.setOptionsMenuEnabled(true);
/*
mLocationOverlay.runOnFirstFix(() -> {
mapController.setCenter(mLocationOverlay.getMyLocation());
mapController.animateTo(mLocationOverlay.getMyLocation());
});
*/
map.getOverlays().add(this.mLocationOverlay);
//add stops overlay
map.getOverlays().add(this.stopsFolderOverlay);
loadMarkers();
if (marker != null) {
// make a marker with the info window open for the searched marker
makeMarker(startPoint, name , ID, true);
}
}
public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) {
// add a marker
Marker marker = new Marker(map);
// set custom info window as info window
CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName);
marker.setInfoWindow(popup);
// make the marker clickable
marker.setOnMarkerClickListener((thisMarker, mapView) -> {
if (thisMarker.isInfoWindowOpen()) {
// on second click
// create an intent with these extras
Intent intent = new Intent(ActivityMap.this, ActivityMain.class);
Bundle b = new Bundle();
b.putString("bus-stop-ID", ID);
b.putString("bus-stop-display-name", stopName);
intent.putExtras(b);
intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
// start ActivityMain with the previous intent
startActivity(intent);
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map);
// show this particular info window
thisMarker.showInfoWindow();
// move the map to its position
map.getController().animateTo(thisMarker.getPosition());
}
return true;
});
// set its position
marker.setPosition(geoPoint);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
// add to it an icon
marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
marker.setSnippet(ID);
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow();
}
return marker;
}
public void loadMarkers() {
// get rid of the previous markers
//map.getOverlays().clear();
//stopsFolderOverlay = new FolderOverlay();
List stopsOverlays = stopsFolderOverlay.getItems();
/*if (stopsOverlays != null){
stopsOverlays.clear();
}*/
// get the top, bottom, left and right screen's coordinate
BoundingBox bb = map.getBoundingBox();
double latFrom = bb.getLatSouth();
double latTo = bb.getLatNorth();
double lngFrom = bb.getLonWest();
double lngTo = bb.getLonEast();
// get the stops located in those coordinates
+ /*
StopsDB stopsDB = new StopsDB(ctx);
stopsDB.openIfNeeded();
Stop[] stops = stopsDB.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo);
stopsDB.closeIfNeeded();
+ */
+
+ NextGenDB dbHelper = new NextGenDB(ctx);
+ Stop[] stops = dbHelper.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo);
// add new markers of those stops
for (Stop stop : stops) {
if (shownStops.contains(stop.ID)){
continue;
}
try{
stop.getLatitude();
stop.getLongitude();
} catch (NullPointerException e) {
Log.e(TAG,"Stop "+stop.ID+ " gives null coordinates");
e.printStackTrace();
continue;
}
shownStops.add(stop.ID);
GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude());
Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false);
stopsFolderOverlay.add(stopMarker);
}
}
protected boolean detachMapFromPosition(){
if (mLocationOverlay.isFollowLocationEnabled()) {
mLocationOverlay.disableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me);
return true;
} return false;
}
public void onResume(){
super.onResume();
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
//Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this));
map.onResume(); //needed for compass, my location overlays, v6.0.0 and up
mLocationOverlay.enableMyLocation();
}
public void onPause(){
super.onPause();
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
//Configuration.getInstance().save(this, prefs);
map.onPause(); //needed for compass, my location overlays, v6.0.0 and up
mLocationOverlay.disableMyLocation();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble());
outState.putDouble(MAP_CENTER_LAT_KEY, map.getMapCenter().getLatitude());
outState.putDouble(MAP_CENTER_LON_KEY, map.getMapCenter().getLongitude());
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java
index f805f51..56394a4 100644
--- a/src/it/reyboz/bustorino/backend/Route.java
+++ b/src/it/reyboz/bustorino/backend/Route.java
@@ -1,356 +1,370 @@
/*
BusTO (backend components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
public class Route implements Comparable {
final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY};
final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY};
private final static int BRANCHID_MISSING = -1;
private final String name;
public String destinazione;
public final List passaggi;
//create a copy of the list, so that
private List sortedPassaggi;
public final Type type;
public String description;
//ordered list of stops, from beginning to end of line
private List stopsList = null;
public int branchid = BRANCHID_MISSING;
public int[] serviceDays ={};
//0=>feriale, 1=>festivo -2=>unknown
public FestiveInfo festivo = FestiveInfo.UNKNOWN;
public enum Type { // "long distance" sono gli extraurbani.
BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2);
//TODO: decide to give some special parameter to each field
private int code;
Type(int code){
this.code = code;
}
public int getCode(){
return this.code;
}
@Nullable
public static Type fromCode(int i){
switch (i){
case 1:
return BUS;
case 2:
return LONG_DISTANCE_BUS;
case 3:
return METRO;
case 4:
return RAILWAY;
case 5:
return TRAM;
case -2:
return UNKNOWN;
default:
return null;
}
}
}
public enum FestiveInfo{
FESTIVO(1),FERIALE(0),UNKNOWN(-2);
private int code;
FestiveInfo(int code){
this.code = code;
}
public int getCode() {
return code;
}
public static FestiveInfo fromCode(int i){
switch (i){
case -2:
return UNKNOWN;
case 0:
return FERIALE;
case 1:
return FESTIVO;
default:
return UNKNOWN;
}
}
}
/**
* Constructor.
*
* @param name route ID
* @param destinazione terminus\end of line
* @param type bus, long distance bus, underground, and so on
* @param passaggi timetable, a good choice is an ArrayList of size 6
* @param description the description of the line, usually given by the FiveTAPIFetcher
* @see Palina Palina.addRoute() method
*/
public Route(String name, String destinazione, List passaggi, Type type, String description) {
this.name = name;
this.destinazione = parseDestinazione(destinazione);
this.passaggi = passaggi;
this.type = type;
this.description = description;
}
/**
* Constructor used in GTTJSONFetcher, see above
*/
public Route(String name, String destinazione, Type type, List passaggi) {
this(name,destinazione,passaggi,type,null);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,Type t,String description){
this(name,null,new ArrayList<>(),t,description);
}
/**
* Exactly what it says on the tin.
*
* @return times from the timetable
*/
public List getPassaggi() {
return this.passaggi;
}
public void setStopsList(List stopsList) {
this.stopsList = Collections.unmodifiableList(stopsList);
}
public List getStopsList(){
return this.stopsList;
}
/**
* Adds a time (passaggio) to the timetable for this route
*
* @param TimeGTT time in GTT format (e.g. "11:22*")
*/
public void addPassaggio(String TimeGTT, Passaggio.Source source) {
this.passaggi.add(new Passaggio(TimeGTT, source));
}
//Overloaded
public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
this.passaggi.add(new Passaggio(hour, minutes, realtime, source));
}
+
+ public static Route.Type getTypeFromSymbol(String route) {
+ switch (route) {
+ case "M":
+ return Route.Type.METRO;
+ case "T":
+ return Route.Type.RAILWAY;
+ }
+
+ // default with case "B"
+ return Route.Type.BUS;
+ }
+
private String parseDestinazione(String direzione){
if(direzione==null) return null;
//trial to add space to the parenthesis
String[] exploded = direzione.split("\\(");
if(exploded.length>1){
StringBuilder sb = new StringBuilder();
sb.append(exploded[0]);
for(int i=1; i arrivals;
int max;
if(sort){
if(sortedPassaggi==null){
sortedPassaggi = new ArrayList<>(passaggi.size());
sortedPassaggi.addAll(passaggi);
Collections.sort(sortedPassaggi);
}
arrivals = sortedPassaggi;
} else arrivals = passaggi;
max = Math.min(start_idx + number, arrivals.size());
for(int j= start_idx; j0){
this.passaggi.addAll(other.passaggi);
}
if(this.destinazione == null && other.destinazione!=null) {
this.destinazione = other.destinazione;
adjusted = true;
}
if(!this.isBranchIdValid() && other.isBranchIdValid()) {
this.branchid = other.branchid;
adjusted = true;
}
if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){
this.festivo = other.festivo;
adjusted = true;
}
if(other.description!=null && (this.description==null ||
(this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) ||
(this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) {
this.description = other.description;
}
return adjusted;
}
}
diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
index eddd9cc..70cc018 100644
--- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java
+++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
@@ -1,230 +1,230 @@
/*
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.SQLiteException;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.PalinaAdapter;
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.lang.ref.WeakReference;
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;
private WeakReference lastTaskRef;
private NextGenDB newDBHelper;
private boolean shouldHaltAllActivities=false;
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;
- newDBHelper = NextGenDB.getInstance(act.getApplicationContext());
+ newDBHelper = new NextGenDB(act.getApplicationContext());
}
/**
* Get the last successfully searched bus stop or NULL
*
* @return
*/
public Stop getLastSuccessfullySearchedBusStop() {
return lastSuccessfullySearchedBusStop;
}
public void setLastSuccessfullySearchedBusStop(Stop stop) {
this.lastSuccessfullySearchedBusStop = stop;
}
public void setLastTaskRef(WeakReference lastTaskRef) {
this.lastTaskRef = lastTaskRef;
}
/**
* 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 sameFragment;
ArrivalsFragment arrivalsFragment;
if(act==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
return;
}
SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID);
FragmentManager fm = act.getSupportFragmentManager();
if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame);
sameFragment = arrivalsFragment.isFragmentForTheSameStop(p);
} else
sameFragment = false;
setLastSuccessfullySearchedBusStop(p);
if(!sameFragment) {
//set the String to be displayed on the fragment
String displayName = p.getStopDisplayName();
String displayStuff;
if (displayName != null && displayName.length() > 0) {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName);
} else {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID);
}
attachFragmentToContainer(fm,arrivalsFragment,true,ResultListFragment.getFragmentTag(p));
} else {
Log.d("BusTO", "Same bus stop, accessing existing fragment");
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame);
}
arrivalsFragment.setListAdapter(new PalinaAdapter(act.getApplicationContext(),p));
act.hideKeyboard();
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();
StopListFragment listfragment = StopListFragment.newInstance(query);
attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query);
listfragment.setStopList(resultList);
toggleSpinner(false);
}
/**
* Wrapper for toggleSpinner in Activity
* @param on new status of spinner system
*/
public void toggleSpinner(boolean on){
if (act instanceof FragmentListener)
((FragmentListener) act).toggleSpinner(on);
else {
SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID);
srl.setRefreshing(false);
}
}
/**
* Attach a new fragment to a cointainer
* @param fm the FragmentManager
* @param fragment the Fragment
* @param sendToSecondaryFrame needs to be displayed in secondary frame or not
* @param tag tag for the fragment
*/
public void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){
FragmentTransaction ft = fm.beginTransaction();
if(sendToSecondaryFrame && secondaryFrameLayout!=NO_FRAME)
ft.replace(secondaryFrameLayout,fragment,tag);
else ft.replace(primaryFrameLayout,fragment,tag);
ft.addToBackStack("state_"+tag);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
ft.commit();
//fm.executePendingTransactions();
}
synchronized public int insertBatchDataInNextGenDB(ContentValues[] valuesArr,String tableName){
if(newDBHelper !=null)
try {
return newDBHelper.insertBatchContent(valuesArr, tableName);
} catch (SQLiteException exc){
Log.w("DB Batch inserting: ","ERROR Inserting the data batch: ",exc.fillInStackTrace());
return -2;
}
else return -1;
}
synchronized public ContentResolver getContentResolver(){
return act.getContentResolver();
}
public void setBlockAllActivities(boolean shouldI) {
this.shouldHaltAllActivities = shouldI;
}
public void stopLastRequestIfNeeded(){
if(lastTaskRef == null) return;
AsyncDataDownload task = lastTaskRef.get();
if(task!=null){
task.cancel(true);
}
}
/**
* 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 c2460f9..c0362b5 100644
--- a/src/it/reyboz/bustorino/middleware/AppDataProvider.java
+++ b/src/it/reyboz/bustorino/middleware/AppDataProvider.java
@@ -1,268 +1,268 @@
/*
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.*;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
import it.reyboz.bustorino.backend.DBStatusManager;
import it.reyboz.bustorino.middleware.NextGenDB.Contract.*;
import java.util.List;
public class AppDataProvider extends ContentProvider {
public static final String AUTHORITY = "it.reyboz.bustorino.provider";
private static final int STOP_OP = 1;
private static final int LINE_OP = 2;
private static final int BRANCH_OP = 3;
private static final int FAVORITES_OP =4;
private static final int MANY_STOPS = 5;
private static final int ADD_UPDATE_BRANCHES = 6;
private static final int LINE_INSERT_OP = 7;
private static final int CONNECTIONS = 8;
private static final int LOCATION_SEARCH = 9;
private static final String DEBUG_TAG="AppDataProvider";
private Context con;
private NextGenDB appDBHelper;
private UserDB udbhelper;
private SQLiteDatabase db;
private DBStatusManager preferences;
public AppDataProvider() {
}
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize.
*/
sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP);
sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS);
sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP);
sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP);
sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP);
sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES);
sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS);
sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
db = appDBHelper.getWritableDatabase();
int rows;
switch (sUriMatcher.match(uri)){
case MANY_STOPS:
rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null);
break;
default:
throw new UnsupportedOperationException("Not yet implemented");
}
return rows;
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
int match = sUriMatcher.match(uri);
String baseTypedir = "vnd.android.cursor.dir/";
String baseTypeitem = "vnd.android.cursor.item/";
switch (match){
case LOCATION_SEARCH:
return baseTypedir+"stop";
case LINE_OP:
return baseTypeitem+"line";
case CONNECTIONS:
return baseTypedir+"stops";
}
return baseTypedir+"/item";
}
@Override
public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{
//AVOID OPENING A DB CONNECTION, WILL THROW VERY NASTY ERRORS
if(preferences.isDBUpdating(true))
return null;
db = appDBHelper.getWritableDatabase();
Uri finalUri;
long last_rowid = -1;
switch (sUriMatcher.match(uri)){
case ADD_UPDATE_BRANCHES:
Log.d("InsBranchWithProvider","new Insert request");
String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME);
if(line_name==null) throw new IllegalArgumentException("No line name given");
long lineid = -1;
Cursor c = db.query(LinesTable.TABLE_NAME,
new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?",
new String[]{line_name},null,null,null);
Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches");
if(c.getCount() == 0){
//There are no lines, insert?
//NOPE
/*
c.close();
ContentValues cv = new ContentValues();
cv.put(LinesTable.COLUMN_NAME,line_name);
lineid = db.insert(LinesTable.TABLE_NAME,null,cv);
*/
break;
}else {
c.moveToFirst();
/*
while(c.moveToNext()){
Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n"
+c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION)));
}*/
lineid = c.getInt(c.getColumnIndex(NextGenDB.Contract.LinesTable._ID));
c.close();
}
values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME);
values.put(BranchesTable.COL_LINE,lineid);
last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE);
break;
case MANY_STOPS:
//Log.d("AppDataProvider_busTO","New stop insert request");
try{
last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values);
} catch (SQLiteConstraintException e){
Log.w("AppDataProvider_busTO","Insert failed because of constraint");
last_rowid = -1;
e.printStackTrace();
}
break;
case CONNECTIONS:
try{
last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values);
} catch (SQLiteConstraintException e){
Log.w("AppDataProvider_busTO","Insert failed because of constraint");
last_rowid = -1;
e.printStackTrace();
}
break;
default:
throw new IllegalArgumentException("Invalid parameters");
}
finalUri = ContentUris.withAppendedId(uri,last_rowid);
return finalUri;
}
@Override
public boolean onCreate() {
con = getContext();
- appDBHelper = NextGenDB.getInstance(getContext());
+ appDBHelper = new NextGenDB(getContext());
udbhelper = new UserDB(getContext());
if(con!=null) {
preferences = new DBStatusManager(con,null);
} else {
preferences = null;
Log.e(DEBUG_TAG,"Cannot get shared preferences");
}
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException {
//IMPORTANT
//The app should not query when the DB is updating, but apparently, it does
if(preferences.isDBUpdating(true))
//throw new UnsupportedOperationException("DB is updating");
return null;
SQLiteDatabase db = appDBHelper.getReadableDatabase();
List parts = uri.getPathSegments();
switch (sUriMatcher.match(uri)){
case LOCATION_SEARCH:
//authority/stops/location/"Lat"/"Lon"/"distance"
//distance in metres (integer)
if(parts.size()>=4 && "location".equals(parts.get(1))){
Double latitude = Double.parseDouble(parts.get(2));
Double longitude = Double.parseDouble(parts.get(3));
//converting distance to a float to not lose precision
float distance = parts.size()>=5 ? Float.parseFloat(parts.get(4))/1000 : 0.1f;
if(parts.size()>=5)
Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m");
Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres
String whereClause = StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND "
+StopsTable.COL_LAT +" > "+(latitude-distasAngle)+" AND "+
StopsTable.COL_LONG+" < "+(longitude+distasAngle)+" AND "+StopsTable.COL_LONG+" > "+(longitude-distasAngle);
//Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause);
return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null);
}
else {
Log.w(DEBUG_TAG,"Not enough parameters");
if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s);
return null;
}
case FAVORITES_OP:
final String stopFavSelection = UserDB.getFavoritesColumnNamesAsArray[0]+" = ?";
db = udbhelper.getReadableDatabase();
Log.d(DEBUG_TAG,"Asked information on Favorites about stop with id "+uri.getLastPathSegment());
return db.query(UserDB.TABLE_NAME,projection,stopFavSelection,new String[]{uri.getLastPathSegment()},null,null,sortOrder);
case STOP_OP:
//Let's try this plain and simple
final String[] selectionValues = {uri.getLastPathSegment()};
final String stopSelection = StopsTable.COL_ID+" = ?";
Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]);
return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder);
default:
Log.d("DataProvider","got request "+uri.getPath()+" which doesn't match anything");
}
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
// public static Uri getBaseUriGivenOp(int operationType);
public static Uri.Builder getUriBuilderToComplete(){
final Uri.Builder b = new Uri.Builder();
b.scheme("content").authority(AUTHORITY);
return b;
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
}
diff --git a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
index 99f8963..5492b47 100644
--- a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
+++ b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
@@ -1,278 +1,278 @@
/*
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.app.IntentService;
import android.content.*;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.Nullable;
import android.util.Log;
import it.reyboz.bustorino.ActivityMain;
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.lang.ref.WeakReference;
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 String COMPULSORY = "compulsory_update";
private static final int MAX_TRIALS = 5;
private static final int VERSION_UNAIVALABLE = -2;
public DatabaseUpdateService() {
super("DatabaseUpdateService");
}
private boolean isRunning;
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 con, int trial, @Nullable Boolean mustUpdate){
Intent intent = new Intent(con, DatabaseUpdateService.class);
intent.setAction(ACTION_UPDATE);
intent.putExtra(TRIAL,trial);
if(mustUpdate!=null){
intent.putExtra(COMPULSORY,mustUpdate);
}
con.startService(intent);
}
public static void startDBUpdate(Context con) {
startDBUpdate(con, 0, false);
}
@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);
final SharedPreferences.Editor editor = shPr.edit();
updateTrial = trial;
UpdateRequestParams params = new UpdateRequestParams(intent);
int newVersion = getNewVersion(params);
if(newVersion==VERSION_UNAIVALABLE){
//NOTHING LEFT TO DO
return;
}
Log.d(DEBUG_TAG,"newDBVersion: "+newVersion+" oldVersion: "+versionDB);
if(params.mustUpdate || versionDB==-1 || newVersion>versionDB){
Log.d(DEBUG_TAG,"Downloading the bus stops info");
final AtomicReference gres = new AtomicReference<>();
if(!performDBUpdate(gres))
restartDBUpdateifPossible(params,gres);
else {
editor.putInt(DB_VERSION,newVersion);
// BY COMMENTING THIS, THE APP WILL CONTINUOUSLY UPDATE THE DATABASE
editor.apply();
}
} else {
Log.d(DEBUG_TAG,"No update needed");
}
Log.d(DEBUG_TAG,"Finished update");
setDBUpdatingFlag(shPr,false);
}
}
}
private boolean setDBUpdatingFlag(SharedPreferences shPr,boolean value){
final SharedPreferences.Editor editor = shPr.edit();
editor.putBoolean(getString(R.string.databaseUpdatingPref),value);
return editor.commit();
}
private boolean setDBUpdatingFlag(boolean value){
final SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE);
return setDBUpdatingFlag(shPr,value);
}
private 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;
}
if(!setDBUpdatingFlag(true))
return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database
- final NextGenDB dbHelp = NextGenDB.getInstance(getApplicationContext());
+ 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.replace(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");
dbHelp.close();
return false;
}
db.beginTransaction();
startTime = System.currentTimeMillis();
for (Route r: routes){
final ContentValues cv = new ContentValues();
cv.put(LinesTable.COLUMN_NAME,r.getName());
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);
int rows = db.update(LinesTable.TABLE_NAME,cv,LinesTable.COLUMN_NAME+" = ?",new String[]{r.getName()});
if(rows<1){ //we haven't changed anything
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");
dbHelp.close();
return true;
}
private int getNewVersion(UpdateRequestParams params){
AtomicReference gres = new AtomicReference<>();
String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres);
if(networkRequest == null){
restartDBUpdateifPossible(params,gres);
return VERSION_UNAIVALABLE;
}
boolean needed;
try {
JSONObject resp = new JSONObject(networkRequest);
return resp.getInt("id");
} catch (JSONException e) {
e.printStackTrace();
Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest);
return -4;
}
}
private void restartDBUpdateifPossible(UpdateRequestParams pars, AtomicReference res){
if (pars.trial.
*/
package it.reyboz.bustorino.middleware;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
+import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
+import android.support.annotation.Nullable;
import android.util.Log;
import it.reyboz.bustorino.R;
+import it.reyboz.bustorino.backend.Route;
+import it.reyboz.bustorino.backend.Stop;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
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 = 2;
- //Singleton instance
- private static volatile NextGenDB instance = null;
+ public static final String DEBUG_TAG = "NextGenDB-BusTO";
+ //NO Singleton instance
+ //private static volatile NextGenDB instance = null;
//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_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, "+ 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_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
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 +"), "+
"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+" 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 )";
private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+
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 )";
+ private static final String[] QUERY_COLUMN_stops_all = {
+ StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_LOCATION,
+ StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING};
+
+ private static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " +
+ StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG +
+ " >= ? AND "+ StopsTable.COL_LONG + " <= ?";
+
+
private Context appContext;
- private NextGenDB(Context context) {
+ public NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
/**
* Lazy initialization singleton getter, thread-safe with double checked locking
* from https://en.wikipedia.org/wiki/Singleton_pattern
- * @param context needed context
* @return the instance
*/
+ /*
public static NextGenDB getInstance(Context context){
if(instance==null){
synchronized (NextGenDB.class){
if(instance==null){
instance = new NextGenDB(context);
}
}
}
return instance;
- }
+ }*/
@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) {
if(oldVersion<2 && newVersion == 2){
//DROP ALL TABLES
db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME);
db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME);
//RECREATE THE TABLES WITH THE NEW SCHEMA
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);
DatabaseUpdateService.startDBUpdate(appContext,0,true);
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.execSQL("PRAGMA foreign_keys=ON");
}
+
public static String getSqlCreateStopsTable(String tableName){
return "CREATE TABLE "+tableName+" ("+
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 )";
}
+ /**
+ * Query some bus stops inside a map view
+ *
+ * You can obtain the coordinates from OSMDroid using something like this:
+ * BoundingBoxE6 bb = mMapView.getBoundingBox();
+ * double latFrom = bb.getLatSouthE6() / 1E6;
+ * double latTo = bb.getLatNorthE6() / 1E6;
+ * double lngFrom = bb.getLonWestE6() / 1E6;
+ * double lngTo = bb.getLonEastE6() / 1E6;
+ */
+ public synchronized Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
+ Stop[] stops = new Stop[0];
+ SQLiteDatabase db = this.getReadableDatabase();
+
+ Cursor result;
+ int count;
+
+ // coordinates must be strings in the where condition
+ String minLatRaw = String.valueOf(minLat);
+ String maxLatRaw = String.valueOf(maxLat);
+ String minLngRaw = String.valueOf(minLng);
+ String maxLngRaw = String.valueOf(maxLng);
+
+ String[] queryColumns = {};
+ String stopID;
+ Route.Type type;
+
+ if(db == null) {
+ return stops;
+ }
+
+ try {
+ result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE,
+ new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw},
+ null, null, null);
+
+ int colID = result.getColumnIndex(StopsTable.COL_ID);
+ int colName = result.getColumnIndex(StopsTable.COL_NAME);
+ int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
+ int colType = result.getColumnIndex(StopsTable.COL_TYPE);
+ int colLat = result.getColumnIndex(StopsTable.COL_LAT);
+ int colLon = result.getColumnIndex(StopsTable.COL_LONG);
+ int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
+
+ count = result.getCount();
+ stops = new Stop[count];
+
+ int i = 0;
+ while(result.moveToNext()) {
+
+ stopID = result.getString(colID);
+ type = Route.getTypeFromSymbol(result.getString(colType));
+ String lines = result.getString(colLines).trim();
+
+ String locationSometimesEmpty = result.getString(colLocation);
+ if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
+ locationSometimesEmpty = null;
+ }
+
+ stops[i++] = new Stop(stopID, result.getString(colName), null,
+ locationSometimesEmpty, type, splitLinesString(lines),
+ result.getDouble(colLat), result.getDouble(colLon));
+ }
+
+ } catch(SQLiteException e) {
+ Log.e(DEBUG_TAG, "SQLiteException occurred");
+ e.printStackTrace();
+ return stops;
+ }
+
+ result.close();
+ db.close();
+
+ return stops;
+ }
+
/**
* Insert batch content, already prepared as
* @param content ContentValues array
* @return number of lines inserted
*/
public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException {
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 List splitLinesString(String linesStr){
+ return Arrays.asList(linesStr.split("\\s*,\\s*"));
+ }
+
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 interface DataTables {
String getTableName();
String[] getFields();
}
public static final class LinesTable implements BaseColumns, DataTables {
//The fields
public static final String TABLE_NAME = "lines";
public static final String COLUMN_NAME = "line_name";
public static final String COLUMN_DESCRIPTION = "line_description";
public static final String COLUMN_TYPE = "line_bacino";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE};
}
}
public static final class BranchesTable implements BaseColumns, DataTables {
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 = "branch_direzione";
public static final String COL_FESTIVO = "branch_festivo";
public static final String COL_TYPE = "branch_type";
public static final String COL_LUN="runs_lun";
public static final String COL_MAR="runs_mar";
public static final String COL_MER="runs_mer";
public static final String COL_GIO="runs_gio";
public static final String COL_VEN="runs_ven";
public static final String COL_SAB="runs_sab";
public static final String COL_DOM="runs_dom";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION,
COL_DIRECTION,COL_FESTIVO,COL_TYPE,
COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM
};
}
}
public static final class ConnectionsTable implements DataTables {
public static final String TABLE_NAME = "connections";
public static final String COLUMN_BRANCH = "branchid";
public static final String COLUMN_STOP_ID = "stopid";
public static final String COLUMN_ORDER = "ordine";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER};
}
}
public static final class StopsTable implements DataTables {
public static final String TABLE_NAME = "stops";
public static final String COL_ID = "stopid"; //integer
public static final String COL_TYPE = "stop_type";
public static final String COL_NAME = "stop_name";
public static final String COL_LAT = "stop_latitude";
public static final String COL_LONG = "stop_longitude";
public static final String COL_LOCATION = "stop_location";
public static final String COL_PLACE = "stop_placeName";
public static final String COL_LINES_STOPPING = "stop_lines";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING};
}
}
}
public static final class DBUpdatingException extends Exception{
public DBUpdatingException(String message) {
super(message);
}
}
}
diff --git a/src/it/reyboz/bustorino/middleware/StopsDB.java b/src/it/reyboz/bustorino/middleware/StopsDB.java
index 8f0df0f..4ddb5d8 100644
--- a/src/it/reyboz/bustorino/middleware/StopsDB.java
+++ b/src/it/reyboz/bustorino/middleware/StopsDB.java
@@ -1,306 +1,308 @@
/*
BusTO ("backend" components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.middleware;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsDBInterface;
public class StopsDB extends SQLiteAssetHelper implements StopsDBInterface {
private static String QUERY_TABLE_stops = "stops";
private static String QUERY_WHERE_ID = "ID = ?";
private static String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = "lat >= ? AND lat <= ? AND lon >= ? AND lon <= ?";
private static String[] QUERY_COLUMN_name = {"name"};
- private static String[] QUERY_COLUMN_location = {"location"};
- private static String[] QUERY_COLUMN_route = {"route"};
- private static String[] QUERY_COLUMN_everything = {"name", "location", "type", "lat", "lon"};
- private static String[] QUERY_COLUMN_everything_and_ID = {"ID", "name", "location", "type", "lat", "lon"};
+ private static final String[] QUERY_COLUMN_location = {"location"};
+ private static final String[] QUERY_COLUMN_route = {"route"};
+ private static final String[] QUERY_COLUMN_everything = {"name", "location", "type", "lat", "lon"};
+ private static final String[] QUERY_COLUMN_everything_and_ID = {"ID", "name", "location", "type", "lat", "lon"};
private static String DB_NAME = "stops.sqlite";
private static int DB_VERSION = 1;
private SQLiteDatabase db;
private AtomicInteger openCounter = new AtomicInteger();
public StopsDB(Context context) {
super(context, DB_NAME, null, DB_VERSION);
// WARNING: do not remove the following line, do not save anything in this database, it will be overwritten on every update!
setForcedUpgrade();
// remove old database (BusTo version 1.8.5 and below)
File filename = new File(context.getFilesDir(), "busto.sqlite");
if(filename.exists()) {
//noinspection ResultOfMethodCallIgnored
filename.delete();
}
}
/**
* Through the magic of an atomic counter, the database gets opened and closed without race
* conditions between threads (HOPEFULLY).
*
* @return database or null if cannot be opened
*/
@Nullable
public synchronized SQLiteDatabase openIfNeeded() {
openCounter.incrementAndGet();
this.db = getReadableDatabase();
return this.db;
}
/**
* Through the magic of an atomic counter, the database gets really closed only when no thread
* is using it anymore (HOPEFULLY).
*/
public synchronized void closeIfNeeded() {
// is anybody still using the database or can we close it?
if(openCounter.decrementAndGet() <= 0) {
super.close();
this.db = null;
}
}
public List getRoutesByStop(@NonNull String stopID) {
String[] uselessArray = {stopID};
int count;
Cursor result;
if(this.db == null) {
return null;
}
try {
result = this.db.query("routemap", QUERY_COLUMN_route, "stop = ?", uselessArray, null, null, null);
} catch(SQLiteException e) {
return null;
}
count = result.getCount();
if(count == 0) {
return null;
}
List routes = new ArrayList<>(count);
while(result.moveToNext()) {
routes.add(result.getString(0));
}
result.close();
return routes;
}
public String getNameFromID(@NonNull String stopID) {
String[] uselessArray = {stopID};
int count;
String name;
Cursor result;
if(this.db == null) {
return null;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_name, QUERY_WHERE_ID, uselessArray, null, null, null);
} catch(SQLiteException e) {
return null;
}
count = result.getCount();
if(count == 0) {
return null;
}
result.moveToNext();
name = result.getString(0);
result.close();
return name;
}
public String getLocationFromID(@NonNull String stopID) {
String[] uselessArray = {stopID};
int count;
String name;
Cursor result;
if(this.db == null) {
return null;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_location, QUERY_WHERE_ID, uselessArray, null, null, null);
} catch(SQLiteException e) {
return null;
}
count = result.getCount();
if(count == 0) {
return null;
}
result.moveToNext();
name = result.getString(0);
result.close();
return name;
}
public Stop getAllFromID(@NonNull String stopID) {
Cursor result;
int count;
Stop s;
if(this.db == null) {
return null;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_everything, QUERY_WHERE_ID, new String[] {stopID}, null, null, null);
int colName = result.getColumnIndex("name");
int colLocation = result.getColumnIndex("location");
int colType = result.getColumnIndex("type");
int colLat = result.getColumnIndex("lat");
int colLon = result.getColumnIndex("lon");
count = result.getCount();
if(count == 0) {
return null;
}
result.moveToNext();
Route.Type type = routeTypeFromSymbol(result.getString(colType));
String locationWhichSometimesIsAnEmptyString = result.getString(colLocation);
if(locationWhichSometimesIsAnEmptyString.length() <= 0) {
locationWhichSometimesIsAnEmptyString = null;
}
s = new Stop(stopID, result.getString(colName), null, locationWhichSometimesIsAnEmptyString, type, getRoutesByStop(stopID), result.getDouble(colLat), result.getDouble(colLon));
} catch(SQLiteException e) {
return null;
}
result.close();
return s;
}
/**
* Query some bus stops inside a map view
*
* You can obtain the coordinates from OSMDroid using something like this:
* BoundingBoxE6 bb = mMapView.getBoundingBox();
* double latFrom = bb.getLatSouthE6() / 1E6;
* double latTo = bb.getLatNorthE6() / 1E6;
* double lngFrom = bb.getLonWestE6() / 1E6;
* double lngTo = bb.getLonEastE6() / 1E6;
*/
public Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
Stop[] stops = new Stop[0];
Cursor result;
int count;
// coordinates must be strings in the where condition
String minLatRaw = String.valueOf(minLat);
String maxLatRaw = String.valueOf(maxLat);
String minLngRaw = String.valueOf(minLng);
String maxLngRaw = String.valueOf(maxLng);
String stopID;
Route.Type type;
if(this.db == null) {
return stops;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_everything_and_ID, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null);
int colID = result.getColumnIndex("ID");
int colName = result.getColumnIndex("name");
int colLocation = result.getColumnIndex("location");
int colType = result.getColumnIndex("type");
int colLat = result.getColumnIndex("lat");
int colLon = result.getColumnIndex("lon");
count = result.getCount();
stops = new Stop[count];
int i = 0;
while(result.moveToNext()) {
stopID = result.getString(colID);
type = routeTypeFromSymbol(result.getString(colType));
String locationWhichSometimesIsAnEmptyString = result.getString(colLocation);
if (locationWhichSometimesIsAnEmptyString.length() <= 0) {
locationWhichSometimesIsAnEmptyString = null;
}
- stops[i++] = new Stop(stopID, result.getString(colName), null, locationWhichSometimesIsAnEmptyString, type, getRoutesByStop(stopID), result.getDouble(colLat), result.getDouble(colLon));
+ stops[i++] = new Stop(stopID, result.getString(colName), null,
+ locationWhichSometimesIsAnEmptyString, type, getRoutesByStop(stopID),
+ result.getDouble(colLat), result.getDouble(colLon));
}
} catch(SQLiteException e) {
// TODO: put a warning in the log
return stops;
}
result.close();
return stops;
}
/**
* Get a Route Type from its char symbol
*
* @param route The route symbol (e.g. "B")
* @return The related Route.Type (e.g. Route.Type.Bus)
*/
public static Route.Type routeTypeFromSymbol(String route) {
switch (route) {
case "M":
return Route.Type.METRO;
case "T":
return Route.Type.RAILWAY;
}
// default with case "B"
return Route.Type.BUS;
}
}