diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt b/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt index 8e3a71e..b65224f 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt +++ b/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt @@ -1,138 +1,143 @@ package it.reyboz.bustorino import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log import android.view.View import android.widget.ImageButton import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator +import it.reyboz.bustorino.data.DBUpdateCheckWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.fragments.IntroFragment import it.reyboz.bustorino.middleware.GeneralActivity class ActivityIntro : GeneralActivity(), IntroFragment.IntroListener { private lateinit var viewPager : ViewPager2 private lateinit var btnForward: ImageButton private lateinit var btnBackward: ImageButton private lateinit var closeBottomButton: ImageButton private var restartMain = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_intro) - + //check default settings and apply them + checkApplyDefaultSettingsValues() viewPager = findViewById(R.id.viewPager) btnBackward = findViewById(R.id.btnPrevious) btnForward = findViewById(R.id.btnNext) closeBottomButton = findViewById(R.id.btnCompactClose) val extras = intent.extras if(extras!=null){ restartMain = extras.getBoolean(RESTART_MAIN) } val adapter = IntroPagerAdapter(this) viewPager.adapter = adapter val tabLayout = findViewById(R.id.tab_layout) val tabLayoutMediator = TabLayoutMediator(tabLayout, viewPager) { tab, pos -> Log.d(DEBUG_TAG, "tabview on position $pos") } tabLayoutMediator.attach() btnForward.setOnClickListener { viewPager.setCurrentItem(viewPager.currentItem+1,true) } btnBackward.setOnClickListener { viewPager.setCurrentItem(viewPager.currentItem-1, true) } /*closeBottomButton.setOnClickListener { closeIntroduction() } */ viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { override fun onPageSelected(position: Int) { if(position == 0){ btnBackward.visibility = View.INVISIBLE } else{ btnBackward.visibility = View.VISIBLE } if(position == NUM_ITEMS-1){ btnForward.visibility = View.INVISIBLE closeBottomButton.visibility = View.VISIBLE }else if(position == NUM_ITEMS-2){ if(closeBottomButton.visibility == View.VISIBLE) { closeBottomButton.visibility = View.INVISIBLE btnForward.visibility = View.VISIBLE } //btnForward.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.arrow_forward_white, null)) //btnForward.setBackgroundColor(ResourcesCompat.getColor(resources,R.attr.colorAccent, theme)) /*val GET THE COLOR VALUE OF THE THEMER colo = TypedValue() theme.resolveAttribute(R.attr.colorAccent,colo, true) btnForward.backgroundTintList //(colo.data) */ } } }) closeBottomButton.setOnClickListener { closeIntroduction() } ViewCompat.setOnApplyWindowInsetsListener( findViewById(R.id.theConstraintLayout), this.applyBottomAndBordersInsetsListener ) + // Start the DB Update now + DBUpdateCheckWorker.schedulePeriodicCheck(this, true) + } /** * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in * sequence. */ private inner class IntroPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { override fun getItemCount(): Int = NUM_ITEMS override fun createFragment(position: Int): Fragment = IntroFragment.newInstance(position) } companion object{ const private val DEBUG_TAG = "BusTO-IntroActivity" const val RESTART_MAIN = "restartMainActivity" val NUM_ITEMS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 8 else 7 } override fun closeIntroduction() { if(restartMain) startActivity(Intent(this, ActivityPrincipal::class.java)) val pref = PreferencesHolder.getMainSharedPreferences(this) val editor = pref.edit() editor.putBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN, true) //use commit so we don't "lose" info editor.commit() finish() } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index 8677da1..7295c22 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,865 +1,849 @@ /* 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.preference.PreferenceManager; import androidx.work.WorkInfo; -import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; import it.reyboz.bustorino.backend.Stop; -import it.reyboz.bustorino.backend.utils; +import it.reyboz.bustorino.data.DBUpdateCheckWorker; import it.reyboz.bustorino.data.DBUpdateWorker; -import it.reyboz.bustorino.data.DatabaseUpdate; 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 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 final OnBackPressedCallback callback = new OnBackPressedCallback(false) { + @Override + public void handleOnBackPressed() { + activityCustomBackPressed(); + } + }; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); /*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); 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 - GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); - - final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); - boolean dataUpdateRequested = false; - final SharedPreferences theShPr = getMainSharedPreferences(); - - final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); - Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); - if (old_version < db_version){ - //decide update conditions in the future - if(old_version < 2 && db_version >= 2) { - dataUpdateRequested = true; - DatabaseUpdate.requestDBUpdateWithWork(this, true, true); - } - PreferencesHolder.setGtfsDBVersion(theShPr, db_version); - } - //Try (hopefully) database update + // THIS CHECK IS DUPLICATED, TODO: REMOVE + final boolean dataUpdateRequested = checkIfNeedSpecialUpgradeDB(); if(!dataUpdateRequested) - DatabaseUpdate.requestDBUpdateWithWork(this, false, false); - + // DatabaseUpdate.requestDBUpdateWithWork(this, false, false); + DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false); /* Watch for database update */ - final WorkManager workManager = WorkManager.getInstance(this); - workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) + 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(); } 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 - setDefaultSettingsValuesWhenMissing(); + 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(); } } 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); } + private boolean checkIfNeedSpecialUpgradeDB(){ + final GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); + + final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); + boolean dataUpdateRequested = false; + final SharedPreferences theShPr = getMainSharedPreferences(); + + final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); + Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); + if (old_version < db_version){ + //decide update conditions in the future + if(old_version < 2 && db_version >= 2) { + dataUpdateRequested = true; + DBUpdateWorker.requestDBUpdateUniqueWork(this, true); + } + PreferencesHolder.setGtfsDBVersion(theShPr, db_version); + } + + return dataUpdateRequested; + } + /** * 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 + /*@Override public void onBackPressed() { - boolean foundFragment = false; + 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 - super.onBackPressed(); + 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() { 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); if (frag instanceof ScreenBaseFragment){ ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(this.snackbar); } this.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; } } - /** - * Adjust setting to match the default ones - */ - private void setDefaultSettingsValuesWhenMissing(){ - SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this); - SharedPreferences.Editor editor = mainSharedPref.edit(); - //Main fragment to show - String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); - boolean edit = false; - if (screen.isEmpty()){ - editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals"); - edit=true; - } - //Fetchers - final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>()); - if (setSelected.isEmpty()){ - String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default); - editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals)); - edit=true; - } - //Live bus positions - final String keySourcePositions=getString(R.string.pref_positions_source); - final String positionsSource = mainSharedPref.getString(keySourcePositions, ""); - if(positionsSource.isEmpty()){ - String[] defaultVals = getResources().getStringArray(R.array.positions_source_values); - editor.putString(keySourcePositions, defaultVals[0]); - edit=true; - } - //Map style - final String mapStylePref = mainSharedPref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, ""); - if(mapStylePref.isEmpty()){ - final String[] defaultVals = getResources().getStringArray(R.array.map_style_pref_values); - editor.putString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, defaultVals[0]); - edit=true; - } - if (edit){ - editor.commit(); - } - - - } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java index 207d897..c1b1e4a 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java @@ -1,121 +1,121 @@ package it.reyboz.bustorino.backend; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; import androidx.core.app.NotificationCompat; import it.reyboz.bustorino.R; public class Notifications { public static final String DEFAULT_CHANNEL_ID ="Default"; public static final String DB_UPDATE_CHANNELS_ID ="Database Update"; public static final String MATO_LIVE_POSITIONS_CHANNEL="Live Positions"; //match this value to the one used by the MQTTAndroidClient MANUALLY public static void createDefaultNotificationChannel(Context context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getString(R.string.default_notification_channel); String description = context.getString(R.string.default_notification_channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } /** * Register a notification channel on Android Oreo and above * @param con a Context * @param name channel name * @param description channel description * @param importance channel importance (from NotificationManager) * @param ID channel ID */ public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = con.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } public static Notification makeMatoDownloadNotification(Context context,String title){ return new NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID) //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) .setSmallIcon(R.drawable.ic_bus_stilized_transparent) .setOngoing(true) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setContentTitle(context.getString(R.string.app_name)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setContentText(title) .build(); } public static Notification makeLivePositionsNotification(Context context,String title){ return new NotificationCompat.Builder(context, Notifications.MATO_LIVE_POSITIONS_CHANNEL) //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) .setSmallIcon(R.drawable.ic_bus_stilized_transparent) .setOngoing(true) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setContentTitle(context.getString(R.string.app_name)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setContentText(title) .build(); } public static Notification makeMatoDownloadNotification(Context context){ return makeMatoDownloadNotification(context, context.getString(R.string.downloading_data_mato)); } public static Notification makeMQTTServiceNotification(Context context){ return makeLivePositionsNotification(context, context.getString(R.string.mqtt_notification_text)); } public static void cancelNotification(Context context, int notificationID){ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(notificationID); } - public static void createDBNotificationChannel(Context context){ + public static void createDBNotificationChannelIfNeeded(Context context){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( Notifications.DB_UPDATE_CHANNELS_ID, context.getString(R.string.database_notification_channel), NotificationManager.IMPORTANCE_MIN ); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } public static void createLivePositionsChannel(Context context){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( Notifications.MATO_LIVE_POSITIONS_CHANNEL, context.getString(R.string.live_positions_notification_channel), NotificationManager.IMPORTANCE_MIN ); channel.setDescription(context.getString(R.string.live_positions_notification_channel_desc)); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt new file mode 100644 index 0000000..8b49f1b --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt @@ -0,0 +1,78 @@ +/* + 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.content.Context +import android.util.Log +import androidx.work.* +import java.util.concurrent.TimeUnit + +/** + * Lightweight periodic worker that checks local state and enqueues [DBUpdateWorker] + * only when an update is actually needed, without making any network calls itself. + */ +class DBUpdateCheckWorker(context: Context, workerParams: WorkerParameters) + : CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + val con = applicationContext + val sharedPrefs = PreferencesHolder.getMainSharedPreferences(con) + + val currentDBVersion = sharedPrefs.getInt(PreferencesHolder.DB_GTT_VERSION_KEY, -10) + val lastDBUpdateTime = sharedPrefs.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, 0L) + val currentTime = System.currentTimeMillis() / 1000 + + val neverUpdated = currentDBVersion < 0 || lastDBUpdateTime <= 0 + val timeElapsed = currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY + + if (neverUpdated || timeElapsed) { + Log.d(DEBUG_TAG, "Scheduling DBUpdateWorker") + DBUpdateWorker.requestDBUpdateUniqueWork(con, forced = true) + } else { + Log.d(DEBUG_TAG, "No update needed") + } + + return Result.success() + } + + companion object { + const val DEBUG_TAG = "BusTO-DBUpdateScheduler" + const val WORK_NAME = "DBUpdateChecker" + + private const val UPDATE_MIN_DELAY = (3 * 24 * 3600L) // + + fun schedulePeriodicCheck(context: Context, restart: Boolean = false) { + val workRequest = PeriodicWorkRequest.Builder( + DBUpdateCheckWorker::class.java, 1, TimeUnit.DAYS + ) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + + val policy = if (restart) ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + else ExistingPeriodicWorkPolicy.KEEP + + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(WORK_NAME, policy, workRequest) + } + + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java deleted file mode 100644 index 81866e9..0000000 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - BusTO - Data components - Copyright (C) 2021 Fabio Mazza - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -package it.reyboz.bustorino.data; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.work.*; -import it.reyboz.bustorino.R; -import it.reyboz.bustorino.backend.Fetcher; -import it.reyboz.bustorino.backend.Notifications; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import static android.content.Context.MODE_PRIVATE; - -//TODO: Move to code to Kotlin -public class DBUpdateWorker extends Worker{ - - - public static final String ERROR_CODE_KEY ="Error_Code"; - public static final String ERROR_REASON_KEY = "ERROR_REASON"; - public static final int ERROR_FETCHING_VERSION = 4; - public static final int ERROR_DOWNLOADING_STOPS = 5; - public static final int ERROR_DOWNLOADING_LINES = 6; - public static final int ERROR_CODE_DB_CLOSED=-2; - - public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; - public static final int SUCCESS_NO_ACTION_NEEDED = 9; - public static final int SUCCESS_UPDATE_DONE = 1; - - private final static int NOTIFIC_ID =32198; - - public static final String FORCED_UPDATE = "FORCED-UPDATE"; - - public static final String DEBUG_TAG = "Busto-UpdateWorker"; - - private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days - - - public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @SuppressLint("RestrictedApi") - @NonNull - @Override - public Result doWork() { - //register Notification channel - final Context con = getApplicationContext(); - //Notifications.createDefaultNotificationChannel(con); - //Use the new notification channels - Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), - con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, - Notifications.DB_UPDATE_CHANNELS_ID - ); - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); - final int notification_ID = 32198; - final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); - final int current_DB_version = shPr.getInt(PreferencesHolder.DB_GTT_VERSION_KEY,-10); - - final int new_DB_version = DatabaseUpdate.getNewVersion(); - - final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); - - final long lastDBUpdateTime = shPr.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, 0); - long currentTime = System.currentTimeMillis()/1000; - - //showNotification(notificationManager, notification_ID); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, - Notifications.DB_UPDATE_CHANNELS_ID) - .setContentTitle(con.getString(R.string.database_update_msg_notif)) - .setProgress(0,0,true) - .setPriority(NotificationCompat.PRIORITY_LOW); - builder.setSmallIcon(R.drawable.ic_bus_stilized); - - - notificationManager.notify(notification_ID,builder.build()); - - Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); - Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); - /* - SKIP CHECK (Reason: The Old API might fail at any moment) - if (new_DB_version < 0){ - //there has been an error - final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) - .putInt(ERROR_CODE_KEY,new_DB_version).build(); - cancelNotification(notificationID); - return ListenableWorker.Result.failure(out); - } - */ - - //we got a good version - if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) - && !isUpdateCompulsory) { - //don't need to update - cancelNotification(notification_ID); - return ListenableWorker.Result.success(new Data.Builder(). - putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); - } - //start the real update - AtomicReference resultAtomicReference = new AtomicReference<>(); - DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); - final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); - DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); - - if (resultUpdate != DatabaseUpdate.Result.DONE){ - //Fetcher.Result result = resultAtomicReference.get(); - final Data.Builder dataBuilder = new Data.Builder(); - switch (resultUpdate){ - case ERROR_STOPS_DOWNLOAD: - dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); - break; - case ERROR_LINES_DOWNLOAD: - dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); - break; - case DB_CLOSED: - dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DB_CLOSED); - break; - } - cancelNotification(notification_ID); - return ListenableWorker.Result.failure(dataBuilder.build()); - } - Log.d(DEBUG_TAG, "Update finished successfully!"); - //update the version in the shared preference - final SharedPreferences.Editor editor = shPr.edit(); - editor.putInt(PreferencesHolder.DB_GTT_VERSION_KEY, new_DB_version); - currentTime = System.currentTimeMillis()/1000; - editor.putLong(PreferencesHolder.DB_LAST_UPDATE_KEY, currentTime); - editor.apply(); - cancelNotification(notification_ID); - - return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); - } - - public static Constraints getWorkConstraints(){ - return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresCharging(false).build(); - } - - public static WorkRequest newFirstTimeWorkRequest(){ - return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) - .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) - //.setInputData(new Data.Builder().putBoolean()) - .build(); - } - - /* - private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, - final String channel_ID){ - final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) - .setContentTitle("Libre BusTO - Updating Database") - .setProgress(0,0,true) - .setPriority(NotificationCompat.PRIORITY_LOW); - builder.setSmallIcon(R.drawable.ic_bus_orange); - - - notificManager.notify(notification_ID,builder.build()); - - return notification_ID; - } - */ - - private void cancelNotification(int notificationID){ - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); - - notificationManager.cancel(notificationID); - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt new file mode 100644 index 0000000..c4a5a1f --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt @@ -0,0 +1,205 @@ +/* + 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.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 + + 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 d5df45c..d002457 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,329 +1,327 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.gtfs.GtfsAgency; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.data.gtfs.GtfsFeed; import it.reyboz.bustorino.data.gtfs.GtfsRoute; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.PatternStop; import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.*; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; - enum Result { + public enum Result { DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DB_CLOSED } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } try { JSONObject resp = new JSONObject(networkRequest); return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); return JSON_PARSING_ERROR; } } private static boolean updateGTFSAgencies(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( con, res ); dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); return true; } private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final List routes= MatoAPIFetcher.Companion.getRoutes(con, res); final HashMap> routesStoppingInStop = new HashMap<>(); dao.insertRoutes(routes); if(res.get()!= Fetcher.Result.OK){ return routesStoppingInStop; } final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); final HashMap routesMap = new HashMap<>(routes.size()); for(GtfsRoute r: routes){ gtfsRoutesIDs.add(r.getGtfsId()); routesMap.put(r.getGtfsId(),r); } long t0 = System.currentTimeMillis(); final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); long tend = System.currentTimeMillis() - t0; Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); if(res.get()!=Fetcher.Result.OK){ Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); return routesStoppingInStop; } //match patterns with routes final ArrayList patternStops = makeStopsForPatterns(patterns); final List allPatternsCodeInDB = dao.getPatternsCodes(); final HashSet patternsCodesToDelete = new HashSet<>(allPatternsCodeInDB); for(MatoPattern p: patterns){ //scan patterns final ArrayList stopsIDs = p.getStopsGtfsIDs(); final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId()); if (mRoute == null) { Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map"); } for (final String sID : stopsIDs) { //add stops to pattern stops // save routes stopping in the stop if (!routesStoppingInStop.containsKey(sID)) { routesStoppingInStop.put(sID, new HashSet<>()); } Set mset = routesStoppingInStop.get(sID); assert mset != null; mset.add(mRoute.getShortName()); } //finally, remove from deletion list patternsCodesToDelete.remove(p.getCode()); } // final time for insert dao.insertPatterns(patterns); // clear patterns that are unused Log.d(DEBUG_TAG, "Have to remove "+patternsCodesToDelete.size()+ " patterns from the DB"); dao.deletePatternsWithCodes(new ArrayList<>(patternsCodesToDelete)); dao.insertPatternStops(patternStops); return routesStoppingInStop; } /** * Make the list of stops that each pattern does, to be inserted into the DB * @param patterns the MatoPattern * @return a list of PatternStop */ public static ArrayList makeStopsForPatterns(List patterns){ final ArrayList patternStops = new ArrayList<>(patterns.size()); for (MatoPattern p: patterns){ final ArrayList stopsIDs = p.getStopsGtfsIDs(); for (int i=0; i gres) { // GTFS data fetching AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); updateGTFSAgencies(con, gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); } else{ Log.d(DEBUG_TAG, "Done downloading agencies"); } gtfsRes.set(Fetcher.Result.OK); final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the routes into DB"); } else{ Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); } /*db.beginTransaction(); startTime = System.currentTimeMillis(); int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop); if(countStop!= routesStoppingByStop.size()){ Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with " +routesStoppingByStop.size()+" stops to update"); } db.setTransactionSuccessful(); db.endTransaction(); endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms"); */ // Stops insertion final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading stops"); return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; } final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); final SQLiteDatabase db = dbHelp.getWritableDatabase(); if(!db.isOpen()){ //catch errors like: java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase //we have to abort the work and restart it return Result.DB_CLOSED; } //TODO: Get the type of stop from the lines //Empty the needed tables db.beginTransaction(); //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); //db.delete(LinesTable.TABLE_NAME,null,null); //put new data long startTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); String routesStoppingString=""; int patternsStopsHits = 0; for (final Palina p : palinasMatoAPI) { final ContentValues cv = new ContentValues(); cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); if (p.location != null) cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){ final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID)); routesStoppingString = Palina.buildRoutesStringFromNames(routesSs); patternsStopsHits++; } else{ routesStoppingString = p.routesThatStopHereToString(); } cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString); if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); } db.setTransactionSuccessful(); db.endTransaction(); long endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); db.close(); dbHelp.close(); return DatabaseUpdate.Result.DONE; } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } - /** + /* * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now - */ public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); final Data reqData = new Data.Builder() .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); - PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) + 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.DEBUG_TAG, + workManager.enqueueUniquePeriodicWork(DBUpdateWorker.WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, wr); - else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, + 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) { - WorkManager workManager = WorkManager.getInstance(context); - workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( + DBUpdateWorker.getWorkInfoLiveData(context).observe( lifecycleOwner, observer ); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsMaintenanceWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsMaintenanceWorker.kt index 1801129..ea4489e 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/GtfsMaintenanceWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsMaintenanceWorker.kt @@ -1,70 +1,58 @@ package it.reyboz.bustorino.data -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context -import android.os.Build -import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.work.* import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Notifications class GtfsMaintenanceWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { val data = inputData.getString(OPERATION_TYPE) if(data ==null){ return Result.failure() } val result = when (data){ CLEAR_GTFS_TRIPS ->clearGtfsTrips() else -> {Result.failure()} } return result } private fun clearGtfsTrips(): Result{ val gtfsRepository = GtfsRepository(applicationContext) gtfsRepository.gtfsDao.deleteAllTrips() return Result.success() } override suspend fun getForegroundInfo(): ForegroundInfo { - val notificationManager = - applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val context = applicationContext - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - Notifications.DB_UPDATE_CHANNELS_ID, - context.getString(R.string.database_notification_channel), - NotificationManager.IMPORTANCE_MIN - ) - notificationManager.createNotificationChannel(channel) - } + Notifications.createDBNotificationChannelIfNeeded(context) val notification = NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID) //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) .setSmallIcon(R.drawable.bus) .setOngoing(true) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setContentTitle(context.getString(R.string.app_name)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setContentText("Database maintenance") .build() return ForegroundInfo(3671672811121.toInt(), notification) } companion object{ const val CLEAR_GTFS_TRIPS="trips_clear" const val OPERATION_TYPE="oper_type" fun makeOneTimeRequest(type: String): OneTimeWorkRequest { val data = Data.Builder().putString(OPERATION_TYPE, type).build() return OneTimeWorkRequest.Builder(GtfsMaintenanceWorker::class.java) .setInputData(data).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .addTag(type) .build() } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt index c6edb5d..be89336 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt @@ -1,101 +1,101 @@ /* BusTO - Data components Copyright (C) 2023 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.app.NotificationManager import android.content.Context import android.util.Log import androidx.work.* import it.reyboz.bustorino.backend.Fetcher import it.reyboz.bustorino.backend.Notifications import it.reyboz.bustorino.backend.mato.MatoAPIFetcher import java.util.concurrent.atomic.AtomicReference class MatoPatternsDownloadWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { val routesList = inputData.getStringArray(ROUTES_KEYS) if (routesList== null){ Log.e(DEBUG_TAG,"routes list given is null") return Result.failure() } val res = AtomicReference(Fetcher.Result.OK) val gtfsRepository = GtfsRepository(applicationContext) val patterns = MatoAPIFetcher.getPatternsWithStops(applicationContext, routesList.asList().toMutableList(), res) if (res.get() != Fetcher.Result.OK) { Log.e(DatabaseUpdate.DEBUG_TAG, "Something went wrong downloading patterns") return Result.failure() } gtfsRepository.gtfsDao.insertPatterns(patterns) //Insert the PatternStops gtfsRepository.gtfsDao.insertPatternStops(DatabaseUpdate.makeStopsForPatterns(patterns)) return Result.success() } override suspend fun getForegroundInfo(): ForegroundInfo { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val context = applicationContext - Notifications.createDBNotificationChannel(context) + Notifications.createDBNotificationChannelIfNeeded(context) return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context)) } companion object{ const val ROUTES_KEYS = "routesToDownload" const val DEBUG_TAG="BusTO:MatoPattrnDownWRK" const val NOTIFICATION_ID=21983102 const val TAG_PATTERNS ="matoPatternsDownload" fun downloadPatternsForRoutes(routesIds: List, context: Context): Boolean{ if(routesIds.isEmpty()) return false; val workManager = WorkManager.getInstance(context); val info = workManager.getWorkInfosForUniqueWork(TAG_PATTERNS).get() val runNewWork = if(info.isEmpty()) true else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED val addDat = if(info.isEmpty()) null else info[0].state Log.d(DEBUG_TAG, "Request to download and insert patterns for ${routesIds.size} routes, proceed: $runNewWork, workstate: $addDat") if(runNewWork){ val routeIdsArray: Array = routesIds.toTypedArray() val dataBuilder = Data.Builder().putStringArray(ROUTES_KEYS,routeIdsArray) val requ = OneTimeWorkRequest.Builder(MatoPatternsDownloadWorker::class.java) .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .addTag(TAG_PATTERNS) .build() workManager.enqueueUniqueWork(TAG_PATTERNS, ExistingWorkPolicy.KEEP, requ) } return true } } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt index 8d7ea7b..f2015b3 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt @@ -1,158 +1,157 @@ /* BusTO - Data components Copyright (C) 2023 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.app.NotificationManager import android.content.Context import android.util.Log import androidx.work.* -import com.android.volley.toolbox.ClearCacheRequest import it.reyboz.bustorino.backend.Notifications import it.reyboz.bustorino.data.gtfs.GtfsTrip import java.util.concurrent.CountDownLatch import kotlin.math.min class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { val tripsList = inputData.getStringArray(TRIPS_KEYS) if (tripsList== null){ Log.e(DEBUG_TAG,"trips list given is null") return Result.failure() } val numTrips = tripsList.size var i = 0 var totDown = 0 while (i ): Int{ val gtfsRepository = GtfsRepository(applicationContext) val matoRepository = MatoRepository(applicationContext) //clear the matoTrips val queriedMatoTrips = HashSet() val downloadedMatoTrips = ArrayList() val failedMatoTripsDownload = HashSet() Log.i(DEBUG_TAG, "Requesting download for ${tripsList.size} trips") val requestCountDown = CountDownLatch(tripsList.size); for(trip in tripsList){ queriedMatoTrips.add(trip) matoRepository.requestTripUpdate(trip,{error-> Log.e(DEBUG_TAG, "Cannot download Gtfs Trip $trip, error: $error") //val stacktrace = error.stackTrace.take(5) //Log.w(DEBUG_TAG, "Stacktrace:\n$stacktrace") failedMatoTripsDownload.add(trip) requestCountDown.countDown() }){ if(it.isSuccess){ if (it.result == null){ Log.e(DEBUG_TAG, "Got null result"); } downloadedMatoTrips.add(it.result!!) } else{ failedMatoTripsDownload.add(trip) } Log.i( DEBUG_TAG,"Result download, so far, trips: ${queriedMatoTrips.size}, failed: ${failedMatoTripsDownload.size}," + " succeded: ${downloadedMatoTrips.size}") //check if we can insert the trips requestCountDown.countDown() } } requestCountDown.await() val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID } //clear cache to avoid tripping memory matoRepository.clearVolleyCache() if (tripsIDsCompleted.isEmpty()){ Log.d(DEBUG_TAG, "No trips have been downloaded, set work to fail") return -5 } else { val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted) Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert") if (doInsert) { gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips) } return downloadedMatoTrips.size } } override suspend fun getForegroundInfo(): ForegroundInfo { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val context = applicationContext - Notifications.createDBNotificationChannel(context) + Notifications.createDBNotificationChannelIfNeeded(context) return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context)) } companion object{ const val TRIPS_KEYS = "tripsToDownload" const val DEBUG_TAG="BusTO:MatoTripDownWRK" const val NOTIFICATION_ID=42424221 const val TAG_TRIPS ="gtfsTripsDownload" fun requestMatoTripsDownload(trips: List, context: Context, debugTag: String): OneTimeWorkRequest? { if (trips.isEmpty()) return null val workManager = WorkManager.getInstance(context) val info = workManager.getWorkInfosForUniqueWork(TAG_TRIPS).get() val runNewWork = if(info.isEmpty()) true else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED val addDat = if(info.isEmpty()) null else info[0].state Log.d(debugTag, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat") if(runNewWork) { val tripsArr: Array = trips.toTypedArray() val dataBuilder = Data.Builder().putStringArray(TRIPS_KEYS, tripsArr) //build() val requ = OneTimeWorkRequest.Builder(MatoTripsDownloadWorker::class.java) .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .addTag(TAG_TRIPS) .build() workManager.enqueueUniqueWork(TAG_TRIPS, ExistingWorkPolicy.KEEP, requ) return requ } else return null; } } } 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 e3dbbef..147e4d6 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java @@ -1,582 +1,583 @@ /* 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.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.provider.BaseColumns; import android.util.Log; import androidx.annotation.NonNull; import it.reyboz.bustorino.backend.Palina; 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+" = ?"; private final Context appContext; 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); } 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); + //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) { 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; } 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; }catch (Exception e){ Log.e(DEBUG_TAG, "Exception occurred when getting stops"); e.printStackTrace(); return stops; } finally { db.close(); } return stops; } /** * 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){ 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; } 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); stops.addAll(getStopsFromCursorAllFields(result)); result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); } return stops; } /** * 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; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i 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 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/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt index 8799cc4..11c1a67 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -1,445 +1,440 @@ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.util.Log import android.view.* import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.ImageView import android.widget.TextView import androidx.appcompat.widget.SearchView import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo -import androidx.work.WorkManager import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.adapters.StringListAdapter import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.DBUpdateWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel class LinesGridShowingFragment : ScreenBaseFragment() { private val viewModel: LinesGridShowingViewModel by viewModels() //private lateinit var gridLayoutManager: AutoFitGridLayoutManager private lateinit var favoritesRecyclerView: RecyclerView private lateinit var urbanRecyclerView: RecyclerView private lateinit var extraurbanRecyclerView: RecyclerView private lateinit var touristRecyclerView: RecyclerView private lateinit var favoritesTitle: TextView private lateinit var urbanLinesTitle: TextView private lateinit var extrurbanLinesTitle: TextView private lateinit var touristLinesTitle: TextView private lateinit var updateMessageTextView: TextView //private lateinit var searchBar: SearchView private var routesByAgency = HashMap>() /*hashMapOf( AG_URBAN to ArrayList(), AG_EXTRAURB to ArrayList(), AG_TOUR to ArrayList() )*/ private lateinit var fragmentListener: CommonFragmentListener private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private val linesPriorityComparator = Comparator> { pa, pb -> if (pa.second != pb.second){ return@Comparator pa.second - pb.second } else{ return@Comparator linesNameSorter.compare(pa.first.shortName, pb.first.shortName) } } private val routeClickListener = RouteAdapter.ItemClicker { fragmentListener.openLineFromStop(it.gtfsId, null) } private val arrows = HashMap() private val durations = HashMap() //private val recyclerViewAdapters= HashMap() private val lastQueryEmptyForAgency = HashMap(3) private var openRecyclerView = "AG_URBAN" private fun getFlexLayoutManager(context: Context): FlexboxLayoutManager{ val layoutManager = FlexboxLayoutManager(context) layoutManager.flexDirection = FlexDirection.ROW layoutManager.justifyContent = JustifyContent.FLEX_START return layoutManager } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView) urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) updateMessageTextView = rootView.findViewById(R.id.updateMessageTextView) favoritesTitle = rootView.findViewById(R.id.favoritesTitleView) urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites) //show urban expanded by default val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) for (recyView in recViews) { val gridLayoutManager = AutoFitGridLayoutManager( requireContext().applicationContext, (utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt() ) recyView.layoutManager = gridLayoutManager } //init favorites recyclerview favoritesRecyclerView.layoutManager = getFlexLayoutManager(requireContext()) viewModel.getLinesLiveData().observe(viewLifecycleOwner){ rL -> routesByAgency.clear() for (k in AGENCIES){ routesByAgency[k] = ArrayList() } val routesPrioByAg = HashMap>>() for (ag in AGENCIES){ routesPrioByAg[ag] = ArrayList() } for(p in rL){ val route = p.first val agency = route.agencyID if(agency !in routesByAgency.keys){ Log.e(DEBUG_TAG, "The agency $agency for route ${p.first.gtfsId} is not in the predefined agencies (${routesByAgency.keys})") } routesByAgency[agency]?.add(route) routesPrioByAg[agency]?.add(p) // I would print a debug here, but it's the same as above } //zip agencies and recyclerviews AGENCIES.zip(recViews) { ag, recView -> routesPrioByAg[ag]?.let { routePrioList -> if (routePrioList.isNotEmpty()) { routePrioList.sortWith(linesPriorityComparator) val adapter = RouteAdapter(routePrioList.map { it.first }, routeClickListener) val lastQueryEmpty = if(ag in lastQueryEmptyForAgency.keys) lastQueryEmptyForAgency[ag]!! else true if (lastQueryEmpty) recView.adapter = adapter else recView.swapAdapter(adapter, false) lastQueryEmptyForAgency[ag] = false } else { val messageString = if(viewModel.getLineQueryValue().isNotEmpty()) getString(R.string.no_lines_found_query) else getString(R.string.no_lines_found) val extraAdapter = StringListAdapter(listOf(messageString)) recView.adapter = extraAdapter lastQueryEmptyForAgency[ag] = true } durations[ag] = if(routePrioList.size < 20) ViewUtils.DEF_DURATION else 1000 } } } viewModel.favoritesLines.observe(viewLifecycleOwner){ routes-> val routesNames = routes.map { it.shortName } //create new item click listener every time val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> val r = routes[pos] fragmentListener.openLineFromStop(r.gtfsId, null) } favoritesRecyclerView.adapter = adapter } //onClicks urbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_URBAN) } extrurbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_EXTRAURB) } touristLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_TOUR) } favoritesTitle.setOnClickListener { closeOpenFavorites() } arrows[AG_FAV]?.setOnClickListener { closeOpenFavorites() } //arrows onClicks for(k in Companion.AGENCIES){ //k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) } } // watch for the db update - WorkManager.getInstance(requireContext()).getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe(viewLifecycleOwner){ + DBUpdateWorker.getWorkInfoLiveData(requireContext()).observe(viewLifecycleOwner){ workInfoList -> if (workInfoList == null || workInfoList.isEmpty()) { return@observe } var showProgress = false for (workInfo in workInfoList) { if (workInfo.state == WorkInfo.State.RUNNING) { updateMessageTextView.visibility = View.VISIBLE } else{ updateMessageTextView.visibility = View.GONE } break } } return rootView } fun setUserSearch(textSearch:String){ viewModel.setLineQuery(textSearch) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val menuHost: MenuHost = requireActivity() // Add menu items without using the Fragment Menu APIs // Note how we can tie the MenuProvider to the viewLifecycleOwner // and an optional Lifecycle.State (here, RESUMED) to indicate when // the menu should be visible menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { // Add menu items here menuInflater.inflate(R.menu.menu_search, menu) val search = menu.findItem(R.id.searchMenuItem).actionView as SearchView search.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ override fun onQueryTextSubmit(query: String?): Boolean { setUserSearch(query ?: "") return true } override fun onQueryTextChange(query: String?): Boolean { setUserSearch(query ?: "") return true } }) search.queryHint = getString(R.string.search_box_lines_suggestion_filter) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { // Handle the menu selection if (menuItem.itemId == R.id.searchMenuItem){ Log.d(DEBUG_TAG, "Clicked on search menu") } else{ Log.d(DEBUG_TAG, "Clicked on something else") } return false } }, viewLifecycleOwner, Lifecycle.State.RESUMED) } private fun closeOpenFavorites(){ if(favoritesRecyclerView.visibility == View.VISIBLE){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) viewModel.favoritesExpanded.value = false } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) viewModel.favoritesExpanded.value = true } } private fun openLinesAndCloseOthersIfNeeded(agency: String){ if(openRecyclerView!="" && openRecyclerView!= agency) { switchRecyclerViewStatus(openRecyclerView) } switchRecyclerViewStatus(agency) } private fun switchRecyclerViewStatus(agency: String){ val recyclerView = when(agency){ AG_TOUR -> touristRecyclerView AG_EXTRAURB -> extraurbanRecyclerView AG_URBAN -> urbanRecyclerView else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val expandedLiveData = when(agency){ AG_TOUR -> viewModel.isTouristExpanded AG_URBAN -> viewModel.isUrbanExpanded AG_EXTRAURB -> viewModel.isExtraUrbanExpanded else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val duration = durations[agency] val arrow = arrows[agency] val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration if(duration!=null&&arrow!=null) when (recyclerView.visibility){ View.GONE -> { Log.d(DEBUG_TAG, "Open recyclerview $agency") //val a =ViewUtils.expand(recyclerView, duration, 0) recyclerView.visibility = View.VISIBLE expandedLiveData.value = true Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}") setOpen(arrow, true) //arrow.startAnimation(rotateArrow(true,durArrow)) openRecyclerView = agency } View.VISIBLE -> { Log.d(DEBUG_TAG, "Close recyclerview $agency") //ViewUtils.collapse(recyclerView, duration) recyclerView.visibility = View.GONE expandedLiveData.value = false //arrow.rotation = 90f Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate") setOpen(arrow, false) //arrow.startAnimation(rotateArrow(false,durArrow)) openRecyclerView = "" } View.INVISIBLE -> { TODO() } } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } override fun getBaseViewForSnackBar(): View? { return null } override fun onResume() { super.onResume() val pref = PreferencesHolder.getMainSharedPreferences(requireContext()) val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))} //restore state viewModel.favoritesExpanded.value?.let { if(!it){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) } } viewModel.isUrbanExpanded.value?.let { if(it) { urbanRecyclerView.visibility = View.VISIBLE arrows[AG_URBAN]?.rotation= 90f openRecyclerView = AG_URBAN Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded") } else { urbanRecyclerView.visibility = View.GONE arrows[AG_URBAN]?.rotation= 0f } } viewModel.isTouristExpanded.value?.let { val recview = touristRecyclerView if(it) { recview.visibility = View.VISIBLE arrows[AG_TOUR]?.rotation=90f openRecyclerView = AG_TOUR } else { recview.visibility = View.GONE arrows[AG_TOUR]?.rotation= 0f } } viewModel.isExtraUrbanExpanded.value?.let { val recview = extraurbanRecyclerView if(it) { openRecyclerView = AG_EXTRAURB recview.visibility = View.VISIBLE arrows[AG_EXTRAURB]?.rotation=90f } else { recview.visibility = View.GONE arrows[AG_EXTRAURB]?.rotation=0f } } fragmentListener.readyGUIfor(FragmentKind.LINES) } companion object { private const val COLUMN_WIDTH_DP=250 private const val AG_FAV = "fav" private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" private const val AG_TOUR ="gtt:T" private const val DEBUG_TAG ="BusTO-LinesGridFragment" const val FRAGMENT_TAG = "LinesGridShowingFragment" private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR) fun newInstance() = LinesGridShowingFragment() @JvmStatic fun setOpen(imageView: ImageView, value: Boolean){ if(value) imageView.rotation = 90f else imageView.rotation = 0f } @JvmStatic fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{ val start = if (toOpen) 0f else 90f val stop = if(toOpen) 90f else 0f Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop") val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) rotate.duration = duration rotate.interpolator = LinearInterpolator() //rotate.fillAfter = true rotate.fillBefore = false return rotate } } - - override fun showSnackbarOnDBUpdate(): Boolean { - return false - } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java index fff1183..49a0b9e 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,234 +1,234 @@ /* BusTO - Fragments components Copyright (C) 2020 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.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import androidx.preference.*; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import it.reyboz.bustorino.ActivityBackup; import it.reyboz.bustorino.R; -import it.reyboz.bustorino.data.DatabaseUpdate; +import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.GtfsMaintenanceWorker; import org.jetbrains.annotations.NotNull; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; // Matching preferences.xml public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show"; public final static String KEY_ARRIVALS_FETCHERS_USE = "arrivals_fetchers_use_setting"; public final static String LIVE_POSITIONS_PREF_MQTT_VALUE="mqtt"; public final static String LIBREMAP_STYLE_PREF_KEY = "libremap_style_1"; private boolean setSummaryStartupPref = false; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN); if(startupScreenPref !=null){ if (startupScreenPref.getValue()==null){ startupScreenPref.setSummary(getString(R.string.nav_arrivals_text)); setSummaryStartupPref = true; } } //Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue()); Preference dbUpdateNow = findPreference("pref_db_update_now"); if (dbUpdateNow!=null) dbUpdateNow.setOnPreferenceClickListener( preference -> { - //trigger update + //force update if(getContext()!=null) { - DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true); + DBUpdateWorker.requestDBUpdateUniqueWork(requireContext(), true); Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show(); return true; } return false; } ); //set click listener on backup item final Preference backupPref = findPreference("pref_backup_open"); if (backupPref!=null) backupPref.setOnPreferenceClickListener( preference -> { if(getActivity()!=null){ startActivity( new Intent(getActivity().getApplicationContext(), ActivityBackup.class) ); return true; } else { return false; } } ); else { Log.e("BusTO-Preferences", "Cannot find db update preference"); } Preference clearGtfsTrips = findPreference("pref_clear_gtfs_trips"); if (clearGtfsTrips != null) { clearGtfsTrips.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull @NotNull Preference preference) { if (getContext() != null) { OneTimeWorkRequest requ = GtfsMaintenanceWorker.Companion.makeOneTimeRequest(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS); WorkManager.getInstance(getContext()).enqueue(requ); WorkManager.getInstance(getContext()).getWorkInfosByTagLiveData(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS).observe(getViewLifecycleOwner(), (Observer>) workInfos -> { if(workInfos.isEmpty()) return; if(workInfos.get(0).getState()==(WorkInfo.State.SUCCEEDED)){ Toast.makeText( getContext(), R.string.all_trips_removed, Toast.LENGTH_SHORT ).show(); } }); return true; } return false; } }); } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); if (key.equals(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE)){ Log.d(TAG, "New value is: "+sharedPreferences.getStringSet(key, new HashSet<>())); } //sometimes this happens if(getContext()==null) return; if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){ ListPreference listPref = (ListPreference) pref; pref.setSummary(listPref.getEntry()); } /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/GeneralActivity.java b/app/src/main/java/it/reyboz/bustorino/middleware/GeneralActivity.java index 3bfda66..b848a7d 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/GeneralActivity.java +++ b/app/src/main/java/it/reyboz/bustorino/middleware/GeneralActivity.java @@ -1,230 +1,274 @@ /* 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.middleware; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Rect; import android.os.Build; import android.view.ViewGroup; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.preference.PreferenceManager; import com.google.android.material.snackbar.Snackbar; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.PreferencesHolder; +import it.reyboz.bustorino.fragments.SettingsFragment; /** * Activity class that contains all the generally useful methods */ public abstract class GeneralActivity extends AppCompatActivity { final static protected int PERMISSION_REQUEST_POSITION = 33; final static protected String LOCATION_PERMISSION_GIVEN = "loc_permission"; final static protected int STORAGE_PERMISSION_REQ = 291; final static protected int PERMISSION_OK = 0; final static protected int PERMISSION_ASKING = 11; final static protected int PERMISSION_NEG_CANNOT_ASK = -3; final static private String DEBUG_TAG = "BusTO-GeneralAct"; /* * Permission stuff */ protected HashMap permissionDoneRunnables = new HashMap<>(); protected HashMap permissionAsked = new HashMap<>(); protected void setOption(String optionName, boolean value) { SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { SharedPreferences preferences = getPreferences(MODE_PRIVATE); return preferences.getBoolean(optionName, optDefault); } protected SharedPreferences getMainSharedPreferences(){ return PreferencesHolder.getMainSharedPreferences(this); } public void hideKeyboard() { View view = getCurrentFocus(); if (view != null) { ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Toast.makeText(getApplicationContext(), messageID, length).show(); } public int askForPermissionIfNeeded(String permission, int requestID){ if(ContextCompat.checkSelfPermission(getApplicationContext(),permission)==PackageManager.PERMISSION_GRANTED){ return PERMISSION_OK; } //need to ask for the permission //consider scenario when we have already asked for permission boolean alreadyAsked = false; Integer num_trials = 0; synchronized (this){ if (permissionAsked.containsKey(permission)){ num_trials = permissionAsked.get(permission); if (num_trials != null && num_trials > 4) alreadyAsked = true; } } Log.d(DEBUG_TAG,"Already asked for permission: "+permission+" -> "+num_trials); if(!alreadyAsked){ ActivityCompat.requestPermissions(this,new String[]{permission}, requestID); synchronized (this){ if (num_trials!=null){ permissionAsked.put(permission, num_trials+1); } } return PERMISSION_ASKING; } else { return PERMISSION_NEG_CANNOT_ASK; } } public void createSnackbar(int ViewID, String message,int duration){ Snackbar.make(findViewById(ViewID),message,duration); } /* METHOD THAT MIGHT BE USEFUL LATER public void assertPermissions(String[] permissions){ ArrayList permissionstoRequest = new ArrayList<>(); for(int i=0;i marginOfError; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); } protected void setSystemBarAppearance(boolean isSystemInDarkTheme) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (isSystemInDarkTheme) { if (getWindow() != null && getWindow().getInsetsController() != null) { getWindow().getInsetsController().setSystemBarsAppearance( 0, android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS ); } } else { if (getWindow() != null && getWindow().getInsetsController() != null) { getWindow().getInsetsController().setSystemBarsAppearance( android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS ); } } } } protected OnApplyWindowInsetsListener applyBottomAndBordersInsetsListener = (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 // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }; protected OnApplyWindowInsetsListener applyBottomInsetsListener = (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.bottomMargin = insets.bottom; v.setLayoutParams(mlp); //set for toolbar // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }; + /** + * Adjust setting to match the default ones + */ + protected void checkApplyDefaultSettingsValues(){ + SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = mainSharedPref.edit(); + //Main fragment to show + String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); + boolean edit = false; + if (screen.isEmpty()){ + editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals"); + edit=true; + } + //Fetchers + final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>()); + if (setSelected.isEmpty()){ + String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default); + editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals)); + edit=true; + } + //Live bus positions + final String keySourcePositions=getString(R.string.pref_positions_source); + final String positionsSource = mainSharedPref.getString(keySourcePositions, ""); + if(positionsSource.isEmpty()){ + String[] defaultVals = getResources().getStringArray(R.array.positions_source_values); + editor.putString(keySourcePositions, defaultVals[0]); + edit=true; + } + //Map style + final String mapStylePref = mainSharedPref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, ""); + if(mapStylePref.isEmpty()){ + final String[] defaultVals = getResources().getStringArray(R.array.map_style_pref_values); + editor.putString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, defaultVals[0]); + edit=true; + } + if (edit){ + editor.commit(); + } + + } }