diff --git a/AndroidManifest.xml b/AndroidManifest.xml
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2,8 +2,6 @@
-
-
@@ -14,13 +12,19 @@
+ android:name=".BustoApp"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/networks_security_config"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:theme="@style/AppTheme.NoActionBar">
+
+
+
+ android:name=".ActivityMap"
+ android:label="@string/title_activity_map"
+ android:parentActivityName=".ActivityMain"
+ android:theme="@style/MapTheme">
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".ActivityMain"/>
-
+ android:label="@string/app_name"
+ android:screenOrientation="portrait">
-
-
+
@@ -132,4 +134,4 @@
-
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
--- a/build.gradle
+++ b/build.gradle
@@ -6,11 +6,9 @@
}
- dependencies {
-
- classpath 'com.android.tools.build:gradle:4.1.3'
- }
ext {
+ //multidex
+ multidex_version = "2.0.1"
//libraries versions
fragment_version = "1.3.6"
activity_version = "1.2.4"
@@ -21,8 +19,17 @@
acra_version = "5.7.0"
lifecycle_version = "2.3.1"
arch_version = "2.1.0"
+ room_version = "2.3.0"
+ //kotlin
+ kotlin_version = '1.5.0'
+ coroutines_version = "1.5.0"
}
+ dependencies {
+
+ classpath 'com.android.tools.build:gradle:4.1.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
}
allprojects {
repositories {
@@ -34,6 +41,8 @@
}
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
@@ -41,11 +50,12 @@
defaultConfig {
applicationId "it.reyboz.bustorino"
- minSdkVersion 14
+ minSdkVersion 16
targetSdkVersion 29
versionCode 35
versionName "1.15.4"
vectorDrawables.useSupportLibrary = true
+ multiDexEnabled true
}
compileOptions {
@@ -91,8 +101,8 @@
implementation "androidx.preference:preference:$preference_version"
implementation "androidx.work:work-runtime:$work_version"
-
- implementation "com.google.android.material:material:1.3.0"
+ implementation "com.google.android.material:material:1.4.0"
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'org.jsoup:jsoup:1.13.1'
@@ -113,5 +123,18 @@
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"
+ // Room components
+ implementation "androidx.room:room-ktx:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
+ androidTestImplementation "androidx.room:room-testing:$room_version"
+ //multidex - we need this to build the app
+ implementation "androidx.multidex:multidex:$multidex_version"
}
}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+}
\ No newline at end of file
diff --git a/res/layout/activity_experiments.xml b/res/layout/activity_experiments.xml
new file mode 100644
--- /dev/null
+++ b/res/layout/activity_experiments.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/extra_menu_items.xml b/res/menu/principal_menu.xml
rename from res/menu/extra_menu_items.xml
rename to res/menu/principal_menu.xml
--- a/res/menu/extra_menu_items.xml
+++ b/res/menu/principal_menu.xml
@@ -21,4 +21,9 @@
android:orderInCategory="8"
android:title="@string/action_licence"
app:showAsAction="never" />
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -157,7 +157,8 @@
storage
The application has crashed because you encountered a bug.
\nIf you want, you can help the developers by sending the crash report via email.
- \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state.
+ \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app
+ configuration/state.
The application crashed and the crash report is in the attachments. Please
describe what you were doing before the crash: \n
@@ -171,4 +172,6 @@
Buy us a coffee
Map
Search by stop
+
+
diff --git a/res/values/theme.xml b/res/values/theme.xml
--- a/res/values/theme.xml
+++ b/res/values/theme.xml
@@ -21,4 +21,6 @@
+
+
\ No newline at end of file
diff --git a/res/xml/networks_security_config.xml b/res/xml/networks_security_config.xml
--- a/res/xml/networks_security_config.xml
+++ b/res/xml/networks_security_config.xml
@@ -3,6 +3,6 @@
5t.torino.it
gtt.to.it
-
+ comune.torino.it
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivityExperiments.java b/src/it/reyboz/bustorino/ActivityExperiments.java
new file mode 100644
--- /dev/null
+++ b/src/it/reyboz/bustorino/ActivityExperiments.java
@@ -0,0 +1,89 @@
+package it.reyboz.bustorino;
+
+import android.os.AsyncTask;
+import android.util.Log;
+import android.view.View;
+import androidx.appcompat.app.AppCompatActivity;
+import android.os.Bundle;
+import it.reyboz.bustorino.backend.Fetcher;
+import it.reyboz.bustorino.backend.gtfs.GtfsDataParser;
+import it.reyboz.bustorino.backend.networkTools;
+import it.reyboz.bustorino.backend.utils;
+import it.reyboz.bustorino.middleware.GeneralActivity;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class ActivityExperiments extends GeneralActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_experiments);
+ }
+
+ public void runExp(View v){
+
+ Thread th = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ final String DEBUG_TAG = "ExperimentsGTFS";
+ AtomicReference res = new AtomicReference<>();
+ //List files = GtfsDataParser.readFilesList(res);
+ Date updateDate = GtfsDataParser.getLastGTFSUpdateDate(res);
+ Log.w(
+ "ExperimentGTFS", "Last update date is "+updateDate//utils.joinList(files, "\n")
+ );
+
+ File saveFile = new File(getFilesDir(), "gtfs_data.zip");
+ if(!saveFile.isDirectory() && saveFile.exists()){
+ Log.w(DEBUG_TAG, "Zip exists: "+saveFile);
+ try (FileInputStream fileStream = new FileInputStream(saveFile);) {
+ ZipInputStream stream = new ZipInputStream(fileStream);
+ // now iterate through each item in the stream. The get next
+ // entry call will return a ZipEntry for each file in the
+ // stream
+ ZipEntry entry;
+ while ((entry = stream.getNextEntry()) != null) {
+ String s = String.format(Locale.ENGLISH, "Entry: %s len %d added",
+ entry.getName(),
+ entry.getSize()
+ );
+ Log.d(DEBUG_TAG,s);
+
+ // Once we get the entry from the stream, the stream is
+ // positioned read to read the raw data, and we keep
+ // reading until read returns 0 or less.
+ //result.add(entry.getName());
+ }
+ stream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ //saveFile.delete();
+ } else
+ try {
+ networkTools.saveFileInCache(saveFile,new URL(GtfsDataParser.GTFS_ADDRESS));
+ Log.w(DEBUG_TAG, "File saved");
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ th.start();
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java
--- a/src/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/src/it/reyboz/bustorino/ActivityPrincipal.java
@@ -23,6 +23,7 @@
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
+import androidx.preference.PreferenceManager;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
@@ -300,7 +301,11 @@
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.extra_menu_items, menu);
+ getMenuInflater().inflate(R.menu.principal_menu, menu);
+ MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments);
+ SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
+ experimentsMenuItem.setVisible(exper_On);
return super.onCreateOptionsMenu(menu);
}
@@ -569,6 +574,8 @@
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext);
return true;
+ case R.id.action_experiments:
+ startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class));
default:
}
return false;
diff --git a/src/it/reyboz/bustorino/BustoApp.java b/src/it/reyboz/bustorino/BustoApp.java
--- a/src/it/reyboz/bustorino/BustoApp.java
+++ b/src/it/reyboz/bustorino/BustoApp.java
@@ -1,8 +1,8 @@
package it.reyboz.bustorino;
-import android.app.Application;
import android.content.Context;
+import androidx.multidex.MultiDexApplication;
import org.acra.ACRA;
import org.acra.BuildConfig;
import org.acra.ReportField;
@@ -14,7 +14,7 @@
import static org.acra.ReportField.*;
-public class BustoApp extends Application {
+public class BustoApp extends MultiDexApplication {
private static final ReportField[] REPORT_FIELDS = {REPORT_ID, APP_VERSION_CODE, APP_VERSION_NAME,
PACKAGE_NAME, PHONE_MODEL, BRAND, PRODUCT, ANDROID_VERSION, BUILD_CONFIG, CUSTOM_DATA,
IS_SILENT, STACK_TRACE, INITIAL_CONFIGURATION, CRASH_CONFIGURATION, DISPLAY, USER_COMMENT,
diff --git a/src/it/reyboz/bustorino/backend/Fetcher.java b/src/it/reyboz/bustorino/backend/Fetcher.java
--- a/src/it/reyboz/bustorino/backend/Fetcher.java
+++ b/src/it/reyboz/bustorino/backend/Fetcher.java
@@ -31,6 +31,7 @@
* QUERY_TOO_SHORT: input more characters and retry.
*/
enum Result {
- OK, CLIENT_OFFLINE, SERVER_ERROR, SETUP_ERROR,PARSER_ERROR, EMPTY_RESULT_SET, QUERY_TOO_SHORT,SERVER_ERROR_404
+ OK, CLIENT_OFFLINE, SERVER_ERROR, SETUP_ERROR,PARSER_ERROR, EMPTY_RESULT_SET, QUERY_TOO_SHORT, SERVER_ERROR_404,
+ CONNECTION_ERROR
}
}
diff --git a/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java b/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java
new file mode 100644
--- /dev/null
+++ b/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java
@@ -0,0 +1,142 @@
+package it.reyboz.bustorino.backend.gtfs;
+
+import android.util.Log;
+import it.reyboz.bustorino.backend.Fetcher;
+import it.reyboz.bustorino.backend.networkTools;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+abstract public class GtfsDataParser {
+ public static final String GTFS_ADDRESS="https://www.gtt.to.it/open_data/gtt_gtfs.zip";
+ public static final String GTFS_PAGE_ADDRESS="http://aperto.comune.torino.it/dataset/feed-gtfs-trasporti-gtt";
+
+ private static final String DEBUG_TAG = "BusTO-GTFSDataParser";
+ /**
+ * First trial for a function to download the zip
+ * @param res Fetcher.result
+ * @return the list of files inside the ziè
+ */
+ public static ArrayList readFilesList(AtomicReference res){
+
+ HttpURLConnection urlConnection;
+ InputStream in;
+ ArrayList result = new ArrayList<>();
+ try {
+ final URL gtfsUrl = new URL(GTFS_ADDRESS);
+ urlConnection = (HttpURLConnection) gtfsUrl.openConnection();
+ } catch(IOException e) {
+ //e.printStackTrace();
+ res.set(Fetcher.Result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY.
+ return null;
+ }
+ urlConnection.setConnectTimeout(4000);
+ urlConnection.setReadTimeout(50*1000);
+
+ 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;
+
+ }
+ try (ZipInputStream stream = new ZipInputStream(in)) {
+
+ // now iterate through each item in the stream. The get next
+ // entry call will return a ZipEntry for each file in the
+ // stream
+ ZipEntry entry;
+ while ((entry = stream.getNextEntry()) != null) {
+ String s = String.format(Locale.ENGLISH, "Entry: %s len %d added",
+ entry.getName(),
+ entry.getSize()
+ );
+ System.out.println(s);
+
+ // Once we get the entry from the stream, the stream is
+ // positioned read to read the raw data, and we keep
+ // reading until read returns 0 or less.
+ result.add(entry.getName());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // we must always close the zip file.
+ return result;
+ }
+
+ public static Date getLastGTFSUpdateDate(AtomicReference res) {
+ URL theURL;
+ final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+ //final Date baseDate = dateFormat.parse("1970-00-00T00:00:00+0000");
+ final Date nullDate = new Date(0);
+ try{
+ theURL = new URL(GTFS_PAGE_ADDRESS);
+ } catch (IOException ex){
+ Log.e(DEBUG_TAG, "Fixed URL is null, this is a real issue");
+ return nullDate;
+ }
+ res.set(Fetcher.Result.OK);
+ final String fullPageDOM = networkTools.getDOM(theURL, res);
+ if(fullPageDOM== null){
+ //Something wrong happend
+ Log.e(DEBUG_TAG, "Cannot get URL");
+ return nullDate;
+ }
+ res.set(Fetcher.Result.OK);
+ Document doc = Jsoup.parse(fullPageDOM);
+
+ Elements sections = doc.select("section.additional-info");
+ Date finalDate = new Date(0);
+ for (Element sec: sections){
+ Element head = sec.select("h3").first();
+ String headTitle = head.text();
+ if(!headTitle.trim().toLowerCase(Locale.ITALIAN).equals("informazioni supplementari"))
+ continue;
+
+ for (Element row: sec.select("tr")){
+ if(!row.selectFirst("th").text().trim()
+ .toLowerCase(Locale.ITALIAN).equals("ultimo aggiornamento"))
+ continue;
+
+ Attributes spanAttributes = row.selectFirst("td > span").attributes();
+ String dateAsString = spanAttributes.get("data-datetime");
+ try {
+ finalDate = dateFormat.parse(dateAsString);
+ return finalDate;
+ }catch (ParseException ex){
+ Log.e(DEBUG_TAG, "Wrong date for the last update of GTFS Data: "+dateAsString);
+ res.set(Fetcher.Result.PARSER_ERROR);
+ ex.printStackTrace();
+ }
+ break;
+ }
+ }
+ res.set(Fetcher.Result.PARSER_ERROR);
+ return finalDate;
+
+ }
+}
diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java
--- a/src/it/reyboz/bustorino/backend/networkTools.java
+++ b/src/it/reyboz/bustorino/backend/networkTools.java
@@ -18,22 +18,20 @@
package it.reyboz.bustorino.backend;
+import android.content.Context;
import androidx.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.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.util.ArrayList;
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) {
+ public static String getDOM(final URL url, final AtomicReference res) {
//Log.d("asyncwget", "Catching URL in background: " + uri[0]);
HttpURLConnection urlConnection;
StringBuilder result = null;
@@ -70,6 +68,52 @@
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();
}
+
+ public static Fetcher.Result saveFileInCache(File outputFile, URL url) {
+ HttpURLConnection urlConnection;
+ try {
+ urlConnection = (HttpURLConnection) url.openConnection();
+ } catch (IOException e) {
+ //e.printStackTrace();
+ return Fetcher.Result.CONNECTION_ERROR;
+ }
+ urlConnection.setConnectTimeout(4000);
+ urlConnection.setReadTimeout(50 * 1000);
+
+ Log.d("BusTO net Tools", "Download file "+url);
+ try (InputStream inputStream = urlConnection.getInputStream()) {
+ //File outputFile = new File(con.getFilesDir(), fileName);
+ //BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
+ FileOutputStream outputStream = new FileOutputStream(outputFile);
+ byte buffer[] = new byte[16384];
+ boolean inProgress = true;
+ while(inProgress){
+ int numread = inputStream.read(buffer);
+ inProgress = (numread > 0);
+ if(inProgress) outputStream.write(buffer, 0, numread);
+ }
+ outputStream.close();
+ //while (bufferedInputStream.available())
+ } catch (IOException e) {
+ e.printStackTrace();
+ try {
+ final Fetcher.Result res;
+ if(urlConnection.getResponseCode()==404)
+ res= Fetcher.Result.SERVER_ERROR_404;
+ else if(urlConnection.getResponseCode()!=200)
+ res= Fetcher.Result.SERVER_ERROR;
+ else res= Fetcher.Result.PARSER_ERROR;
+ urlConnection.disconnect();
+ return res;
+ } catch (IOException ioException) {
+ ioException.printStackTrace();
+ urlConnection.disconnect();
+ return Fetcher.Result.PARSER_ERROR;
+ }
+ }
+ urlConnection.disconnect();
+ return Fetcher.Result.OK;
+ }
@Nullable
static String queryURL(URL url, AtomicReference res){
return queryURL(url,res,null);
diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java
--- a/src/it/reyboz/bustorino/backend/utils.java
+++ b/src/it/reyboz/bustorino/backend/utils.java
@@ -8,10 +8,12 @@
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
+import androidx.annotation.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
+import java.util.List;
public abstract class utils {
private static final double EarthRadius = 6371e3;
@@ -153,4 +155,17 @@
return "Trace too Short.";
}
*/
+ public static String joinList(@Nullable List dat, String separator){
+ StringBuilder sb = new StringBuilder();
+ if(dat==null || dat.size()==0)
+ return "";
+ else if(dat.size()==1)
+ return dat.get(0);
+ sb.append(dat.get(0));
+ for (int i=1; i>
+
+ @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID")
+ fun getStopByStopID(queryID: String): LiveData>
+
+ @Query("SELECT * FROM "+GtfsShape.DB_TABLE+
+ " WHERE "+GtfsShape.COL_SHAPEID+" LIKE :shapeID"+
+ " ORDER BY "+GtfsShape.COL_POINT_SEQ+ " ASC"
+ )
+ fun getShapeByID(shapeID: String) : LiveData>
+
+ @Transaction
+ open fun clearAndInsertRoutes(routes: List){
+ deleteAllRoutes()
+ insertRoutes(routes)
+ }
+
+ @Insert
+ fun insertRoutes(users: List)
+ @Insert
+ fun insertStops(stops: List)
+
+
+ @Query("DELETE FROM "+GtfsRoute.DB_TABLE)
+ fun deleteAllRoutes()
+ @Query("DELETE FROM "+GtfsStop.DB_TABLE)
+ fun deleteAllStops()
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ fun updateShapes(shapes: List) : Int
+
+ @Transaction
+ fun updateStops(stops: List){
+ deleteAllStops()
+ insertStops(stops)
+ }
+}
\ No newline at end of file