diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java --- 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.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 @@ 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 @@ */ //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 @@ } } if (showProgress) { - createDefaultSnackbar(); + createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); @@ -493,8 +504,7 @@ * 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 @@ 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 --- 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 static Result success(@Nullable T result) { + public static Result success(@NonNull T result) { return new Result<>(result); } @@ -25,7 +26,7 @@ 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 --- /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 --- 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 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 @@ } 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 --- 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 @@ 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 @@ 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 @@ 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 --- 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 @@ - + //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 @@ } 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 --- 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.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.lifecycle.LiveData; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -75,7 +75,7 @@ } - private void loadData() { + public void loadData() { loadData(false); } private static Uri.Builder getStopsBuilder(){ @@ -100,9 +100,19 @@ } 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 @@ @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 --- 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 --- /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 --- 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.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 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) { @@ -215,19 +218,16 @@ /** * 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 +246,90 @@ 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 +377,6 @@ 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 +387,7 @@ //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 +562,29 @@ * @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 --- a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt @@ -18,33 +18,72 @@ 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( @@ -89,4 +128,8 @@ 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 --- /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 --- 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.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 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 @@ /** * 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 @@ /** * 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 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 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 @@ 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 @@ 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 --- 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.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 @@ private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false - private var stopIsInFavorites = false //Views protected lateinit var addToFavorites: ImageButton @@ -133,6 +132,8 @@ stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID + arrivalsViewModel.setStopId(stopID) + //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this @@ -210,7 +211,7 @@ addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites - toggleLastStopToFavorites() + toggleStopFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) @@ -245,9 +246,10 @@ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times - if(lastUpdatedPalina == null && arrivalsViewModel.palinaLiveData.value != null) { + val tentPalina = arrivalsViewModel.palinaToShow.value + if(lastUpdatedPalina == null && tentPalina != null) { //this updates lastUpdatedPalina and also shows the arrival source - updateFragmentData(arrivalsViewModel.palinaLiveData.value!!) + updateFragmentData(tentPalina) } //lastUpdatedPalina?.let { showArrivalsSources(it) } @@ -272,7 +274,7 @@ } }) - arrivalsViewModel.palinaLiveData.observe(viewLifecycleOwner){ + arrivalsViewModel.palinaToShow.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "New result palina observed, has coords: ${it.hasCoords()}, title ${it?.stopDisplayName}, number of passages: ${it.totalNumberOfPassages}") val palinaIsValid = it!=null && (it.totalNumberOfPassages>0 || it.stopDisplayName!=null) @@ -319,6 +321,9 @@ else -> showFetcherMessage(R.string.internal_error, src) } } + arrivalsViewModel.stopInFavorites.observe(viewLifecycleOwner, { isFavorite -> + updateStarIcon(isFavorite) + }) return root } @@ -609,7 +614,7 @@ when (id) { loaderFavId -> { 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 @@ } override fun onLoadFinished(loader: Loader, 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 @@ stopIsInFavorites = false } updateStarIcon() - /* + if (stopName == null) { //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment$tag", "Stop wasn't in the favorites and has no name, looking in the DB") @@ -655,10 +661,8 @@ arguments, this ) } - 6 - */ } - /* + loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( @@ -673,10 +677,14 @@ Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } - */ + } + + */ } + + override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } @@ -687,20 +695,22 @@ arrivalsRecyclerView.visibility = View.VISIBLE } - fun toggleLastStopToFavorites() { + fun toggleStopFavorites() { val stop: Stop? = lastUpdatedPalina if (stop != null) { // toggle the status in background + CoroutineFavoriteAction(requireContext().applicationContext, CoroutineFavoriteAction.Action.TOGGLE){ - AsyncStopFavoriteAction( - requireContext().applicationContext, AsyncStopFavoriteAction.Action.TOGGLE - ) { v: Boolean -> 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 @@ else toggleDone updateStarIcon() - - // check if there is a last Stop - /* - if (stopID == null) { - addToFavorites.setVisibility(View.INVISIBLE); - } else { - // filled or outline? - if (isStopInFavorites(stopID)) { - addToFavorites.setImageResource(R.drawable.ic_star_filled); - } else { - addToFavorites.setImageResource(R.drawable.ic_star_outline); - } - - addToFavorites.setVisibility(View.VISIBLE); - } - */ } + */ + /** * Update the star icon according to `stopIsInFavorites` */ - fun updateStarIcon() { + fun updateStarIcon(stopIsInFavorites: Boolean) { // no favorites no party! // check if there is a last Stop diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt @@ -192,6 +192,7 @@ val csvWriter = CsvWriter.builder().build(outWriter) val userDB = UserDB.getInstance(context) userDB.writeFavoritesToCsv(csvWriter) + csvWriter.flush() outWriter.flush() zipOutputStream.closeEntry() diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -35,7 +35,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; @@ -50,8 +49,8 @@ import it.reyboz.bustorino.adapters.StopRecyclerAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DatabaseUpdate; -import it.reyboz.bustorino.data.FavoritesViewModel; -import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; +import it.reyboz.bustorino.middleware.CoroutineFavoriteAction; +import it.reyboz.bustorino.viewmodels.FavoritesViewModel; public class FavoritesFragment extends ScreenBaseFragment { @@ -140,7 +139,7 @@ registerForContextMenu(favoriteRecyclerView); - model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); + model.getFavoritesWithStop().observe(getViewLifecycleOwner(), this::showStops); // watch the DB update DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, workInfos -> { @@ -153,7 +152,7 @@ //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 @@ 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 @@ 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/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java --- 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.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 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 @@ 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 @@ 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 @@ || 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 @@ }); - 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 @@ } }; - + */ //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; @@ -337,15 +335,14 @@ 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 @@ 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 @@ } 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 @@ @Override public void onPause() { //mainHandler = null; - locationManager.removeLocationRequestFor(requester); + //locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); @@ -648,7 +645,6 @@ } @Nullable - @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; @@ -718,7 +714,7 @@ 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 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -487,11 +487,12 @@ } */ //save last location - if (locationInitialized) - map?.locationComponent?.lastKnownLocation?.let{ - stopsViewModel.lastUserLocation = it + //if (locationInitialized) + map?.locationComponent?.let{ + if(locationInitialized && it.isLocationComponentActivated){ + stopsViewModel.lastUserLocation = it.lastKnownLocation } - + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ gridRecyclerView.setAdapter(null); Log.d(DEBUG_TAG,"On paused called"); + + locationProvider.stopUpdates(); } @Override @@ -465,6 +474,10 @@ 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 --- 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 @@ } } - /** - * 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 --- 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 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 @@ 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 @@ } }); } + /*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 --- 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.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 --- 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.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 @@ @Override public void run() { - final NextGenDB nextGenDB = NextGenDB.getInstance(context); - //ContentValues[] values = new ContentValues[routesToInsert.size()]; - ArrayList 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 --- 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 --- /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 --- 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 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 @@ 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 @@ } 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 @@ return palina } + fun requestArrivalsForStop(stopId: String, fetchers: List){ val context = appContext //application.applicationContext currentFetchers.value = fetchers @@ -222,8 +250,8 @@ 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 --- /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 --- 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 @@ 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 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -34,7 +34,7 @@ private val executor = Executors.newFixedThreadPool(2) - private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + private val oldRepo = OldDataRepository(executor, application) val stopsToShow = MutableLiveData(ArrayList()) private var stopsShownIDs = HashSet() diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml --- 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 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -391,4 +391,6 @@ Italian English Checking new alerts now + Press back again to close the app +