diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index e90e6df..f9a96d7 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,928 +1,914 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.*; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.*; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.Consumer; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateCheckWorker; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; - private boolean showingMainFragmentFromOther = false; - private boolean onCreateComplete = false; - private ServiceAlertsViewModel serviceAlertsViewModel; private FragmentKind showingFragmentKind; private final Map menuActions = Map.of( R.id.drawer_action_settings, () -> { Log.d("MAINBusTO", "Pressed button preferences"); startActivity(new Intent(this, ActivitySettings.class)); }, R.id.nav_favorites_item, () -> checkAndShowFavoritesFragment(getSupportFragmentManager(), true), - R.id.nav_home, () -> - showHomeMainFragmentFromClick(true), + R.id.nav_home, this::showHomeMainFragmentFromClick, R.id.nav_map_item, () -> requestMapFragment(true), R.id.nav_lines_item, () -> showLinesFragment(getSupportFragmentManager(), true, null), R.id.drawer_action_info, () -> startActivity(new Intent(this, ActivityAbout.class)), R.id.nav_nearby, this::openNearbyStopsFragment ); + private long lastClosingAttempt = -1L; private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { boolean isResolved = activityCustomBackPressed(); Log.d(DEBUG_TAG, "backpress resolved: " + isResolved); if(!isResolved){ long currentTime = System.currentTimeMillis(); if(currentTime - lastClosingAttempt < 2000){ finish(); } else{ lastClosingAttempt = currentTime; Toast.makeText(getApplicationContext(),R.string.back_again_to_close,Toast.LENGTH_SHORT).show(); } } } }; - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); serviceAlertsViewModel = new ViewModelProvider(this).get(ServiceAlertsViewModel.class); //Use LiveModel to sync fragment state /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getWindow().setNavigationBarContrastEnforced(false); } */ //onBackPressed solution required from Android 16 backPressedCallback.setEnabled(true); this.getOnBackPressedDispatcher().addCallback(backPressedCallback); boolean showingArrivalsFromIntent = false; final Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } //database check // DatabaseUpdate.requestDBUpdateWithWork(this, false, false); DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false); /* Watch for database update */ DBUpdateWorker.getWorkInfoLiveData(this) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { var framan = getSupportFragmentManager(); //we are not restarting the activity from nothing switch (vl){ case "map" -> {requestMapFragment(false);} case "favorites" -> checkAndShowFavoritesFragment(framan, false); case "lines" -> showLinesFragment(framan, false, null); case "nearby" -> createShowMainFragment(framan, MainScreenFragment.makeArgsNearby(), false); - default -> showHomeMainFragmentFromClick(false); + default -> createShowMainFragment(framan, MainScreenFragment.makeArgsButtonsScreen(), false); } } - onCreateComplete = true; + //boolean onCreateComplete = true; //last but not least, set the good default values checkApplyDefaultSettingsValues(); // handle the device "insets" /* ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rootRelativeLayout), (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Apply the insets as a margin to the view. This solution sets only the // bottom, left, and right dimensions, but you can apply whichever insets are // appropriate to your layout. You can also update the view padding if that's // more appropriate. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; mlp.rightMargin = insets.right; v.setLayoutParams(mlp); //set for toolbar //mlp = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams(); //mlp.topMargin = insets.top; //mToolbar.setLayoutParams(mlp); mToolbar.setPadding(0, insets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); //to properly handle IME WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { insetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } */ // Toolbar: solo inset superiore (status bar) ViewCompat.setOnApplyWindowInsetsListener(mToolbar, (v, windowInsets) -> { Insets statusBar = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); v.setPadding(0, statusBar.top, 0, 0); return windowInsets; // NON consumare: passa gli insets ai figli }); // Content frame: insets laterali e inferiori (navigation bar) // I fragment figli riceveranno gli insets e potranno gestirli a loro volta ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.mainActContentFrame), (v, windowInsets) -> { Insets systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Solo left/right, bottom lo gestisce ogni fragment v.setPadding(systemBars.left, 0, systemBars.right, 0); return windowInsets; //WindowInsetsCompat.CONSUMED; // NON consumare: passa ai fragment }); //check if first run activity (IntroActivity) has been started once or not final SharedPreferences theShPr = getMainSharedPreferences(); boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false); if(!hasIntroRun){ startIntroductionActivity(); } serviceAlertsViewModel.getLastTimeRunningDownload().observe(this, (timeRunning) -> { if (timeRunning != null) { Log.d(DEBUG_TAG, "requested alerts download at time: "+timeRunning); } }); serviceAlertsViewModel.launchAlertsPeriodCheck(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { int menuId = menuItem.getItemId(); if( menuActions.containsKey(menuId)){ closeDrawerIfOpen(); var runnable = menuActions.get(menuId); if(runnable!=null) runnable.run(); return true; } else{ return false; } }); } 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) { Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } /*@Override public void onBackPressed() { if (!activityCustomBackPressed()) super.onBackPressed(); } */ private boolean activityCustomBackPressed(){ - boolean resolved = true; var mainFragManager = getSupportFragmentManager(); + boolean resolved = mainFragManager.getBackStackEntryCount() > 0; + Fragment shownFrag = mainFragManager.findFragmentById(R.id.mainActContentFrame); - if (mDrawer.isDrawerOpen(GravityCompat.START)) + if (mDrawer.isDrawerOpen(GravityCompat.START)) { mDrawer.closeDrawer(GravityCompat.START); - else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ - if(shownFrag instanceof MainScreenFragment){ - //we have to stop the arrivals reload - ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); + resolved = true; + } + else if (shownFrag instanceof MainScreenFragment mainFrag) { //the only case when we have to pop both stacks + mainFrag.cancelReloadArrivalsIfNeeded(); + + var chManager = mainFrag.getChildFragmentManager(); + + if(chManager.getBackStackEntryCount() > 0){ + chManager.popBackStack(); + Log.d(DEBUG_TAG, "Child back stack popped"); + resolved = true; + } else{ + Log.d(DEBUG_TAG, "No back stack on child fragment manager"); } - shownFrag.getChildFragmentManager().popBackStack(); - if(showingMainFragmentFromOther && mainFragManager.getBackStackEntryCount() > 0){ - getSupportFragmentManager().popBackStack(); - Log.d(DEBUG_TAG, "Popping main back stack also"); + boolean haveToPopMainFromFragment = mainFrag.needToPopMainStackOnBack(); + //mainFrag.getChildFragmentManager().popBackStack(); + if(haveToPopMainFromFragment){ // pops the stack + mainFragManager.popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { mainFragManager.popBackStack(); var newFrag = mainFragManager.findFragmentById(R.id.mainActContentFrame); if(newFrag != null) { int backStackCount = newFrag.getChildFragmentManager().getBackStackEntryCount(); Log.d(DEBUG_TAG, "new fragment is "+newFrag.getClass().getSimpleName() +", count of the backstack: "+backStackCount); } Log.d(DEBUG_TAG, "Popping main backstack"); } 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 createDatabaseUpdateSnackbar() { View baseView = null; boolean showSnackbar = true; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); showSnackbar = ((ScreenBaseFragment) frag).showSnackbarOnDBUpdate(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); //if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); if (baseView !=null && showSnackbar) { snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.setTextColor(getColor(android.R.color.white)); snackbar.setBackgroundTint(getColor(R.color.grey_800)); if (frag instanceof ScreenBaseFragment){ ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(snackbar); } snackbar.show(); } else{ Log.e(DEBUG_TAG, "Asked to show the snackbar but the baseView is null"); } } private void updateShowingFragmentKindInternal(@NonNull FragmentKind newKind){ if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Updating fragment kind, new: "+newKind+", current: "+showingFragmentKind); + boolean showingMainFragmentFromOther = false; if(showingFragmentKind == null){ showingFragmentKind = newKind; showingMainFragmentFromOther = false; } else if(newKind != showingFragmentKind) { showingMainFragmentFromOther = ( FragmentKind.getSuperKind(newKind) != FragmentKind.getSuperKind(showingFragmentKind)); showingFragmentKind = newKind; } } /** * Show the actual 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); + if (addToBackStack) { + ft.addToBackStack(null); + } ft.commit(); } /** * Create a new MainFragment for the arguments provided and show it in the layout * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ - //var frag = MainScreenFragment.newInstance(); 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 showMainFragmentFromClick(@Nullable Bundle argsToCreate, boolean addToBackStack){ - FragmentManager fraMan = getSupportFragmentManager(); - Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); - final MainScreenFragment mainScreenFragment; - if (fragment==null | !(fragment instanceof MainScreenFragment)){ - createShowMainFragment(fraMan, argsToCreate, addToBackStack); - } - else if(!fragment.isVisible()){ - - mainScreenFragment = (MainScreenFragment) fragment; - showMainFragment(fraMan, mainScreenFragment, addToBackStack); - Log.d(DEBUG_TAG, "Found the main fragment"); - } else{ - mainScreenFragment = (MainScreenFragment) fragment; - } - } - - private void showHomeMainFragmentFromClick(boolean addToBackStack){ + private void showHomeMainFragmentFromClick(){ FragmentManager fraMan = getSupportFragmentManager(); var fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if(fragment instanceof MainScreenFragment mainFrag){ + var visible = mainFrag.isVisible(); if(!mainFrag.isVisible()){ - showMainFragment(fraMan, mainFrag, addToBackStack); + showMainFragment(fraMan, mainFrag, true); } - mainFrag.showButtonsFragmentIfNotNearby(addToBackStack); + mainFrag.showButtonsFragmentIfNotNearby(true); } else{ - createShowMainFragment(fraMan, MainScreenFragment.makeArgsButtonsScreen(), addToBackStack); + createShowMainFragment(fraMan, MainScreenFragment.makeArgsButtonsScreen(), true); } } private void requestMapFragment(final boolean allowReturn){ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.mainActContentFrame); if(fragment instanceof MapLibreFragment){ Log.d(DEBUG_TAG, "Requested map fragment, but it is already open"); } else { fragment = fm.findFragmentByTag(MapLibreFragment.FRAGMENT_TAG); if(fragment != null){ Log.d(DEBUG_TAG, "Found map fragment, reopening it"); var ft = fm.beginTransaction(); ft.replace(R.id.mainActContentFrame,fragment, MapLibreFragment.FRAGMENT_TAG); if(allowReturn) ft.addToBackStack(null); ft.commit(); } else { //create from scratch //The permissions are handled in the MapLibreFragment instead createAndShowMapFragment(null, allowReturn); } } } private void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ if(getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame) instanceof FavoritesFragment){ Log.d(DEBUG_TAG, "Requested favorites fragment, but it is already open"); return; } 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 void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ if(getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame) instanceof LinesGridShowingFragment){ Log.d(DEBUG_TAG, "Requested lines grid fragment, but it is already open"); return; } 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(); } @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) { var frag = getMainFragmentIfVisible(); if(frag!=null){ frag.showFloatingActionButton(yes); } } /* 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); - } + updateShowingFragmentKindInternal(fragmentType); Integer titleResId = null; 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_home); //TODO: Figure out way to change title //mNavView.getCheckedItem().setTitle(R.string.nav_arrivals_text); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_home); break; case MAIN_SCREEN_FRAGMENT: case HOME_BUTTONS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_home); //mNavView.getCheckedItem().setTitle(R.string.nav_home_text); break; case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_nearby); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; } if(getSupportActionBar()!=null && titleResId!=null) getSupportActionBar().setTitle(titleResId); + MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); + if (mainFragmentIfVisible!=null){ + mainFragmentIfVisible.readyGUIfor(fragmentType); + } } @Override public void requestArrivalsForStopID(String ID) { - //register if the request came from the main fragment or not - MainScreenFragment probableFragment = getMainFragmentIfVisible(); - - // this has some contorted logic, but it works - if (probableFragment == null){ - 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 - //if onCreate is complete, then we are not asking for the first showing fragment - final Bundle args = MainScreenFragment.makeArgsArrivals(ID); - boolean addtobackstack = onCreateComplete; - createShowMainFragment(fraMan, args ,addtobackstack); - } - } else { - //the MainScreeFragment is shown, nothing to do - probableFragment.requestArrivalsForStopID(ID); + Consumer consumer = fragment -> { + fragment.setSuppressArrivalsReload(true); + fragment.requestArrivalsForStopID(ID); + }; + boolean done = getMainFragmentAndDoStuff(consumer); + if(!done){ + //create the fragment + final Bundle args = MainScreenFragment.makeArgsArrivals(ID); + createShowMainFragment(getSupportFragmentManager(), args ,true); } - - mNavView.setCheckedItem(R.id.nav_home); } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); } + private boolean getMainFragmentAndDoStuff(Consumer consumer){ + FragmentManager fraMan = getSupportFragmentManager(); + var frag = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if( frag instanceof MainScreenFragment mainFrag){ + if(!mainFrag.isVisible()) { + showMainFragment(fraMan, mainFrag, true); + mainFrag.setMainFragmentManagerTransition(true); + } else{ + mainFrag.setMainFragmentManagerTransition(false); + } + consumer.accept(mainFrag); + return true; + } else{ + return false; + } + } + @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { 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 openNearbyStopsFragment() { - FragmentManager fraMan = getSupportFragmentManager(); - var fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); - if(fragment instanceof MainScreenFragment mainFrag){ - if(!mainFrag.isVisible()){ - showMainFragment(fraMan, mainFrag, true); - } - mainFrag.openNearbyStopsFragment(); - } else{ - // there is no fragment and it is not visible - // add to back stack the main fragment, as the NearbyStopsFragment will not be added - createShowMainFragment(fraMan, MainScreenFragment.makeArgsNearby(), true); + boolean done = getMainFragmentAndDoStuff(MainScreenFragment::openNearbyStopsFragment); + if(!done){ + createShowMainFragment(getSupportFragmentManager(), MainScreenFragment.makeArgsNearby(), true); } } @Override public void openLinesFragment() { showLinesFragment(getSupportFragmentManager(), true, null); } @Override public void openFavoritesFragment() { checkAndShowFavoritesFragment(getSupportFragmentManager(), true); } @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(@Nullable Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ final FragmentManager fm = getSupportFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); final MapLibreFragment fragment = MapLibreFragment.newInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapLibreFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } void startIntroductionActivity(){ Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); intent.putExtra(ActivityIntro.RESTART_MAIN, false); startActivity(intent); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if(id == R.id.action_about){ startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } else if (id == R.id.action_hack) { openIceweasel(getString(R.string.hack_url), activityContext); return true; } else if (id == R.id.action_source){ openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; } else if (id == R.id.action_licence){ openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; } else if (id == R.id.action_experiments) { startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); return true; } else if (id == R.id.action_tutorial) { startIntroductionActivity(); return true; } return false; } } @Override protected void onPause() { super.onPause(); // stop updating the alerts serviceAlertsViewModel.setRunningDownloadRequests(false); } @Override protected void onResume() { super.onResume(); serviceAlertsViewModel.launchAlertsPeriodCheck(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index dc192c6..7102c78 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,294 +1,301 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; -import it.reyboz.bustorino.util.RoutePositionSorter; -import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; private @NonNull GPSPoint userPosition; private FragmentListenerMain listener; - private List< Pair > routesPairList; + private List< RouteWithStop > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable private String KEY_CAPITALIZE; private NameCapitalize capit; - public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @NonNull GPSPoint pos) { + public ArrivalsStopAdapter(@Nullable List< RouteWithStop > routesPairList, FragmentListenerMain fragmentListener, Context con, @NonNull GPSPoint pos) { listener = fragmentListener; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); this.capit = NameCapitalize.getCapitalize(capitalizeKey); } @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS - final Pair stopRoutePair = routesPairList.get(position); - if(stopRoutePair!=null && stopRoutePair.first!=null){ - final Stop stop = stopRoutePair.first; - final Route r = stopRoutePair.second; + final var stopRoutePair = routesPairList.get(position); + if(stopRoutePair != null){ + final Stop stop = stopRoutePair.getStop(); + final Route r = stopRoutePair.getRoute(); final Double distance = stop.getDistanceFromLocation(userPosition.getLatitude(), userPosition.longitude); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getDisplayCode()); holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return routesPairList.size(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); capit = NameCapitalize.getCapitalize(k); notifyDataSetChanged(); } } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } - + /* public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } } - public void setUserPosition(@Nullable GPSPoint userPosition) { - this.userPosition = userPosition; - } + */ + + - public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable GPSPoint pos) { - if(pos!=null){ - this.userPosition = pos; + public void setRoutesPairListAndPosition(@NonNull List newList) { + if (routesPairList == null) { + routesPairList = new ArrayList<>(newList); + notifyItemRangeInserted(0, newList.size()); + return; } - if(mRoutesPairList!=null){ - //this.routesPairList = routesPairList; - //remove duplicates - sortAndRemoveDuplicates(mRoutesPairList, this.userPosition); - //routesPairList = mRoutesPairList; - //STUPID CODE - if (this.routesPairList == null || routesPairList.size() == 0){ - routesPairList = mRoutesPairList; - notifyDataSetChanged(); - } else{ - - final HashMap, Integer> indexMapIn = getRouteIndexMap(mRoutesPairList); - final HashMap, Integer> indexMapExisting = getRouteIndexMap(routesPairList); - //List> oldList = routesPairList; - routesPairList = mRoutesPairList; - /* - for (Pair pair: indexMapIn.keySet()){ - final Integer posIn = indexMapIn.get(pair); - if (posIn == null) continue; - if (indexMapExisting.containsKey(pair)){ - final Integer posExisting = indexMapExisting.get(pair); - //THERE IS ALREADY - //routesPairList.remove(posExisting.intValue()); - //routesPairList.add(posIn,mRoutesPairList.get(posIn)); - - notifyItemMoved(posExisting, posIn); - indexMapExisting.remove(pair); - } else{ - //INSERT IT - //routesPairList.add(posIn,mRoutesPairList.get(posIn)); - notifyItemInserted(posIn); + + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return routesPairList.size(); + } + + @Override + public int getNewListSize() { + return newList.size(); } - }// - //REMOVE OLD STOPS - for (Pair pair: indexMapExisting.keySet()) { + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + + RouteWithStop oldItem = routesPairList.get(oldItemPosition); + RouteWithStop newItem = newList.get(newItemPosition); + + // usa un ID univoco + return oldItem.getId().equals(newItem.getId()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + + RouteWithStop oldItem = routesPairList.get(oldItemPosition); + RouteWithStop newItem = newList.get(newItemPosition); + + // confronta il contenuto + return oldItem.equals(newItem); + } + } + ); + + routesPairList.clear(); + routesPairList.addAll(newList); + + diffResult.dispatchUpdatesTo(this); + + + /*if (this.routesPairList == null || routesPairList.size() == 0){ + routesPairList = mRoutesPairList; + notifyDataSetChanged(); + } else{ + + final var indexMapIn = getRouteIndexMap(mRoutesPairList); + final var indexMapExisting = getRouteIndexMap(routesPairList); + //List> oldList = routesPairList; + routesPairList = mRoutesPairList; + /* + for (Pair pair: indexMapIn.keySet()){ + final Integer posIn = indexMapIn.get(pair); + if (posIn == null) continue; + if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); - if (posExisting == null) continue; + //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); - notifyItemRemoved(posExisting); + //routesPairList.add(posIn,mRoutesPairList.get(posIn)); + + notifyItemMoved(posExisting, posIn); + indexMapExisting.remove(pair); + } else{ + //INSERT IT + //routesPairList.add(posIn,mRoutesPairList.get(posIn)); + notifyItemInserted(posIn); } - //*/notifyDataSetChanged(); - + }// + //REMOVE OLD STOPS + for (Pair pair: indexMapExisting.keySet()) { + final Integer posExisting = indexMapExisting.get(pair); + if (posExisting == null) continue; + //routesPairList.remove(posExisting.intValue()); + notifyItemRemoved(posExisting); } - //remove and join the - } + + */ } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ - Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); - //All of this to get only the first occurrences of a line (name & direction) - ListIterator> iterator = routesPairList.listIterator(); - Set> allRoutesDirections = new HashSet<>(); - while(iterator.hasNext()){ - final Pair stopRoutePair = iterator.next(); - if (stopRoutePair.second != null) { - final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); - if (allRoutesDirections.contains(routeNameDirection)) { - iterator.remove(); - } else { - allRoutesDirections.add(routeNameDirection); - } - } - } - } - /** - * Sort and remove the repetitions in the list - */ - private static void sortAndRemoveDuplicates(List< Pair > routesPairList, GPSPoint positionToSort ){ - Collections.sort(routesPairList,new RoutePositionSorter(positionToSort)); + //Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) - ListIterator> iterator = routesPairList.listIterator(); + var iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ - final Pair stopRoutePair = iterator.next(); - if (stopRoutePair.second != null) { - final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); - if (allRoutesDirections.contains(routeNameDirection)) { - iterator.remove(); - } else { - allRoutesDirections.add(routeNameDirection); - } + final var stopRoutePair = iterator.next(); + stopRoutePair.getRoute(); + final Pair routeNameDirection = new Pair<>(stopRoutePair.getRoute().getName(), stopRoutePair.getRoute().destinazione); + if (allRoutesDirections.contains(routeNameDirection)) { + iterator.remove(); + } else { + allRoutesDirections.add(routeNameDirection); } } } - private static HashMap, Integer> getRouteIndexMap(List> routesPairList){ - final HashMap, Integer> myMap = new HashMap<>(); + + private static HashMap getRouteIndexMap(List routesPairList){ + final HashMap myMap = new HashMap<>(); for (int i=0; i(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); + myMap.put(routesPairList.get(i), i); } return myMap; } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java index 059b9ff..f1757e8 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -1,393 +1,395 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.util.Log; +import androidx.annotation.NonNull; /** * Converts some weird stop IDs found on the 5T website to the form used everywhere else (including GTT website). *

* A stop ID in normalized form is:
* - a string containing a number without leading zeros
* - a string beginning with "ST" and then a number
* - whatever the GTT website uses.
*

* A bus route ID in normalized form is:
* - a string containing a number and optionally: "B", "CS", "CD", "N", "S", "C" at the end
* - the string "METRO"
* - "ST1" or "ST2" (Star 1 and Star 2)
* - a string beginning with E, N, S or W, followed by two digits (with a leading zero)
* - "RV2", "OB1"
* - "CAN" or "FTC" (railway lines)
* - ...screw it, let's just hope all the websites and APIs return something sane as route IDs.
*

* This class exists because Java doesn't support traits.
*
* Note: this class also just became useless, as 5T now uses the same format as GTT website. */ public abstract class FiveTNormalizer { public static String FiveTNormalizeRoute(String RouteID) { while (RouteID.startsWith("0")) { RouteID = RouteID.substring(1); } return RouteID; } // public static String FiveTNormalizeStop(String StopID) { // StopID = FiveTNormalizeRoute(StopID); // // is this faster than a regex? // if (StopID.length() == 5 && StopID.startsWith("ST") && Character.isLetter(StopID.charAt(2)) && Character.isLetter(StopID.charAt(3)) && Character.isLetter(StopID.charAt(4))) { // switch (StopID) { // case "STFER": // return "8210"; // case "STPAR": // return "8211"; // case "STMAR": // return "8212"; // case "STMAS": // return "8213"; // case "STPOS": // return "8214"; // case "STMGR": // return "8215"; // case "STRIV": // return "8216"; // case "STRAC": // return "8217"; // case "STBER": // return "8218"; // case "STPDA": // return "8219"; // case "STDOD": // return "8220"; // case "STPSU": // return "8221"; // case "STVIN": // return "8222"; // case "STREU": // return "8223"; // case "STPNU": // return "8224"; // case "STMCI": // return "8225"; // case "STNIZ": // return "8226"; // case "STDAN": // return "8227"; // case "STCAR": // return "8228"; // case "STSPE": // return "8229"; // case "STLGO": // return "8230"; // } // } // return StopID; // } // // public static String NormalizedToFiveT(final String StopID) { // if(StopID.startsWith("82") && StopID.length() == 4) { // switch (StopID) { // case "8230": // return "STLGO"; // case "8229": // return "STSPE"; // case "8228": // return "STCAR"; // case "8227": // return "STDAN"; // case "8226": // return "STNIZ"; // case "8225": // return "STMCI"; // case "8224": // return "STPNU"; // case "8223": // return "STREU"; // case "8222": // return "STVIN"; // case "8221": // return "STPSU"; // case "8220": // return "STDOD"; // case "8219": // return "STPDA"; // case "8218": // return "STBER"; // case "8217": // return "STRAC"; // case "8216": // return "STRIV"; // case "8215": // return "STMGR"; // case "8214": // return "STPOS"; // case "8213": // return "STMAS"; // case "8212": // return "STMAR"; // case "8211": // return "STPAR"; // case "8210": // return "STFER"; // } // } // // return StopID; // } public static Route.Type decodeType(final String routename, final String bacino) { if(routename.equals("METRO")) { return Route.Type.METRO; } else if(routename.equals("79")) { return Route.Type.RAILWAY; } switch (bacino) { case "U": return Route.Type.BUS; case "F": return Route.Type.RAILWAY; case "E": return Route.Type.LONG_DISTANCE_BUS; default: return Route.Type.BUS; } } /** * Converts a route ID from internal format to display format, returns null if it has the same name. * * @param routeID ID in "internal" and normalized format * @return string with display name, null if unchanged */ public static String routeInternalToDisplay(final String routeID) { if(routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0,2).concat("/"); } //TODO: Decide what to do about the "+" lines (68+, 13+) switch(routeID) { case "1C": return "1 Chieri"; case "1N": return "1 Nichelino"; case "OB1": return "1 Orbassano"; case "2C": return "2 Chieri"; case "RV2": return "2 Rivalta"; case "CO1": return "Circolare Collegno"; case "SE1": // I wonder why GTT calls this "SE1" while other absurd names have a human readable name too. return "1 Settimo"; case "16CD": return "16 CD"; case "16CS": return "16 CS"; case "79": return "Cremagliera Sassi-Superga"; case "W01": return "Night Buster 1 Arancio"; case "N10": return "Night Buster 10 Gialla"; case "W15": return "Night Buster 15 Rosa"; case "S18": return "Night Buster 18 Blu"; case "S04": return "Night Buster 4 Azzurra"; case "N4": return "Night Buster 4 Rossa"; case "N57": return "Night Buster 57 Oro"; case "W60": return "Night Buster 60 Argento"; case "E68": return "Night Buster 68 Verde"; case "S05": return "Night Buster 5 Viola"; case "ST1": return "Star 1"; case "ST2": return "Star 2"; case "4N": return "4 Navetta"; case "10N": return "10 Navetta"; case "13N": return "13 Navetta"; case "35N": return "35 Navetta"; case "36N": return "36 Navetta"; case "36S": return "36 Speciale"; case "38S": return "38 Speciale"; case "44S": return "44 Scolastico"; case "46N": return "46 Navetta"; case "M1S": return "MetroBus sostitutivo"; default: return null; } } + @NonNull public static String fixShortNameForDisplay(String routeID, boolean withBarratoSpace) { /*if (routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0, 2).concat("/"); } else if (routeID.charAt(routeID.length() - 1) == '/' && routeID.charAt(routeID.length() - 2) == ' ') { //remove last space return routeID.substring(0, routeID.length() - 2).concat("/"); } else return routeID; */ int len = routeID.length(); final boolean isBarrato = (routeID.charAt(len-1) == 'B') || (routeID.charAt(len-1) == '/'); if(isBarrato) { String output; if ((routeID.charAt(len - 2) == ' ')) output = routeID.substring(0, len - 2); else output = routeID.substring(0, len - 1); if(withBarratoSpace) output = output.concat(" /"); else output = output.concat("/"); return output; } else return routeID; } public static String fixShortNameForDisplay(String routeID){ return fixShortNameForDisplay(routeID, false); } public static String routeDisplayToInternal(String displayName){ String name = displayName.trim(); if(name.charAt(displayName.length()-1)=='/'){ return displayName.replace(" ","").replace("/","B"); } switch (name.toLowerCase()){ //DEFAULT CASES case "star 1": return "ST1"; case "star 2": return "ST2"; case "night buster 1 arancio": return "W01"; case "night buster 10 gialla": return "N10"; case "night buster 15 rosa": return "W15"; case "night buster 18 blu": return "S18"; case "night buster 4 azzurra": return "S04"; case "night buster 4 rossa": return "N4"; case "night buster 57 oro": return "N57"; case "night buster 60 argento": return "W60"; case "night buster 68 verde": return "E68"; case "night buster 5 viola": return "S05"; case "1 nichelino": return "1N"; case "1 chieri": return "1C"; case "1 orbassano": return "OB1"; case "2 chieri": return "2C"; case "2 rivalta": return "RV2"; default: // return displayName.trim(); } String[] arr = name.toLowerCase().split("\\s+"); try { if (arr.length == 2 && arr[1].trim().equals("navetta") && Integer.decode(arr[0]) > 0) return arr[0].trim().concat("N"); } catch (NumberFormatException e){ //It's not "# navetta" Log.w("FivetNorm","checking number when it's not"); } if(name.toLowerCase().contains("night buster")){ if(name.toLowerCase().contains("viola")) return "S05"; else if(name.toLowerCase().contains("verde")) return "E68"; } //Everything failed, let's at least compact the the (probable) code return name.replace(" ",""); } /** * Create the line name in GTFS format (e.g., "gtt:10U") from a more human readable name ("10") * @param route the route object * @return the code for the line in GTFS format */ public static String getGtfsRouteID(Route route){ String routeName = route.getName(); String cutName = routeName.replace("\\s", ""); int len = cutName.length(); StringBuilder sb = new StringBuilder("gtt:"); if (cutName.charAt(len-1) == '/'){ sb.append(cutName.substring(0, len-2)); sb.append("B"); //cutName = cutName.substring(0, len-2).concat("B"); } else { sb.append(cutName); } //determine service kind switch (route.type){ case UNKNOWN: case BUS: case TRAM: //tourist lines have "U" in the routeid sb.append("U"); break; case RAILWAY: sb.append("F"); break; case LONG_DISTANCE_BUS: sb.append("E"); } return sb.toString(); } public static String filterFullStarName(String name){ String outName = name; if(name.contains("STAR ")){ //FIX FOR THE MaTO data outName = outName.replace("STAR ","ST"); } return outName; } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/RouteWithStop.kt b/app/src/main/java/it/reyboz/bustorino/backend/RouteWithStop.kt new file mode 100644 index 0000000..50e0baa --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/RouteWithStop.kt @@ -0,0 +1,10 @@ +package it.reyboz.bustorino.backend + +data class RouteWithStop( + val stop: Stop, + val route: Route, +) { + val id by lazy { + "stop${stop.ID}route${route.displayCode}" + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java index 5dc985f..b29e6f4 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,283 +1,298 @@ /* BusTO (fragments) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; - private final FragmentListenerMain listenerMain; + private final FragmentListenerMain mainFragment; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; private final StopSearcher stopSearcher; + //this is for deciding whether to popMain Fragment stack with children + private final LinkedBlockingDeque popMainQueueOnMainFragStack = new LinkedBlockingDeque<>(); + public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { - this.listenerMain = listener; + this.mainFragment = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); stopSearcher = new StopSearcher(this); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } - + public boolean needToPopMainStackOnBack(){ + if(popMainQueueOnMainFragStack.isEmpty()){ + return true; + } + return popMainQueueOnMainFragStack.pop(); + } + public void setMainFragmentManagerTransition(boolean popMain){ + Log.d(DEBUG_TAG, "Adding child fragment pop for main screen: " + popMain); + popMainQueueOnMainFragStack.addFirst(popMain); + } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ public void showArrivalsFragmentForStop(@NonNull Palina p, boolean addToBackStack){ boolean sameFragment = false; ArrivalsFragment arrivalsFragment = null; final FragmentManager fm = managerWeakRef.get(); if(fm == null) return; if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment frag) { sameFragment = frag.isFragmentForTheSameStop(p); if(sameFragment) { arrivalsFragment = frag; Log.d("BusTO", "Same bus stop, accessing existing fragment"); } } if(!sameFragment) { // get old fragment var frag = fm.findFragmentByTag(ArrivalsFragment.getFragmentTag(p)); if(frag instanceof ArrivalsFragment) { attachFragmentToContainer(fm, frag, null, true, addToBackStack); arrivalsFragment = (ArrivalsFragment) frag; } else { // create new fragment //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); if (displayName != null && !displayName.isEmpty()) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID, displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } String probableTag = ArrivalsFragment.getFragmentTag(p); attachFragmentToContainer(fm, arrivalsFragment, probableTag, true, addToBackStack); } } setLastSuccessfullySearchedBusStop(p); // update the data only if I have information about the passaggi if(p.getTotalNumberOfPassages() > 0) arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); - listenerMain.hideKeyboard(); + mainFragment.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ - listenerMain.hideKeyboard(); + mainFragment.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); if(managerWeakRef.get()==null) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } attachFragmentToContainer(managerWeakRef.get(), listfragment, "search_"+query, false, addToBackStack); + //DO NOT DO THE SAME ON THE ARRIVALS (the call goes through MainActivity) + setMainFragmentManagerTransition(false); listfragment.setStopList(resultList); //listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ - listenerMain.toggleSpinner(on); + mainFragment.toggleSpinner(on); } /** * Attach a new fragment to the appropriate container * @param fm the FragmentManager * @param fragment the Fragment * @param tagAttach attach tag (can be null, the fragment's own tag has preference) * @param addToBackStack if the transaction is to be added to the stack * @param toSecondaryFrame if the fragment goes to the secondary frame */ protected void attachFragmentToContainer(FragmentManager fm, Fragment fragment, @Nullable String tagAttach, boolean toSecondaryFrame, boolean addToBackStack){ FragmentTransaction ft = fm.beginTransaction(); int frameID; if(toSecondaryFrame && secondaryFrameLayout!=NO_FRAME) frameID = secondaryFrameLayout; else frameID = primaryFrameLayout; var tag = fragment.getTag(); if(tag == null) tag = tagAttach; // there is only one case //switch (pars.transaction){ // case REPLACE: ft.replace(frameID,fragment,tag); //} if (addToBackStack) ft.addToBackStack("state_"+tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); ft.commit(); //fm.executePendingTransactions(); } public void stopLastRequestIfNeeded(){ /*if(lastTaskRef == null) return; AsyncTask task = lastTaskRef.get(); if(task!=null){ task.cancel(interruptIfRunning); } */ stopSearcher.cancelLastRequest(); } public void requestStopSearch(String query){ stopSearcher.cancelLastRequest(); stopSearcher.runRequest(query, new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); // run with the default fetchers } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.Result res, SearchRequestType type){ //TODO: implement a common set of errors for all fragments if (res==null){ Log.e(DEBUG_TAG, "Asked to show result with null result"); return; } Log.d(DEBUG_TAG, "Showing result for "+res); switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: if (type == SearchRequestType.STOPS) showShortToast(R.string.no_bus_stop_have_this_name); else if(type == SearchRequestType.ARRIVALS){ showShortToast(R.string.no_arrivals_stop); } break; case NOT_FOUND: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } /* // 18/05/2026: Commenting, do not remove, might be useful later enum Transaction{ REPLACE, } private static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } */ } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 1282296..b4724b3 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,959 +1,970 @@ /* BusTO - Fragments 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.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; +import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.util.Permissions; import it.reyboz.bustorino.viewmodels.IntroViewModel; import org.jetbrains.annotations.NotNull; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ -public class MainScreenFragment extends BarcodeFragment implements FragmentListenerMain{ +public class MainScreenFragment extends BarcodeFragment implements FragmentListenerMain, ParentFragmentManagerFromChild{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String ARG_INITIAL_CONTENT = "initial_content"; public static final String ARG_STOP_ID = "pending_stop_id"; public static final String ARG_SEARCH_QUERY = "pending_search_query"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private enum SearchMode {SEARCH_ID,SEARCH_NAME,INITIAL} public enum InternalScreen { HOME_BUTTONS(0), NEARBY_STOPS(1), ARRIVALS(2), STOP_SEARCH(3), NEARBY_ARRIVALS(4); public final int code; InternalScreen(int code) { this.code = code; } @Nullable public static InternalScreen fromCode(int code) { for (InternalScreen c : values()) if (c.code == code) return c; return null; } @NonNull public static InternalScreen fromFragmentKind(@NonNull FragmentKind kind){ switch (kind){ case HOME_BUTTONS -> { return InternalScreen.HOME_BUTTONS; } case NEARBY_STOPS -> { return InternalScreen.NEARBY_STOPS; } case FragmentKind.ARRIVALS -> { return InternalScreen.ARRIVALS; } case FragmentKind.STOPS -> { return InternalScreen.STOP_SEARCH; } case FragmentKind.NEARBY_ARRIVALS -> { return InternalScreen.NEARBY_ARRIVALS; } default -> { throw new IllegalArgumentException("Unknown fragment kind"); } } } } private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private FloatingActionButton floatingActionButton; /// VIEW MODELS in BaseFragment private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean initialScreenShown = false; private SearchMode searchMode = SearchMode.INITIAL; private FragmentManager childFragMan; /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean pendingNearbyAddToBackStack = false; boolean locationPermissionGranted, locationPermissionAsked = false; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private String pendingSearchQuery = null; private InternalScreen internalScreen = InternalScreen.HOME_BUTTONS; private CoordinatorLayout coordLayout; //this is really a hackish thing, but it works - private LinkedBlockingQueue thingsToDoOnStart = new LinkedBlockingQueue<>(); + private final LinkedBlockingQueue thingsToDoOnStart = new LinkedBlockingQueue<>(); + private void refreshStop() { if(getContext() == null){ Log.w(DEBUG_TAG,"Asked to refresh stop but context is null"); return; } if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); } else{ //String stopName = fragment.getStopID(); fragment.requestArrivalsForTheFragment(); } } else { //we create a new fragment, which is WRONG Log.w(DEBUG_TAG, "Asked to refresh stop when there is no fragment"); } } private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() { @Override public void onActivityResult(Map result) { if (result == null) return; if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: " + result); if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) { locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); /*if (mListener != null && getContext() != null) { if (locationManager == null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } */ // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if (pendingNearbyStopsFragmentRequest) { showNearbyFragmentIfPossible(pendingNearbyAddToBackStack); pendingNearbyStopsFragmentRequest = false; } } if (pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest = false; } }); public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance(@NonNull InternalScreen kind, @Nullable String stopId, @Nullable String query) { MainScreenFragment f = new MainScreenFragment(); f.setArguments(makeArgs(kind, stopId, query)); return f; } public static MainScreenFragment newInstance(@NonNull InternalScreen kind, @Nullable Bundle args){ MainScreenFragment f = new MainScreenFragment(); if (args != null) { f.setArguments(args); } return f; } /** * Create the bundle for the arguments of the fragment * @param kind the kind of initial screen * @param stopId * @param query * @return */ public static Bundle makeArgs(@NonNull InternalScreen kind, @Nullable String stopId, @Nullable String query) { Bundle b = new Bundle(); b.putInt(ARG_INITIAL_CONTENT, kind.code); if (stopId != null) b.putString(ARG_STOP_ID, stopId); if (query != null) b.putString(ARG_SEARCH_QUERY, query); return b; } public static Bundle makeArgsArrivals(@NonNull String stopID){ return makeArgs(InternalScreen.ARRIVALS, stopID, null); } public static Bundle makeArgsStops(@NonNull String query){ return makeArgs(InternalScreen.STOP_SEARCH, query, null); } public static Bundle makeArgsNearby(){ return makeArgs(InternalScreen.NEARBY_STOPS, null, null); } public static Bundle makeArgsButtonsScreen(){ return makeArgs(InternalScreen.HOME_BUTTONS, null, null); } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); if (args != null) { Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+ args); if (args.containsKey(ARG_INITIAL_CONTENT)) { int code = args.getInt(ARG_INITIAL_CONTENT, InternalScreen.HOME_BUTTONS.code); InternalScreen parsed = InternalScreen.fromCode(code); internalScreen = (parsed != null) ? parsed : InternalScreen.HOME_BUTTONS; } String stopId = args.getString(ARG_STOP_ID); if (stopId != null) pendingStopID = stopId; pendingSearchQuery = args.getString(ARG_SEARCH_QUERY); } + fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); + + } + + @Override + public boolean needToPopMainStackOnBack() { + return fragmentHelper.needToPopMainStackOnBack(); + } + + @Override + public void setMainFragmentManagerTransition(boolean yes) { + fragmentHelper.setMainFragmentManagerTransition(yes); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setImageResource(R.drawable.magnifying_glass_larger); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); busStopSearchByIDEditText.setOnFocusChangeListener((v, hasFocus) -> { //Log.d(DEBUG_TAG, "stop search by ID has focus: " + hasFocus); if(hasFocus) setSearchModeBusStopID(); }); busStopSearchByNameEditText.setOnFocusChangeListener((v, hasFocus) -> { //Log.d(DEBUG_TAG, "stop search by Name has focus: " + hasFocus); if(hasFocus) setSearchModeBusStopName(); }); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff childFragMan = getChildFragmentManager(); childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); - fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); - /* cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); */ //locationManager = AppLocationManager.getInstance(requireContext()); IntroViewModel introViewModel = new ViewModelProvider(requireActivity()).get(IntroViewModel.class); introViewModel.getIntroIsRunning().observe(getViewLifecycleOwner(), isRunning -> { pendingIntroRun = isRunning; }); // TODO: Figure out how to go back to home when pressing home in the nav side bar /*fragShowingViewModel.getKindShowingFragment().observe(getViewLifecycleOwner(), kind -> { Log.w(DEBUG_TAG, "showing fragment kind: " + kind); try { var screenType = InternalScreen.fromFragmentKind(kind); if(screenType != internalScreen) { showDifferentSubFragments(screenType); internalScreen = screenType; } } catch (IllegalArgumentException e) { //ignored Log.d(DEBUG_TAG, "no update from fragment kind"); } }); */ Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); // The child FragmentManager has restored its content — don't dispatch again return; } if (savedInstanceState != null) return; showDifferentSubFragments(internalScreen); } /** * Installs the initial child fragment based on the arguments supplied as arguments */ private void showDifferentSubFragments(@NonNull InternalScreen screen) { boolean firstTime = !initialScreenShown; switch (screen) { case NEARBY_STOPS: case NEARBY_ARRIVALS: // TODO differentiate later //add to back stack if it is not just created showNearbyStopsFragmentChecking(!firstTime); break; case ARRIVALS: // pendingStopID is consumed in onResume → requestArrivalsForStopID break; case STOP_SEARCH: if (pendingSearchQuery != null && pendingSearchQuery.length() >= 2) { fragmentHelper.requestStopSearch(pendingSearchQuery); } else { showButtonsFragment(firstTime); } pendingSearchQuery = null; break; case HOME_BUTTONS: default: showButtonsFragment(firstTime); } if(!initialScreenShown){ initialScreenShown = true; } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); //if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); try { while (!thingsToDoOnStart.isEmpty()) { var task = thingsToDoOnStart.take(); task.run(); } } catch (InterruptedException e) { Log.w(DEBUG_TAG, "Interrupted while doing task for start"); thingsToDoOnStart.clear(); } if (setupOnStart) { if (pendingStopID==null){ if(!pendingIntroRun){ //show the fragment //showButtonsFragment(); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } private void showButtonsFragment(boolean addInsteadOfReplace){ swipeRefreshLayout.setVisibility(View.VISIBLE); var ft = childFragMan.beginTransaction(); var frag = ButtonsFragment.newInstance(); if(addInsteadOfReplace) ft.add(R.id.resultFrame,frag, ButtonsFragment.FRAGMENT_TAG); else{ ft.replace(R.id.resultFrame, frag, ButtonsFragment.FRAGMENT_TAG); ft.addToBackStack(null); } ft.commit(); } public void showButtonsFragmentIfNotNearby(boolean addToBackStack){ if(isAdded()) { var framan = getChildFragmentManager(); var showingFrag = framan.findFragmentById(R.id.resultFrame); if (showingFrag == null || showingFrag instanceof NearbyStopsFragment) { var fragHome = ButtonsFragment.newInstance(); var ft = framan.beginTransaction(); if (showingFrag == null) { ft.add(R.id.resultFrame, fragHome, ButtonsFragment.FRAGMENT_TAG); } else { ft.replace(R.id.resultFrame, fragHome, ButtonsFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack(null); ft.commit(); } else { Log.d(DEBUG_TAG, "attempting to show buttons home fragment but have other types (different than nearby)"); } } else{ Log.d(DEBUG_TAG, "Fragment is not added, putting in queue of things to do"); try { thingsToDoOnStart.put(() -> { showButtonsFragmentIfNotNearby(addToBackStack); }); } catch (InterruptedException e) { Log.e(DEBUG_TAG,"Cannot add task"); } } } private void showNearbyStopsFragmentChecking(boolean addToBackStack){ if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; pendingNearbyAddToBackStack = addToBackStack; Log.d(DEBUG_TAG, "requesting location permission for nearby fragment"); } else { Log.d(DEBUG_TAG, "Showing nearby stops fragment"); showNearbyFragmentIfPossible(addToBackStack); } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); //recheck the introduction activity has been run if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if(!suppressArrivalsReload && pendingStopID==null){ //none of the following cases are true // check if we are showing any fragment /* //TODO: check if this is needed final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ //we are not showing anything if(Permissions.anyLocationPermissionsGranted(getContext())){ showNearbyFragmentIfPossible(); } } */ } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } //mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); //fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; //locationManager.removeLocationRequestFor(requester); //fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(); super.onPause(); } /* GUI METHODS */ @Override public void onQrScanSuccess(@NotNull String busIDToSearch) { busStopSearchByIDEditText.setText(busIDToSearch); requestArrivalsForStopID(busIDToSearch); } /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { launchBarcodeScan(); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { //final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SearchMode.SEARCH_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(); requestArrivalsForStopID(busStopID); } else if (searchMode == SearchMode.SEARCH_NAME) { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.requestStopSearch(query); } } } } public void onToggleKeyboardLayout(View v) { switch (searchMode){ case SEARCH_ID: setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } break; case SEARCH_NAME: case INITIAL: setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view; if(searchMode == SearchMode.SEARCH_ID) view= busStopSearchByIDEditText; else if(searchMode == SearchMode.SEARCH_NAME) view = busStopSearchByNameEditText; else{ Log.e(DEBUG_TAG, "Asking to show keyboard but SearchMode is "+searchMode+", ignoring"); return; } imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SearchMode.SEARCH_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SearchMode.SEARCH_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForArrivals() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } @Override public void showFloatingActionButton(boolean yes) { //mListener.showFloatingActionButton(yes); if(yes) floatingActionButton.setVisibility(View.VISIBLE); else floatingActionButton.setVisibility(View.GONE); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { - - + if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Readying main fragment for type "+fragmentType); //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { //locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForArrivals(); break; case STOPS: prepareGUIForBusStops(); break; default: - Log.d(DEBUG_TAG, "Fragment type is unknown"); + //Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity if(mListener!=null) mListener.openLineFromStop(routeGtfsId, stopIDFrom); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { if(mListener!=null) mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); } @Override public void openNearbyStopsFragment() { if(isAdded()) showNearbyStopsFragmentChecking(true); else try{ thingsToDoOnStart.put(() -> showNearbyStopsFragmentChecking(true)); } catch (InterruptedException e) { Log.e(DEBUG_TAG, "trying to put open nearby in task but was interrupted"); } } @Override public void openLinesFragment() { if(mListener!=null) mListener.openLinesFragment(); } @Override public void openFavoritesFragment() { if(mListener!=null) mListener.openFavoritesFragment(); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (ID == null || ID.isEmpty()) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else{ var palinaTrial = new Palina(ID); if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment fragment) { if (fragment.isFragmentForTheSameStop(palinaTrial)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() fragment.requestArrivalsForTheFragment(); } else{ // The rest of the case is handled by the fragment Helper fragmentHelper.showArrivalsFragmentForStop(palinaTrial, true); } } else { // this is not needed any more //prepareGUIForArrivals(); fragmentHelper.showArrivalsFragmentForStop(palinaTrial, true); } } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED; return !noPermission; } private void requestLocationPermission(){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ makeToast(R.string.enable_position_message_nearby); } requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfPossible(boolean addToBackStack) { if (isNearbyFragmentShown()) { //nothing to do Log.d(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } if (!childFragMan.isDestroyed()) { //Go ahead with the request swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing var nearbyFrag = (NearbyStopsFragment) childFragMan.findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); if(nearbyFrag==null){ nearbyFrag = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); } FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, nearbyFrag, NearbyStopsFragment.FRAGMENT_TAG); if(addToBackStack) ft.addToBackStack(null); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } pendingNearbyStopsFragmentRequest = false; } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.java deleted file mode 100644 index 0f2c81e..0000000 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.java +++ /dev/null @@ -1,122 +0,0 @@ -package it.reyboz.bustorino.fragments; - -import android.content.Context; -import android.util.Log; -import com.android.volley.NetworkError; -import com.android.volley.ParseError; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import it.reyboz.bustorino.backend.NetworkVolleyManager; -import it.reyboz.bustorino.backend.Palina; -import it.reyboz.bustorino.backend.Route; -import it.reyboz.bustorino.backend.Stop; -import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - -class NearbyArrivalsDownloader implements Response.Listener, Response.ErrorListener { - final static String DEBUG_TAG = "BusTO-NearbyArrivDowns"; - //final Map> routesToAdd = new HashMap<>(); - final ArrayList nonEmptyPalinas = new ArrayList<>(); - final HashMap completedRequests = new HashMap<>(); - final static String REQUEST_TAG = "NearbyArrivals"; - final NetworkVolleyManager volleyManager; - int activeRequestCount = 0, reqErrorCount = 0, reqSuccessCount = 0; - - final ArrivalsListener listener; - - NearbyArrivalsDownloader(Context context, ArrivalsListener arrivalsListener) { - volleyManager = NetworkVolleyManager.getInstance(context); - - listener = arrivalsListener; - //flatProgressBar.setMax(numreq); - } - - public int requestArrivalsForStops(List stops){ - int MAX_ARRIVAL_STOPS = 35; - Date currentDate = new Date(); - int timeRange = 3600; - int departures = 10; - int numreq = 0; - activeRequestCount = 0; - reqErrorCount = 0; - reqSuccessCount = 0; - nonEmptyPalinas.clear(); - completedRequests.clear(); - - for (Stop s : stops.subList(0, Math.min(stops.size(), MAX_ARRIVAL_STOPS))) { - - final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); - req.setTag(REQUEST_TAG); - volleyManager.addToRequestQueue(req); - activeRequestCount++; - numreq++; - completedRequests.put(s.ID, false); - } - listener.setProgress(reqErrorCount+reqSuccessCount, activeRequestCount); - return numreq; - } - - private int totalRequests(){ - return activeRequestCount + reqSuccessCount + reqErrorCount; - } - - - @Override - public void onErrorResponse(VolleyError error) { - if (error instanceof ParseError) { - //TODO - Log.w(DEBUG_TAG, "Parsing error for stop request"); - } else if (error instanceof NetworkError) { - String s; - if (error.networkResponse != null) - s = new String(error.networkResponse.data); - else s = ""; - Log.w(DEBUG_TAG, "Network error: " + s); - } else { - Log.w(DEBUG_TAG, "Volley Error: " + error.getMessage()); - } - if (error.networkResponse != null) { - Log.w(DEBUG_TAG, "Error status code: " + error.networkResponse.statusCode); - } - //counters - activeRequestCount--; - reqErrorCount++; - //flatProgressBar.setProgress(reqErrorCount + reqSuccessCount); - listener.setProgress(reqErrorCount + reqSuccessCount, activeRequestCount); - } - - @Override - public void onResponse(Palina palinaResult) { - //counter for requests - activeRequestCount--; - reqSuccessCount++; - listener.setProgress(reqErrorCount + reqSuccessCount, activeRequestCount); - - //add the palina to the successful one - if(palinaResult!=null) { - final List routes = palinaResult.queryAllRoutes(); - if (routes != null && !routes.isEmpty()) { - nonEmptyPalinas.add(palinaResult); - listener.showCompletedArrivals(nonEmptyPalinas); - } - } - } - - void cancelAllRequests() { - volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); - //flatProgressBar.setVisibility(View.GONE); - listener.onAllRequestsCancelled(); - } - - public interface ArrivalsListener{ - void setProgress(int completedRequests, int pendingRequests); - - void onAllRequestsCancelled(); - - void showCompletedArrivals(ArrayList completedPalinas); - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.kt new file mode 100644 index 0000000..8e7007a --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyArrivalsDownloader.kt @@ -0,0 +1,113 @@ +package it.reyboz.bustorino.fragments + +import android.content.Context +import android.util.Log +import com.android.volley.NetworkError +import com.android.volley.ParseError +import com.android.volley.Response +import com.android.volley.VolleyError +import it.reyboz.bustorino.backend.NetworkVolleyManager +import it.reyboz.bustorino.backend.Palina +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.mato.MapiArrivalRequest +import java.util.* +import kotlin.math.min + +internal class NearbyArrivalsDownloader(context: Context, val listener: ArrivalsListener) : Response.Listener, + Response.ErrorListener { + //final Map> routesToAdd = new HashMap<>(); + val nonEmptyPalinas = ArrayList() + val completedRequests: HashMap = HashMap() + val volleyManager: NetworkVolleyManager = NetworkVolleyManager.getInstance(context) + var activeRequestCount: Int = 0 + var reqErrorCount: Int = 0 + var reqSuccessCount: Int = 0 + + + fun requestArrivalsForStops(stops: List): Int { + val currentDate = Date() + val timeRange = 3600 + val departures = 10 + var numreq = 0 + activeRequestCount = 0 + reqErrorCount = 0 + reqSuccessCount = 0 + nonEmptyPalinas.clear() + completedRequests.clear() + + for (s in stops.subList(0, min(stops.size, MAX_ARRIVAL_STOPS))) { + val req = MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this) + req.setTag(REQUEST_TAG) + volleyManager.addToRequestQueue(req) + activeRequestCount++ + numreq++ + completedRequests[s.ID] = false + } + listener.setProgress(reqErrorCount + reqSuccessCount, activeRequestCount) + return numreq + } + + private fun totalRequests(): Int { + return activeRequestCount + reqSuccessCount + reqErrorCount + } + + + override fun onErrorResponse(error: VolleyError) { + if (error is ParseError) { + //TODO + Log.w(DEBUG_TAG, "Parsing error for stop request") + } else if (error is NetworkError) { + val s: String? + if (error.networkResponse != null) s = String(error.networkResponse.data) + else s = "" + Log.w(DEBUG_TAG, "Network error: " + s) + } else { + Log.w(DEBUG_TAG, "Volley Error: " + error.message) + } + if (error.networkResponse != null) { + Log.w(DEBUG_TAG, "Error status code: " + error.networkResponse.statusCode) + } + //counters + activeRequestCount-- + reqErrorCount++ + //flatProgressBar.setProgress(reqErrorCount + reqSuccessCount); + listener.setProgress(reqErrorCount + reqSuccessCount, activeRequestCount) + } + + override fun onResponse(palinaResult: Palina?) { + //counter for requests + activeRequestCount-- + reqSuccessCount++ + listener.setProgress(reqErrorCount + reqSuccessCount, activeRequestCount) + + //add the palina to the successful one + if (palinaResult != null) { + val routes = palinaResult.queryAllRoutes() + if (routes != null && !routes.isEmpty()) { + nonEmptyPalinas.add(palinaResult) + listener.showCompletedArrivals(nonEmptyPalinas) + } + } + } + + fun cancelAllRequests() { + volleyManager.getRequestQueue().cancelAll(REQUEST_TAG) + //flatProgressBar.setVisibility(View.GONE); + listener.onAllRequestsCancelled() + } + + interface ArrivalsListener { + fun setProgress(completedRequests: Int, pendingRequests: Int) + + fun onAllRequestsCancelled() + + fun showCompletedArrivals(completedPalinas: ArrayList) + } + + companion object { + private const val DEBUG_TAG: String = "BusTO-NearbyArrivDowns" + const val REQUEST_TAG: String = "NearbyArrivals" + private const val MAX_ARRIVAL_STOPS = 35 + + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java deleted file mode 100644 index cc0eca8..0000000 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ /dev/null @@ -1,716 +0,0 @@ -/* - BusTO - Fragments components - Copyright (C) 2018 Fabio Mazza - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -package it.reyboz.bustorino.fragments; - -import android.annotation.SuppressLint; -import android.content.Context; - -import android.content.SharedPreferences; -import android.location.Location; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; -import androidx.core.util.Pair; -import androidx.preference.PreferenceManager; -import androidx.appcompat.widget.AppCompatButton; -import androidx.recyclerview.widget.RecyclerView; -import androidx.work.WorkInfo; - -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import android.widget.ProgressBar; -import android.widget.TextView; -import it.reyboz.bustorino.BuildConfig; -import it.reyboz.bustorino.R; -import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; -import it.reyboz.bustorino.backend.*; -import it.reyboz.bustorino.data.DatabaseUpdate; -import it.reyboz.bustorino.adapters.SquareStopAdapter; -import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; -import it.reyboz.bustorino.middleware.FusedNativeLocationProvider; -import it.reyboz.bustorino.util.Permissions; -import it.reyboz.bustorino.util.StopSorterByDistance; -import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -public class NearbyStopsFragment extends ScreenBaseFragment { - - @Nullable - @Override - public View getBaseViewForSnackBar() { - return null; - } - - public enum FragType{ - STOPS(1), ARRIVALS(2); - private final int num; - FragType(int num){ - this.num = num; - } - public static FragType fromNum(int i){ - switch (i){ - case 1: return STOPS; - case 2: return ARRIVALS; - default: - throw new IllegalArgumentException("type not recognized"); - } - } - } - private enum LocationShowingStatus {SEARCHING, FIRST_FIX, DISABLED, NO_PERMISSION} - - private FragmentListenerMain mListener; - - private final static String DEBUG_TAG = "NearbyStopsFragment"; - private final static String FRAGMENT_TYPE_KEY = "FragmentType"; - //public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; - private FragType fragment_type = FragType.STOPS; - - public final static String FRAGMENT_TAG="NearbyStopsFrag"; - - private RecyclerView gridRecyclerView; - - private SquareStopAdapter dataAdapter; - private AutoFitGridLayoutManager gridLayoutManager; - private GPSPoint lastPosition = null; - private ProgressBar circlingProgressBar,flatProgressBar; - //protected SharedPreferences globalSharedPref; - //private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; - private TextView messageTextView,titleTextView, loadingTextView; - private CommonScrollListener scrollListener; - private AppCompatButton switchButton; - private boolean firstLocForStops = true,firstLocForArrivals = true; - public static final int COLUMN_WIDTH_DP = 250; - - - private Integer MAX_DISTANCE = -3; - private int MIN_NUM_STOPS = -1; - - //These are useful for the case of nearby arrivals - private NearbyArrivalsDownloader arrivalsManager = null; - private ArrivalsStopAdapter arrivalsStopAdapter = null; - - private ArrayList currentNearbyStops = new ArrayList<>(); - - private LocationShowingStatus showingStatus = LocationShowingStatus.NO_PERMISSION; - private boolean isLocationEnabled = false; - - private final FusedNativeLocationProvider.LocationUpdateListener locationUpdateListener = new FusedNativeLocationProvider.LocationUpdateListener() { - @Override - public void onLocationUpdate(@NotNull Location location) { - updateLocationViewModel(location); - } - - @Override - public void onFusedStatusChanged(boolean isEnabled) { - Log.d(DEBUG_TAG, "Location provider is enabled: " + isEnabled); - isLocationEnabled = isEnabled; - if(isEnabled){ - setShowingStatus(LocationShowingStatus.SEARCHING); - } else{ - setShowingStatus(LocationShowingStatus.DISABLED); - } - } - }; - private final FusedNativeLocationProvider.Options locationOptionsArrivals = new FusedNativeLocationProvider.Options(5*1000L, 50f), - locationOptionsStops = new FusedNativeLocationProvider.Options(1000L, 5f);; - - - - /* - TODO: we do not request the permission in this fragment, only showing it when we have the location. Request position if this changes. - private final ActivityResultLauncher permissionsResultLauncher = getPositionRequestLauncher( - granted ->{ - - } - ); - */ - private FusedNativeLocationProvider locationProvider = null; - - - private final NearbyArrivalsDownloader.ArrivalsListener arrivalsListener = new NearbyArrivalsDownloader.ArrivalsListener() { - @Override - public void setProgress(int completedRequests, int pendingRequests) { - if(flatProgressBar!=null) { - if (pendingRequests == 0) { - flatProgressBar.setIndeterminate(true); - flatProgressBar.setVisibility(View.GONE); - } else { - flatProgressBar.setIndeterminate(false); - flatProgressBar.setProgress(completedRequests); - } - } - } - - @Override - public void onAllRequestsCancelled() { - if(flatProgressBar!=null) flatProgressBar.setVisibility(View.GONE); - } - - @Override - public void showCompletedArrivals(ArrayList completedPalinas) { - showArrivalsInRecycler(completedPalinas); - } - }; - - //ViewModel - private NearbyStopsViewModel viewModel; - - public NearbyStopsFragment() { - // Required empty public constructor - } - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * @return A new instance of fragment NearbyStopsFragment. - */ - public static NearbyStopsFragment newInstance(FragType type) { - //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) - // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); - NearbyStopsFragment fragment = new NearbyStopsFragment(); - final Bundle args = new Bundle(1); - args.putInt(FRAGMENT_TYPE_KEY,type.num); - fragment.setArguments(args); - return fragment; - } - - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - setFragmentType(FragType.fromNum(getArguments().getInt(FRAGMENT_TYPE_KEY))); - } - //locManager = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE); - //fragmentLocationListener = new FragmentLocationListener(); - if (getContext()!=null) { - //globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); - //globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); - } - - //NearbyArrivalsDownloader nearbyArrivalsDownloader = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); - locationProvider = new FusedNativeLocationProvider(requireContext()); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - if (getContext() == null) throw new RuntimeException(); - View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); - gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); - gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); - gridRecyclerView.setLayoutManager(gridLayoutManager); - gridRecyclerView.setHasFixedSize(false); - circlingProgressBar = root.findViewById(R.id.circularProgressBar); - flatProgressBar = root.findViewById(R.id.horizontalProgressBar); - messageTextView = root.findViewById(R.id.messageTextView); - titleTextView = root.findViewById(R.id.titleTextView); - loadingTextView = root.findViewById(R.id.positionLoadingTextView); - switchButton = root.findViewById(R.id.switchButton); - - scrollListener = new CommonScrollListener(mListener,false); - switchButton.setOnClickListener(v -> switchFragmentType()); - if(BuildConfig.DEBUG) - Log.d(DEBUG_TAG, "onCreateView"); - - final Context appContext =requireContext().getApplicationContext(); - DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { - @SuppressLint("MissingPermission") - @Override - public void onChanged(List workInfos) { - if(workInfos.isEmpty()) { - viewModel.setDBUpdateRunning(false); - return; - } - - WorkInfo wi = workInfos.get(0); - if (wi.getState() == WorkInfo.State.RUNNING && locationProvider.isRunning()) { - locationProvider.stopUpdates(); - viewModel.setDBUpdateRunning(true); - } else{ - //start the request - if(Permissions.bothLocationPermissionsGranted(requireContext())) { - if(!locationProvider.isRunning()){ - startLocationUpdatesByType(); - } - } else{ - setShowingStatus(LocationShowingStatus.NO_PERMISSION); - } - - viewModel.setDBUpdateRunning(false); - //actually restart request - } - } - }); - - //observe the livedata - viewModel.getStopsAtDistance().observe(getViewLifecycleOwner(), stops -> { - Log.d(DEBUG_TAG, "Received "+stops.size()+" stops nearby"); - Integer distance = viewModel.getDistanceMtLiveData().getValue(); - if(distance == null){ - distance = 40; - } - if ((stops.size() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { - viewModel.setDistance(distance + 40); - //viewModel.requestStopsAtDistance(distance, true); - //Log.d(DEBUG_TAG, "Doubling distance now!"); - return; // THIS WORKS AS AN `else` - } - if(!stops.isEmpty()) { - currentNearbyStops =stops; - showStopsInViews(currentNearbyStops, lastPosition); - } - }); - if(Permissions.anyLocationPermissionsGranted(appContext)){ - setShowingStatus(LocationShowingStatus.SEARCHING); - } else { - setShowingStatus(LocationShowingStatus.NO_PERMISSION); - - } - //add location listener - locationProvider.addListener(locationUpdateListener); - - return root; - } - - //because linter is stupid and cannot look inside *anyLocationPermissionGranted* - @SuppressLint("MissingPermission") - private boolean requestLocationUpdates(){ - if(Permissions.anyLocationPermissionsGranted(requireContext())) { - startLocationUpdatesByType(); - return true; - } else return false; - } - - /** - * Internal bit used to start location updates - */ - private void startLocationUpdatesByType(){ - switch (fragment_type) { - case STOPS: locationProvider.startUpdates(locationOptionsStops); break; - case ARRIVALS: locationProvider.startUpdates(locationOptionsArrivals); break; - } - } - - - - /** - * Use this method to set the fragment type - * @param type the type, TYPE_ARRIVALS or TYPE_STOPS - */ - private void setFragmentType(FragType type){ - boolean isChanged = fragment_type != type; - this.fragment_type = type; - /*switch(type){ - case ARRIVALS: - TIME_INTERVAL_REQUESTS = 5*1000; - break; - case STOPS: - TIME_INTERVAL_REQUESTS = 1000; - - } - - */ - if(isChanged){ - startLocationUpdatesByType(); - setShowingStatus(LocationShowingStatus.SEARCHING); - } - } - /** - * Set the location in the view model if it is good - * @param location new location - */ - private void updateLocationViewModel(@NonNull Location location, float accuracy){ - if(viewModel==null) { - return; - } - if(location.getAccuracy() stops, GPSPoint location){ - if (stops.isEmpty()) { - setNoStopsLayout(); - return; - } - if (location == null){ - // we could do something better, but it's better to do this for now - return; - } - - double minDistance = Double.POSITIVE_INFINITY; - for(Stop s: stops){ - minDistance = Math.min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude())); - } - - - //quick trial to hopefully always get the stops in the correct order - Collections.sort(stops,new StopSorterByDistance(location)); - switch (fragment_type){ - case STOPS: - showStopsInRecycler(stops); - break; - case ARRIVALS: - if(getContext()==null) break; //don't do anything if we're not attached - if(arrivalsManager==null) - arrivalsManager = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); - arrivalsManager.requestArrivalsForStops(stops); - /*flatProgressBar.setVisibility(View.VISIBLE); - flatProgressBar.setProgress(0); - flatProgressBar.setIndeterminate(false); - */ - //for the moment, be satisfied with only one location - //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); - break; - default: - } - - } - - /** - * To enable targeting from the Button - */ - public void switchFragmentType(View v){ - switchFragmentType(); - } - - /** - * Call when you need to switch the type of fragment - */ - private void switchFragmentType(){ - switch (fragment_type){ - case ARRIVALS: - setFragmentType(FragType.STOPS); - break; - case STOPS: - setFragmentType(FragType.ARRIVALS); - break; - default: - } - prepareForFragmentType(); - //locManager.removeLocationRequestFor(fragmentLocationListener); - //locManager.addLocationRequestFor(fragmentLocationListener); - if(lastPosition!=null) { - // we have at least one fix on the position - showStopsInViews(currentNearbyStops, lastPosition); - } - } - - /** - * Prepare the views for the set fragment type - */ - private void prepareForFragmentType(){ - if(fragment_type==FragType.STOPS){ - switchButton.setText(getString(R.string.show_arrivals)); - titleTextView.setText(getString(R.string.nearby_stops_message)); - if(arrivalsManager!=null) - arrivalsManager.cancelAllRequests(); - if(dataAdapter!=null) - gridRecyclerView.setAdapter(dataAdapter); - - } else if (fragment_type==FragType.ARRIVALS){ - titleTextView.setText(getString(R.string.nearby_arrivals_message)); - switchButton.setText(getString(R.string.show_stops)); - if(arrivalsStopAdapter!=null) - gridRecyclerView.setAdapter(arrivalsStopAdapter); - } - } - - //useful methods - - /////// GUI METHODS //////// - private void showStopsInRecycler(List stops){ - - if(firstLocForStops) { - dataAdapter = new SquareStopAdapter(stops, mListener, lastPosition); - gridRecyclerView.setAdapter(dataAdapter); - firstLocForStops = false; - }else { - dataAdapter.setStops(stops); - dataAdapter.setUserPosition(lastPosition); - } - dataAdapter.notifyDataSetChanged(); - - //showRecyclerHidingLoadMessage(); - if (gridRecyclerView.getVisibility() != View.VISIBLE) { - circlingProgressBar.setVisibility(View.GONE); - loadingTextView.setVisibility(View.GONE); - gridRecyclerView.setVisibility(View.VISIBLE); - } - messageTextView.setVisibility(View.GONE); - - if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); - } - - private void showArrivalsInRecycler(List palinas){ - Collections.sort(palinas,new StopSorterByDistance(lastPosition)); - - final ArrayList> routesPairList = new ArrayList<>(10); - //int maxNum = Math.min(MAX_STOPS, stopList.size()); - for(Palina p: palinas){ - //if there are no routes available, skip stop - if(p.queryAllRoutes().isEmpty()) continue; - for(Route r: p.queryAllRoutes()){ - //if there are no routes, should not do anything - if (r.passaggi != null && !r.passaggi.isEmpty()) - routesPairList.add(new Pair<>(p,r)); - } - } - if (getContext()==null){ - Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); - return; - } - if(firstLocForArrivals){ - arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastPosition); - gridRecyclerView.setAdapter(arrivalsStopAdapter); - firstLocForArrivals = false; - } else { - arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastPosition); - } - - //arrivalsStopAdapter.notifyDataSetChanged(); - - showRecyclerHidingLoadMessage(); - if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); - - } - - private void setNoStopsLayout(){ - messageTextView.setVisibility(View.VISIBLE); - messageTextView.setText(R.string.no_stops_nearby); - circlingProgressBar.setVisibility(View.GONE); - loadingTextView.setVisibility(View.GONE); - } - - /** - * Does exactly what is says on the tin - */ - private void showRecyclerHidingLoadMessage(){ - if (gridRecyclerView.getVisibility() != View.VISIBLE) { - circlingProgressBar.setVisibility(View.GONE); - loadingTextView.setVisibility(View.GONE); - gridRecyclerView.setVisibility(View.VISIBLE); - } - messageTextView.setVisibility(View.GONE); - } - - /* - * Local locationListener, to use for the GPS - */ - /* - class FragmentLocationListener implements LocationListenerCompat { - - private long lastUpdateTime = -1; - public boolean isRegistered = false; - - @Override - public void onLocationChanged(@NonNull Location location) { - if(viewModel==null){ - return; - } - if(location.getAccuracy()<200) { - - lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); - //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); - viewModel.setLastLocation(location); - } - lastUpdateTime = System.currentTimeMillis(); - //Log.d("BusTO:NearPositListen","can start request for stops: "+ !dbUpdateRunning); - } - - @Override - public void onProviderEnabled(@NonNull String provider) { - Log.d(DEBUG_TAG, "Location provider "+provider+" enabled"); - if(provider.equals(LocationManager.GPS_PROVIDER)){ - setShowingStatus(LocationShowingStatus.SEARCHING); - } - } - - @Override - public void onProviderDisabled(@NonNull String provider) { - Log.d(DEBUG_TAG, "Location provider "+provider+" disabled"); - if(provider.equals(LocationManager.GPS_PROVIDER)) { - setShowingStatus(LocationShowingStatus.DISABLED); - } - } - - @Override - public void onStatusChanged(@NonNull String provider, int status, @Nullable Bundle extras) { - LocationListenerCompat.super.onStatusChanged(provider, status, extras); - } - } - - */ - -} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt new file mode 100644 index 0000000..1478be5 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt @@ -0,0 +1,681 @@ +/* + BusTO - Fragments components + Copyright (C) 2018 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.fragments + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Location +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.widget.AppCompatButton +import androidx.core.util.Pair +import androidx.fragment.app.viewModels +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView +import androidx.work.WorkInfo +import it.reyboz.bustorino.BuildConfig +import it.reyboz.bustorino.R +import it.reyboz.bustorino.adapters.ArrivalsStopAdapter +import it.reyboz.bustorino.adapters.SquareStopAdapter +import it.reyboz.bustorino.backend.* +import it.reyboz.bustorino.data.DatabaseUpdate +import it.reyboz.bustorino.fragments.NearbyArrivalsDownloader.ArrivalsListener +import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager +import it.reyboz.bustorino.middleware.FusedNativeLocationProvider +import it.reyboz.bustorino.middleware.FusedNativeLocationProvider.LocationUpdateListener +import it.reyboz.bustorino.util.Permissions.Companion.anyLocationPermissionsGranted +import it.reyboz.bustorino.util.Permissions.Companion.bothLocationPermissionsGranted +import it.reyboz.bustorino.util.StopSorterByDistance +import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel +import java.util.* +import kotlin.math.min + +class NearbyStopsFragment : ScreenBaseFragment() { + override fun getBaseViewForSnackBar(): View? { + return null + } + + enum class FragType(val num: Int) { + STOPS(1), ARRIVALS(2); + + companion object { + @JvmStatic + fun fromNum(i: Int): FragType { + when (i) { + 1 -> return STOPS + 2 -> return ARRIVALS + else -> throw IllegalArgumentException("type not recognized") + } + } + } + } + + private enum class LocationShowingStatus { + SEARCHING, FIRST_FIX, DISABLED, NO_PERMISSION + } + + private var mListener: FragmentListenerMain? = null + + private var fragment_type = FragType.STOPS + + private lateinit var gridRecyclerView: RecyclerView + + private var dataAdapter: SquareStopAdapter? = null + private var gridLayoutManager: AutoFitGridLayoutManager? = null + private var lastPosition: GPSPoint? = null + private var circlingProgressBar: ProgressBar? = null + private lateinit var flatProgressBar: ProgressBar + + //protected SharedPreferences globalSharedPref; + //private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; + private var messageTextView: TextView? = null + private var titleTextView: TextView? = null + private var loadingTextView: TextView? = null + private var scrollListener: CommonScrollListener? = null + private var switchButton: AppCompatButton? = null + private var firstLocForStops = true + private var firstLocForArrivals = true + private var stopsMaxDistance = -3 + private var stopsMinNumber = -1 + + //These are useful for the case of nearby arrivals + private var arrivalsManager: NearbyArrivalsDownloader? = null + private var arrivalsStopAdapter: ArrivalsStopAdapter? = null + + private var currentNearbyStops = ArrayList() + + private var showingStatus = LocationShowingStatus.NO_PERMISSION + private var isLocationEnabled = false + + private val locationUpdateListener: LocationUpdateListener = object : LocationUpdateListener { + override fun onLocationUpdate(location: Location) { + updateLocationViewModel(location) + } + + override fun onFusedStatusChanged(isEnabled: Boolean) { + Log.d(DEBUG_TAG, "Location provider is enabled: " + isEnabled) + isLocationEnabled = isEnabled + if (isEnabled) { + setShowingStatus(LocationShowingStatus.SEARCHING) + } else { + setShowingStatus(LocationShowingStatus.DISABLED) + } + } + } + private val locationOptionsArrivals = FusedNativeLocationProvider.Options(5 * 1000L, 50f) + private val locationOptionsStops = FusedNativeLocationProvider.Options(1000L, 5f) + + + /* + TODO: we do not request the permission in this fragment, only showing it when we have the location. Request position if this changes. + private final ActivityResultLauncher permissionsResultLauncher = getPositionRequestLauncher( + granted ->{ + + } + ); + */ + private var locationProvider: FusedNativeLocationProvider? = null + + + /*private val arrivalsListener: ArrivalsListener = object : ArrivalsListener { + override fun setProgress(completedRequests: Int, pendingRequests: Int) { + if (pendingRequests == 0) { + flatProgressBar.setIndeterminate(true) + flatProgressBar.setVisibility(View.GONE) + } else { + flatProgressBar.setIndeterminate(false) + flatProgressBar.progress = completedRequests + } + } + + /*override fun onAllRequestsCancelled() { + if (flatProgressBar != null) flatProgressBar!!.setVisibility(View.GONE) + } + + */ + + override fun showCompletedArrivals(completedPalinas: ArrayList) { + showArrivalsInRecycler(completedPalinas) + } + } + + */ + + //ViewModel + private val viewModel : NearbyStopsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let{ + + setFragmentType(FragType.fromNum(it.getInt(FRAGMENT_TYPE_KEY))) + } + //locManager = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE); + //fragmentLocationListener = new FragmentLocationListener(); + if (getContext() != null) { + //globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); + //globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + } + + //NearbyArrivalsDownloader nearbyArrivalsDownloader = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); + locationProvider = FusedNativeLocationProvider(requireContext()) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Inflate the layout for this fragment + if (getContext() == null) throw RuntimeException() + val root = inflater.inflate(R.layout.fragment_nearby_stops, container, false) + gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView) + gridLayoutManager = AutoFitGridLayoutManager( + requireContext().getApplicationContext(), + utils.convertDipToPixels(getContext(), COLUMN_WIDTH_DP.toFloat()).toInt() + ) + gridRecyclerView.setLayoutManager(gridLayoutManager) + gridRecyclerView.setHasFixedSize(false) + circlingProgressBar = root.findViewById(R.id.circularProgressBar) + flatProgressBar = root.findViewById(R.id.horizontalProgressBar) + messageTextView = root.findViewById(R.id.messageTextView) + titleTextView = root.findViewById(R.id.titleTextView) + loadingTextView = root.findViewById(R.id.positionLoadingTextView) + switchButton = root.findViewById(R.id.switchButton) + + scrollListener = CommonScrollListener(mListener, false) + switchButton!!.setOnClickListener(View.OnClickListener { v: View? -> switchFragmentType() }) + if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "onCreateView") + + val appContext = requireContext().applicationContext + DatabaseUpdate.watchUpdateWorkStatus(context, this){ workInfos -> + if (workInfos.isEmpty()) { + viewModel.setDBUpdateRunning(false) + return@watchUpdateWorkStatus + } + + val wi = workInfos.get(0) + if (wi.state == WorkInfo.State.RUNNING && locationProvider!!.isRunning()) { + locationProvider!!.stopUpdates() + viewModel.setDBUpdateRunning(true) + } else { + //start the request + if (bothLocationPermissionsGranted(requireContext())) { + if (!locationProvider!!.isRunning()) { + startLocationUpdatesByType() + } + } else { + setShowingStatus(LocationShowingStatus.NO_PERMISSION) + } + + viewModel.setDBUpdateRunning(false) + //actually restart request + } + } + + + if (anyLocationPermissionsGranted(appContext)) { + setShowingStatus(LocationShowingStatus.SEARCHING) + } else { + setShowingStatus(LocationShowingStatus.NO_PERMISSION) + } + //add location listener + locationProvider!!.addListener(locationUpdateListener) + + return root + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + gridRecyclerView.setVisibility(View.INVISIBLE) + gridRecyclerView.addOnScrollListener(scrollListener!!) + mListener?.readyGUIfor(FragmentKind.NEARBY_STOPS) + + //observe the livedata + viewModel.stopsAtDistance.observe(getViewLifecycleOwner()) {stops -> + Log.d(DEBUG_TAG, "Received " + stops.size + " stops nearby") + var distance = viewModel.distanceMtLiveData.getValue() + if (distance == null) { + distance = 40 + } + if ((stops.size < stopsMinNumber && distance <= stopsMaxDistance)) { + viewModel.setDistance(distance + 40) + //viewModel.requestStopsAtDistance(distance, true); + //Log.d(DEBUG_TAG, "Doubling distance now!"); + return@observe // THIS WORKS AS AN `else` + } + if (!stops.isEmpty()) { + currentNearbyStops = stops + showStopsInViews(currentNearbyStops, lastPosition) + } + } + + viewModel.downloadingArrivals.observe(viewLifecycleOwner){ running -> + if(!running) flatProgressBar.visibility = View.GONE + else flatProgressBar.visibility = View.VISIBLE + } + viewModel.progressPerc.observe(viewLifecycleOwner){ progress -> + flatProgressBar.isIndeterminate = false + flatProgressBar.progress = progress + flatProgressBar.max = 100 + + if (progress<100){ + flatProgressBar.visibility = View.VISIBLE + } + + } + + viewModel.arrivalsDecoupled.observe(viewLifecycleOwner){ stoprouteList -> + if (getContext() == null) { + Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached") + return@observe + } + if (firstLocForArrivals) { + arrivalsStopAdapter = ArrivalsStopAdapter(stoprouteList, mListener, getContext(), lastPosition!!) + gridRecyclerView.setAdapter(arrivalsStopAdapter) + firstLocForArrivals = false + } else { + arrivalsStopAdapter!!.setRoutesPairListAndPosition(stoprouteList) + } + + //arrivalsStopAdapter.notifyDataSetChanged(); + showRecyclerHidingLoadMessage() + if (mListener != null) mListener!!.readyGUIfor(FragmentKind.NEARBY_ARRIVALS) + } + } + + + /** + * Internal bit used to start location updates + */ + private fun startLocationUpdatesByType() { + when (fragment_type) { + FragType.STOPS -> locationProvider!!.startUpdates(locationOptionsStops) + FragType.ARRIVALS -> locationProvider!!.startUpdates(locationOptionsArrivals) + } + } + + + /** + * Use this method to set the fragment type + * @param type the type, TYPE_ARRIVALS or TYPE_STOPS + */ + private fun setFragmentType(type: FragType) { + val isChanged = fragment_type != type + this.fragment_type = type + /*switch(type){ + case ARRIVALS: + TIME_INTERVAL_REQUESTS = 5*1000; + break; + case STOPS: + TIME_INTERVAL_REQUESTS = 1000; + + } + + */ + if (isChanged) { + startLocationUpdatesByType() + setShowingStatus(LocationShowingStatus.SEARCHING) + } + } + + /** + * Set the location in the view model if it is good + * @param location new location + */ + private fun updateLocationViewModel(location: Location, accuracy: Float = 150f) { + if (location.getAccuracy() < accuracy) { + lastPosition = GPSPoint(location.getLatitude(), location.getLongitude()) + //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); + viewModel.setLastLocation(location) + } + } + + private fun setShowingStatus(newStatus: LocationShowingStatus) { + var newStatus = newStatus + if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Asked to set showing status : $newStatus") + if (newStatus == showingStatus) { + return + } + if (!isLocationEnabled && newStatus != LocationShowingStatus.NO_PERMISSION) { + Log.d(DEBUG_TAG, "asked to show status: $newStatus but the position is disabled") + newStatus = LocationShowingStatus.DISABLED + } + + when (newStatus) { + LocationShowingStatus.FIRST_FIX -> { + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + gridRecyclerView.setVisibility(View.VISIBLE) + messageTextView!!.setVisibility(View.GONE) + } + + LocationShowingStatus.NO_PERMISSION -> { + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + messageTextView!!.setText(R.string.enable_position_message_nearby) + messageTextView!!.setVisibility(View.VISIBLE) + } + + LocationShowingStatus.DISABLED -> { + //if (showingStatus== LocationShowingStatus.SEARCHING){ + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + //} + messageTextView!!.setText(R.string.enable_location_message) + messageTextView!!.setVisibility(View.VISIBLE) + } + + LocationShowingStatus.SEARCHING -> { + circlingProgressBar!!.setVisibility(View.VISIBLE) + loadingTextView!!.setVisibility(View.VISIBLE) + gridRecyclerView.setVisibility(View.GONE) + messageTextView!!.setVisibility(View.GONE) + } + } + showingStatus = newStatus + } + + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is FragmentListenerMain) { + mListener = context as FragmentListenerMain + } else { + throw RuntimeException( + context + .toString() + " must implement OnFragmentInteractionListener" + ) + } + Log.d(DEBUG_TAG, "OnAttach called") + //viewModel = ViewModelProvider(this).get(NearbyStopsViewModel::class.java) + } + + override fun onPause() { + super.onPause() + + //gridRecyclerView.setAdapter(null) + Log.d(DEBUG_TAG, "On paused called") + + locationProvider!!.stopUpdates() + } + + override fun onResume() { + super.onResume() + //fix view if we were showing the stops or the arrivals + prepareForFragmentType() + when (fragment_type) { + FragType.STOPS -> if (dataAdapter != null) { + //gridRecyclerView.setAdapter(dataAdapter); + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + } + + FragType.ARRIVALS -> if (arrivalsStopAdapter != null) { + //gridRecyclerView.setAdapter(arrivalsStopAdapter); + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + } + } + + mListener!!.enableRefreshLayout(false) + Log.d(DEBUG_TAG, "OnResume called") + if (getContext() == null) { + Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now") + stopsMinNumber = 5 + stopsMaxDistance = 600 + return + } + //Re-read preferences + val shpr = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext()) + //For some reason, they are all saved as strings + stopsMaxDistance = shpr.getInt(getString(R.string.pref_key_radius_recents), 600) + var isMinStopInt = true + try { + stopsMinNumber = shpr.getInt(getString(R.string.pref_key_num_recents), 5) + } catch (ex: ClassCastException) { + isMinStopInt = false + } + if (!isMinStopInt) try { + stopsMinNumber = shpr.getString(getString(R.string.pref_key_num_recents), "5")!!.toInt() + } catch (ex: NumberFormatException) { + stopsMinNumber = 5 + } + if (BuildConfig.DEBUG) Log.d( + DEBUG_TAG, + "Max distance for stops: $stopsMaxDistance, Min number of stops: $stopsMinNumber" + ) + + if (!locationProvider!!.isRunning()) { + startLocationUpdatesByType() + } + } + + + + + override fun onDetach() { + super.onDetach() + mListener = null + if (arrivalsManager != null) arrivalsManager!!.cancelAllRequests() + } + + /** + * Display the stops, or run new set of requests for arrivals + */ + private fun showStopsInViews(stops: ArrayList, location: GPSPoint?) { + if (stops.isEmpty()) { + setNoStopsLayout() + return + } + if (location == null) { + // we could do something better, but it's better to do this for now + return + } + + /*var minDistance = Double.POSITIVE_INFINITY + for (s in stops) { + minDistance = min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude())) + } + + */ + + + //quick trial to hopefully always get the stops in the correct order + Collections.sort(stops, StopSorterByDistance(location)) + when (fragment_type) { + FragType.STOPS -> showStopsInRecycler(stops) + FragType.ARRIVALS -> { + //don't do anything if we're not attached + /*context?.let{ + if (arrivalsManager == null) arrivalsManager = + NearbyArrivalsDownloader(it.applicationContext, arrivalsListener) + arrivalsManager!!.requestArrivalsForStops(stops) + } + + */ + viewModel.requestArrivalsForStops(stops) + } + } + } + + /** + * To enable targeting from the Button + */ + fun switchFragmentType(v: View?) { + switchFragmentType() + } + + /** + * Call when you need to switch the type of fragment + */ + private fun switchFragmentType() { + when (fragment_type) { + FragType.ARRIVALS -> setFragmentType(FragType.STOPS) + FragType.STOPS -> setFragmentType(FragType.ARRIVALS) + else -> {} + } + prepareForFragmentType() + //locManager.removeLocationRequestFor(fragmentLocationListener); + //locManager.addLocationRequestFor(fragmentLocationListener); + if (lastPosition != null) { + // we have at least one fix on the position + showStopsInViews(currentNearbyStops, lastPosition) + } + } + + /** + * Prepare the views for the set fragment type + */ + private fun prepareForFragmentType() { + if (fragment_type == FragType.STOPS) { + switchButton!!.setText(getString(R.string.show_arrivals)) + titleTextView!!.setText(getString(R.string.nearby_stops_message)) + if (arrivalsManager != null) arrivalsManager!!.cancelAllRequests() + if (dataAdapter != null) gridRecyclerView!!.setAdapter(dataAdapter) + } else if (fragment_type == FragType.ARRIVALS) { + titleTextView!!.setText(getString(R.string.nearby_arrivals_message)) + switchButton!!.setText(getString(R.string.show_stops)) + if (arrivalsStopAdapter != null) gridRecyclerView!!.setAdapter(arrivalsStopAdapter) + } + } + + //useful methods + /**//// GUI METHODS //////// */ + private fun showStopsInRecycler(stops: MutableList?) { + if (firstLocForStops) { + dataAdapter = SquareStopAdapter(stops, mListener, lastPosition) + gridRecyclerView!!.setAdapter(dataAdapter) + firstLocForStops = false + } else { + dataAdapter!!.setStops(stops) + dataAdapter!!.setUserPosition(lastPosition) + } + dataAdapter!!.notifyDataSetChanged() + + //showRecyclerHidingLoadMessage(); + if (gridRecyclerView!!.getVisibility() != View.VISIBLE) { + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + gridRecyclerView!!.setVisibility(View.VISIBLE) + } + messageTextView!!.setVisibility(View.GONE) + + if (mListener != null) mListener!!.readyGUIfor(FragmentKind.NEARBY_STOPS) + } + + private fun showArrivalsInRecycler(routesPairList: List>) { + + + } + + private fun setNoStopsLayout() { + messageTextView!!.setVisibility(View.VISIBLE) + messageTextView!!.setText(R.string.no_stops_nearby) + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + } + + /** + * Does exactly what is says on the tin + */ + private fun showRecyclerHidingLoadMessage() { + if (gridRecyclerView.getVisibility() != View.VISIBLE) { + circlingProgressBar!!.setVisibility(View.GONE) + loadingTextView!!.setVisibility(View.GONE) + gridRecyclerView.setVisibility(View.VISIBLE) + } + messageTextView!!.setVisibility(View.GONE) + } /* + * Local locationListener, to use for the GPS + */ + /* + class FragmentLocationListener implements LocationListenerCompat { + + private long lastUpdateTime = -1; + public boolean isRegistered = false; + + @Override + public void onLocationChanged(@NonNull Location location) { + if(viewModel==null){ + return; + } + if(location.getAccuracy()<200) { + + lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); + //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); + viewModel.setLastLocation(location); + } + lastUpdateTime = System.currentTimeMillis(); + //Log.d("BusTO:NearPositListen","can start request for stops: "+ !dbUpdateRunning); + } + + @Override + public void onProviderEnabled(@NonNull String provider) { + Log.d(DEBUG_TAG, "Location provider "+provider+" enabled"); + if(provider.equals(LocationManager.GPS_PROVIDER)){ + setShowingStatus(LocationShowingStatus.SEARCHING); + } + } + + @Override + public void onProviderDisabled(@NonNull String provider) { + Log.d(DEBUG_TAG, "Location provider "+provider+" disabled"); + if(provider.equals(LocationManager.GPS_PROVIDER)) { + setShowingStatus(LocationShowingStatus.DISABLED); + } + } + + @Override + public void onStatusChanged(@NonNull String provider, int status, @Nullable Bundle extras) { + LocationListenerCompat.super.onStatusChanged(provider, status, extras); + } + } + + */ + + companion object { + private const val DEBUG_TAG = "NearbyStopsFragment" + private const val FRAGMENT_TYPE_KEY = "FragmentType" + const val FRAGMENT_TAG: String = "NearbyStopsFrag" + + const val COLUMN_WIDTH_DP: Int = 250 + + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * @return A new instance of fragment NearbyStopsFragment. + */ + @JvmStatic + fun newInstance(type: FragType): NearbyStopsFragment { + //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) + // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); + val fragment = NearbyStopsFragment() + val args = Bundle(1) + args.putInt(FRAGMENT_TYPE_KEY, type.num) + fragment.setArguments(args) + return fragment + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ParentFragmentManagerFromChild.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ParentFragmentManagerFromChild.kt new file mode 100644 index 0000000..f1a2697 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ParentFragmentManagerFromChild.kt @@ -0,0 +1,9 @@ +package it.reyboz.bustorino.fragments + + +interface ParentFragmentManagerFromChild { + + fun needToPopMainStackOnBack() : Boolean + + fun setMainFragmentManagerTransition(yes: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java index 7012d7d..678f663 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java +++ b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java @@ -1,77 +1,75 @@ /* BusTO (util) Copyright (C) 2019 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.util; -import android.location.Location; import androidx.core.util.Pair; import android.util.Log; import it.reyboz.bustorino.backend.*; -import java.time.ZonedDateTime; import java.util.Collections; import java.util.Comparator; import java.util.List; -public class RoutePositionSorter implements Comparator> { +public class RoutePositionSorter implements Comparator { private final double latPos, longPos; - private final double minutialmetro = 6.0/100; //v = 5km/h - private final double distancemultiplier = 2./3; + public final double MINUTI_PER_METRO = 6.0/100; //v = 5km/h + public final double DISTANCE_MULTIPLIER = 2./3; public RoutePositionSorter(double latitude, double longitude){ latPos = latitude; longPos = longitude; } public RoutePositionSorter(GPSPoint position){ this(position.getLatitude(), position.getLongitude()); } @Override - public int compare(Pair pair1, Pair pair2) throws NullPointerException{ + public int compare(RouteWithStop pair1, RouteWithStop pair2) throws NullPointerException{ int delta = 0; - final Stop stop1 = pair1.first, stop2 = pair2.first; + final Stop stop1 = pair1.getStop(), stop2 = pair2.getStop(); double dist1 = utils.measuredistanceBetween(latPos,longPos, stop1.getLatitude(),stop1.getLongitude()); double dist2 = utils.measuredistanceBetween(latPos,longPos, stop2.getLatitude(),stop2.getLongitude()); - final List passaggi1 = pair1.second.passaggi, - passaggi2 = pair2.second.passaggi; - if(passaggi1.size()<=0 || passaggi2.size()<=0){ + final List passaggi1 = pair1.getRoute().passaggi, + passaggi2 = pair2.getRoute().passaggi; + if(passaggi1.isEmpty() || passaggi2.isEmpty()){ Log.e("ArrivalsStopAdapter","Cannot compare: No arrivals in one of the stops"); } else { Collections.sort(passaggi1); Collections.sort(passaggi2); /*int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh; if(deltaOre>12) deltaOre -= 24; else if (deltaOre<-12) deltaOre += 24; delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm; */ delta = (int) passaggi1.get(0).getDifferenceMinutes(passaggi2.get(0)); } - delta += (int)((dist1 -dist2)*minutialmetro*distancemultiplier); + delta += (int)((dist1 -dist2)* MINUTI_PER_METRO * DISTANCE_MULTIPLIER); return delta; } @Override public boolean equals(Object obj) { return obj instanceof RoutePositionSorter; } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt index fc182cb..4eb4a5e 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt @@ -1,156 +1,252 @@ /* BusTO - View Model 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.viewmodels import android.app.Application import android.location.Location import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map import it.reyboz.bustorino.BuildConfig -import it.reyboz.bustorino.backend.GPSPoint -import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.OldDataRepository -import java.util.ArrayList +import it.reyboz.bustorino.fragments.NearbyArrivalsDownloader +import it.reyboz.bustorino.util.StopSorterByDistance +import java.util.* import java.util.concurrent.Executors class NearbyStopsViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, application) + val arrivalsNearby = MutableLiveData>() + + val progressPerc = MutableLiveData() + + val downloadingArrivals = MutableLiveData() + private val arrivalsListener = object : NearbyArrivalsDownloader.ArrivalsListener { + override fun setProgress(completedRequests: Int, pendingRequests: Int) { + val totalReq = completedRequests + pendingRequests + progressPerc.postValue( (completedRequests * 100) / totalReq ) + + if(pendingRequests == 0) + downloadingArrivals.postValue(false) + } + + override fun onAllRequestsCancelled() { + downloadingArrivals.postValue(false) + } + + override fun showCompletedArrivals(completedPalinas: ArrayList) { + arrivalsNearby.postValue(completedPalinas) + } + + } + + private val nearbyArrivalsDownloader = NearbyArrivalsDownloader(application,arrivalsListener ) + + fun requestArrivalsForStops(stops: List) { + nearbyArrivalsDownloader.requestArrivalsForStops(stops) + } val locationLiveData = MutableLiveData() val distanceMtLiveData = MutableLiveData(40) val stopsAtDistance = MediatorLiveData>() private val dbUpdateRunning = MutableLiveData(false) private val callback = OldDataRepository.Callback> { res -> if(res.isSuccess){ stopsAtDistance.postValue(res.result) if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Setting value of stops in bounding box") } } fun setLastLocation(location: Location) { locationLiveData.value = GPSPoint(location.latitude, location.longitude) } fun setDistance(distance: Int) { distanceMtLiveData.value = distance } fun setDBUpdateRunning(running: Boolean) { dbUpdateRunning.value = (running) } /** * Request stop in location [latitude], [longitude], at distance [distanceMeters] * If [saveValues] is true, store the position and the distance used */ fun requestStopsAtDistance(latitude: Double, longitude: Double, distanceMeters: Int, saveValues: Boolean){ if(saveValues){ locationLiveData.postValue(GPSPoint(latitude, longitude)) distanceMtLiveData.postValue(distanceMeters) } oldRepo.requestStopsWithinDistance(latitude, longitude, distanceMeters, callback) } /** * Request stops using the previously saved location */ fun requestStopsAtDistance(distanceMeters: Int, saveValue: Boolean){ if(saveValue){ distanceMtLiveData.postValue(distanceMeters) } oldRepo.requestStopsWithinDistance( locationLiveData.value!!.latitude, locationLiveData.value!!.longitude, distanceMeters, callback) } fun requestStopsCheckDBRunning(position: GPSPoint, distanceMt: Int){ if(dbUpdateRunning.value==null || !(dbUpdateRunning.value!!)){ oldRepo.requestStopsWithinDistance(position.latitude, position.longitude, distanceMt, callback) } else{ Log.d(DEBUG_TAG, "Database update is running, cannot do it") } } fun postLocation(location: Location){ locationLiveData.postValue(GPSPoint(location.latitude, location.longitude)) } fun postLocation(location: GPSPoint){ locationLiveData.postValue(location) } fun postLastDistance(distanceMeters: Int){ distanceMtLiveData.postValue(distanceMeters) } init { stopsAtDistance.addSource(locationLiveData){ point-> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "New location: $point") val distance = distanceMtLiveData.value ?: 40 //oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, distance, callback) requestStopsCheckDBRunning(point, distance) } stopsAtDistance.addSource(distanceMtLiveData){ dist-> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "New distance: $dist") if(locationLiveData.value != null){ val point: GPSPoint = locationLiveData.value!! //oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, dist, callback) requestStopsCheckDBRunning(point, dist) } else{ Log.d(DEBUG_TAG, "Modified distance but locationLiveData value is null") } } stopsAtDistance.addSource(dbUpdateRunning){ running -> if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "DB update running: $running") if(!running) { reRequestStops() } } } private fun reRequestStops(){ var req = false locationLiveData.value?.let{ point -> distanceMtLiveData.value ?.let { dist-> req = true oldRepo.requestStopsWithinDistance(point.latitude, point.longitude, dist, callback) } } if(!req){ Log.w(DEBUG_TAG, "Requested to rerun stops, but position or distance (or both) are null") } } + val arrivalsDecoupled = arrivalsNearby.map { palinas -> + locationLiveData.value?.let { loc -> + Collections.sort(palinas, StopSorterByDistance(loc)) + } + + var routesPairList = ArrayList(10) + //int maxNum = Math.min(MAX_STOPS, stopList.size()); + for (p in palinas) { + //if there are no routes available, skip stop + if (p.queryAllRoutes().isEmpty()) continue + for (r in p.queryAllRoutes()) { + //if there are no routes, should not do anything + if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(RouteWithStop(p, r)) + } + } + + val pos = locationLiveData.value + if(pos != null) { + routesPairList.sortWith { p1, p2 -> + comparePairsRoutesArrivals(p1,p2,pos) + } + } + routesPairList + } + + fun comparePairsRoutesArrivals(pair1: RouteWithStop, pair2: RouteWithStop, pos: GPSPoint) : Int{ + var delta = 0 + val stop1: Stop = pair1.stop + val stop2: Stop = pair2.stop + + val dist1 = utils.measuredistanceBetween( + pos.latitude, pos.longitude, + stop1.getLatitude()!!, stop1.getLongitude()!! + ) + val dist2 = utils.measuredistanceBetween( + pos.latitude, pos.longitude, + stop2.getLatitude()!!, stop2.getLongitude()!! + ) + val passaggi1 = pair1.route.passaggi + val passaggi2 = pair2.route.passaggi + if (passaggi1.size <= 0 || passaggi2.size <= 0) { + Log.e("ArrivalsStopAdapter", "Cannot compare: No arrivals in one of the stops") + } else { + Collections.sort(passaggi1) + Collections.sort(passaggi2) + + /*int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh; + if(deltaOre>12) + deltaOre -= 24; + else if (deltaOre<-12) + deltaOre += 24; + delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm; + + */ + delta = passaggi1[0]!!.getDifferenceMinutes(passaggi2[0]!!).toInt() + } + delta += ((dist1 - dist2) * MINUTI_PER_METRO * DISTANCE_MULTIPLIER).toInt() + + return delta + + } + companion object{ private const val DEBUG_TAG = "BusTO-NearbyStopVwModel" + + const val MINUTI_PER_METRO: Double = 6.0 / 100 //v = 5km/h + const val DISTANCE_MULTIPLIER: Double = 2.0 / 3 } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml index 33c749c..4c96a87 100644 --- a/app/src/main/res/layout/fragment_main_screen.xml +++ b/app/src/main/res/layout/fragment_main_screen.xml @@ -1,152 +1,152 @@ - + + - - + - - + android:layout_height="match_parent" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:animateLayoutChanges="true" + android:visibility="gone"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nearby_stops.xml b/app/src/main/res/layout/fragment_nearby_stops.xml index f858094..4fc237a 100644 --- a/app/src/main/res/layout/fragment_nearby_stops.xml +++ b/app/src/main/res/layout/fragment_nearby_stops.xml @@ -1,99 +1,100 @@ + android:layout_below="@+id/titleTextView" + android:layout_centerHorizontal="true"/>