diff --git a/app/build.gradle b/app/build.gradle index f3b2cd8..9ab4c32 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,143 +1,143 @@ apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android' apply plugin: 'com.android.application' android { compileSdk 34 namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 21 targetSdkVersion 34 buildToolsVersion = '34.0.0' versionCode 56 versionName "2.1.6" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } testOptions { unitTests.returnDefaultValues = true } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlin { jvmToolchain 17 } lint { abortOnError false } } 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" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.9.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.15.3' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.19.6' // mqtt library implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'com.github.hannesa2:paho.mqtt.android:4.2' //implementation 'com.github.fabmazz:paho.mqtt.android:v0.0.1' // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" - implementation 'de.siegmar:fastcsv:2.0.0' + implementation 'de.siegmar:fastcsv:2.2.2' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index 1b32665..5a3ccc0 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,93 +1,93 @@ /* BusTO - Data components Copyright (C) 2021 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.os.Bundle; import android.util.Log; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.FragmentTransaction; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { - final static String DEBUG_TAG = "ExperimentsGTFS"; + final static String DEBUG_TAG = "ExperimentsActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_experiments); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); actionBar.setIcon(R.drawable.ic_launcher); } if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) /* .add(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs("gtt:4U")) */ //.add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) //.add(R.id.fragment_container_view, IntroFragment.class, IntroFragment.makeArguments(0)) //.commit(); //.add(R.id.fragment_container_view, LinesDetailFragment.class, // LinesDetailFragment.Companion.makeArgs("gtt:4U")) - .add(R.id.fragment_container_view, TestRealtimeGtfsFragment.class, null) + .add(R.id.fragment_container_view, TestSavingFragment.class, null) .commit(); } } @Override public void showFloatingActionButton(boolean yes) { Log.d(DEBUG_TAG, "Asked to show the action button"); } @Override public void readyGUIfor(FragmentKind fragmentType) { Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType); } @Override public void requestArrivalsForStopID(String ID) { } @Override public void showMapCenteredOnStop(Stop stop) { } @Override public void showLineOnMap(String routeGtfsId){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java index 8acd918..9ad512e 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,188 +1,188 @@ /* BusTO - Data components Copyright (C) 2021 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.data; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Notifications; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DBUpdateWorker extends Worker{ public static final String ERROR_CODE_KEY ="Error_Code"; public static final String ERROR_REASON_KEY = "ERROR_REASON"; public static final int ERROR_FETCHING_VERSION = 4; public static final int ERROR_DOWNLOADING_STOPS = 5; public static final int ERROR_DOWNLOADING_LINES = 6; public static final int ERROR_CODE_DB_CLOSED=-2; public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; private final static int NOTIFIC_ID =32198; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @SuppressLint("RestrictedApi") @NonNull @Override public Result doWork() { //register Notification channel final Context con = getApplicationContext(); //Notifications.createDefaultNotificationChannel(con); //Use the new notification channels Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, Notifications.DB_UPDATE_CHANNELS_ID ); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); final int notification_ID = 32198; final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); - final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); + final int current_DB_version = shPr.getInt(PreferencesHolder.DB_GTT_VERSION_KEY,-10); final int new_DB_version = DatabaseUpdate.getNewVersion(); final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); - final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); + final long lastDBUpdateTime = shPr.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, 0); long currentTime = System.currentTimeMillis()/1000; //showNotification(notificationManager, notification_ID); final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, Notifications.DB_UPDATE_CHANNELS_ID) .setContentTitle(con.getString(R.string.database_update_msg_notif)) .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_stilized); notificationManager.notify(notification_ID,builder.build()); Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); /* SKIP CHECK (Reason: The Old API might fail at any moment) if (new_DB_version < 0){ //there has been an error final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) .putInt(ERROR_CODE_KEY,new_DB_version).build(); cancelNotification(notificationID); return ListenableWorker.Result.failure(out); } */ //we got a good version if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) && !isUpdateCompulsory) { //don't need to update cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } //start the real update AtomicReference resultAtomicReference = new AtomicReference<>(); DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ //Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); break; case ERROR_LINES_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; case DB_CLOSED: dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DB_CLOSED); break; } cancelNotification(notification_ID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); //update the version in the shared preference final SharedPreferences.Editor editor = shPr.edit(); - editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); + editor.putInt(PreferencesHolder.DB_GTT_VERSION_KEY, new_DB_version); currentTime = System.currentTimeMillis()/1000; - editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); + editor.putLong(PreferencesHolder.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } public static Constraints getWorkConstraints(){ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build(); } public static WorkRequest newFirstTimeWorkRequest(){ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) //.setInputData(new Data.Builder().putBoolean()) .build(); } /* private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, final String channel_ID){ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); notificManager.notify(notification_ID,builder.build()); return notification_ID; } */ private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.cancel(notificationID); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java index 4036afe..d5df45c 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,330 +1,329 @@ /* BusTO - Data components Copyright (C) 2021 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.data; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.gtfs.GtfsAgency; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.data.gtfs.GtfsFeed; import it.reyboz.bustorino.data.gtfs.GtfsRoute; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.PatternStop; import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; - public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; - public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; + enum Result { DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DB_CLOSED } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } 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 JSON_PARSING_ERROR; } } private static boolean updateGTFSAgencies(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( con, res ); dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); return true; } private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final List routes= MatoAPIFetcher.Companion.getRoutes(con, res); final HashMap> routesStoppingInStop = new HashMap<>(); dao.insertRoutes(routes); if(res.get()!= Fetcher.Result.OK){ return routesStoppingInStop; } final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); final HashMap routesMap = new HashMap<>(routes.size()); for(GtfsRoute r: routes){ gtfsRoutesIDs.add(r.getGtfsId()); routesMap.put(r.getGtfsId(),r); } long t0 = System.currentTimeMillis(); final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); long tend = System.currentTimeMillis() - t0; Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); if(res.get()!=Fetcher.Result.OK){ Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); return routesStoppingInStop; } //match patterns with routes final ArrayList patternStops = makeStopsForPatterns(patterns); final List allPatternsCodeInDB = dao.getPatternsCodes(); final HashSet patternsCodesToDelete = new HashSet<>(allPatternsCodeInDB); for(MatoPattern p: patterns){ //scan patterns final ArrayList stopsIDs = p.getStopsGtfsIDs(); final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId()); if (mRoute == null) { Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map"); } for (final String sID : stopsIDs) { //add stops to pattern stops // save routes stopping in the stop if (!routesStoppingInStop.containsKey(sID)) { routesStoppingInStop.put(sID, new HashSet<>()); } Set mset = routesStoppingInStop.get(sID); assert mset != null; mset.add(mRoute.getShortName()); } //finally, remove from deletion list patternsCodesToDelete.remove(p.getCode()); } // final time for insert dao.insertPatterns(patterns); // clear patterns that are unused Log.d(DEBUG_TAG, "Have to remove "+patternsCodesToDelete.size()+ " patterns from the DB"); dao.deletePatternsWithCodes(new ArrayList<>(patternsCodesToDelete)); dao.insertPatternStops(patternStops); return routesStoppingInStop; } /** * Make the list of stops that each pattern does, to be inserted into the DB * @param patterns the MatoPattern * @return a list of PatternStop */ public static ArrayList makeStopsForPatterns(List patterns){ final ArrayList patternStops = new ArrayList<>(patterns.size()); for (MatoPattern p: patterns){ final ArrayList stopsIDs = p.getStopsGtfsIDs(); for (int i=0; i gres) { // GTFS data fetching AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); updateGTFSAgencies(con, gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); } else{ Log.d(DEBUG_TAG, "Done downloading agencies"); } gtfsRes.set(Fetcher.Result.OK); final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the routes into DB"); } else{ Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); } /*db.beginTransaction(); startTime = System.currentTimeMillis(); int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop); if(countStop!= routesStoppingByStop.size()){ Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with " +routesStoppingByStop.size()+" stops to update"); } db.setTransactionSuccessful(); db.endTransaction(); endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms"); */ // Stops insertion final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading stops"); return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; } final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); final SQLiteDatabase db = dbHelp.getWritableDatabase(); if(!db.isOpen()){ //catch errors like: java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase //we have to abort the work and restart it return Result.DB_CLOSED; } //TODO: Get the type of stop from the lines //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 " + palinasMatoAPI.size() + " stops"); String routesStoppingString=""; int patternsStopsHits = 0; for (final Palina p : palinasMatoAPI) { final ContentValues cv = new ContentValues(); cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); if (p.location != null) cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){ final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID)); routesStoppingString = Palina.buildRoutesStringFromNames(routesSs); patternsStopsHits++; } else{ routesStoppingString = p.routesThatStopHereToString(); } cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString); if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(NextGenDB.Contract.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"); Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); db.close(); dbHelp.close(); return DatabaseUpdate.Result.DONE; } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /** * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now */ public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); final Data reqData = new Data.Builder() .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .setInputData(reqData) .build(); - final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); - final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10); + final int version = theShPr.getInt(PreferencesHolder.DB_GTT_VERSION_KEY, -10); + final long lastDBUpdateTime = theShPr.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, -10); if ((version >= 0 || lastDBUpdateTime >=0) && !restart) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); } /* public static boolean isDBUpdating(){ return false; TODO } */ public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, @NonNull Observer> observer) { WorkManager workManager = WorkManager.getInstance(context); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( lifecycleOwner, observer ); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java index 3581adc..ae3fb99 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java +++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java @@ -1,91 +1,93 @@ /* BusTO - Data components Copyright (C) 2021 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.data; import android.content.Context; import android.content.SharedPreferences; -import android.util.Log; import it.reyboz.bustorino.R; import static android.content.Context.MODE_PRIVATE; import androidx.preference.PreferenceManager; import java.util.HashSet; import java.util.Set; /** * Static class for commonly used SharedPreference operations */ public abstract class PreferencesHolder { public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version"; public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run"; - + public static final String DB_GTT_VERSION_KEY = "NextGenDB.GTTVersion"; + public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; public static final String PREF_FAVORITE_LINES = "pref_favorite_lines"; + public static final Set IGNORE_KEYS_LOAD_MAIN = Set.of(PREF_GTFS_DB_VERSION, PREF_INTRO_ACTIVITY_RUN, DB_GTT_VERSION_KEY, DB_LAST_UPDATE_KEY); + public static SharedPreferences getMainSharedPreferences(Context context){ return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE); } public static SharedPreferences getAppPreferences(Context con){ return PreferenceManager.getDefaultSharedPreferences(con); } public static int getGtfsDBVersion(SharedPreferences pref){ return pref.getInt(PREF_GTFS_DB_VERSION,-1); } public static void setGtfsDBVersion(SharedPreferences pref,int version){ SharedPreferences.Editor ed = pref.edit(); ed.putInt(PREF_GTFS_DB_VERSION,version); ed.apply(); } /** * Check if the introduction activity has been run at least one * @param con the context needed * @return true if it has been run */ public static boolean hasIntroFinishedOneShot(Context con){ final SharedPreferences pref = getMainSharedPreferences(con); return pref.getBoolean(PREF_INTRO_ACTIVITY_RUN, false); } public static boolean addOrRemoveLineToFavorites(Context con, String gtfsLineId, boolean addToFavorites){ final SharedPreferences pref = getMainSharedPreferences(con); final HashSet favorites = new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); boolean modified = true; if(addToFavorites) favorites.add(gtfsLineId); else if(favorites.contains(gtfsLineId)) favorites.remove(gtfsLineId); else modified = false; // we are not changing anything if(modified) { final SharedPreferences.Editor editor = pref.edit(); editor.putStringSet(PREF_FAVORITE_LINES, favorites); editor.apply(); } return modified; } public static HashSet getFavoritesLinesGtfsIDs(Context con){ final SharedPreferences pref = getMainSharedPreferences(con); return new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java index cc0a09c..a4c1742 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java @@ -1,326 +1,394 @@ /* 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.data; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.content.Context; import android.net.Uri; import android.util.Log; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.io.IOException; +import java.util.*; +import de.siegmar.fastcsv.reader.CloseableIterator; +import de.siegmar.fastcsv.reader.CsvReader; +import de.siegmar.fastcsv.reader.CsvRow; +import de.siegmar.fastcsv.writer.CsvWriter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsDBInterface; public class UserDB extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "user.db"; static final String TABLE_NAME = "favorites"; private final Context c; // needed during upgrade + public final static String COL_ID = "ID"; + public final static String COL_USERNAME="username"; + + public static final int FILE_INVALID=-10; private final static String[] usernameColumnNameAsArray = {"username"}; - public final static String[] getFavoritesColumnNamesAsArray = {"ID", "username"}; + public final static String[] getFavoritesColumnNamesAsArray = {COL_ID, COL_USERNAME}; private static final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( AppDataProvider.FAVORITES).build(); public UserDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.c = context; } @Override public void onCreate(SQLiteDatabase db) { // exception intentionally left unhandled db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)"); if(OldDB.doesItExist(this.c)) { upgradeFromOldDatabase(db); } } private void upgradeFromOldDatabase(SQLiteDatabase newdb) { OldDB old; try { old = new OldDB(this.c); } catch(IllegalStateException e) { // can't create database => it doesn't really exist, no matter what doesItExist() says return; } int ver = old.getOldVersion(); /* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app * has crashed midway through the upgrade and the user is retrying, that should never show * up here. And if it does, try to recover favorites anyway. * Versions < 8 already got dropped during the update process, so let's do the same. * * Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it * more complicated. Workaround added in OldDB. */ if(ver >= 8) { ArrayList ID = new ArrayList<>(); ArrayList username = new ArrayList<>(); int len; int len2; try { Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {}); int zero = c.getColumnIndex("busstop_ID"); int one = c.getColumnIndex("busstop_username"); while(c.moveToNext()) { try { ID.add(c.getString(zero)); } catch(Exception e) { // no ID = can't add this continue; } if(c.getString(one) == null || c.getString(one).length() <= 0) { username.add(null); } else { username.add(c.getString(one)); } } c.close(); old.close(); } catch(Exception ignored) { // there's no hope, go ahead and nuke old database. } len = ID.size(); len2 = username.size(); if(len2 < len) { len = len2; } if (len > 0) { try { for (int i = 0; i < len; i++) { final Stop mStop = new Stop(ID.get(i)); mStop.setStopUserName(username.get(i)); addOrUpdateStop(mStop, newdb); } } catch(Exception ignored) { // partial data is better than no data at all, no transactions here } } } if(!OldDB.destroy(this.c)) { // TODO: notify user somehow? Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user."); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } /** * Check if a stop ID is in the favorites * * @param db readable database * @param stopId stop ID * @return boolean */ public static boolean isStopInFavorites(SQLiteDatabase db, String stopId) { boolean found = false; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopId}, null, null, null); if(c.moveToNext()) { found = true; } c.close(); } catch(SQLiteException ignored) { // don't care } return found; } /** * Gets stop name set by the user. * * @param db readable database * @param stopID stop ID * @return name set by user, or null if not set\not found */ public static String getStopUserName(SQLiteDatabase db, String stopID) { String username = null; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null); if(c.moveToNext()) { int userNameIndex = c.getColumnIndex("username"); if (userNameIndex>=0) username = c.getString(userNameIndex); } c.close(); } catch(SQLiteException ignored) {} return username; } /** * Get all the bus stops marked as favorites * * @param db * @param dbi * @return */ public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) { List l = new ArrayList<>(); Stop s; String stopID, stopUserName; try { Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null); int colID = c.getColumnIndex("ID"); int colUser = c.getColumnIndex("username"); while(c.moveToNext()) { stopUserName = c.getString(colUser); stopID = c.getString(colID); s = dbi.getAllFromID(stopID); if(s == null) { // can't find it in database l.add(new Stop(stopUserName, stopID, null, null, null)); } else { // setStopName() already does sanity checks s.setStopUserName(stopUserName); l.add(s); } } c.close(); } catch(SQLiteException ignored) {} // comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database Collections.sort(l); return l; } public static void notifyContentProvider(Context context){ context. getContentResolver(). notifyChange(FAVORITES_URI, null); } public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){ List colsList = Arrays.asList(columns); if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){ throw new IllegalArgumentException(); } ArrayList l = new ArrayList<>(); if (cursor==null){ Log.e("UserDB-BusTO", "Null cursor given in getFavoritesFromCursor"); return l; } final int colID = cursor.getColumnIndex("ID"); final int colUser = cursor.getColumnIndex("username"); while(cursor.moveToNext()) { final String stopUserName = cursor.getString(colUser); final String stopID = cursor.getString(colID); final Stop s = new Stop(stopID.trim()); if (stopUserName!=null) s.setStopUserName(stopUserName); l.add(s); } return l; } public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { ContentValues cv = new ContentValues(); long result = -1; String un = s.getStopUserName(); cv.put("ID", s.ID); // is there an username? if(un == null) { // no: see if it's in the database cv.put("username", getStopUserName(db, s.ID)); } else { // yes: use it cv.put("username", un); } try { //ignore and throw -1 if the row is already in the DB result = db.insertWithOnConflict(TABLE_NAME, null, cv,SQLiteDatabase.CONFLICT_IGNORE); } catch (SQLiteException ignored) {} // Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return return (result != -1) || updateStop(s, db); } public static boolean updateStop(Stop s, SQLiteDatabase db) { try { ContentValues cv = new ContentValues(); cv.put("username", s.getStopUserName()); db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean deleteStop(Stop s, SQLiteDatabase db) { try { db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean checkStopInFavorites(String stopID, Context con){ boolean found = false; // no stop no party if (stopID != null) { SQLiteDatabase userDB = new UserDB(con).getReadableDatabase(); found = UserDB.isStopInFavorites(userDB, stopID); } return found; } + + //extract rows into CSV + public boolean writeFavoritesToCsv(CsvWriter writer){ + SQLiteDatabase db = this.getReadableDatabase(); + + String sortOrder = + COL_ID + " DESC"; + Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder); + + final int nCols = 2;//cursor.getColumnCount(); + writer.writeRow(cursor.getColumnNames()); + while (cursor.moveToNext()){ + String[] arr = {cursor.getString(0), cursor.getString(1)}; + writer.writeRow(arr); + } + cursor.close(); + return true; + } + + public int insertRowsFromCSV(CsvReader reader){ + SQLiteDatabase db = this.getWritableDatabase(); + + boolean firstrow = true; + final HashMap colIndexByRows = new HashMap<>(); + + final CloseableIterator rowsIter = reader.iterator(); + if (!rowsIter.hasNext()){ + //nothing to do, it's an empty file + return -1; + } + final CsvRow firstRow = rowsIter.next(); + // close if there isn't another rows + if(!rowsIter.hasNext()) return -2; + for (int i =0; i= 0) + updated +=1; + } + db.setTransactionSuccessful(); + db.endTransaction(); + + db.close(); + + return updated; + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/TestSavingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/TestSavingFragment.kt new file mode 100644 index 0000000..daf62c6 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/TestSavingFragment.kt @@ -0,0 +1,297 @@ +package it.reyboz.bustorino.fragments + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckBox +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import de.siegmar.fastcsv.reader.CsvReader +import de.siegmar.fastcsv.writer.CsvWriter +import it.reyboz.bustorino.R +import it.reyboz.bustorino.data.PreferencesHolder +import it.reyboz.bustorino.data.UserDB +import it.reyboz.bustorino.util.Saving +import java.io.* +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + + +/** + * A simple [Fragment] subclass. + * Use the [TestSavingFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class TestSavingFragment : Fragment() { + + private val saveFileLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.also { uri -> + writeDataZip(uri) + } + } + } + + private val openFileLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (!(loadFavorites|| loadPreferences)){ + Toast.makeText(context, R.string.message_check_at_least_one, Toast.LENGTH_SHORT).show() + } + else if (result.resultCode == Activity.RESULT_OK) { + + result.data?.data?.also { uri -> + + loadZipData(uri,loadFavorites, loadPreferences) + } + } + } + + + private lateinit var saveButton: Button + private var loadFavorites = true + private var loadPreferences = true + private lateinit var checkFavorites: CheckBox + private lateinit var checkPreferences: CheckBox + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + /*arguments?.let { + param1 = it.getString(ARG_PARAM1) + param2 = it.getString(ARG_PARAM2) + }*/ + } + + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val rootview= inflater.inflate(R.layout.fragment_test_saving, container, false) + + saveButton = rootview.findViewById(R.id.saveButton) + saveButton.setOnClickListener { + startFileSaveIntent() + } + checkFavorites = rootview.findViewById(R.id.favoritesCheckBox) + checkFavorites.setOnCheckedChangeListener { _, isChecked -> + loadFavorites = isChecked + + } + checkPreferences = rootview.findViewById(R.id.preferencesCheckBox) + checkPreferences.setOnCheckedChangeListener { _, isChecked -> + loadPreferences = isChecked + + } + val readFavoritesButton = rootview.findViewById