commit ab06076a77c5698c8c34bc98c9f34736593d032e Author: Fabio Mazza Date: Thu May 14 12:18:25 2026 +0200 Fix many bugs in map, favorites and snackbar, revise and modernize DB Summary: - Fix location handle not released - Fix snackbar text color - Rewrite many parts of the database to produce LiveData directly - Close the app after back press Fix T1439 Fix T1434 Fix T1437 Reviewers: #libre_busto_hackers, valerio.bozzolan Reviewed By: #libre_busto_hackers, valerio.bozzolan Subscribers: rosto, valerio.bozzolan Project Tags: #libre_busto Maniphest Tasks: T1439, T1434, T1437 Differential Revision: https://gitpull.it/D239 diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index 1c90d03..eecec7f 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -53,7 +53,6 @@ import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateCheckWorker; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.PreferencesHolder; -import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel; @@ -74,10 +73,22 @@ public class ActivityPrincipal extends GeneralActivity implements FragmentListen private boolean onCreateComplete = false; private ServiceAlertsViewModel serviceAlertsViewModel; - private final OnBackPressedCallback callback = new OnBackPressedCallback(false) { + + private long lastClosingAttempt = -1L; + private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { - activityCustomBackPressed(); + boolean isResolved = activityCustomBackPressed(); + Log.d(DEBUG_TAG, "backpress resolved: " + isResolved); + if(!isResolved){ + long currentTime = System.currentTimeMillis(); + if(currentTime - lastClosingAttempt < 2000){ + finish(); + } else{ + lastClosingAttempt = currentTime; + Toast.makeText(getApplicationContext(),R.string.back_again_to_close,Toast.LENGTH_SHORT).show(); + } + } } }; @@ -95,8 +106,8 @@ public class ActivityPrincipal extends GeneralActivity implements FragmentListen */ //onBackPressed solution required from Android 16 - callback.setEnabled(true); - this.getOnBackPressedDispatcher().addCallback( callback); + backPressedCallback.setEnabled(true); + this.getOnBackPressedDispatcher().addCallback(backPressedCallback); boolean showingArrivalsFromIntent = false; final Toolbar mToolbar = findViewById(R.id.default_toolbar); @@ -218,7 +229,7 @@ public class ActivityPrincipal extends GeneralActivity implements FragmentListen } } if (showProgress) { - createDefaultSnackbar(); + createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); @@ -493,8 +504,7 @@ public class ActivityPrincipal extends GeneralActivity implements FragmentListen * Create and show the SnackBar with the message * The fragment shown points to which view to attach the snackbar */ - private void createDefaultSnackbar() { - + private void createDatabaseUpdateSnackbar() { View baseView = null; boolean showSnackbar = true; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); @@ -505,11 +515,13 @@ public class ActivityPrincipal extends GeneralActivity implements FragmentListen if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); //if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); if (baseView !=null && showSnackbar) { - this.snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); + snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); + snackbar.setTextColor(getColor(android.R.color.white)); + snackbar.setBackgroundTint(getColor(R.color.grey_800)); if (frag instanceof ScreenBaseFragment){ - ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(this.snackbar); + ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(snackbar); } - this.snackbar.show(); + snackbar.show(); } else{ Log.e(DEBUG_TAG, "Asked to show the snackbar but the baseView is null"); diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Result.java b/app/src/main/java/it/reyboz/bustorino/backend/Result.java index c420425..5beb94e 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Result.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Result.java @@ -1,5 +1,6 @@ package it.reyboz.bustorino.backend; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,7 +15,7 @@ public class Result { } - public static Result success(@Nullable T result) { + public static Result success(@NonNull T result) { return new Result<>(result); } @@ -25,7 +26,7 @@ public class Result { return new Result<>(error); } - private Result(@Nullable T result) { + private Result(@NonNull T result) { this.result = result; this.exception = null; } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/StopFavoritesData.kt b/app/src/main/java/it/reyboz/bustorino/backend/StopFavoritesData.kt new file mode 100644 index 0000000..022a3d9 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/StopFavoritesData.kt @@ -0,0 +1,18 @@ +package it.reyboz.bustorino.backend + +import android.util.Log + +data class StopFavoritesData( + val stopID: String, + val stopUserName: String? = null +) { + constructor(s: Stop) : this(s.ID,s.stopUserName) + + fun addToStop(s: Stop) { + if(s.ID!=stopID){ + Log.e("BusTO-FavoritesData", "Trying to add info to stop with different ID") + } else{ + s.stopUserName =stopUserName + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java b/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java index a813606..310be77 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java +++ b/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java @@ -28,13 +28,12 @@ import android.util.Log; import androidx.annotation.Nullable; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.DBStatusManager; -import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.util.List; -import static it.reyboz.bustorino.data.UserDB.getFavoritesColumnNamesAsArray; +import static it.reyboz.bustorino.data.UserDB.FAVORITES_COLUMNS_ARRAY; public class AppDataProvider extends ContentProvider { @@ -249,7 +248,7 @@ public class AppDataProvider extends ContentProvider { } case FAVORITES_OP: - final String stopFavSelection = getFavoritesColumnNamesAsArray[0]+" = ?"; + final String stopFavSelection = FAVORITES_COLUMNS_ARRAY[0]+" = ?"; db = userDBHelper.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); diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt index c4a5a1f..5a60bc7 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt @@ -19,8 +19,6 @@ package it.reyboz.bustorino.data import android.annotation.SuppressLint import android.content.Context -import android.content.pm.ServiceInfo -import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -83,7 +81,7 @@ class DBUpdateWorker(context: Context, workerParams: WorkerParameters) : Corouti when (resultUpdate) { DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD -> dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS) DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD -> dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES) - DatabaseUpdate.Result.DB_CLOSED -> dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DB_CLOSED) + DatabaseUpdate.Result.DATABASE_ERROR -> dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DATABASE) DatabaseUpdate.Result.DONE -> {} } //cancelNotification(NOTIFICATION_ID) @@ -151,7 +149,7 @@ class DBUpdateWorker(context: Context, workerParams: WorkerParameters) : Corouti const val ERROR_FETCHING_VERSION: Int = 4 const val ERROR_DOWNLOADING_STOPS: Int = 5 const val ERROR_DOWNLOADING_LINES: Int = 6 - val ERROR_CODE_DB_CLOSED: Int = -2 + val ERROR_CODE_DATABASE: Int = -2 const val SUCCESS_REASON_KEY: String = "SUCCESS_REASON" const val SUCCESS_NO_ACTION_NEEDED: Int = 9 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 92824e0..4d818df 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -17,10 +17,8 @@ */ 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; @@ -58,9 +56,9 @@ public class DatabaseUpdate { - + //todo: do something with this enum to show the user public enum Result { - DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DB_CLOSED + DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DATABASE_ERROR } /** @@ -219,62 +217,12 @@ public class DatabaseUpdate { } 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"); - // These should NOT be closed: the database is a singleton, the connections are recycled. - //db.close(); - //dbHelp.close(); - - return DatabaseUpdate.Result.DONE; + //final SQLiteDatabase db = dbHelp.getWritableDatabase(); + boolean done= dbHelp.updateDataStops(palinasMatoAPI, routesStoppingByStop); + if(done) + return DatabaseUpdate.Result.DONE; + else + return Result.DATABASE_ERROR; } public static boolean setDBUpdatingFlag(Context con, boolean value){ diff --git a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java index 8a88895..a5f9819 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java +++ b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; +import android.database.DatabaseErrorHandler; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -33,7 +34,6 @@ import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -75,7 +75,7 @@ public class FavoritesLiveData extends LiveData> implements CustomAsy } - private void loadData() { + public void loadData() { loadData(false); } private static Uri.Builder getStopsBuilder(){ @@ -100,9 +100,19 @@ public class FavoritesLiveData extends LiveData> implements CustomAsy } isQueryRunning = true; - queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null); + startQuery(); + } + + private void startQuery(){ + Log.d(TAG, "startQuery for token "+FAV_TOKEN); + queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.FAVORITES_COLUMNS_ARRAY, null, null, null); + + } + public void stopQuery(){ + queryHandler.cancelOperation(FAV_TOKEN); + isQueryRunning = false; } public void forceReload(){ @@ -140,17 +150,24 @@ public class FavoritesLiveData extends LiveData> implements CustomAsy @Override public void onQueryComplete(int token, Object cookie, Cursor cursor) { if (cursor == null){ - //Nothing to do Log.e(TAG, "Null cursor for token "+token); + if(token == FAV_TOKEN){ + //restart query + Log.d(TAG, "Restarting query"); + queryHandler.cancelOperation(FAV_TOKEN); + + isQueryRunning = false; + loadData(true); + } return; } if (token == FAV_TOKEN) { - stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.getFavoritesColumnNamesAsArray); + stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.FAVORITES_COLUMNS_ARRAY); cursor.close(); //reset counters stopNeededCount = stopsFromFavorites.size(); stopsDone = new ArrayList<>(); - if(stopsFromFavorites.size() == 0){ + if(stopsFromFavorites.isEmpty()){ //we don't need to call the other query setValue(stopsDone); isQueryRunning = false; diff --git a/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java deleted file mode 100644 index e71f98f..0000000 --- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package it.reyboz.bustorino.data; - -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; - -import java.util.List; - -import it.reyboz.bustorino.backend.Stop; - -public class FavoritesViewModel extends AndroidViewModel { - - FavoritesLiveData favoritesLiveData; - - public FavoritesViewModel(@NonNull Application application) { - super(application); - //appContext = application.getApplicationContext(); - } - - @Override - protected void onCleared() { - favoritesLiveData.onClear(); - super.onCleared(); - } - - public FavoritesLiveData getFavorites(){ - if (favoritesLiveData==null){ - favoritesLiveData= new FavoritesLiveData(getApplication(), true); - } - return favoritesLiveData; - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/data/InvalidationTracker.kt b/app/src/main/java/it/reyboz/bustorino/data/InvalidationTracker.kt new file mode 100644 index 0000000..402a088 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/InvalidationTracker.kt @@ -0,0 +1,34 @@ +package it.reyboz.bustorino.data + +import android.util.Log +import it.reyboz.bustorino.BuildConfig + +/** + * Invalidation tracker to use to make auto-updating LiveData from SQLite database + */ +class InvalidationTracker { + private val tableObservers = mutableMapOf>() + + fun addObserver(table: String, onInvalidate: Observer) { + tableObservers.getOrPut(table) { mutableSetOf() }.add(onInvalidate) + } + + fun removeObserver(table: String, onInvalidate: Observer) { + tableObservers[table]?.remove(onInvalidate) + } + + fun notifyInvalidation(vararg tables: String) { + if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "gitpull") + Log.d(DEBUG_TAG, "invalidating tables: ${tables.contentToString()}") + tables.forEach { table -> + tableObservers[table]?.forEach { it.onInvalidate() } + } + } + + fun interface Observer { + fun onInvalidate() + } + companion object { + const val DEBUG_TAG = "BusTO-InvalidTracker" + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java index 8e288b7..f5a408a 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java @@ -28,7 +28,9 @@ import android.provider.BaseColumns; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import it.reyboz.bustorino.backend.Palina; +import it.reyboz.bustorino.backend.Result; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; @@ -91,9 +93,10 @@ public class NextGenDB extends SQLiteOpenHelper{ public static final String QUERY_FROM_GTFS_ID_IN_TO_COMPLETE= StopsTable.COL_GTFS_ID +" IN "; public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?"; - + public static final int RESULT_SQLITE_ERROR = -4; private final Context appContext; + private final InvalidationTracker invalidationTracker = new InvalidationTracker(); private static NextGenDB INSTANCE; private NextGenDB(Context context) { @@ -176,7 +179,8 @@ public class NextGenDB extends SQLiteOpenHelper{ * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ - public synchronized ArrayList queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) throws SQLiteDatabaseLockedException { + @NonNull + public synchronized Result> queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) { ArrayList stops = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); @@ -186,48 +190,52 @@ public class NextGenDB extends SQLiteOpenHelper{ String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); - if(db == null) { - return stops; + return Result.failure(new SQLiteException("Database is null")); } - try { - final Cursor 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); + try (final Cursor 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)) { stops = getStopsFromCursorAllFields(result); - result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); - e.printStackTrace(); - return stops; + return Result.failure(e); }catch (Exception e){ Log.e(DEBUG_TAG, "Exception occurred when getting stops"); - e.printStackTrace(); - return stops; - } - finally { - db.close(); + return Result.failure(e); } - return stops; + return Result.success(stops); + } + + @NonNull + public QueryLiveData> queryAllInsideMapViewLiveData(double minLat, double maxLat, double minLng, double maxLng) { + return new QueryLiveData<>(List.of(StopsTable.TABLE_NAME), invalidationTracker,()->{ + Result> queryResult = queryAllInsideMapView(minLat, maxLat, minLng, maxLng); + if(queryResult.isSuccess()){ + return queryResult.result; + } else{ + Log.w(DEBUG_TAG, "queryAllInsideMapViewLiveData failed", queryResult.exception); + return new ArrayList<>(); + } + }); } + + /** * Query stops in the database having these IDs - * REMEMBER TO CLOSE THE DB CONNECTION AFTERWARDS * @param bustoDB readable database instance * @param gtfsIDs gtfs IDs to query * @return list of stops */ - public static synchronized ArrayList queryAllStopsWithGtfsIDs(SQLiteDatabase bustoDB, List gtfsIDs){ + @NonNull + public static synchronized Result> queryAllStopsWithGtfsIDs(@NonNull SQLiteDatabase bustoDB,@NonNull List gtfsIDs){ final ArrayList stops = new ArrayList<>(); - if(bustoDB == null){ - Log.e(DEBUG_TAG, "Asked query for IDs but database is null"); - return stops; - } else if (gtfsIDs == null || gtfsIDs.isEmpty()) { - return stops; + if (gtfsIDs.isEmpty()) { + return Result.success(stops); } final StringBuilder builder = new StringBuilder(QUERY_FROM_GTFS_ID_IN_TO_COMPLETE); @@ -246,20 +254,90 @@ public class NextGenDB extends SQLiteOpenHelper{ final String[] idsQuery = gtfsIDs.toArray(new String[0]); - try { - final Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause, - idsQuery, - null, null, null); + try(Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause, + idsQuery, + null, null, null)) { stops.addAll(getStopsFromCursorAllFields(result)); - result.close(); } catch(SQLiteException e) { - Log.e(DEBUG_TAG, "SQLiteException occurred"); - e.printStackTrace(); + Log.w(DEBUG_TAG, "SQLiteException occurred in getting stops"); + return Result.failure(e); + } + return Result.success(stops); + } + public static String buildWhereClause(String colName, List args){ + final StringBuilder builder = new StringBuilder().append(colName).append(" IN "); + boolean first = true; + builder.append(" ( "); + for(int i=0; i< args.size(); i++){ + if(first){ + first = false; + } else{ + builder.append(", "); + } + builder.append("?"); } - return stops; + builder.append(") "); + return builder.toString(); } + /** + * Query stops in the database having these IDs + * @param bustoDB readable database instance + * @param stopIds to query + * @return list of stops + */ + @NonNull + public static synchronized Result> queryStopsWithStopIds(@NonNull SQLiteDatabase bustoDB,@NonNull List stopIds){ + ArrayList stops = null; + + if (stopIds.isEmpty()) { + return Result.success(stops); + } + + final StringBuilder builder = new StringBuilder().append(StopsTable.COL_ID).append(" IN "); + boolean first = true; + builder.append(" ( "); + for(int i=0; i< stopIds.size(); i++){ + if(first){ + first = false; + } else{ + builder.append(", "); + } + builder.append("?");//.append("\"").append(id).append("\""); + } + builder.append(") "); + final String whereClause = builder.toString(); + Log.d(DEBUG_TAG, "Asking for all stops with IDs, query: "+whereClause); + + final String[] idsQuery = stopIds.toArray(new String[0]); + + try(Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause, + idsQuery, + null, null, null)) { + stops = getStopsFromCursorAllFields(result); + } catch(SQLiteException e) { + Log.e(DEBUG_TAG, "SQLiteException occurred"); + return Result.failure(e); + } + return Result.success(stops); + } + + @NonNull + public QueryLiveData> queryStopsWithStopIdsLiveData(@NonNull List stopIds){ + return new QueryLiveData<>(List.of(StopsTable.TABLE_NAME), invalidationTracker, ()->{ + Log.d(DEBUG_TAG, "Table stops changed, redoing query"); + SQLiteDatabase db = this.getReadableDatabase(); + Result> result = queryStopsWithStopIds(db, stopIds); + if(result.isSuccess()){ + return result.result; + } else{ + return null; + } + }); + } + + /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} * @param result cursor from query @@ -307,34 +385,6 @@ public class NextGenDB extends SQLiteOpenHelper{ return stops; } - public static synchronized int writeLinesStoppingHere(SQLiteDatabase db, HashMap> linesStoppingBy){ - int rowsUpdated = 0; - for (String stopGtfsID : linesStoppingBy.keySet()){ - if (linesStoppingBy.get(stopGtfsID)==null) continue; - if (linesStoppingBy.get(stopGtfsID).isEmpty()) continue; - ArrayList ll = new ArrayList<>(linesStoppingBy.get(stopGtfsID)); - String stringForStops = Palina.buildRoutesStringFromNames(ll); - - ContentValues cv = new ContentValues(); - cv.put(StopsTable.COL_LINES_STOPPING, stringForStops); - - // Which row to update, based on the title - String selection = StopsTable.COL_GTFS_ID + " LIKE ?"; - String[] selectionArgs = { stopGtfsID }; - - int count = db.update( - StopsTable.TABLE_NAME, - cv, - selection, - selectionArgs); - if (count > 1){ - Log.e(DEBUG_TAG, "Updated the linesStoppingBy for more than one stop"); - } - rowsUpdated += count; - } - return rowsUpdated; - } - public static boolean insertBranchesIntoDB(@NonNull Context context, @NonNull List routesToInsert){ final NextGenDB nextGenDB = NextGenDB.getInstance(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; @@ -345,40 +395,7 @@ public class NextGenDB extends SQLiteOpenHelper{ //if it has received an interrupt, stop if(Thread.interrupted()) return false; //otherwise, build contentValues - final ContentValues cv = new ContentValues(); - cv.put(BranchesTable.COL_BRANCHID,r.branchid); - cv.put(LinesTable.COLUMN_NAME,r.getName()); - cv.put(BranchesTable.COL_DIRECTION,r.destinazione); - cv.put(BranchesTable.COL_DESCRIPTION,r.description); - for (int day :r.serviceDays) { - switch (day){ - case Calendar.MONDAY: - cv.put(BranchesTable.COL_LUN,1); - break; - case Calendar.TUESDAY: - cv.put(BranchesTable.COL_MAR,1); - break; - case Calendar.WEDNESDAY: - cv.put(BranchesTable.COL_MER,1); - break; - case Calendar.THURSDAY: - cv.put(BranchesTable.COL_GIO,1); - break; - case Calendar.FRIDAY: - cv.put(BranchesTable.COL_VEN,1); - break; - case Calendar.SATURDAY: - cv.put(BranchesTable.COL_SAB,1); - break; - case Calendar.SUNDAY: - cv.put(BranchesTable.COL_DOM,1); - break; - } - } - if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); - cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); - - //values[routesToInsert.indexOf(r)] = cv; + final ContentValues cv = createContentValuesBranch(r); branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i palinas, HashMap> routesStoppingByStop){ + SQLiteDatabase db = getWritableDatabase(); + boolean completed = false; + 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 completed; + } + int patternsStopsHits = 0; + long startTime = System.currentTimeMillis(); + + try { + //TODO: Get the type of stop from the lines + //Empty the needed tables + + db.beginTransaction(); + //put new data + + Log.d(DEBUG_TAG, "Inserting " + palinas.size() + " stops"); + String routesStoppingString = ""; + + for (final Palina p : palinas) { + final ContentValues cv = new ContentValues(); + + cv.put(StopsTable.COL_ID, p.ID); + cv.put(StopsTable.COL_NAME, p.getStopDefaultName()); + if (p.location != null) + cv.put(StopsTable.COL_LOCATION, p.location); + cv.put(StopsTable.COL_LAT, p.getLatitude()); + cv.put(StopsTable.COL_LONG, p.getLongitude()); + if (p.getAbsurdGTTPlaceName() != null) + cv.put(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(StopsTable.COL_LINES_STOPPING, routesStoppingString); + if (p.type != null) cv.put(StopsTable.COL_TYPE, p.type.getCode()); + if (p.gtfsID != null) cv.put(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(StopsTable.TABLE_NAME, null, cv); + + } + db.setTransactionSuccessful(); + completed = true; + }catch (SQLException exc){ + Log.w(DEBUG_TAG, "SQlite Exception: " + exc.getMessage()); + } finally { + db.endTransaction(); + } + + long endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s, successful: "+completed); + Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); + if(completed){ + invalidationTracker.notifyInvalidation(StopsTable.TABLE_NAME); + } + return completed; + } + + @NonNull + private static ContentValues createContentValuesBranch(Route r) { + final ContentValues cv = new ContentValues(); + cv.put(BranchesTable.COL_BRANCHID, r.branchid); + cv.put(LinesTable.COLUMN_NAME, r.getName()); + cv.put(BranchesTable.COL_DIRECTION, r.destinazione); + cv.put(BranchesTable.COL_DESCRIPTION, r.description); + for (int day : r.serviceDays) { + switch (day){ + case Calendar.MONDAY: + cv.put(BranchesTable.COL_LUN,1); + break; + case Calendar.TUESDAY: + cv.put(BranchesTable.COL_MAR,1); + break; + case Calendar.WEDNESDAY: + cv.put(BranchesTable.COL_MER,1); + break; + case Calendar.THURSDAY: + cv.put(BranchesTable.COL_GIO,1); + break; + case Calendar.FRIDAY: + cv.put(BranchesTable.COL_VEN,1); + break; + case Calendar.SATURDAY: + cv.put(BranchesTable.COL_SAB,1); + break; + case Calendar.SUNDAY: + cv.put(BranchesTable.COL_DOM,1); + break; + } + } + if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); + cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); + return cv; + } /* static ArrayList createStopListFromCursor(Cursor data){ ArrayList stopList = new ArrayList<>(); @@ -448,33 +570,29 @@ public class NextGenDB extends SQLiteOpenHelper{ * @param content ContentValues array * @return number of lines inserted */ - public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException { + public int insertBatchContent(ContentValues[] content,String tableName, int sqliteConflictStrategy) { 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); + try{ + db.beginTransaction(); + for(ContentValues cv:content){ + db.insertWithOnConflict(tableName, null, cv, sqliteConflictStrategy); } + db.setTransactionSuccessful(); + success = content.length; + } catch (SQLException e) { + Log.w(DEBUG_TAG, "Error inserting batch content into table "+tableName, e); + success = RESULT_SQLITE_ERROR; + } finally { + db.endTransaction(); + } + if (success > 0) { + invalidationTracker.notifyInvalidation(tableName); // ✅ Notify only after confirmed commit } - db.setTransactionSuccessful(); - db.endTransaction(); return success; } - int updateLinesStoppingInStop(List stops){ - return 0; - } - public static List splitLinesString(String linesStr){ return Arrays.asList(linesStr.split("\\s*,\\s*")); } diff --git a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt index 19d2b35..cb67613 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt @@ -18,34 +18,73 @@ package it.reyboz.bustorino.data import android.content.Context +import android.util.Log import androidx.sqlite.SQLiteException import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.StopFavoritesData import it.reyboz.bustorino.backend.utils import java.util.ArrayList import java.util.concurrent.Executor class OldDataRepository(private val executor: Executor, private val nextGenDB: NextGenDB, + private val userDB: UserDB ) { - constructor(executor: Executor, context: Context): this(executor, NextGenDB.getInstance(context)) + constructor(executor: Executor, context: Context): this(executor, NextGenDB.getInstance(context), UserDB.getInstance(context)) fun requestStopsWithGtfsIDs( - gtfsIDs: List?, - callback: Callback> + gtfsIDs: List, + callback: Callback> ) { + executor.execute { + //final NextGenDB dbHelper = new NextGenDB(context); + val db = nextGenDB.readableDatabase + val stopResult= NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs) + //Result> result = Result.success; + callback.onComplete(stopResult) + } + } + fun requestStopsWithIds(ids: List, callback: Callback>) { executor.execute { try { //final NextGenDB dbHelper = new NextGenDB(context); val db = nextGenDB.readableDatabase - val stops: List = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs) + val stopsResult= NextGenDB.queryStopsWithStopIds(db, ids) //Result> result = Result.success; - callback.onComplete(Result.success(stops)) + callback.onComplete(stopsResult); + } catch (e: Exception) { + callback.onComplete(Result.failure(e)) + } + } + + } + + fun getFavoritesData(ids: List, callback: Callback>){ + executor.execute { + try { + val data =userDB.queryDataForStopIds(ids) + Log.d(DEBUG_TAG, "received favorites data: $data") + if(data != null){ + val res = Result.success(data) + callback.onComplete(res) + } + else{ + callback.onComplete(Result.failure(android.database.sqlite.SQLiteException())) + } } catch (e: Exception) { callback.onComplete(Result.failure(e)) } } } + fun getFavoritesLiveData(): QueryLiveData> { + return userDB.favoritesLiveData + } + fun getFavoritesLiveDataByStopId(ids: List) = userDB.getLiveDataForStopIds(ids) + + fun getStopsForIdsLiveData(ids: List): QueryLiveData> { + return nextGenDB.queryStopsWithStopIdsLiveData(ids) + } fun requestStopsInArea( latitFrom: Double, @@ -56,21 +95,23 @@ class OldDataRepository(private val executor: Executor, ){ //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); executor.execute { - var result = ArrayList() - try { - result = nextGenDB.queryAllInsideMapView( + //var result = ArrayList() + callback.onComplete(nextGenDB.queryAllInsideMapView( latitFrom, latitTo, longitFrom, longitTo - ) - } catch (e: SQLiteException){ - callback.onComplete(Result.failure(e)) - } + )) - callback.onComplete(Result.success(result)) } } + fun requestStopsInAreaLiveData(minLat: Double, + maxLat: Double, + minLong: Double, + maxLong: Double): QueryLiveData> { + return nextGenDB.queryAllInsideMapViewLiveData(minLat, maxLat, minLong, maxLong) + } + /** * Request all the stops in position [latitude], [longitude], in the "square" with radius [distanceMeters] * Returns nothing, [callback] will be called if the query succeeds @@ -89,4 +130,8 @@ class OldDataRepository(private val executor: Executor, fun interface Callback { fun onComplete(result: Result) } + + companion object { + private const val DEBUG_TAG = "BusTO-OldDataRepo" + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/QueryLiveData.kt b/app/src/main/java/it/reyboz/bustorino/data/QueryLiveData.kt new file mode 100644 index 0000000..b1c5a9b --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/QueryLiveData.kt @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.data + +import android.util.Log +import androidx.lifecycle.LiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch + +/** + * Class to observe the result of queries from database + */ +class QueryLiveData( + private val tablesToObserve: List, + private val tracker: InvalidationTracker, + private val queryRunner: () -> T +) : LiveData() { + + private val liveDataScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val invalidationCallback = InvalidationTracker.Observer { fetchData() } + + override fun onActive() { + tablesToObserve.forEach { + tracker.addObserver(it, invalidationCallback) + } + fetchData() + } + + override fun onInactive() { + tablesToObserve.forEach { + tracker.removeObserver(it, invalidationCallback) + } + liveDataScope.coroutineContext.cancelChildren() // Cancel any in-flight queries + } + + private fun fetchData() { + liveDataScope.coroutineContext.cancelChildren() + liveDataScope.launch { + val newValue = queryRunner() + if(newValue == null){ + Log.w("BusTO-QueryLiveData", "Attempting to post value but it is null, tables $tablesToObserve") + } + postValue(newValue) // postValue is safe to call from a background thread + } + } +} \ No newline at end of file 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 8a02ddf..6844bc8 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java @@ -28,15 +28,16 @@ import android.content.Context; import android.net.Uri; import android.util.Log; -import java.io.IOException; import java.util.*; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import de.siegmar.fastcsv.reader.CloseableIterator; import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.CsvRecord; import de.siegmar.fastcsv.writer.CsvWriter; import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.StopFavoritesData; import it.reyboz.bustorino.backend.StopsDBInterface; public class UserDB extends SQLiteOpenHelper { @@ -48,8 +49,11 @@ public class UserDB extends SQLiteOpenHelper { public final static String COL_USERNAME="username"; public static final int FILE_INVALID=-10; + private static final String DEBUG_TAG = "BusTO-FavoritesUserDB"; private final static String[] usernameColumnNameAsArray = {"username"}; - public final static String[] getFavoritesColumnNamesAsArray = {COL_ID, COL_USERNAME}; + public final static String[] FAVORITES_COLUMNS_ARRAY = {COL_ID, COL_USERNAME}; + + private final InvalidationTracker invalidationTracker = new InvalidationTracker(); private static final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( AppDataProvider.FAVORITES).build(); @@ -169,12 +173,13 @@ public class UserDB extends SQLiteOpenHelper { /** * 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) { + public boolean isStopInFavorites(String stopId) { + + SQLiteDatabase db = this.getReadableDatabase(); boolean found = false; try { @@ -193,43 +198,45 @@ public class UserDB extends SQLiteOpenHelper { /** * 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 @Nullable String getStopUserName(SQLiteDatabase db, String stopID) { + private @Nullable String getStopUserName(SQLiteDatabase db,String stopID) { String username = null; - try { - Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, 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 e) { Log.e("BusTO-UserDB","Cannot get stop User name for stop "+stopID+":\n"+e); } return username; } + public @Nullable String getStopUserName(String stopID) { + SQLiteDatabase db = this.getReadableDatabase(); + return getStopUserName(db,stopID); + } /** * Get all the bus stops marked as favorites * - * @param db * @param dbi * @return */ - public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) { + public List getFavorite( StopsDBInterface dbi) { + SQLiteDatabase db = this.getReadableDatabase(); List l = new ArrayList<>(); Stop s; String stopID, stopUserName; try { - Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null); + Cursor c = db.query(TABLE_NAME, FAVORITES_COLUMNS_ARRAY, null, null, null, null, null, null); int colID = c.getColumnIndex("ID"); int colUser = c.getColumnIndex("username"); @@ -264,7 +271,7 @@ public class UserDB extends SQLiteOpenHelper { public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){ List colsList = Arrays.asList(columns); - if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){ + if (!colsList.contains(FAVORITES_COLUMNS_ARRAY[0]) || !colsList.contains(FAVORITES_COLUMNS_ARRAY[1])){ throw new IllegalArgumentException(); } ArrayList l = new ArrayList<>(); @@ -286,55 +293,162 @@ public class UserDB extends SQLiteOpenHelper { } - public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { + @NonNull + public ArrayList getAllFavoritesData(){ + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, FAVORITES_COLUMNS_ARRAY, null, null, null, null, null); + + ArrayList l = new ArrayList<>(); + 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); + + l.add(new StopFavoritesData(stopID, stopUserName)); + } + cursor.close(); + return l; + + } + @Nullable + public ArrayList queryDataForStopIds(List stopIds) { + SQLiteDatabase db = this.getReadableDatabase(); + ArrayList result = null; + + final String whereClause = NextGenDB.buildWhereClause(COL_ID, stopIds); + final String[] whereArgs = stopIds.toArray(new String[0]); + Log.d(DEBUG_TAG, "queryDtaForStopId: " + whereClause+ " args: " + Arrays.toString(whereArgs)); + try(Cursor c = db.query( + TABLE_NAME, FAVORITES_COLUMNS_ARRAY, whereClause, + whereArgs, null, null, null, null)){ + + result = getFavoritesDataFromCursor(c, FAVORITES_COLUMNS_ARRAY); + } + catch(SQLiteException e) { + Log.e(DEBUG_TAG, "queryDataForStopIds favorites failed for " + stopIds, e); + return null; + } + + return result; + } + + @NonNull + public QueryLiveData> getLiveDataForStopIds(List stopIds) { + return new QueryLiveData<>(List.of(TABLE_NAME), invalidationTracker, () -> { + Log.d(DEBUG_TAG, "Favorites table changed, redoing query"); + return queryDataForStopIds(stopIds); + }); + } + @NonNull + public QueryLiveData> getFavoritesLiveData() { + return new QueryLiveData<>(List.of(TABLE_NAME), invalidationTracker, () -> { + Log.d(DEBUG_TAG, "Favorites table changed, redoing query"); + return getAllFavoritesData(); + }); + } + + + @NonNull + public static ArrayList getFavoritesDataFromCursor(@NonNull Cursor cursor, String[] columns){ + List colsList = Arrays.asList(columns); + if (!colsList.contains(FAVORITES_COLUMNS_ARRAY[0]) || !colsList.contains(FAVORITES_COLUMNS_ARRAY[1])){ + throw new IllegalArgumentException(); + } + ArrayList l = new ArrayList<>(); + 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); + l.add(new StopFavoritesData(stopID, stopUserName)); + } + return l; + + } + public boolean addOrUpdateStop(Stop s) { + return addOrUpdateStop(s.ID, s.getStopUserName()); + } + public boolean addOrUpdateStop(@NonNull String stopID, @Nullable String stopUserName) { + SQLiteDatabase db = this.getWritableDatabase(); + return addOrUpdateStop(stopID, stopUserName, db); + } + private boolean addOrUpdateStop(@NonNull String stopID, @Nullable String stopUserName, SQLiteDatabase db) { ContentValues cv = new ContentValues(); long result = -1; - String un = s.getStopUserName(); - cv.put("ID", s.ID); + cv.put("ID", stopID); // is there an username? - if(un == null) { + if(stopUserName == null) { // no: see if it's in the database - cv.put("username", getStopUserName(db, s.ID)); + cv.put("username", getStopUserName(db,stopID)); } else { // yes: use it - cv.put("username", un); + cv.put("username", stopUserName); } 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) {} - + } catch (SQLiteException ignored) { + Log.e(DEBUG_TAG, "cannot insert stop in user db, error: " + ignored); + } + if(result!=-1) + invalidationTracker.notifyInvalidation(TABLE_NAME); // Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return - return (result != -1) || updateStop(s, db); + return (result != -1) || (updateStop(stopID,stopUserName, db)); + } + private boolean addOrUpdateStop(@NonNull Stop s, SQLiteDatabase db) { + return addOrUpdateStop(s.ID, s.getStopUserName(), db); } - public static boolean updateStop(Stop s, SQLiteDatabase db) { + private boolean updateStop(@NonNull String stopID, @Nullable String stopUsername, @NonNull SQLiteDatabase db) { try { ContentValues cv = new ContentValues(); - cv.put("username", s.getStopUserName()); - db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID}); + cv.put("username", stopUsername); + db.update(TABLE_NAME, cv, "ID = ?", new String[]{stopID}); + invalidationTracker.notifyInvalidation(TABLE_NAME); + return true; } catch(SQLiteException e) { + Log.w(DEBUG_TAG, "setStopUsername failed",e); return false; } } + public boolean updateStop(@NonNull Stop s) { + SQLiteDatabase db = this.getWritableDatabase(); + return updateStop(s.ID, s.getStopUserName(), db); + } - public static boolean deleteStop(Stop s, SQLiteDatabase db) { + private boolean deleteStop(@NonNull String stopID,@NonNull SQLiteDatabase db) { try { - db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID}); + db.delete(TABLE_NAME, "ID = ?", new String[]{stopID}); + invalidationTracker.notifyInvalidation(TABLE_NAME); return true; } catch(SQLiteException e) { + Log.w(DEBUG_TAG, "failed to remove stop, ID: "+stopID); return false; } } - public static boolean checkStopInFavorites(String stopID, Context con){ + private boolean deleteStop(@NonNull Stop s, @NonNull SQLiteDatabase db) { + return deleteStop(s.ID, db); + } + public boolean deleteStop(@NonNull String stopID) { + SQLiteDatabase db = this.getWritableDatabase(); + return deleteStop(stopID, db); + } + public boolean deleteStop(@NonNull Stop s) { + return deleteStop(s.ID); + } + + + + public 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); + UserDB userDB = UserDB.getInstance(con); + found = userDB.isStopInFavorites(stopID); } return found; @@ -346,7 +460,7 @@ public class UserDB extends SQLiteOpenHelper { String sortOrder = COL_ID + " DESC"; - Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder); + Cursor cursor = db.query(TABLE_NAME, FAVORITES_COLUMNS_ARRAY,null,null,null,null, sortOrder); final int nCols = 2;//cursor.getColumnCount(); writer.writeRecord(cursor.getColumnNames()); @@ -401,4 +515,6 @@ public class UserDB extends SQLiteOpenHelper { return updated; } + + //TODO: Copy method from @AppDataProvider to get all the favorites } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt index 850352c..07a8ce5 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -44,7 +44,7 @@ import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB -import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction +import it.reyboz.bustorino.middleware.CoroutineFavoriteAction import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.viewmodels.ArrivalsViewModel import java.util.* @@ -61,7 +61,6 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks // add/remove the stop in the favorites - toggleLastStopToFavorites() + toggleStopFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) @@ -245,9 +246,10 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks0 || it.stopDisplayName!=null) @@ -319,6 +321,9 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks showFetcherMessage(R.string.internal_error, src) } } + arrivalsViewModel.stopInFavorites.observe(viewLifecycleOwner, { isFavorite -> + updateStarIcon(isFavorite) + }) return root } @@ -609,7 +614,7 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { builder.appendPath("favorites").appendPath(stopID) - cl = CursorLoader(requireContext(), builder.build(), UserDB.getFavoritesColumnNamesAsArray, null, null, null) + cl = CursorLoader(requireContext(), builder.build(), UserDB.FAVORITES_COLUMNS_ARRAY, null, null, null) } loaderStopId -> { @@ -630,9 +635,10 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks, data: Cursor) { + /* when (loader.id) { loaderFavId -> { - val colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]) + val colUserName = data.getColumnIndex(UserDB.FAVORITES_COLUMNS_ARRAY[1]) if (data.count > 0) { // IT'S IN FAVORITES data.moveToFirst() @@ -646,7 +652,7 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( @@ -673,10 +677,14 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks) { //NOTHING TO DO } @@ -687,20 +695,22 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks updateStarIconFromLastBusStop(v) }.execute(stop) - } else { + }.execute(stop) + + + } else { // this case have no sense, but just immediately update the favorite icon - updateStarIconFromLastBusStop(true) + //updateStarIconFromLastBusStop(true) + Log.d(DEBUG_TAG, "Stop is null!") } } - + /* /** * Update the star "Add to favorite" icon */ @@ -709,28 +719,14 @@ class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { @@ -153,7 +152,7 @@ public class FavoritesFragment extends ScreenBaseFragment { //force reload if it was previously running if(model!=null && dbUpdateRunning) { Log.d(DEBUG_TAG,"DB Finished updating, reload favorites"); - model.getFavorites().forceReload(); + //model.getFavorites().forceReload(); } dbUpdateRunning = false; } @@ -219,11 +218,9 @@ public class FavoritesFragment extends ScreenBaseFragment { switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) - new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, - result -> { - - }).execute(busStop); - + new CoroutineFavoriteAction(requireContext().getApplicationContext(), CoroutineFavoriteAction.Action.REMOVE, + result -> {} + ).execute(busStop); return true; case R.id.action_rename_bus_stop_username: @@ -325,10 +322,17 @@ public class FavoritesFragment extends ScreenBaseFragment { private void launchUpdate(Stop busStop){ if (getContext()!=null) - new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, + + new CoroutineFavoriteAction(requireContext().getApplicationContext(), CoroutineFavoriteAction.Action.UPDATE, + result -> { + //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); + }).execute(busStop); + /*new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, result -> { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); + */ + } /* THIS LOOKS TERRIBLE diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt index bdca6a9..da915fc 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt @@ -106,7 +106,7 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback protected var fragmentListener: CommonFragmentListener? = null // Declare a variable for MapView - protected lateinit var mapView: MapView + protected var mapView: MapView? = null protected lateinit var mapStyle: Style protected lateinit var stopsSource: GeoJsonSource protected lateinit var busesSource: GeoJsonSource @@ -120,7 +120,7 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback protected lateinit var locationProvider: FusedNativeLocationProvider protected var shownToastNoPosition = false - private var locationEnabledOnDevice = true + protected var locationEnabledOnDevice = true //TODO ACTIVATE THIS private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key -> @@ -271,8 +271,8 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback } override fun onResume() { - mapView.onResume() super.onResume() + mapView?.onResume() val newMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext()) Log.d(DEBUG_TAG, "onResume newMapStyle: $newMapStyle, lastMapStyle: $lastMapStyle") if(newMapStyle!=lastMapStyle){ @@ -280,32 +280,38 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback } } - - - @Deprecated("Deprecated in Java") override fun onLowMemory() { - mapView.onLowMemory() + mapView?.onLowMemory() super.onLowMemory() } override fun onStart() { super.onStart() - mapView.onStart() + mapView?.onStart() } override fun onDestroy() { - mapView.onDestroy() + mapView?.onDestroy() Log.d(DEBUG_TAG, "Destroyed mapView Fragment!!") super.onDestroy() } + override fun onStop() { + mapView?.onStop() + super.onStop() + } + override fun onPause() { + mapView?.onPause() super.onPause() } + override fun onDestroyView() { bottomLayout = null locationProvider.removeListener(deviceLocationStatusListener) + mapInitialized = false + locationInitialized = false super.onDestroyView() } @@ -405,7 +411,7 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback } protected fun initSymbolManager(mapReady: MapLibreMap , style: Style){ - val sm = SymbolManager(mapView, mapReady, style) + val sm = SymbolManager(mapView!!, mapReady, style) sm.iconAllowOverlap = true sm.textAllowOverlap = false sm.addClickListener { _ -> @@ -1086,6 +1092,8 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback } + + companion object{ private const val DEBUG_TAG="GeneralMapLibreFragment" diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index b862d2b..82fe686 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -234,7 +234,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { //map stuff mapView = rootView.findViewById(R.id.lineMap) - mapView.getMapAsync(this) + mapView!!.getMapAsync(this) // Setup close button @@ -267,7 +267,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { initializeRecyclerView() switchButton.setOnClickListener{ - if(mapView.visibility == View.VISIBLE){ + if(mapView?.visibility == View.VISIBLE){ hideMapAndShowStopList() } else{ hideStopListAndShowMap() @@ -328,7 +328,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { Log.w(DEBUG_TAG, "The selectedPattern is null!") return@observe } - if(mapView.visibility ==View.VISIBLE) { + if(mapView?.visibility ==View.VISIBLE) { // We have the pattern and the stops here, time to display them //TODO: Decide if we should follow the camera view given by the previous screen (probably the map fragment) // use !restoredCameraInMap to do so @@ -383,7 +383,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { Log.d(DEBUG_TAG, "request stops for pattern ${patternWithStops.pattern.code}") setPatternAndReqStops(patternWithStops) - if(mapView.visibility == View.VISIBLE) { + if(mapView?.visibility == View.VISIBLE) { //Clear buses if we are changing direction currentShownPattern?.let { patt -> if(patt.directionId != patternWithStops.pattern.directionId){ @@ -425,7 +425,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { // ------------- UI switch stuff --------- private fun hideMapAndShowStopList(){ - mapView.visibility = View.GONE + mapView?.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE locationIcon?.visibility = View.GONE busPositionsIconButton?.visibility = View.GONE @@ -447,7 +447,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { private fun hideStopListAndShowMap(){ stopsRecyclerView.visibility = View.GONE - mapView.visibility = View.VISIBLE + mapView?.visibility = View.VISIBLE locationIcon?.visibility = View.VISIBLE busPositionsIconButton.visibility = View.VISIBLE @@ -648,7 +648,7 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { //Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions") val updates = pair.first val vehiclesNotOnCorrectDir = pair.second - if(mapView.visibility == View.GONE || patternShown ==null){ + if(mapView?.visibility == View.GONE || patternShown ==null){ //DO NOTHING Log.w(DEBUG_TAG, "not doing anything because map is not visible") return@observe @@ -1063,7 +1063,6 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { override fun onPause() { super.onPause() - mapView.onPause() if(usingMQTTPositions) livePositionsViewModel.stopMatoUpdates() pausedFragment = true //save map @@ -1076,7 +1075,6 @@ class LinesDetailFragment() : GeneralMapLibreFragment() { override fun onStop() { super.onStop() - mapView.onStop() if(locationInitialized) shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled else diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 70ed479..b0e5c7d 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -21,8 +21,6 @@ import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.location.Criteria; -import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -61,13 +59,11 @@ import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; -import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; -import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; @@ -112,7 +108,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// - FragmentManager childFragMan; + private FragmentManager childFragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { @@ -171,7 +167,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; - AppLocationManager locationManager; + //AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() { @Override @@ -187,11 +183,13 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) { locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); - if (mListener != null && getContext() != null) { + /*if (mListener != null && getContext() != null) { if (locationManager == null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } + + */ // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); @@ -205,9 +203,9 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL }); - private final LocationCriteria cr = new LocationCriteria(2000, 10000); + //private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location - private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { + /*private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { @@ -255,7 +253,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL } }; - + */ //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; @@ -337,15 +335,14 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); - - + /* cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); - - locationManager = AppLocationManager.getInstance(requireContext()); + */ + //locationManager = AppLocationManager.getInstance(requireContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); @@ -470,8 +467,8 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); - if (locationManager == null) - locationManager = AppLocationManager.getInstance(con); + //if (locationManager == null) + // locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ //request position permission if needed @@ -487,8 +484,8 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); - if(!locationManager.isRequesterRegistered(requester)) - locationManager.addLocationRequestFor(requester); + //if(!locationManager.isRequesterRegistered(requester)) + // locationManager.addLocationRequestFor(requester); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); @@ -533,7 +530,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL @Override public void onPause() { //mainHandler = null; - locationManager.removeLocationRequestFor(requester); + //locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); @@ -648,7 +645,6 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL } @Nullable - @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; @@ -718,7 +714,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { - locationManager.removeLocationRequestFor(requester); + //locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt index 4c7821d..eea90d9 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -133,8 +133,8 @@ class MapLibreFragment : GeneralMapLibreFragment() { // Init the MapView mapView = rootView.findViewById(R.id.libreMapView) - mapView.onCreate(savedInstanceState) - mapView.getMapAsync(this) + mapView!!.onCreate(savedInstanceState) + mapView!!.getMapAsync(this) //init bottom sheet val bottomSheet = rootView.findViewById(R.id.bottom_sheet) @@ -166,7 +166,7 @@ class MapLibreFragment : GeneralMapLibreFragment() { val location = locationComponent.lastKnownLocation location?.let { - mapView.getMapAsync { map -> + mapView?.getMapAsync { map -> map.animateCamera(CameraUpdateFactory.newCameraPosition( CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500) } @@ -260,8 +260,8 @@ class MapLibreFragment : GeneralMapLibreFragment() { addImagesStyle(style) //init stop layer with this - val stopsInCache = stopsViewModel.getAllStopsLoaded() - if(stopsInCache.isEmpty()) + val stopsInCache = stopsViewModel.stopsToShow.value + if(stopsInCache.isNullOrEmpty()) initStopsLayer(style, null) else displayStops(stopsInCache) @@ -465,13 +465,22 @@ class MapLibreFragment : GeneralMapLibreFragment() { override fun onPause() { super.onPause() - mapView.onPause() Log.d(DEBUG_TAG, "Fragment paused") map?.let{ //if map is initialized mapStateViewModel.saveMapState(it) } + try{ + //save last location + map?.locationComponent?.let{ + if(locationInitialized && it.isLocationComponentActivated){ + stopsViewModel.lastUserLocation = it.lastKnownLocation + } + } + }catch (e: Exception){ + Log.w(DEBUG_TAG, "Cannot save lastKnowLocation from map location component,error: ${e.message}") + } mapStateViewModel.lastOpenStopID.postValue(shownStopInBottomSheet?.ID) if (livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.stopMatoUpdates() @@ -479,20 +488,7 @@ class MapLibreFragment : GeneralMapLibreFragment() { override fun onStop() { super.onStop() - mapView.onStop() Log.d(DEBUG_TAG, "Fragment stopped!") - /* stopsViewModel.savedState = Bundle().let { - mapView.onSaveInstanceState(it) - it - } - */ - //save last location - if (locationInitialized) - map?.locationComponent?.lastKnownLocation?.let{ - stopsViewModel.lastUserLocation = it - } - - } override fun onMapDestroy() { @@ -614,8 +610,13 @@ class MapLibreFragment : GeneralMapLibreFragment() { override fun onSuccess(res: LocationEngineResult?) { Log.d(DEBUG_TAG, "Got the last location, ${res?.lastLocation}") res?.lastLocation?.let { loc -> - if(mapInitialized) - map?.cameraPosition = CameraPosition.Builder().target(LatLng(loc.latitude, loc.longitude)).build() + if(mapInitialized){ + val newLocation = LatLng(loc.latitude, loc.longitude) + //center the position only if it is close enough + if(newLocation.distanceTo(DEFAULT_LATLNG) < MAX_DIST_KM * 1000) + map?.cameraPosition = CameraPosition.Builder().target(LatLng(loc.latitude, loc.longitude)).build() + + } else mapStateViewModel.locationToShow = loc } @@ -630,6 +631,10 @@ class MapLibreFragment : GeneralMapLibreFragment() { }) } + if(locationEnabledOnDevice){ + setFollowUserLocation(true) + } + } override fun onMapLocationEnabled(active: Boolean) { @@ -644,11 +649,7 @@ class MapLibreFragment : GeneralMapLibreFragment() { if(locationInitialized && !receivedFirstLocation) { //only zoom if the user position is close enough to the center val newPoint = LatLng(it.latitude, it.longitude) - if(newPoint.distanceTo(LatLng( - MapLibreFragment.DEFAULT_CENTER_LAT, - MapLibreFragment.DEFAULT_CENTER_LON - )) - > MAX_DIST_KM * 1000){ + if(newPoint.distanceTo(DEFAULT_LATLNG) > MAX_DIST_KM * 1000){ //show Toast if(!shownToastNoPosition) context?.let{ c-> Toast.makeText(c, R.string.too_far_not_showing_location, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 35df677..0800925 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -115,6 +115,7 @@ public class NearbyStopsFragment extends ScreenBaseFragment { private ArrayList currentNearbyStops = new ArrayList<>(); private LocationShowingStatus showingStatus = LocationShowingStatus.NO_PERMISSION; + private boolean isLocationEnabled = false; private final FusedNativeLocationProvider.LocationUpdateListener locationUpdateListener = new FusedNativeLocationProvider.LocationUpdateListener() { @Override @@ -125,6 +126,7 @@ public class NearbyStopsFragment extends ScreenBaseFragment { @Override public void onFusedStatusChanged(boolean isEnabled) { Log.d(DEBUG_TAG, "Location provider is enabled: " + isEnabled); + isLocationEnabled = isEnabled; if(isEnabled){ setShowingStatus(LocationShowingStatus.SEARCHING); } else{ @@ -363,6 +365,11 @@ public class NearbyStopsFragment extends ScreenBaseFragment { if(newStatus == showingStatus){ return; } + if(!isLocationEnabled && newStatus!=LocationShowingStatus.NO_PERMISSION){ + Log.d(DEBUG_TAG, "asked to show status: "+newStatus+" but the position is disabled"); + newStatus = LocationShowingStatus.DISABLED; + } + switch (newStatus){ case FIRST_FIX: circlingProgressBar.setVisibility(View.GONE); @@ -377,10 +384,10 @@ public class NearbyStopsFragment extends ScreenBaseFragment { messageTextView.setVisibility(View.VISIBLE); break; case DISABLED: - if (showingStatus== LocationShowingStatus.SEARCHING){ + //if (showingStatus== LocationShowingStatus.SEARCHING){ circlingProgressBar.setVisibility(View.GONE); loadingTextView.setVisibility(View.GONE); - } + //} messageTextView.setText(R.string.enable_location_message); messageTextView.setVisibility(View.VISIBLE); break; @@ -415,6 +422,8 @@ public class NearbyStopsFragment extends ScreenBaseFragment { gridRecyclerView.setAdapter(null); Log.d(DEBUG_TAG,"On paused called"); + + locationProvider.stopUpdates(); } @Override @@ -465,6 +474,10 @@ public class NearbyStopsFragment extends ScreenBaseFragment { if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ ", Min number of stops: "+MIN_NUM_STOPS); + if(!locationProvider.isRunning()){ + startLocationUpdatesByType(); + } + } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java index 66bf32f..dd07dee 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -103,22 +103,6 @@ public class ResultListFragment extends Fragment{ } } - /** - * Check if the last Bus Stop is in the favorites - * @return true if it iss - */ - public boolean isStopInFavorites(String busStopId) { - boolean found = false; - - // no stop no party - if(busStopId != null) { - SQLiteDatabase userDB = UserDB.getInstance(getContext()).getReadableDatabase(); - found = UserDB.isStopInFavorites(userDB, busStopId); - } - - return found; - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index 8e0a403..c81589f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -15,13 +15,12 @@ import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.google.android.material.snackbar.Snackbar; import it.reyboz.bustorino.BuildConfig; -import it.reyboz.bustorino.R; import java.util.Map; @@ -33,6 +32,7 @@ public abstract class ScreenBaseFragment extends Fragment { protected void setOption(String optionName, boolean value) { Context mContext = getContext(); + assert mContext != null; SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); @@ -108,7 +108,49 @@ public abstract class ScreenBaseFragment extends Fragment { } }); } + /*protected void runActionFavorites(@NonNull Stop s, @NonNull FavoritesChangeWorker.Action action, @NonNull FavoritesChangeWorker.Companion.ResultListener resultListener){ + Context mContext = requireContext(); + + WorkManager workManager = WorkManager.getInstance(mContext); + + WorkRequest req = FavoritesChangeWorker.makeRequest(s, action); + workManager.enqueue(req); + Context appContext = mContext.getApplicationContext(); + + //FavoritesChangeWorker.registerListener(mContext, getViewLifecycleOwner(), s, action, resultListener); + workManager.getWorkInfosByTagLiveData(FavoritesChangeWorker.getTag(s, action)) + .observe(getViewLifecycleOwner(), wi -> { + Log.d("BusTO-BaseFragment", "workinfo for stop "+s.ID+" has arrived"); + if(wi.isEmpty()){ + return; + } + WorkInfo workInfo = wi.get(wi.size() - 1); + Data progress = wi.get(wi.size()-1).getProgress(); + + int actvalue = progress.getInt(ACTION_ARG,-1); + boolean done = progress.getBoolean(DONE_ARG, false); + if (done) { + // at this point the action should be just ADD or REMOVE + + if (actvalue == FavoritesChangeWorker.Action.ADD.getValue()) { + // now added + Toast.makeText(appContext, R.string.added_in_favorites, Toast.LENGTH_SHORT).show(); + } else if (actvalue == FavoritesChangeWorker.Action.REMOVE.getValue()) { + // now removed + Toast.makeText(appContext, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show(); + } + } else { + // wtf + Toast.makeText(appContext, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show(); + } + Log.d("busTO-ScreenBaseFragm", "favorites action="+actvalue+ ",done="+done); + + // aggiorna UI + resultListener.doStuffWithResult(done); + }); + } + */ public interface LocationRequestListener{ void onPermissionResult(boolean locationGranted); diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt index 5b2967d..25c5db5 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt @@ -24,9 +24,7 @@ import android.location.* import android.os.Bundle import android.util.Log import androidx.core.content.ContextCompat -import androidx.core.location.LocationListenerCompat import it.reyboz.bustorino.util.LocationCriteria -import it.reyboz.bustorino.util.Permissions import java.lang.ref.WeakReference import kotlin.math.min diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java index 023651c..fd47fe4 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; @@ -257,84 +258,7 @@ public class AsyncArrivalsSearcher extends AsyncTask branchesValues = new ArrayList<>(routesToInsert.size()*4); - ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); - long starttime,endtime; - for (Route r:routesToInsert){ - //if it has received an interrupt, stop - if(Thread.interrupted()) return; - //otherwise, build contentValues - final ContentValues cv = new ContentValues(); - cv.put(BranchesTable.COL_BRANCHID,r.branchid); - cv.put(LinesTable.COLUMN_NAME,r.getName()); - cv.put(BranchesTable.COL_DIRECTION,r.destinazione); - cv.put(BranchesTable.COL_DESCRIPTION,r.description); - for (int day :r.serviceDays) { - switch (day){ - case Calendar.MONDAY: - cv.put(BranchesTable.COL_LUN,1); - break; - case Calendar.TUESDAY: - cv.put(BranchesTable.COL_MAR,1); - break; - case Calendar.WEDNESDAY: - cv.put(BranchesTable.COL_MER,1); - break; - case Calendar.THURSDAY: - cv.put(BranchesTable.COL_GIO,1); - break; - case Calendar.FRIDAY: - cv.put(BranchesTable.COL_VEN,1); - break; - case Calendar.SATURDAY: - cv.put(BranchesTable.COL_SAB,1); - break; - case Calendar.SUNDAY: - cv.put(BranchesTable.COL_DOM,1); - break; - } - } - if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); - cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); - - //values[routesToInsert.indexOf(r)] = cv; - branchesValues.add(cv); - if(r.getStopsList() != null) - for(int i=0; i0) { - starttime = System.currentTimeMillis(); - ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); - Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); - int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); - endtime = System.currentTimeMillis(); - Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); - } - - //nextGenDB.close(); + NextGenDB.insertBranchesIntoDB(context, routesToInsert); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java deleted file mode 100644 index f397864..0000000 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - BusTO - Middleware components - Copyright (C) 2016 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.Context; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; -import android.widget.Toast; -import it.reyboz.bustorino.R; -import it.reyboz.bustorino.backend.Stop; -import it.reyboz.bustorino.data.AppDataProvider; -import it.reyboz.bustorino.data.UserDB; - -/** - * Handler to add or remove or toggle a Stop in your favorites - */ -public class AsyncStopFavoriteAction extends AsyncTask { - private final Context context; - private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( - AppDataProvider.FAVORITES).build(); - - /** - * Kind of actions available - */ - public enum Action { ADD, REMOVE, TOGGLE , UPDATE}; - - /** - * Action chosen - * - * Note that TOGGLE is not converted to ADD or REMOVE. - */ - private Action action; - - // extra stuff to do after we've done it - private ResultListener listener; - /** - * Constructor - * - * @param context - * @param action - */ - public AsyncStopFavoriteAction(Context context, Action action, ResultListener listener) { - this.context = context.getApplicationContext(); - this.action = action; - this.listener = listener; - } - - @Override - protected Boolean doInBackground(Stop... stops) { - boolean result = false; - - Stop stop = stops[0]; - - // check if the request has sense - if(stop != null) { - - // get a writable database - UserDB userDatabase = UserDB.getInstance(context); - SQLiteDatabase db = userDatabase.getWritableDatabase(); - - // eventually toggle the status - if(Action.TOGGLE.equals(action)) { - if(UserDB.isStopInFavorites(db, stop.ID)) { - action = Action.REMOVE; - } else { - action = Action.ADD; - } - } - - // at this point the action is just ADD or REMOVE - - // add or remove? - if(Action.ADD.equals(action)) { - // add - result = UserDB.addOrUpdateStop(stop, db); - } else if (Action.UPDATE.equals(action)){ - - result = UserDB.updateStop(stop, db); - } else { - // remove - result = UserDB.deleteStop(stop, db); - } - - // These should NOT be closed: the database is a singleton, the connections are recycled. - //db.close(); - } - - return result; - } - - /** - * Callback fired when everything was done - * - * @param result - */ - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - - if(result) { - UserDB.notifyContentProvider(context); - // at this point the action should be just ADD or REMOVE - if(Action.ADD.equals(action)) { - // now added - Toast.makeText(this.context, R.string.added_in_favorites, Toast.LENGTH_SHORT).show(); - } else if (Action.REMOVE.equals(action)) { - // now removed - Toast.makeText(this.context, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show(); - } - } else { - // wtf - Toast.makeText(this.context, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show(); - } - listener.doStuffWithResult(result); - Log.d("BusTO FavoritesAction", "Action "+action+" completed"); - } - - public interface ResultListener{ - /** - * Do what you need to to update the UI with the result - * @param result true if the action is done - */ - void doStuffWithResult(Boolean result); - } - -} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/CoroutineFavoriteAction.kt b/app/src/main/java/it/reyboz/bustorino/middleware/CoroutineFavoriteAction.kt new file mode 100644 index 0000000..8dcf2b2 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/CoroutineFavoriteAction.kt @@ -0,0 +1,67 @@ +package it.reyboz.bustorino.middleware + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.widget.Toast +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.UserDB +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CoroutineFavoriteAction( + context: Context, + private var action: Action, + private val listener: ResultListener +) { + private val context = context.applicationContext + + enum class Action { ADD, REMOVE, TOGGLE, UPDATE } + + fun interface ResultListener { + fun doStuffWithResult(result: Boolean) + } + + fun execute(stop: Stop) { + CoroutineScope(Dispatchers.IO).launch { + val result = doInBackground(stop) + withContext(Dispatchers.Main) { + onPostExecute(result) + } + } + } + + private fun doInBackground(stop: Stop): Boolean { + val userDB = UserDB.getInstance(context) + + if (action == Action.TOGGLE) { + action = if (userDB.isStopInFavorites(stop.ID)) Action.REMOVE else Action.ADD + } + + return when (action) { + Action.ADD -> userDB.addOrUpdateStop(stop) + Action.UPDATE -> userDB.updateStop(stop) + Action.REMOVE -> userDB.deleteStop(stop) + Action.TOGGLE -> false // irraggiungibile, ma richiesto da when exhaustive + } + } + + private fun onPostExecute(result: Boolean) { + if (result) { + UserDB.notifyContentProvider(context) + when (action) { + Action.ADD -> Toast.makeText(context, R.string.added_in_favorites, Toast.LENGTH_SHORT).show() + Action.REMOVE -> Toast.makeText(context, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show() + else -> Unit + } + } else { + Toast.makeText(context, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show() + } + listener.doStuffWithResult(result) + Log.d("BusTO FavoritesAction", "Action $action completed") + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt index ea5d49a..0b4b152 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt @@ -23,6 +23,8 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.mato.MatoAPIFetcher @@ -37,25 +39,33 @@ import java.util.concurrent.atomic.AtomicReference class ArrivalsViewModel(application: Application): AndroidViewModel(application) { // Arrivals of palina - val appContext: Context + val appContext: Context = application.applicationContext + private val executor = Executors.newFixedThreadPool(2) + private val oldRepo = OldDataRepository(executor, application) + - val palinaLiveData = MediatorLiveData() + val palinaFromArrivals = MediatorLiveData() + val palinaToShow = MediatorLiveData() val sourcesLiveData = MediatorLiveData() val resultLiveData = MutableLiveData() val currentFetchers = MediatorLiveData>() - /// OLD REPO for stops instance - private val executor = Executors.newFixedThreadPool(2) - private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + private val stopID = MutableLiveData() + + fun setStopId(stopId: String) { stopID.value = (stopId) } + val stopFavoritesData = stopID.switchMap { oldRepo.getFavoritesLiveDataByStopId(listOf(it))} + val stopInFavorites = stopFavoritesData.map { it!=null && it.isNotEmpty() } + + /// OLD REPO for stops instance private var stopIdRequested = "" private val stopFromDB = MutableLiveData() val arrivalsRequestRunningLiveData = MutableLiveData(false) - private val oldRepoStopCallback = OldDataRepository.Callback>{ stopListRes -> + private val oldRepoStopCallback = OldDataRepository.Callback>{ stopListRes -> if(stopIdRequested.isEmpty()) return@Callback if(stopListRes.isSuccess) { @@ -72,15 +82,32 @@ class ArrivalsViewModel(application: Application): AndroidViewModel(application) } init { - appContext = application.applicationContext - palinaLiveData.addSource(stopFromDB){ + palinaFromArrivals.addSource(stopFromDB){ s -> - val hasSource = palinaLiveData.value?.passaggiSourceIfAny - Log.d(DEBUG_TAG, "Have current palina ${palinaLiveData.value!=null}, source passaggi $hasSource, new incoming stop $s from database") - val newp = if(palinaLiveData.value == null) Palina(s) else Palina.mergePaline(palinaLiveData.value, Palina(s)) - Log.d(DEBUG_TAG, "Merged palina: $newp, num passages: ${newp?.totalNumberOfPassages}, has coords: ${newp?.hasCoords()}") - newp?.let { pal -> palinaLiveData.postValue(pal) } + val hasSource = palinaFromArrivals.value?.passaggiSourceIfAny + //Log.d(DEBUG_TAG, "Have current palina ${palinaLiveData.value!=null}, source passaggi $hasSource, new incoming stop $s from database") + val newp = if(palinaFromArrivals.value == null) Palina(s) else Palina.mergePaline(palinaFromArrivals.value, Palina(s)) + //Log.d(DEBUG_TAG, "Merged palina: $newp, num passages: ${newp?.totalNumberOfPassages}, has coords: ${newp?.hasCoords()}") + newp?.let { pal -> palinaFromArrivals.postValue(pal) } + } + palinaToShow.addSource(stopFavoritesData){ dat -> + val current = palinaFromArrivals.value + Log.d(DEBUG_TAG, "have palina $current and favorites data: $dat") + if(dat!=null && current!=null){ + if(dat.size>0 && dat[0].stopUserName!=null) // is in the favorites + current.stopUserName = dat[0].stopUserName + //set new data in palinaLiveData + palinaToShow.value = current + } + } + palinaToShow.addSource(palinaFromArrivals){ p-> + stopFavoritesData.value?.let {it -> + if(it.isNotEmpty() && it[0].stopUserName!=null) { + p.stopUserName = it[0].stopUserName + } + } + palinaToShow.value = p } } @@ -90,6 +117,7 @@ class ArrivalsViewModel(application: Application): AndroidViewModel(application) return palina } + fun requestArrivalsForStop(stopId: String, fetchers: List){ val context = appContext //application.applicationContext currentFetchers.value = fetchers @@ -222,8 +250,8 @@ class ArrivalsViewModel(application: Application): AndroidViewModel(application) arrivalsRequestRunningLiveData.postValue(false) resultLiveData.postValue(fetcherResult) Log.d(DEBUG_TAG, "Have new result palina for stop ${palina.ID}, source ${palina.passaggiSourceIfAny} has coords: ${palina.hasCoords()}") - Log.d(DEBUG_TAG, "Old palina liveData is: ${palinaLiveData.value?.stopDisplayName}, has Coords ${palinaLiveData.value?.hasCoords()}") - palinaLiveData.postValue(Palina.mergePaline(palina, palinaLiveData.value)) + Log.d(DEBUG_TAG, "Old palina liveData is: ${palinaFromArrivals.value?.stopDisplayName}, has Coords ${palinaFromArrivals.value?.hasCoords()}") + palinaFromArrivals.postValue(Palina.mergePaline(palina, palinaFromArrivals.value)) } companion object{ const val DEBUG_TAG="BusTO-ArrivalsViMo" diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt new file mode 100644 index 0000000..df233b7 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt @@ -0,0 +1,92 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.application +import androidx.lifecycle.map +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import androidx.work.WorkInfo +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.StopFavoritesData +import it.reyboz.bustorino.data.DBUpdateWorker.Companion.getWorkInfoLiveData +import it.reyboz.bustorino.data.FavoritesLiveData +import it.reyboz.bustorino.data.OldDataRepository +import it.reyboz.bustorino.data.QueryLiveData +import kotlinx.coroutines.launch +import java.util.concurrent.Executors + +class FavoritesViewModel(application: Application) : AndroidViewModel(application) { + + val oldRepo: OldDataRepository + + init { + val executor = Executors.newCachedThreadPool() + oldRepo = OldDataRepository(executor, application) + } + /*var favoritesLiveData: FavoritesLiveData? = null + + override fun onCleared() { + if (favoritesLiveData != null) favoritesLiveData!!.onClear() + super.onCleared() + } + + val favorites: FavoritesLiveData + get() { + if (favoritesLiveData == null) { + favoritesLiveData = FavoritesLiveData(application, true) + } + return favoritesLiveData!! + } +*/ + val isDBUpdating = getWorkInfoLiveData(application).map { wilist -> + var isUpdating = false + if(wilist.isNotEmpty()){ + val wi = wilist[0] + isUpdating = wi.state == WorkInfo.State.RUNNING + } + isUpdating + } + + + // ---- NEW CODE ----- + // this code is not active now, but it is gonna be useful for the day when the ContentObserver is gonna be dismissed + //for all favorites + val favoritesWithStop = MediatorLiveData>() + val favoritesNoStop = oldRepo.getFavoritesLiveData() + + val stopsForFavorites = favoritesNoStop.switchMap { + val sids = it.map { d-> d.stopID } + oldRepo.getStopsForIdsLiveData(sids) + } + + init{ + // this fetches the stops when I have gotten the favorites + favoritesWithStop.addSource(favoritesNoStop){ dat -> + if(dat!=null) stopsForFavorites.value?.let{ stops -> + matchFavoritesStopsAndUpdate(dat, stops) + } + } + + favoritesWithStop.addSource(stopsForFavorites) { stops -> + favoritesNoStop.value?.let { fav -> + if(stops!=null){ + matchFavoritesStopsAndUpdate(fav, stops) + } + } + } + } + fun matchFavoritesStopsAndUpdate(fav: List, stops: List) { + //copy favorites info + for(s in stops) { + val di = fav.first{ it.stopID == s.ID} + di.addToStop(s) + } + favoritesWithStop.value = stops + } + companion object { + const val DEBUG_TAG = "BusTO-FavoritesViewM" + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt index 43aa5fe..4750784 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt @@ -40,7 +40,7 @@ class LinesViewModel(application: Application) : AndroidViewModel(application) { init { gtfsRepo = GtfsRepository(application) - oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + oldRepo = OldDataRepository(executor, application) } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt index 70bf47d..0c01ed3 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -22,25 +22,27 @@ import android.location.Location import android.os.Bundle import android.util.Log import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.switchMap import it.reyboz.bustorino.backend.Stop -import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.geojson.BoundingBox import java.util.concurrent.Executors import kotlin.collections.ArrayList +import kotlin.math.abs class StopsMapViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) - private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) - - val stopsToShow = MutableLiveData(ArrayList()) - private var stopsShownIDs = HashSet() - private var allStopsLoaded = HashMap() + private val oldRepo = OldDataRepository(executor, application) + //private var stopsShownIDs = HashSet() + //private var allStopsLoaded = HashMap() + private val boundingBoxLoaded = MutableLiveData() val stopsInBoundingBox = MutableLiveData>() private val callback = @@ -51,11 +53,62 @@ class StopsMapViewModel(application: Application): AndroidViewModel(application) } } - private val addStopsCallback = + fun getStopByID(id: String): Stop? { + return stopsToShow.value?.firstOrNull{ s-> s.ID == id} + } + + fun getAllStopsLoaded(): ArrayList?{ + return stopsToShow.value + } + + /*fun requestStopsInBoundingBox(bb: BoundingBox) { + bb.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) + } + } + + */ + private fun updateBoundingBox(boundingBox: BoundingBox){ + val current = boundingBoxLoaded.value + if(current == null){ + boundingBoxLoaded.value = boundingBox + } else{ + val bb = boundingBox + val mix = BoundingBox.fromLngLats(Math.min(current.west(), bb.west()), + Math.min(current.south(), bb.south()), Math.max(current.north(), bb.east()), + Math.max(current.north(), bb.north())) + + boundingBoxLoaded.value = mix + } + } + + fun loadStopsInLatLngBounds(bb: LatLngBounds){ + val extra = 0.05 + val deltaLong = abs(bb.longitudeEast - bb.longitudeWest) * extra + val deltaLat = abs(bb.latitudeNorth - bb.latitudeSouth) * extra + + val newBB = BoundingBox.fromLngLats(bb.longitudeWest - deltaLat, + bb.latitudeSouth -deltaLong, + bb.longitudeEast + deltaLong, + bb.latitudeNorth +deltaLat) + + updateBoundingBox(newBB) + /*Log.d(DEBUG_TAG, "Launching stop request") + + oldRepo.requestStopsInArea(bb.latitudeSouth-deltaLat, bb.latitudeNorth+deltaLat, + bb.longitudeWest-deltaLong, bb.longitudeEast+deltaLong, + addStopsCallback) + + */ + + } + + /*private val addStopsCallback = OldDataRepository.Callback> { res -> if(res.isSuccess) res.result?.let{ newStops -> - val stopsAdd = stopsToShow.value ?: ArrayList() - for (s in newStops){ + //val stopsAdd = stopsToShow.value ?: ArrayList() + /*for (s in newStops){ if (s.ID !in stopsShownIDs){ stopsShownIDs.add(s.ID) stopsAdd.add(s) @@ -63,41 +116,30 @@ class StopsMapViewModel(application: Application): AndroidViewModel(application) } } - stopsToShow.postValue(stopsAdd) + */ + allStopsLoaded.clear() + for(stop in newStops){ + allStopsLoaded[stop.ID] = stop + } + Log.d(DEBUG_TAG, "Loaded ${newStops.size} stops") + stopsToShow.postValue(newStops) //Log.d(DEBUG_TAG, "Loaded ${stopsAdd.size} stops in total") } } - fun getStopByID(id: String): Stop? { - if (id in allStopsLoaded) return allStopsLoaded[id] - else return null + */ + /*stopsToShow.addSource(boundingBoxLoaded){ + oldRepo.requestStopsInArea(it.south(), it.north(), + it.west(), it.east(), + addStopsCallback) } - fun getAllStopsLoaded(): ArrayList{ - return ArrayList(allStopsLoaded.values) - } + */ - /*fun requestStopsInBoundingBox(bb: BoundingBox) { - bb.let { - Log.d(DEBUG_TAG, "Launching stop request") - oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) - } + val stopsToShow = boundingBoxLoaded.switchMap { + oldRepo.requestStopsInAreaLiveData(it.south(), it.north(), it.west(), it.east()) } - */ - fun requestStopsInLatLng(bb: LatLngBounds) { - bb.let { - Log.d(DEBUG_TAG, "Launching stop request") - oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, callback) - } - } - fun loadStopsInLatLngBounds(bb: LatLngBounds?){ - bb?.let { - Log.d(DEBUG_TAG, "Launching stop request") - oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, - addStopsCallback) - } - } //this is only saved at the end, is it really necessary? var lastUserLocation: Location? = null diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml index 3ae3e6d..af18a3c 100644 --- a/app/src/main/res/layout/fragment_main_screen.xml +++ b/app/src/main/res/layout/fragment_main_screen.xml @@ -121,7 +121,9 @@ android:layout_width="match_parent" android:id="@+id/resultFrame" android:layout_height="fill_parent" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/> + android:layout_alignParentLeft="true" + android:layout_marginTop="5dp" + android:layout_alignParentStart="true"/> @@ -75,11 +83,15 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92024cb..26f4af0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -391,4 +391,6 @@ Look in the Settings to customize the app behaviour, and in the About Italian English Checking new alerts now + Press back again to close the app +