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 @@ -1,850 +1,862 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.*; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.*; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; 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; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; 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(); + } + } } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); serviceAlertsViewModel = new ViewModelProvider(this).get(ServiceAlertsViewModel.class); /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getWindow().setNavigationBarContrastEnforced(false); } */ //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); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } //database check // DatabaseUpdate.requestDBUpdateWithWork(this, false, false); DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false); /* Watch for database update */ DBUpdateWorker.getWorkInfoLiveData(this) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { - createDefaultSnackbar(); + createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); //if (vl.length() == 0 || vl.equals("arrivals")) { // showMainFragment(); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { //we are not restarting the activity from nothing if (vl.equals("map")) { requestMapFragment(false); } else if (vl.equals("favorites")) { checkAndShowFavoritesFragment(getSupportFragmentManager(), false); } else if (vl.equals("lines")) { showLinesFragment(getSupportFragmentManager(), false, null); } else { showMainFragment(false); } } onCreateComplete = true; //last but not least, set the good default values checkApplyDefaultSettingsValues(); // handle the device "insets" ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rootRelativeLayout), (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Apply the insets as a margin to the view. This solution sets only the // bottom, left, and right dimensions, but you can apply whichever insets are // appropriate to your layout. You can also update the view padding if that's // more appropriate. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; mlp.rightMargin = insets.right; v.setLayoutParams(mlp); //set for toolbar //mlp = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams(); //mlp.topMargin = insets.top; //mToolbar.setLayoutParams(mlp); mToolbar.setPadding(0, insets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); /* ViewCompat.setOnApplyWindowInsetsListener(mToolbar, (v, windowInsets) -> { Insets statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); // Apply the insets as a margin to the view. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.topMargin = statusBarInsets.top; v.setLayoutParams(mlp); v.setPadding(0, statusBarInsets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); */ //to properly handle IME WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { insetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } //check if first run activity (IntroActivity) has been started once or not final SharedPreferences theShPr = getMainSharedPreferences(); boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false); if(!hasIntroRun){ startIntroductionActivity(); } serviceAlertsViewModel.getLastTimeRunningDownload().observe(this, (timeRunning) -> { if (timeRunning != null) { Log.d(DEBUG_TAG, "requested alerts download at time: "+timeRunning); } }); serviceAlertsViewModel.launchAlertsPeriodCheck(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment checkAndShowFavoritesFragment(getSupportFragmentManager(), true); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); showLinesFragment(getSupportFragmentManager(), true,null); return true; } else if(menuItem.getItemId() == R.id.drawer_action_info) { closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } //requesting permissions @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } /*@Override public void onBackPressed() { if (!activityCustomBackPressed()) super.onBackPressed(); } */ private boolean activityCustomBackPressed(){ boolean resolved = true; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStack(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); } else{ resolved = false; } return resolved; } /** * 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); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); showSnackbar = ((ScreenBaseFragment) frag).showSnackbarOnDBUpdate(); } 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"); } } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } private void requestMapFragment(final boolean allowReturn){ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache /*if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ //nothing to do Log.d(DEBUG_TAG, "Build codes allow the showing of the map"); createAndShowMapFragment(null, allowReturn); return; } final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); Log.d(DEBUG_TAG, "Permission for storage: "+result); switch (result) { case PERMISSION_OK: createAndShowMapFragment(null, allowReturn); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null, allowReturn)); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = getString(R.string.storage_permission); String text = getString(R.string.too_many_permission_asks, storage_perm); Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } */ //The permissions are handled in the MapLibreFragment instead createAndShowMapFragment(null, allowReturn); } private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); if(fragment!=null){ ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); }else{ //use new method ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); } if (addToBackStack) ft.addToBackStack("favorites_main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setReorderingAllowed(false); ft.commit(); } private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack("linesGrid"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private void showMainFragment(boolean addToBackStack){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ createShowMainFragment(fraMan, null, addToBackStack); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment, addToBackStack); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } //return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); if (mainFragmentIfVisible!=null){ mainFragmentIfVisible.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; default: titleResId = 0; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment, true); probableFragment.requestArrivalsForStopID(ID); } else { // we have no fragment final Bundle args = new Bundle(); args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID); //if onCreate is complete, then we are not asking for the first showing fragment boolean addtobackstack = onCreateComplete; createShowMainFragment(fraMan, args ,addtobackstack); } } else { //the MainScreeFragment is shown, nothing to do probableFragment.requestArrivalsForStopID(ID); } mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); tr.addToBackStack("LineFromOther-"+routeGtfsId); tr.commit(); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ final FragmentManager fm = getSupportFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); final MapLibreFragment fragment = MapLibreFragment.newInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapLibreFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } void startIntroductionActivity(){ Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); intent.putExtra(ActivityIntro.RESTART_MAIN, false); startActivity(intent); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if(id == R.id.action_about){ startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } else if (id == R.id.action_hack) { openIceweasel(getString(R.string.hack_url), activityContext); return true; } else if (id == R.id.action_source){ openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; } else if (id == R.id.action_licence){ openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; } else if (id == R.id.action_experiments) { startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); return true; } else if (id == R.id.action_tutorial) { startIntroductionActivity(); return true; } return false; } } @Override protected void onPause() { super.onPause(); // stop updating the alerts serviceAlertsViewModel.setRunningDownloadRequests(false); } @Override protected void onResume() { super.onResume(); serviceAlertsViewModel.launchAlertsPeriodCheck(); } } 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,42 +1,43 @@ package it.reyboz.bustorino.backend; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class Result { @Nullable public final T result; @Nullable public final Exception exception; public boolean isSuccess() { return exception == null; } - public static Result success(@Nullable T result) { + public static Result success(@NonNull T result) { return new Result<>(result); } /** * Returns a failed response */ public static Result failure(Exception error) { return new Result<>(error); } - private Result(@Nullable T result) { + private Result(@NonNull T result) { this.result = result; this.exception = null; } private Result(Exception error) { this.result = null; this.exception = error; } public interface Callback{ void onComplete(Result result); } } 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 @@ -1,294 +1,293 @@ /* BusTO (middleware) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.*; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabaseLockedException; import android.net.Uri; 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 { public static final String AUTHORITY = BuildConfig.APPLICATION_ID +".provider"; private static final int STOP_OP = 1; private static final int LINE_OP = 2; private static final int BRANCH_OP = 3; private static final int FAVORITES_OP =4; private static final int MANY_STOPS = 5; private static final int ADD_UPDATE_BRANCHES = 6; private static final int LINE_INSERT_OP = 7; private static final int CONNECTIONS = 8; private static final int LOCATION_SEARCH = 9; private static final int GET_ALL_FAVORITES =10; public static final String FAVORITES = "favorites"; private static final String DEBUG_TAG="AppDataProvider"; private Context con; private NextGenDB appDBHelper; private UserDB userDBHelper; private SQLiteDatabase db; private DBStatusManager preferences; public AppDataProvider() { } private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. */ sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP); sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS); sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP); sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP); sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP); sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES); sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS); sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP); sUriMatcher.addURI(AUTHORITY,FAVORITES,GET_ALL_FAVORITES); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Implement this to handle requests to delete one or more rows. db = appDBHelper.getWritableDatabase(); int rows; switch (sUriMatcher.match(uri)){ case MANY_STOPS: rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null); break; default: throw new UnsupportedOperationException("Not yet implemented"); } return rows; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. int match = sUriMatcher.match(uri); String baseTypedir = "vnd.android.cursor.dir/"; String baseTypeitem = "vnd.android.cursor.item/"; switch (match){ case LOCATION_SEARCH: return baseTypedir+"stop"; case LINE_OP: return baseTypeitem+"line"; case CONNECTIONS: return baseTypedir+"stops"; } return baseTypedir+"/item"; } @Override public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{ //AVOID OPENING A DB CONNECTION, WILL THROW VERY NASTY ERRORS if(preferences.isDBUpdating(true)) return null; db = appDBHelper.getWritableDatabase(); Uri finalUri; long last_rowid = -1; switch (sUriMatcher.match(uri)){ case ADD_UPDATE_BRANCHES: Log.d("InsBranchWithProvider","new Insert request"); String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME); if(line_name==null) throw new IllegalArgumentException("No line name given"); long lineid = -1; Cursor c = db.query(LinesTable.TABLE_NAME, new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?", new String[]{line_name},null,null,null); Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches"); if(c.getCount() == 0){ //There are no lines, insert? //NOPE c.close(); break; }else { c.moveToFirst(); /* while(c.moveToNext()){ Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n" +c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION))); }*/ lineid = c.getInt(c.getColumnIndexOrThrow(NextGenDB.Contract.LinesTable._ID)); c.close(); } values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME); values.put(BranchesTable.COL_LINE,lineid); last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE); break; case MANY_STOPS: //Log.d("AppDataProvider_busTO","New stop insert request"); try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; case CONNECTIONS: try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; default: throw new IllegalArgumentException("Invalid parameters"); } finalUri = ContentUris.withAppendedId(uri,last_rowid); return finalUri; } @Override public boolean onCreate() { con = getContext(); appDBHelper = NextGenDB.getInstance(getContext()); userDBHelper = UserDB.getInstance(getContext()); if(con!=null) { preferences = new DBStatusManager(con,null); } else { preferences = null; Log.e(DEBUG_TAG,"Cannot get shared preferences"); } return true; } @Override @Nullable public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException { //IMPORTANT //The app should not query when the DB is updating, but apparently, it does if(preferences.isDBUpdating(true)) //throw new UnsupportedOperationException("DB is updating"); return null; SQLiteDatabase db; try{ //try to get a readable database db = appDBHelper.getReadableDatabase(); } catch (SQLiteDatabaseLockedException ex){ Log.e(DEBUG_TAG,"Database is locked",ex); return null; } List parts = uri.getPathSegments(); switch (sUriMatcher.match(uri)){ case LOCATION_SEARCH: //authority/stops/location/"Lat"/"Lon"/"distance" //distance in metres (integer) if(parts.size()>=4 && "location".equals(parts.get(1))){ Double latitude = Double.parseDouble(parts.get(2)); Double longitude = Double.parseDouble(parts.get(3)); //distance in meters final double distance = parts.size()>=5 ? Double.parseDouble(parts.get(4)) : 50; //if(parts.size()>=5) //Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m"); Double latDelta = utils.latitudeDelta(distance); Double longDelta = utils.longitudeDelta(distance, latitude); Log.d(DEBUG_TAG, "Location search around: "+latitude+" , "+longitude); Log.d(DEBUG_TAG, "Location search: latitude {"+(latitude-latDelta)+", "+(latitude+latDelta)+ "} longitude {"+(longitude-longDelta)+", "+(longitude+longDelta)+"}"); String whereClause = StopsTable.COL_LAT+ "< "+(latitude+latDelta)+" AND " +StopsTable.COL_LAT +" > "+(latitude-latDelta)+" AND "+ StopsTable.COL_LONG+" < "+(longitude+longDelta)+" AND "+StopsTable.COL_LONG+" > "+(longitude-longDelta); //Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause); return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null); } else { Log.w(DEBUG_TAG,"Not enough parameters"); if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s); return null; } case FAVORITES_OP: - final String stopFavSelection = 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); case STOP_OP: //Let's try this plain and simple final String[] selectionValues = {uri.getLastPathSegment()}; final String stopSelection = StopsTable.COL_ID+" = ?"; Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]); return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder); case MANY_STOPS: return db.query(StopsTable.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); case GET_ALL_FAVORITES: db = userDBHelper.getReadableDatabase(); return db.query(UserDB.TABLE_NAME, projection, selection, selectionArgs, null, null,sortOrder); default: Log.e("DataProvider","got request "+uri.getPath()+" which doesn't match anything"); } throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } // public static Uri getBaseUriGivenOp(int operationType); public static Uri.Builder getUriBuilderToComplete(){ final Uri.Builder b = new Uri.Builder(); b.scheme("content").authority(AUTHORITY); return b; } @Override public void onLowMemory() { super.onLowMemory(); } } diff --git a/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 @@ -1,205 +1,203 @@ /* BusTO - Data components Copyright (C) 2021-2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data import android.annotation.SuppressLint import android.content.Context -import android.content.pm.ServiceInfo -import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.LiveData import androidx.work.* import androidx.work.WorkManager.Companion.getInstance import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Fetcher import it.reyboz.bustorino.backend.Notifications import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference /** * Worker class that runs the DB update, without checking if it is needed or not */ class DBUpdateWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { @SuppressLint("RestrictedApi") override suspend fun doWork(): Result { val con = applicationContext val sharedPrefs = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE) val newDBVersion = DatabaseUpdate.getNewVersion() /*val currentDBVersion = sharedPrefs.getInt(PreferencesHolder.DB_GTT_VERSION_KEY, -10) val isUpdateCompulsory = inputData.getBoolean(FORCED_UPDATE, false) val lastDBUpdateTime = sharedPrefs.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, 0) var currentTime = System.currentTimeMillis() / 1000 // ---- RECREATE NOTIFICATION HERE IF YOU WANT TO SHOW IT TO THE USER ---- // ---- create notification channel first Log.d(DEBUG_TAG, "Have previous version: $currentDBVersion and new version $newDBVersion") Log.d(DEBUG_TAG, "Update compulsory: $isUpdateCompulsory") //we got a good version if (!(currentDBVersion < newDBVersion || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY) && !isUpdateCompulsory ) { //don't need to update //cancelNotification(NOTIFICATION_ID) return Result.success( Data.Builder().putInt (SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build() ) } */ //start the real update val resultAtomicReference = AtomicReference() DatabaseUpdate.setDBUpdatingFlag(con, sharedPrefs, true) val resultUpdate = DatabaseUpdate.performDBUpdate(con, resultAtomicReference) DatabaseUpdate.setDBUpdatingFlag(con, sharedPrefs, false) if (resultUpdate != DatabaseUpdate.Result.DONE) { //Fetcher.Result result = resultAtomicReference.get(); val dataBuilder = Data.Builder() 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) return Result.failure(dataBuilder.build()) } Log.d(DEBUG_TAG, "Update finished successfully!") //update the version in the shared preference val editor = sharedPrefs.edit() editor.putInt(PreferencesHolder.DB_GTT_VERSION_KEY, newDBVersion) val currentTime = System.currentTimeMillis() / 1000 editor.putLong(PreferencesHolder.DB_LAST_UPDATE_KEY, currentTime) editor.apply() //cancelNotification(NOTIFICATION_ID) return Result.success(Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()) } override suspend fun getForegroundInfo(): ForegroundInfo { //val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val context = applicationContext Notifications.createDBNotificationChannelIfNeeded(context) val builder = NotificationCompat.Builder( context, Notifications.DB_UPDATE_CHANNELS_ID ) .setContentTitle(context.getString(R.string.database_update_msg_notif)) .setProgress(0, 0, true) .setPriority(NotificationCompat.PRIORITY_LOW) builder.setSmallIcon(R.drawable.ic_bus_stilized) /*val typeInt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC } else 0 */ return ForegroundInfo(NOTIFICATION_ID, builder.build()) } /* private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, final String channel_ID){ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); notificManager.notify(notification_ID,builder.build()); return notification_ID; } */ private fun cancelNotification(notificationID: Int) { val notificationManager = NotificationManagerCompat.from(getApplicationContext()) notificationManager.cancel(notificationID) } companion object { const val ERROR_CODE_KEY: String = "Error_Code" const val ERROR_REASON_KEY: String = "ERROR_REASON" 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 const val SUCCESS_UPDATE_DONE: Int = 1 const val FORCED_UPDATE: String = "FORCED-UPDATE" private const val DEBUG_TAG: String = "BusTO-UpdateWorker" const val STATUS_UPDATE: String = "STATUS_UPDATE" const val WORK_NAME = "BusTO-UpdateWorker" private const val NOTIFICATION_ID = 32198 private const val UPDATE_MIN_DELAY = (9 * 24 * 3600 //9 days ).toLong() val workConstraints: Constraints get() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build() /** * Run the database update immediately */ @JvmStatic fun requestDBUpdateUniqueWork(con: Context, forced: Boolean) { val workManager = getInstance(con) val reqData = Data.Builder() .putBoolean(FORCED_UPDATE, forced).build() val wr = OneTimeWorkRequest.Builder(DBUpdateWorker::class.java) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setConstraints( Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .setInputData(reqData) .build() workManager.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, wr) } @JvmStatic fun getWorkInfoLiveData(context: Context): LiveData> { val workManager = WorkManager.getInstance(context) return workManager.getWorkInfosForUniqueWorkLiveData(WORK_NAME) } } } 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 @@ -1,328 +1,276 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; -import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; -import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.gtfs.GtfsAgency; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.data.gtfs.GtfsFeed; import it.reyboz.bustorino.data.gtfs.GtfsRoute; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.PatternStop; import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; - + //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 } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } try { JSONObject resp = new JSONObject(networkRequest); return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); return JSON_PARSING_ERROR; } } private static boolean updateGTFSAgencies(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( con, res ); dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); return true; } private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final List routes= MatoAPIFetcher.Companion.getRoutes(con, res); final HashMap> routesStoppingInStop = new HashMap<>(); dao.insertRoutes(routes); if(res.get()!= Fetcher.Result.OK){ return routesStoppingInStop; } final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); final HashMap routesMap = new HashMap<>(routes.size()); for(GtfsRoute r: routes){ gtfsRoutesIDs.add(r.getGtfsId()); routesMap.put(r.getGtfsId(),r); } long t0 = System.currentTimeMillis(); final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); long tend = System.currentTimeMillis() - t0; Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); if(res.get()!=Fetcher.Result.OK){ Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); return routesStoppingInStop; } //match patterns with routes final ArrayList patternStops = makeStopsForPatterns(patterns); final List allPatternsCodeInDB = dao.getPatternsCodes(); final HashSet patternsCodesToDelete = new HashSet<>(allPatternsCodeInDB); for(MatoPattern p: patterns){ //scan patterns final ArrayList stopsIDs = p.getStopsGtfsIDs(); final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId()); if (mRoute == null) { Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map"); } for (final String sID : stopsIDs) { //add stops to pattern stops // save routes stopping in the stop if (!routesStoppingInStop.containsKey(sID)) { routesStoppingInStop.put(sID, new HashSet<>()); } Set mset = routesStoppingInStop.get(sID); assert mset != null; mset.add(mRoute.getShortName()); } //finally, remove from deletion list patternsCodesToDelete.remove(p.getCode()); } // final time for insert dao.insertPatterns(patterns); // clear patterns that are unused Log.d(DEBUG_TAG, "Have to remove "+patternsCodesToDelete.size()+ " patterns from the DB"); dao.deletePatternsWithCodes(new ArrayList<>(patternsCodesToDelete)); dao.insertPatternStops(patternStops); return routesStoppingInStop; } /** * Make the list of stops that each pattern does, to be inserted into the DB * @param patterns the MatoPattern * @return a list of PatternStop */ public static ArrayList makeStopsForPatterns(List patterns){ final ArrayList patternStops = new ArrayList<>(patterns.size()); for (MatoPattern p: patterns){ final ArrayList stopsIDs = p.getStopsGtfsIDs(); for (int i=0; i gres) { // GTFS data fetching AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); updateGTFSAgencies(con, gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); } else{ Log.d(DEBUG_TAG, "Done downloading agencies"); } gtfsRes.set(Fetcher.Result.OK); final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the routes into DB"); } else{ Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); } /*db.beginTransaction(); startTime = System.currentTimeMillis(); int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop); if(countStop!= routesStoppingByStop.size()){ Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with " +routesStoppingByStop.size()+" stops to update"); } db.setTransactionSuccessful(); db.endTransaction(); endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms"); */ // Stops insertion final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading stops"); return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; } final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); - final SQLiteDatabase db = dbHelp.getWritableDatabase(); - - if(!db.isOpen()){ - //catch errors like: java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase - //we have to abort the work and restart it - return Result.DB_CLOSED; - } - //TODO: Get the type of stop from the lines - //Empty the needed tables - - db.beginTransaction(); - //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); - //db.delete(LinesTable.TABLE_NAME,null,null); - - //put new data - long startTime = System.currentTimeMillis(); - - Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); - String routesStoppingString=""; - int patternsStopsHits = 0; - for (final Palina p : palinasMatoAPI) { - final ContentValues cv = new ContentValues(); - - cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); - cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); - if (p.location != null) - cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); - cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); - cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); - if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); - if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){ - final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID)); - routesStoppingString = Palina.buildRoutesStringFromNames(routesSs); - patternsStopsHits++; - } else{ - routesStoppingString = p.routesThatStopHereToString(); - } - cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString); - if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); - if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); - //Log.d(DEBUG_TAG,cv.toString()); - //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); - //valuesArr[i] = cv; - db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); - - } - db.setTransactionSuccessful(); - db.endTransaction(); - long endTime = System.currentTimeMillis(); - Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); - Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); - // 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){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /* * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); final Data reqData = new Data.Builder() .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 2, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .setInputData(reqData) .build(); final int version = theShPr.getInt(PreferencesHolder.DB_GTT_VERSION_KEY, -10); final long lastDBUpdateTime = theShPr.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, -10); if ((version >= 0 || lastDBUpdateTime >=0) && !restart) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, wr); } */ /* public static boolean isDBUpdating(){ return false; TODO } */ public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, @NonNull Observer> observer) { DBUpdateWorker.getWorkInfoLiveData(context).observe( lifecycleOwner, observer ); } } 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 @@ -1,227 +1,244 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.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; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.Stop; public class FavoritesLiveData extends LiveData> implements CustomAsyncQueryHandler.AsyncQueryListener { private static final String TAG = "BusTO-FavoritesLiveData"; private final boolean notifyChangesDescendants; @NonNull private final Context mContext; @NonNull private final FavoritesLiveData.ForceLoadContentObserver mObserver; private final CustomAsyncQueryHandler queryHandler; private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( AppDataProvider.FAVORITES).build(); private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=220; @Nullable private List stopsFromFavorites, stopsDone; private boolean isQueryRunning = false; private int stopNeededCount = 0; public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) { super(); mContext = context.getApplicationContext(); mObserver = new FavoritesLiveData.ForceLoadContentObserver(); notifyChangesDescendants = notifyDescendantsChanges; queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this); } - private void loadData() { + public void loadData() { loadData(false); } private static Uri.Builder getStopsBuilder(){ return AppDataProvider.getUriBuilderToComplete().appendPath("stop"); } private void loadData(boolean forceQuery) { Log.d(TAG, "loadData() force: "+forceQuery); if (!forceQuery){ if (getValue()!= null){ //Data already loaded Log.d(TAG, "Data already loaded"); return; } } if (isQueryRunning){ //we are waiting for data, we will get an update soon Log.d(TAG, "Query is running, abort"); return; } 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(){ loadData(true); } @Override protected void onActive() { //Log.d(TAG, "onActive()"); loadData(true); } /** * Clear the data for the cursor */ public void onClear(){ ContentResolver resolver = mContext.getContentResolver(); resolver.unregisterContentObserver(mObserver); } @Override protected void setValue(List stops) { //Log.d("BusTO-FavoritesLiveData","Setting the new values for the stops, have "+ // stops.size()+" stops"); ContentResolver resolver = mContext.getContentResolver(); resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver); super.setValue(stops); } @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; } else for (int i = 0; i < stopsFromFavorites.size(); i++) { Stop s = stopsFromFavorites.get(i); queryHandler.startQuery(STOPS_TOKEN_BASE + i, null, getStopsBuilder().appendPath(s.ID).build(), NextGenDB.QUERY_COLUMN_stops_all, null, null, null); } } else if(token >= STOPS_TOKEN_BASE){ final int index = token - STOPS_TOKEN_BASE; assert stopsFromFavorites != null; Stop stopUpdate = stopsFromFavorites.get(index); Stop finalStop; List result = NextGenDB.getStopsFromCursorAllFields(cursor); cursor.close(); if (result.isEmpty()){ // stop is not in the DB finalStop = stopUpdate; } else { finalStop = result.get(0); if (BuildConfig.DEBUG && !(finalStop.ID.equals(stopUpdate.ID))) { throw new AssertionError("Assertion failed"); } finalStop.setStopUserName(stopUpdate.getStopUserName()); } if (stopsDone!=null) stopsDone.add(finalStop); stopNeededCount--; if (stopNeededCount == 0) { // we have finished the queries isQueryRunning = false; Collections.sort(stopsDone); setValue(stopsDone); } } } /** * Content Observer that forces reload of cursor when data changes * On different thread (new Handler) */ public final class ForceLoadContentObserver extends ContentObserver { public ForceLoadContentObserver() { super(new Handler(Looper.myLooper())); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { Log.d(TAG, "ForceLoadContentObserver.onChange()"); loadData(true); } } } 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 @@ -1,580 +1,698 @@ /* BusTO (middleware) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.*; import android.net.Uri; 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; import java.util.*; import java.util.stream.Collectors; import static it.reyboz.bustorino.data.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; public static final int DATABASE_VERSION = 3; public static final String DEBUG_TAG = "NextGenDB-BusTO"; //NO Singleton instance //private static volatile NextGenDB instance = null; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ StopsTable.COL_GTFS_ID+" TEXT, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public static final String[] QUERY_COLUMN_stops_all = { StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION, StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING}; public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " + StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG + " >= ? AND "+ StopsTable.COL_LONG + " <= ?"; 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) { super(context, DATABASE_NAME, null, DATABASE_VERSION); appContext = context.getApplicationContext(); } public static NextGenDB getInstance(Context context) { if (INSTANCE == null){ INSTANCE = new NextGenDB(context.getApplicationContext()); } return INSTANCE; } @Override public void onCreate(SQLiteDatabase db) { Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion<2 && newVersion == 2){ //DROP ALL TABLES db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME); db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME); db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME); db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME); //RECREATE THE TABLES WITH THE NEW SCHEMA db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); //DatabaseUpdate.requestDBUpdateWithWork(appContext, true, true); DBUpdateWorker.requestDBUpdateUniqueWork(appContext, true); } if(oldVersion < 3 && newVersion == 3){ Log.d("BusTO-Database", "Running upgrades for version 3"); //add the new column db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+ " ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT "); // DatabaseUpdate.requestDBUpdateWithWork(appContext, true); } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } public static String getSqlCreateStopsTable(String tableName){ return "CREATE TABLE "+tableName+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; } /** * Query some bus stops inside a map view * @return stoplist, if empty it means that an error occurred * * You can obtain the coordinates from OSMDroid using something like this: * BoundingBoxE6 bb = mMapView.getBoundingBox(); * double latFrom = bb.getLatSouthE6() / 1E6; * double latTo = bb.getLatNorthE6() / 1E6; * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ - public synchronized 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(); // coordinates must be strings in the where condition String minLatRaw = String.valueOf(minLat); String maxLatRaw = String.valueOf(maxLat); String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); - 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); boolean first = true; builder.append(" ( "); for(int i=0; i< gtfsIDs.size(); i++){ if(first){ first = false; } else{ builder.append(", "); } builder.append("?");//.append("\"").append(id).append("\""); } builder.append(") "); final String whereClause = builder.toString(); 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 * @return an Array of the stops found in the query */ public static ArrayList getStopsFromCursorAllFields(Cursor result){ final int colID = result.getColumnIndex(StopsTable.COL_ID); final int colName = result.getColumnIndex(StopsTable.COL_NAME); final int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION); final int colType = result.getColumnIndex(StopsTable.COL_TYPE); final int colLat = result.getColumnIndex(StopsTable.COL_LAT); final int colGtfsID = result.getColumnIndex(StopsTable.COL_GTFS_ID); final int colLon = result.getColumnIndex(StopsTable.COL_LONG); final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING); int count = result.getCount(); ArrayList stops = new ArrayList<>(count); int i = 0; while(result.moveToNext()) { final String stopID = result.getString(colID).trim(); Route.Type type; //if(result.getString(colType) == null) type = Route.Type.BUS; //else type = Route.getTypeFromSymbol(result.getString(colType)); //if(result.getInt(colType) == null) type = Route.Type.BUS; try{ type = Route.Type.fromCode(result.getInt(colType)); } catch (Exception e){ type = Route.Type.BUS; } String lines = result.getString(colLines).trim(); String locationSometimesEmpty = result.getString(colLocation); if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) { locationSometimesEmpty = null; } stops.add(new Stop(stopID, result.getString(colName), null, locationSometimesEmpty, type, splitLinesString(lines), result.getDouble(colLat), result.getDouble(colLon), result.getString(colGtfsID)) ); } 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()]; ArrayList branchesValues = new ArrayList<>(routesToInsert.size()); ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()); long starttime,endtime; for (Route r:routesToInsert){ //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<>(); final int col_id = data.getColumnIndex(StopsTable.COL_ID); final int latInd = data.getColumnIndex(StopsTable.COL_LAT); final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); data.moveToFirst(); for(int i=0; i 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*")); } public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public interface DataTables { String getTableName(); String[] getFields(); } public static final class LinesTable implements BaseColumns, DataTables { //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "line_name"; public static final String COLUMN_DESCRIPTION = "line_description"; public static final String COLUMN_TYPE = "line_bacino"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE}; } } public static final class BranchesTable implements BaseColumns, DataTables { public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "branch_direzione"; public static final String COL_FESTIVO = "branch_festivo"; public static final String COL_TYPE = "branch_type"; public static final String COL_LUN="runs_lun"; public static final String COL_MAR="runs_mar"; public static final String COL_MER="runs_mer"; public static final String COL_GIO="runs_gio"; public static final String COL_VEN="runs_ven"; public static final String COL_SAB="runs_sab"; public static final String COL_DOM="runs_dom"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION, COL_DIRECTION,COL_FESTIVO,COL_TYPE, COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM }; } } public static final class ConnectionsTable implements DataTables { public static final String TABLE_NAME = "connections"; public static final String COLUMN_BRANCH = "branchid"; public static final String COLUMN_STOP_ID = "stopid"; public static final String COLUMN_ORDER = "ordine"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER}; } } public static final class StopsTable implements DataTables { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "stopid"; //integer public static final String COL_TYPE = "stop_type"; public static final String COL_NAME = "stop_name"; public static final String COL_GTFS_ID = "gtfs_id"; public static final String COL_LAT = "stop_latitude"; public static final String COL_LONG = "stop_longitude"; public static final String COL_LOCATION = "stop_location"; public static final String COL_PLACE = "stop_placeName"; public static final String COL_LINES_STOPPING = "stop_lines"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; } } } public static final class DBUpdatingException extends Exception{ public DBUpdatingException(String message) { super(message); } } } diff --git a/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 @@ -1,92 +1,137 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data import android.content.Context +import android.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, latitTo: Double, longitFrom: Double, longitTo: Double, callback: Callback> ){ //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 */ fun requestStopsWithinDistance(latitude: Double, longitude: Double, distanceMeters: Int, callback: Callback>){ val latDelta = utils.latitudeDelta(distanceMeters.toDouble()) val longDelta = utils.longitudeDelta(distanceMeters.toDouble(), latitude) requestStopsInArea(latitude-latDelta, latitude+latDelta, longitude-longDelta, longitude+longDelta, callback) } 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 @@ -1,404 +1,520 @@ /* BusTO ("backend" components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.content.Context; import android.net.Uri; import android.util.Log; -import java.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 { public static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "user.db"; static final String TABLE_NAME = "favorites"; private final Context c; // needed during upgrade public final static String COL_ID = "ID"; public final static String COL_USERNAME="username"; public static final int FILE_INVALID=-10; + private 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(); private static UserDB mInstance; public static synchronized UserDB getInstance(Context context) { if (mInstance == null) { mInstance = new UserDB(context.getApplicationContext()); } return mInstance; } private UserDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.c = context.getApplicationContext(); } @Override public void onCreate(SQLiteDatabase db) { // exception intentionally left unhandled db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)"); if(OldDB.doesItExist(this.c)) { upgradeFromOldDatabase(db); } } private void upgradeFromOldDatabase(SQLiteDatabase newdb) { OldDB old; try { old = new OldDB(this.c); } catch(IllegalStateException e) { // can't create database => it doesn't really exist, no matter what doesItExist() says return; } int ver = old.getOldVersion(); /* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app * has crashed midway through the upgrade and the user is retrying, that should never show * up here. And if it does, try to recover favorites anyway. * Versions < 8 already got dropped during the update process, so let's do the same. * * Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it * more complicated. Workaround added in OldDB. */ if(ver >= 8) { ArrayList ID = new ArrayList<>(); ArrayList username = new ArrayList<>(); int len; int len2; try { Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {}); int zero = c.getColumnIndex("busstop_ID"); int one = c.getColumnIndex("busstop_username"); while(c.moveToNext()) { try { ID.add(c.getString(zero)); } catch(Exception e) { // no ID = can't add this continue; } if(c.getString(one) == null || c.getString(one).length() <= 0) { username.add(null); } else { username.add(c.getString(one)); } } c.close(); old.close(); } catch(Exception ignored) { // there's no hope, go ahead and nuke old database. } len = ID.size(); len2 = username.size(); if(len2 < len) { len = len2; } if (len > 0) { try { for (int i = 0; i < len; i++) { final Stop mStop = new Stop(ID.get(i)); mStop.setStopUserName(username.get(i)); addOrUpdateStop(mStop, newdb); } } catch(Exception ignored) { // partial data is better than no data at all, no transactions here } } } if(!OldDB.destroy(this.c)) { // TODO: notify user somehow? Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user."); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } /** * Check if a stop ID is in the favorites - * - * @param db readable database + ** * @param stopId stop ID * @return boolean */ - public static boolean isStopInFavorites(SQLiteDatabase db, String stopId) { + public boolean isStopInFavorites(String stopId) { + + SQLiteDatabase db = this.getReadableDatabase(); boolean found = false; try { // better way to check the existence long count = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "ID = ?", new String[]{stopId}); return count > 0; } catch(SQLiteException e) { // don't care Log.w("BusTO-UserDB", "isStopInFavorites failed for " + stopId, e); } return found; } /** * Gets stop name set by the user. * - * @param db readable database * @param stopID stop ID * @return name set by user, or null if not set\not found */ - public static @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"); while(c.moveToNext()) { stopUserName = c.getString(colUser); stopID = c.getString(colID); s = dbi.getAllFromID(stopID); if(s == null) { // can't find it in database l.add(new Stop(stopUserName, stopID, null, null, null)); } else { // setStopName() already does sanity checks s.setStopUserName(stopUserName); l.add(s); } } c.close(); } catch(SQLiteException ignored) {} // comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database Collections.sort(l); return l; } public static void notifyContentProvider(Context context){ context. getContentResolver(). notifyChange(FAVORITES_URI, null); } public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){ List colsList = Arrays.asList(columns); - if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){ + if (!colsList.contains(FAVORITES_COLUMNS_ARRAY[0]) || !colsList.contains(FAVORITES_COLUMNS_ARRAY[1])){ throw new IllegalArgumentException(); } ArrayList l = new ArrayList<>(); if (cursor==null){ Log.e("UserDB-BusTO", "Null cursor given in getFavoritesFromCursor"); return l; } final int colID = cursor.getColumnIndex("ID"); final int colUser = cursor.getColumnIndex("username"); while(cursor.moveToNext()) { final String stopUserName = cursor.getString(colUser); final String stopID = cursor.getString(colID); final Stop s = new Stop(stopID.trim()); if (stopUserName!=null) s.setStopUserName(stopUserName); l.add(s); } return l; } - public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { + @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; } //extract rows into CSV public boolean writeFavoritesToCsv(CsvWriter writer){ SQLiteDatabase db = this.getReadableDatabase(); String sortOrder = COL_ID + " DESC"; - Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder); + Cursor cursor = db.query(TABLE_NAME, FAVORITES_COLUMNS_ARRAY,null,null,null,null, sortOrder); final int nCols = 2;//cursor.getColumnCount(); writer.writeRecord(cursor.getColumnNames()); while (cursor.moveToNext()){ String[] arr = {cursor.getString(0), cursor.getString(1)}; writer.writeRecord(arr); } cursor.close(); return true; } public int insertRowsFromCSV(CsvReader reader){ SQLiteDatabase db = this.getWritableDatabase(); boolean firstrow = true; final HashMap colIndexByRows = new HashMap<>(); final CloseableIterator rowsIter = reader.iterator(); if (!rowsIter.hasNext()){ //nothing to do, it's an empty file return -1; } final CsvRecord firstRow = rowsIter.next(); // close if there isn't another rows if(!rowsIter.hasNext()) return -2; for (int i =0; i= 0) updated +=1; } db.setTransactionSuccessful(); db.endTransaction(); // These should NOT be closed: the database is a singleton, the connections are recycled. //db.close(); 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 @@ -1,840 +1,836 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.database.Cursor import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener 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.* class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { private var DEBUG_TAG = DEBUG_TAG_ALL private lateinit var stopID: String //private set private var stopName: String? = null private var prefs: DBStatusManager? = null private var listener: OnDBUpdateStatusChangeListener? = null private var justCreated = false private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false - private var stopIsInFavorites = false //Views protected lateinit var addToFavorites: ImageButton protected lateinit var openInMapButton: ImageButton protected lateinit var arrivalsSourceTextView: TextView private lateinit var messageTextView: TextView private lateinit var preMessageTextView: TextView // this hold the "Arrivals at: " text protected lateinit var arrivalsRecyclerView: RecyclerView private var mListAdapter: PalinaAdapter? = null private lateinit var resultsLayout : LinearLayout private lateinit var loadingMessageTextView: TextView private lateinit var progressBar: ProgressBar private lateinit var howDoesItWorkTextView: TextView private lateinit var hideHintButton: Button //private NestedScrollView theScrollView; protected lateinit var noArrivalsRecyclerView: RecyclerView private var noArrivalsAdapter: RouteOnlyLineAdapter? = null private var noArrivalsTitleView: TextView? = null private var layoutManager: GridLayoutManager? = null //private View canaryEndView; private var fetchers: List = ArrayList() private val arrivalsViewModel : ArrivalsViewModel by viewModels() private var reloadOnResume = true fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName = route.routeLongDisplayName Log.d(DEBUG_TAG, "Make toast for line " + route.name) if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { Log.d( DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" ) if (route.gtfsId != null) { mListener.openLineFromStop(route.gtfsId, stopID) } else { val gtfsID = FiveTNormalizer.getGtfsRouteID(route) Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") mListener.openLineFromStop(gtfsID, stopID) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) preMessageTextView = root.findViewById(R.id.arrivalsTextView) addToFavorites = root.findViewById(R.id.addToFavorites) openInMapButton = root.findViewById(R.id.openInMapButton) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) arrivalsSourceTextView = root.findViewById(R.id.timesSourceTextView) arrivalsSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider arrivalsSourceTextView.setText(R.string.arrival_source_changing) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } arrivalsSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites - toggleLastStopToFavorites() + toggleStopFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages_fill), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } noArrivalsRecyclerView.setLayoutManager(layoutManager) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ 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) } arrivalsViewModel.arrivalsRequestRunningLiveData.observe(viewLifecycleOwner, { running -> //UI CHANGES TO APPLY WHEN THE REQUEST IS RUNNING mListener.toggleSpinner(running) if(running){ //different way of setting this flag if(lastUpdatedPalina == null || lastUpdatedPalina?.totalNumberOfPassages==0) { showLoadingMessageForFirstTime() } } else{ //stopped running, we can show the palina //val uname = lastUpdatedPalina?.stopDisplayName if (lastUpdatedPalina == null || lastUpdatedPalina?.numRoutesWithArrivals == 0) { //no passages and result is not valid setUIForNoStopFound() } } }) - 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) if (palinaIsValid){ updateFragmentData(it) } if(arrivalsViewModel.arrivalsRequestRunningLiveData.value ==false) { //finished loading if (palinaIsValid) { //the result is true hideLoadingMessageAndShowResults() } else { setUIForNoStopFound() } } } // this is only for the progress arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "Using arrivals source: $it") val srcString = getDisplayArrivalsSource(it,requireContext()) loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString) } arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res -> val src = arrivalsViewModel.sourcesLiveData.value when (res) { Fetcher.Result.OK -> {} Fetcher.Result.CLIENT_OFFLINE -> showFetcherMessage(R.string.network_error, src) Fetcher.Result.SERVER_ERROR -> { if (utils.isConnected(context)) { showFetcherMessage(R.string.parsing_error, src) } else { showFetcherMessage(R.string.network_error, src) } showFetcherMessage(R.string.internal_error,src) } Fetcher.Result.PARSER_ERROR -> showFetcherMessage(R.string.internal_error, src) Fetcher.Result.QUERY_TOO_SHORT -> showFetcherMessage(R.string.query_too_short, src) Fetcher.Result.EMPTY_RESULT_SET -> showFetcherMessage(R.string.no_arrivals_stop, src) Fetcher.Result.NOT_FOUND -> showFetcherMessage(R.string.no_bus_stop_have_this_name, src) else -> showFetcherMessage(R.string.internal_error, src) } } + arrivalsViewModel.stopInFavorites.observe(viewLifecycleOwner, { isFavorite -> + updateStarIcon(isFavorite) + }) return root } private fun showShortToast(id: Int) = showToastMessage(id,true) private fun showFetcherMessage(id: Int, source: Source?){ val srcString = source?.let{ getDisplayArrivalsSource(it,requireContext())} if (srcString!=null){ Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show() } else{ val message = getString(id) Toast.makeText(requireContext(), "$srcString : $message", Toast.LENGTH_SHORT).show() } } /*private fun changeUIFirstSearchActive(yes: Boolean){ if(yes){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } else{ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } } */ private fun showLoadingMessageForFirstTime(){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } private fun hideLoadingMessageAndShowResults(){ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } private fun setUIForNoStopFound(){ progressBar.visibility=View.INVISIBLE // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times if (stopName==null) loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) else loadingMessageTextView.text = getString(R.string.no_arrivals_stop) } override fun onResume() { super.onResume() val loaderManager = loaderManager Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina") mListener.readyGUIfor(FragmentKind.ARRIVALS) //fix bug when the list adapter is null mListAdapter?.let { resetListAdapter(it) } if (noArrivalsAdapter != null) { noArrivalsRecyclerView.adapter = noArrivalsAdapter } if (stopID.isNotEmpty()) { if (!justCreated) { fetchers = utils.getDefaultArrivalsFetchers(context) adjustFetchersToSource() if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID) } else { //start first search requestArrivalsForTheFragment() showLoadingMessageForFirstTime() justCreated = false } //start the loader if (prefs!!.isDBUpdating(true)) { prefs!!.registerListener() } else { Log.d(DEBUG_TAG, "Restarting loader for stop") loaderManager.restartLoader( loaderFavId, arguments, this ) } updateMessage() } if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) { showHints() } } override fun onStart() { super.onStart() if (needUpdateOnAttach) { updateFragmentData(null) needUpdateOnAttach = false } } override fun onPause() { if (listener != null) prefs!!.unregisterListener() super.onPause() val loaderManager = loaderManager Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders()) loaderManager.destroyLoader(loaderFavId) } override fun onAttach(context: Context) { super.onAttach(context) //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context) } fun reloadsOnResume(): Boolean { return reloadOnResume } fun setReloadOnResume(reloadOnResume: Boolean) { this.reloadOnResume = reloadOnResume } // HINT "HOW TO USE" private fun showHints() { howDoesItWorkTextView.visibility = View.VISIBLE hideHintButton.visibility = View.VISIBLE //actionHelpMenuItem.setVisible(false); } private fun hideHints() { howDoesItWorkTextView.visibility = View.GONE hideHintButton.visibility = View.GONE //actionHelpMenuItem.setVisible(true); } fun onHideHint(v: View?) { hideHints() setOption(requireContext(), OPTION_SHOW_LEGEND, false) } fun getCurrentFetchersAsArray(): Array { val r= fetchers.toTypedArray() //?: emptyArray() return r } private fun rotateFetchers() { Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers") fetchers?.let { Collections.rotate(it, -1) } Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers") } /** * Update the UI with the new data * @param p the full Palina */ fun updateFragmentData(p: Palina?) { if (p != null) lastUpdatedPalina = p if (!isAdded) { //defer update at next show if (p == null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null") else needUpdateOnAttach = true } else { //set title if(stopName==null && p?.stopDisplayName != null){ stopName = p.stopDisplayName updateMessage() } val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) p?.let { //only update the sources if we have actual passaggi if (arrivalsViewModel.arrivalsRequestRunningLiveData.value == false) showArrivalsSources(lastUpdatedPalina!!) } resetListAdapter(adapter) lastUpdatedPalina?.let{ pal -> openInMapButton.setOnClickListener { if (pal.hasCoords()) mListener.showMapCenteredOnStop(pal) } } val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) noArrivalsRecyclerView.adapter = noArrivalsAdapter noArrivalsRecyclerView.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) arrivalsSourceTextView.text = base_message arrivalsSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { arrivalsSourceTextView.visibility = View.VISIBLE } else { arrivalsSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the stop title in the fragment */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { //setTextViewMessage(getString(R.string.passages_fill, message)) setTextViewMessage(message) } } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader 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 -> { builder.appendPath("stop").appendPath(stopID) cl = CursorLoader( requireContext(), builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME), null, null, null ) } else -> { cl = CursorLoader(requireContext(), builder.build(), null, null,null,null) Log.d(DEBUG_TAG, "This is probably going to crash") } } cl.setUpdateThrottle(500) return cl } 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() val probableName = data.getString(colUserName) stopIsInFavorites = true if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop //update the message in the textview updateMessage() } else { 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") loaderManager.restartLoader( loaderStopId, arguments, this ) } - 6 - */ } - /* + loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ) if (index == -1) { Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...") } stopName = data.getString(index) updateMessage() } else { Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } - */ + } + + */ } + + override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } protected fun resetListAdapter(adapter: PalinaAdapter) { mListAdapter = adapter arrivalsRecyclerView.adapter = adapter 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 */ fun updateStarIconFromLastBusStop(toggleDone: Boolean) { stopIsInFavorites = if (stopIsInFavorites) !toggleDone 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 if (stopID.isEmpty()) { addToFavorites.visibility = View.INVISIBLE } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled) } else { addToFavorites.setImageResource(R.drawable.ic_star_outline) } addToFavorites.visibility = View.VISIBLE } } override fun onDestroyView() { //arrivalsRecyclerView = null if (arguments != null) { requireArguments().putString(SOURCES_TEXT, arrivalsSourceTextView.text.toString()) requireArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.text.toString()) } super.onDestroyView() } override fun getBaseViewForSnackBar(): View? { return null } fun isFragmentForTheSameStop(p: Palina): Boolean { return if (tag != null) tag == getFragmentTag(p) else false } /** * Request arrivals in the fragment */ fun requestArrivalsForTheFragment(){ // Run with previous fetchers context?.let { mListener.toggleSpinner(true) val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" } //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray()) //val workManager = WorkManager.getInstance(it) //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest) arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray()) //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID") } } companion object { private const val OPTION_SHOW_LEGEND = "show_legend" private const val KEY_STOP_ID = "stopid" private const val KEY_STOP_NAME = "stopname" private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment" private const val loaderFavId = 2 private const val loaderStopId = 1 const val STOP_TITLE: String = "messageExtra" private const val SOURCES_TEXT = "sources_textview_message" @JvmStatic @JvmOverloads fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment { val fragment = ArrivalsFragment() val args = Bundle() args.putString(KEY_STOP_ID, stopID) //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null) { args.putString(KEY_STOP_NAME, stopName) } fragment.arguments = args return fragment } @JvmStatic fun getFragmentTag(p: Palina): String { return "palina_" + p.ID } @JvmStatic fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID" @JvmStatic fun getDisplayArrivalsSource(source: Source, context: Context): String{ return when (source) { Source.GTTJSON -> context.getString(R.string.gttjsonfetcher) Source.FiveTAPI -> context.getString(R.string.fivetapifetcher) Source.FiveTScraper -> context.getString(R.string.fivetscraper) Source.MatoAPI -> context.getString(R.string.source_mato) Source.UNDETERMINED -> //Don't show the view context.getString(R.string.undetermined_source) } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt index c5989c8..1a6adf1 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt @@ -1,314 +1,315 @@ /* BusTO - Fragments components Copyright (C) 2024 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CheckBox import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import de.siegmar.fastcsv.reader.CsvReader import de.siegmar.fastcsv.writer.CsvWriter import it.reyboz.bustorino.R import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.util.ImportExport import java.io.* import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream /** * A simple [Fragment] subclass. * Use the [BackupImportFragment.newInstance] factory method to * create an instance of this fragment. */ class BackupImportFragment : Fragment() { private val saveFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.also { uri -> writeDataZip(uri) } } } private val openFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (!(loadFavorites|| loadPreferences)){ Toast.makeText(context, R.string.message_check_at_least_one, Toast.LENGTH_SHORT).show() } else if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.also { uri -> loadZipData(uri,loadFavorites, loadPreferences) } } } private lateinit var saveButton: Button private var loadFavorites = true private var loadPreferences = true private lateinit var checkFavorites: CheckBox private lateinit var checkPreferences: CheckBox override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /*arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) }*/ } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootview= inflater.inflate(R.layout.fragment_test_saving, container, false) saveButton = rootview.findViewById(R.id.saveButton) saveButton.setOnClickListener { startFileSaveIntent() } checkFavorites = rootview.findViewById(R.id.favoritesCheckBox) checkFavorites.setOnCheckedChangeListener { _, isChecked -> loadFavorites = isChecked } checkPreferences = rootview.findViewById(R.id.preferencesCheckBox) checkPreferences.setOnCheckedChangeListener { _, isChecked -> loadPreferences = isChecked } val readFavoritesButton = rootview.findViewById