diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index c4ee229..e90e6df 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,896 +1,928 @@ /* 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 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_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 - if (vl.equals("map")) { - requestMapFragment(false); - } else if (vl.equals("favorites")) { - checkAndShowFavoritesFragment(framan, false); - } else if (vl.equals("lines")) { - showLinesFragment(framan, false, null); - } else { - showMainFragmentFromClick(false); + 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); } } 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 -> { - if (menuItem.getItemId() == R.id.drawer_action_settings) { - Log.d("MAINBusTO", "Pressed button preferences"); - closeDrawerIfOpen(); - startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); - return true; - } else if(menuItem.getItemId() == R.id.nav_favorites_item){ - closeDrawerIfOpen(); - //get Fragment - checkAndShowFavoritesFragment(getSupportFragmentManager(), true); - return true; - } else if(menuItem.getItemId() == R.id.nav_arrivals){ - closeDrawerIfOpen(); - showMainFragmentFromClick(true); - return true; - } else if(menuItem.getItemId() == R.id.nav_map_item){ + int menuId = menuItem.getItemId(); + if( menuActions.containsKey(menuId)){ closeDrawerIfOpen(); - requestMapFragment(true); - return true; - } else if (menuItem.getItemId() == R.id.nav_lines_item) { - closeDrawerIfOpen(); - showLinesFragment(getSupportFragmentManager(), true,null); - return true; - } else if(menuItem.getItemId() == R.id.drawer_action_info) { - closeDrawerIfOpen(); - startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); + var runnable = menuActions.get(menuId); + if(runnable!=null) runnable.run(); return true; + } else{ + return false; } - //selectDrawerItem(menuItem); - Log.d(DEBUG_TAG, "pressed item "+menuItem); - - return true; - }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } //requesting permissions @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); - - if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } /*@Override public void onBackPressed() { if (!activityCustomBackPressed()) super.onBackPressed(); } */ private boolean activityCustomBackPressed(){ boolean resolved = true; - Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); + var mainFragManager = getSupportFragmentManager(); + Fragment shownFrag = mainFragManager.findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ - //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStack(); - if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ + if(showingMainFragmentFromOther && mainFragManager.getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - getSupportFragmentManager().popBackStack(); - Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); + 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); + 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); 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 showMainFragmentFromClick(boolean addToBackStack){ - showMainFragmentFromClick(MainScreenFragment.makeArgsButtonsScreen(), addToBackStack); + private void showHomeMainFragmentFromClick(boolean addToBackStack){ + FragmentManager fraMan = getSupportFragmentManager(); + var fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if(fragment instanceof MainScreenFragment mainFrag){ + if(!mainFrag.isVisible()){ + showMainFragment(fraMan, mainFrag, addToBackStack); + } + mainFrag.showButtonsFragmentIfNotNearby(addToBackStack); + } else{ + createShowMainFragment(fraMan, MainScreenFragment.makeArgsButtonsScreen(), addToBackStack); + } } 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) { - //TODO + 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_arrivals); + 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_arrivals); + 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: - case HOME_BUTTONS: titleResId=R.string.app_name_full; - mNavView.setCheckedItem(R.id.nav_arrivals); + 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); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); - showingMainFragmentFromOther = (probableFragment==null); - if (showingMainFragmentFromOther){ + // 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); } - mNavView.setCheckedItem(R.id.nav_arrivals); + mNavView.setCheckedItem(R.id.nav_home); } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ - readyGUIfor(FragmentKind.LINES); - FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); + } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { - readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); tr.addToBackStack("LineFromOther-"+routeGtfsId); tr.commit(); } @Override public void openNearbyStopsFragment() { FragmentManager fraMan = getSupportFragmentManager(); var fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if(fragment instanceof MainScreenFragment mainFrag){ if(!mainFrag.isVisible()){ - showMainFragment(fraMan, mainFrag, false); + 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); } } @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/RecyclerViewMargin.java b/app/src/main/java/it/reyboz/bustorino/adapters/RecyclerViewMargin.java index d5a38b7..d765c39 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/RecyclerViewMargin.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RecyclerViewMargin.java @@ -1,145 +1,153 @@ /* BusTO - UI components Copyright (C) 2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.adapters; import android.content.Context; import android.graphics.Rect; import android.util.Log; import android.view.View; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.utils; // based on the answer at https://stackoverflow.com/questions/37507937/margin-between-items-in-recycler-view-android /** * Recycler view margin setter for the elements. If you call "addExternal", it will use the same margins on bordering elements * towards the border (i.e., applying the margin on top for the first row, on right for the last columns, etc.) */ public class RecyclerViewMargin extends RecyclerView.ItemDecoration { private final int margin; private final int columns; private boolean addExternal = false; private static final String DEBUG_TAG = "BusTO-RecViewMargin"; /** * constructor * @param marginPx desirable margin size in px between the views in the recyclerView * @param numColumns number of numColumns of the RecyclerView */ public RecyclerViewMargin(@IntRange(from=0)int marginPx , @IntRange(from=0) int numColumns ) { this.margin = marginPx; this.columns=numColumns; } public static RecyclerViewMargin makeMarginsDip(@NonNull Context context, @IntRange(from=0)int marginDip , @IntRange(from=0) int numColumns) { return new RecyclerViewMargin(utils.convertDipToPixelInt(context, marginDip), numColumns); } public RecyclerViewMargin addExternal(){ addExternal = true; return this; } /** * Set different margins for the items inside the recyclerView: no top margin for the first row * and no left margin for the first column. */ @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { var adapter = parent.getAdapter(); int nrows = adapter!=null ? (int)Math.ceil( (double) adapter.getItemCount() / columns) : -2; int position = parent.getChildLayoutPosition(view); if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "getItemOffsets: position = " + position); var sb = new StringBuilder(); //set right margin to all if(position % columns != columns-1){ outRect.right = margin; sb.append("right "); } + if(position % columns != 0){ + outRect.left = margin; + sb.append("left "); + } + if (position >= columns){ + outRect.top = margin; + sb.append("top "); + } int row = (int)((double) position / columns) ; if(nrows == -2 || row < nrows-1){ outRect.bottom = margin; sb.append("bottom "); } /* //set right margin to all outRect.right = margin; //set bottom margin to all outRect.bottom = margin; //we only add top margin to the first row */ if(addExternal){ if (position = columns){ outRect.top = margin; sb.append("top "); } int row = (int)((double) position / columns) ; if(nrows == -2 || row < nrows-1){ outRect.bottom = margin; sb.append("bottom "); } Log.d(DEBUG_TAG, "margins put: " + sb.toString()); */ \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt index d12e0e8..83470b9 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt @@ -1,64 +1,57 @@ package it.reyboz.bustorino.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.cardview.widget.CardView import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.FiveTNormalizer -import it.reyboz.bustorino.backend.Palina -import java.lang.ref.WeakReference class RouteOnlyLineAdapter (val routeNames: List, onItemClick: OnClick?) : RecyclerView.Adapter() { - private val clickreference: WeakReference? - init { - clickreference = if(onItemClick!=null) WeakReference(onItemClick) else null - } + private val clickreference = onItemClick + /** * Provide a reference to the type of views that you are using * (custom ViewHolder) */ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val textView: TextView + val textView: TextView = view.findViewById(R.id.routeBallID) + val cardView: CardView = view.findViewById(R.id.headerCardView) - init { - // Define click listener for the ViewHolder's View - textView = view.findViewById(R.id.routeBallID) - } } - constructor(palina: Palina, showOnlyEmpty: Boolean): this(palina.routesNamesWithNoPassages, null) // Create new views (invoked by the layout manager) override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { // Create a new view, which defines the UI of the list item val view = LayoutInflater.from(viewGroup.context) .inflate(R.layout.round_line_header, viewGroup, false) return ViewHolder(view) } // Replace the contents of a view (invoked by the layout manager) override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { // Get element from your dataset at this position and replace the // contents of the view with that element // SHOW "STAR" as "ST" viewHolder.textView.text = FiveTNormalizer.filterFullStarName(routeNames[position]) - viewHolder.itemView.setOnClickListener{ - clickreference?.get()?.onItemClick(position, routeNames[position]) + viewHolder.cardView.setOnClickListener{ + clickreference?.onItemClick(position, routeNames[position]) } } // Return the size of your dataset (invoked by the layout manager) override fun getItemCount() = routeNames.size fun interface OnClick{ fun onItemClick(index: Int, name: String) } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java index 4031fb5..8ecafd5 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java @@ -1,546 +1,550 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi Copyright (c) 2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.util.LinesNameSorter; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop implements Parcelable { private ArrayList routes = new ArrayList<>(); // the routes with arrival times private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), s.gtfsID); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { super(ID, name, userName, location, null, null, lat, lon, gtfsID); } public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { super(name, ID, location, type, routesThatStopHere); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; buildRoutesString(); return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } /** * Remove all arrivals from this Palina */ public void clearRoutes(){ routes.clear(); } /** * Check how many routes (from arrival times) we have * @return the number of routes */ public int getNumRoutesWithArrivals(){ return routes.size(); } @Nullable @Override protected String buildRoutesString() { // no routes => no string if(routes == null || routes.size() == 0) { return ""; } /*final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); int i, lenMinusOne = routes.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routes.get(i).getName().trim()).append(", "); } // last one: sb.append(routes.get(i).getName()); */ ArrayList names = new ArrayList<>(); for (Route r: routes){ names.add(r.getName()); } final String routesThatStopHere = buildRoutesStringFromNames(names); setRoutesThatStopHereString(routesThatStopHere); return routesThatStopHereToString(); } /** * Sort the names of the routes for the string "routes stopping here" and make the string * @param names of the Routes that pass in the stop * @return the full string of routes stopping (eg, "10, 13, 42" ecc) */ public static String buildRoutesStringFromNames(List names){ final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(names, nameSorter); int i, lenMinusOne = names.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(names.get(i).trim()).append(", "); } //last one sb.append(names.get(i).trim()); return sb.toString(); } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i0) min = Math.min(min,r.numPassaggi()); } if (min == Integer.MAX_VALUE) return 0; else return min; } public ArrayList getRoutesNamesWithNoPassages(){ ArrayList mList = new ArrayList<>(); if(routes==null || routes.size() == 0){ return mList; } for(Route r: routes){ if(r.numPassaggi()==0) mList.add(r.getDisplayCode()); } return mList; } + public List getRoutesWithNoPassages(){ + return routes.stream().filter(r -> r.numPassaggi()==0).toList(); + } + private static String pick(String a, String b) { return (a != null && !a.isEmpty()) ? a : b; } /** * Merge two Palinas, including information from both * @param p1 the first one, which has priority * @param p2 the second one * @return the merged Palina data */ public static @Nullable Palina mergePaline(@Nullable Palina p1, @Nullable Palina p2) { if (p1 == null) return p2; if (p2 == null) return p1; // --- Campi base (Stop) --- String id = p1.ID; // assumiamo stesso ID String name = pick(p1.getStopDefaultName(), p2.getStopDefaultName()); String userName = pick(p1.getStopUserName(), p2.getStopUserName()); String location = pick(p1.location, p2.location); Double lat = p1.getLatitude() != null ? p1.getLatitude() : p2.getLatitude(); Double lon = p1.getLongitude() != null ? p1.getLongitude() : p2.getLongitude(); String gtfsID = pick(p1.gtfsID, p2.gtfsID); Palina result = new Palina(id, name, userName, location, lat, lon, gtfsID); // --- Routes --- List mergedRoutes = new ArrayList<>(); boolean addFromSecond = false; if (p1.queryAllRoutes() != null) mergedRoutes.addAll(p1.routes); else if (p2.queryAllRoutes() != null) mergedRoutes.addAll(p2.routes); else { //assume the first one has more important imformation mergedRoutes.addAll(p1.routes); addFromSecond = true; } result.setRoutes(mergedRoutes); if(addFromSecond){ result.addInfoFromRoutes(p2.routes); } // Unisci eventuali duplicati (stesso routeID) result.mergeDuplicateRoutes(0); // Aggiorna stringa routes result.buildRoutesString(); return result; } /// ------- Parcelable stuff --- protected Palina(Parcel in) { super(in); routes = in.createTypedArrayList(Route.CREATOR); routesModified = in.readByte() != 0; allSource = in.readByte() == 0 ? null : Passaggio.Source.valueOf(in.readString()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeTypedList(routes); dest.writeByte((byte) (routesModified ? 1 : 0)); if (allSource == null) { dest.writeByte((byte) 0); } else { dest.writeByte((byte) 1); dest.writeString(allSource.name()); } } public static final Creator CREATOR = new Creator() { @Override public Palina createFromParcel(Parcel in) { return new Palina(in); } @Override public Palina[] newArray(int size) { return new Palina[size]; } }; @Override public int describeContents() { return 0; } // Methods using the parcelable public byte[] asByteArray(){ final Parcel p = Parcel.obtain(); writeToParcel(p,0); final byte[] b = p.marshall(); p.recycle(); return b; } public static Palina fromByteArray(byte[] data){ final Parcel p = Parcel.obtain(); p.unmarshall(data, 0, data.length); p.setDataPosition(0); final Palina palina = Palina.CREATOR.createFromParcel(p); p.recycle(); return palina; } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt index 6b7854d..fcca08b 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -1,843 +1,858 @@ /* BusTO - Fragments components - Copyright (C) 2018 Fabio Mazza + Copyright (C) 2018-2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.database.Cursor import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener +import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.middleware.CoroutineFavoriteAction import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.viewmodels.ArrivalsViewModel import java.util.* class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { private var DEBUG_TAG = DEBUG_TAG_ALL private lateinit var stopID: String //private set private var stopName: String? = null private var prefs: DBStatusManager? = null private var listener: OnDBUpdateStatusChangeListener? = null private var justCreated = false private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false //Views protected lateinit var addToFavorites: ImageButton protected lateinit var openInMapButton: ImageButton protected lateinit var arrivalsSourceTextView: TextView private lateinit var messageTextView: TextView private lateinit var preMessageTextView: TextView // this hold the "Arrivals at: " text protected lateinit var arrivalsRecyclerView: RecyclerView private var mListAdapter: PalinaAdapter? = null private lateinit var resultsLayout : LinearLayout private lateinit var loadingMessageTextView: TextView private lateinit var progressBar: ProgressBar private lateinit var howDoesItWorkTextView: TextView private lateinit var hideHintButton: Button //private NestedScrollView theScrollView; protected lateinit var noArrivalsRecyclerView: RecyclerView private var noArrivalsAdapter: RouteOnlyLineAdapter? = null private var noArrivalsTitleView: TextView? = null private var layoutManager: GridLayoutManager? = null //private View canaryEndView; private var fetchers: List = ArrayList() private val arrivalsViewModel : ArrivalsViewModel by viewModels() - private var reloadOnResume = true + private var routesNoPassages = listOf() fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName = route.routeLongDisplayName Log.d(DEBUG_TAG, "Make toast for line " + route.name) if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { - Log.d( - DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" - ) - if (route.gtfsId != null) { - mListener.openLineFromStop(route.gtfsId, stopID) - } else { - val gtfsID = FiveTNormalizer.getGtfsRouteID(route) - Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") - mListener.openLineFromStop(gtfsID, stopID) - } + showRoutesInLinesFragment(route) } } + private fun showRoutesInLinesFragment(route: Route) { + Log.d( + DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" + ) + if (route.gtfsId != null) { + mListener.openLineFromStop(route.gtfsId, stopID) + } else { + val gtfsID = FiveTNormalizer.getGtfsRouteID(route) + Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") + mListener.openLineFromStop(gtfsID, stopID) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID arrivalsViewModel.setStopId(stopID) //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) preMessageTextView = root.findViewById(R.id.arrivalsTextView) addToFavorites = root.findViewById(R.id.addToFavorites) openInMapButton = root.findViewById(R.id.openInMapButton) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) arrivalsSourceTextView = root.findViewById(R.id.timesSourceTextView) arrivalsSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider arrivalsSourceTextView.setText(R.string.arrival_source_changing) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } arrivalsSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites toggleStopFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages_fill), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) - layoutManager = GridLayoutManager(context, 60) + /*layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } - noArrivalsRecyclerView.setLayoutManager(layoutManager) + + */ + noArrivalsRecyclerView.setLayoutManager(getFlexLayoutManager(requireContext())) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times val tentPalina = arrivalsViewModel.palinaToShow.value if(lastUpdatedPalina == null && tentPalina != null) { //this updates lastUpdatedPalina and also shows the arrival source updateFragmentData(tentPalina) } //lastUpdatedPalina?.let { showArrivalsSources(it) } + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) arrivalsViewModel.arrivalsRequestRunningLiveData.observe(viewLifecycleOwner, { running -> //UI CHANGES TO APPLY WHEN THE REQUEST IS RUNNING mListener.toggleSpinner(running) if(running){ //different way of setting this flag if(lastUpdatedPalina == null || lastUpdatedPalina?.totalNumberOfPassages==0) { showLoadingMessageForFirstTime() } } else{ //stopped running, we can show the palina //val uname = lastUpdatedPalina?.stopDisplayName if (lastUpdatedPalina == null || lastUpdatedPalina?.numRoutesWithArrivals == 0) { //no passages and result is not valid setUIForNoStopFound() } } }) arrivalsViewModel.palinaToShow.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "New result palina observed, has coords: ${it.hasCoords()}, title ${it?.stopDisplayName}, number of passages: ${it.totalNumberOfPassages}") val palinaIsValid = it!=null && (it.totalNumberOfPassages>0 || it.stopDisplayName!=null) if (palinaIsValid){ updateFragmentData(it) } if(arrivalsViewModel.arrivalsRequestRunningLiveData.value ==false) { //finished loading if (palinaIsValid) { //the result is true hideLoadingMessageAndShowResults() } else { setUIForNoStopFound() } } } // this is only for the progress arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "Using arrivals source: $it") val srcString = getDisplayArrivalsSource(it,requireContext()) loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString) } arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res -> val src = arrivalsViewModel.sourcesLiveData.value when (res) { Fetcher.Result.OK -> {} Fetcher.Result.CLIENT_OFFLINE -> showFetcherMessage(R.string.network_error, src) Fetcher.Result.SERVER_ERROR -> { if (utils.isConnected(context)) { showFetcherMessage(R.string.parsing_error, src) } else { showFetcherMessage(R.string.network_error, src) } showFetcherMessage(R.string.internal_error,src) } Fetcher.Result.PARSER_ERROR -> showFetcherMessage(R.string.internal_error, src) Fetcher.Result.QUERY_TOO_SHORT -> showFetcherMessage(R.string.query_too_short, src) Fetcher.Result.EMPTY_RESULT_SET -> showFetcherMessage(R.string.no_arrivals_stop, src) Fetcher.Result.NOT_FOUND -> showFetcherMessage(R.string.no_bus_stop_have_this_name, src) else -> showFetcherMessage(R.string.internal_error, src) } } arrivalsViewModel.stopInFavorites.observe(viewLifecycleOwner, { isFavorite -> updateStarIcon(isFavorite) }) - return root } - private fun showShortToast(id: Int) = showToastMessage(id,true) private fun showFetcherMessage(id: Int, source: Source?){ val srcString = source?.let{ getDisplayArrivalsSource(it,requireContext())} if (srcString!=null){ Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show() } else{ val message = getString(id) Toast.makeText(requireContext(), "$srcString : $message", Toast.LENGTH_SHORT).show() } } /*private fun changeUIFirstSearchActive(yes: Boolean){ if(yes){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } else{ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } } */ private fun showLoadingMessageForFirstTime(){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } private fun hideLoadingMessageAndShowResults(){ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } private fun setUIForNoStopFound(){ progressBar.visibility=View.INVISIBLE // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times if (stopName==null) loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) else loadingMessageTextView.text = getString(R.string.no_arrivals_stop) } override fun onResume() { super.onResume() val loaderManager = loaderManager Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina") mListener.readyGUIfor(FragmentKind.ARRIVALS) //fix bug when the list adapter is null mListAdapter?.let { resetListAdapter(it) } if (noArrivalsAdapter != null) { noArrivalsRecyclerView.adapter = noArrivalsAdapter } if (stopID.isNotEmpty()) { if (!justCreated) { fetchers = utils.getDefaultArrivalsFetchers(context) adjustFetchersToSource() if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID) } else { //start first search requestArrivalsForTheFragment() showLoadingMessageForFirstTime() justCreated = false } //start the loader if (prefs!!.isDBUpdating(true)) { prefs!!.registerListener() } else { Log.d(DEBUG_TAG, "Restarting loader for stop") loaderManager.restartLoader( loaderFavId, arguments, this ) } updateMessage() } if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) { showHints() } } override fun onStart() { super.onStart() if (needUpdateOnAttach) { updateFragmentData(null) needUpdateOnAttach = false } } override fun onPause() { if (listener != null) prefs!!.unregisterListener() super.onPause() val loaderManager = loaderManager Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders()) loaderManager.destroyLoader(loaderFavId) } override fun onAttach(context: Context) { super.onAttach(context) //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context) } fun reloadsOnResume(): Boolean { return reloadOnResume } fun setReloadOnResume(reloadOnResume: Boolean) { this.reloadOnResume = reloadOnResume } // HINT "HOW TO USE" private fun showHints() { howDoesItWorkTextView.visibility = View.VISIBLE hideHintButton.visibility = View.VISIBLE //actionHelpMenuItem.setVisible(false); } private fun hideHints() { howDoesItWorkTextView.visibility = View.GONE hideHintButton.visibility = View.GONE //actionHelpMenuItem.setVisible(true); } fun onHideHint(v: View?) { hideHints() setOption(requireContext(), OPTION_SHOW_LEGEND, false) } fun getCurrentFetchersAsArray(): Array { val r= fetchers.toTypedArray() //?: emptyArray() return r } private fun rotateFetchers() { Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers") fetchers?.let { Collections.rotate(it, -1) } Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers") } /** * Update the UI with the new data * @param p the full Palina */ fun updateFragmentData(p: Palina?) { if (p != null) lastUpdatedPalina = p if (!isAdded) { //defer update at next show if (p == null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null") else needUpdateOnAttach = true } else { //set title if(stopName==null && p?.stopDisplayName != null){ stopName = p.stopDisplayName updateMessage() } val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) p?.let { //only update the sources if we have actual passaggi if (arrivalsViewModel.arrivalsRequestRunningLiveData.value == false) showArrivalsSources(lastUpdatedPalina!!) } resetListAdapter(adapter) lastUpdatedPalina?.let{ pal -> openInMapButton.setOnClickListener { if (pal.hasCoords()) mListener.showMapCenteredOnStop(pal) } } - val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages + val routesWithNoPassages = lastUpdatedPalina!!.routesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { - Collections.sort(routesWithNoPassages, LinesNameSorter()) - noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) + val sorter = LinesNameSorter() + + this.routesNoPassages = routesWithNoPassages.sortedWith{ r1, r2 -> sorter.compare(r1.displayCode, r2.displayCode) } + noArrivalsAdapter = RouteOnlyLineAdapter(routesNoPassages.map{r->r.displayCode}, ){ idx,_ -> + val route = routesNoPassages[idx] + showRoutesInLinesFragment(route) + + } noArrivalsRecyclerView.adapter = noArrivalsAdapter noArrivalsRecyclerView.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) arrivalsSourceTextView.text = base_message arrivalsSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { arrivalsSourceTextView.visibility = View.VISIBLE } else { arrivalsSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the stop title in the fragment */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { //setTextViewMessage(getString(R.string.passages_fill, message)) setTextViewMessage(message) } } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader when (id) { loaderFavId -> { builder.appendPath("favorites").appendPath(stopID) cl = CursorLoader(requireContext(), builder.build(), UserDB.FAVORITES_COLUMNS_ARRAY, null, null, null) } loaderStopId -> { builder.appendPath("stop").appendPath(stopID) cl = CursorLoader( requireContext(), builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME), null, null, null ) } else -> { cl = CursorLoader(requireContext(), builder.build(), null, null,null,null) Log.d(DEBUG_TAG, "This is probably going to crash") } } cl.setUpdateThrottle(500) return cl } override fun onLoadFinished(loader: Loader, data: Cursor) { /* when (loader.id) { loaderFavId -> { val colUserName = data.getColumnIndex(UserDB.FAVORITES_COLUMNS_ARRAY[1]) if (data.count > 0) { // IT'S IN FAVORITES data.moveToFirst() val probableName = data.getString(colUserName) stopIsInFavorites = true if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop //update the message in the textview updateMessage() } else { stopIsInFavorites = false } updateStarIcon() if (stopName == null) { //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment$tag", "Stop wasn't in the favorites and has no name, looking in the DB") loaderManager.restartLoader( loaderStopId, arguments, this ) } } loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ) if (index == -1) { Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...") } stopName = data.getString(index) updateMessage() } else { Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } } */ } override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } protected fun resetListAdapter(adapter: PalinaAdapter) { mListAdapter = adapter arrivalsRecyclerView.adapter = adapter arrivalsRecyclerView.visibility = View.VISIBLE } fun toggleStopFavorites() { val stop: Stop? = lastUpdatedPalina if (stop != null) { // toggle the status in background CoroutineFavoriteAction(requireContext().applicationContext, CoroutineFavoriteAction.Action.TOGGLE){ }.execute(stop) } else { // this case have no sense, but just immediately update the favorite icon //updateStarIconFromLastBusStop(true) Log.d(DEBUG_TAG, "Stop is null!") } } /* /** * Update the star "Add to favorite" icon */ fun updateStarIconFromLastBusStop(toggleDone: Boolean) { stopIsInFavorites = if (stopIsInFavorites) !toggleDone else toggleDone updateStarIcon() } */ /** * Update the star icon according to `stopIsInFavorites` */ fun updateStarIcon(stopIsInFavorites: Boolean) { // no favorites no party! // check if there is a last Stop if (stopID.isEmpty()) { addToFavorites.visibility = View.INVISIBLE } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled) } else { addToFavorites.setImageResource(R.drawable.ic_star_outline) } addToFavorites.visibility = View.VISIBLE } } override fun onDestroyView() { //arrivalsRecyclerView = null if (arguments != null) { requireArguments().putString(SOURCES_TEXT, arrivalsSourceTextView.text.toString()) requireArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.text.toString()) } super.onDestroyView() } override fun getBaseViewForSnackBar(): View? { return null } fun isFragmentForTheSameStop(stopID: String) : Boolean{ return if (tag != null) tag == getFragmentTag(stopID) else false } fun isFragmentForTheSameStop(p: Palina): Boolean { return isFragmentForTheSameStop(p.ID) } /** * Request arrivals in the fragment */ fun requestArrivalsForTheFragment(){ // Run with previous fetchers context?.let { mListener.toggleSpinner(true) val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" } //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray()) //val workManager = WorkManager.getInstance(it) //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest) arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray()) //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID") } } companion object { private const val OPTION_SHOW_LEGEND = "show_legend" private const val KEY_STOP_ID = "stopid" private const val KEY_STOP_NAME = "stopname" private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment" private const val loaderFavId = 2 private const val loaderStopId = 1 const val STOP_TITLE: String = "messageExtra" private const val SOURCES_TEXT = "sources_textview_message" @JvmStatic @JvmOverloads fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment { val fragment = ArrivalsFragment() val args = Bundle() args.putString(KEY_STOP_ID, stopID) //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null) { args.putString(KEY_STOP_NAME, stopName) } fragment.arguments = args return fragment } //return "palina_" + p.ID @JvmStatic fun getFragmentTag(stopID: String) = "palina_$stopID" @JvmStatic fun getFragmentTag(p: Palina) = getFragmentTag(p.ID) @JvmStatic fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID" @JvmStatic fun getDisplayArrivalsSource(source: Source, context: Context): String{ return when (source) { Source.GTTJSON -> context.getString(R.string.gttjsonfetcher) Source.FiveTAPI -> context.getString(R.string.fivetapifetcher) Source.FiveTScraper -> context.getString(R.string.fivetscraper) Source.MatoAPI -> context.getString(R.string.source_mato) Source.UNDETERMINED -> //Don't show the view context.getString(R.string.undetermined_source) } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt index 3ae55e8..8ae3d11 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt @@ -1,218 +1,238 @@ +/* + BusTO - Fragments components + Copyright (C) 2026 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ package it.reyboz.bustorino.fragments import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import it.reyboz.bustorino.ActivitySettings import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RecyclerViewMargin /** * A simple [Fragment] subclass. * Use the [ButtonsFragment.newInstance] factory method to * create an instance of this fragment. */ class ButtonsFragment : BarcodeFragment() { //private lateinit var gridLayout: GridLayout private lateinit var recyclerView: RecyclerView private var listener: CommonFragmentListener? = null private lateinit var items: List override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { } if(listener is FragmentListenerMain){ val ll = listener as FragmentListenerMain ll.enableRefreshLayout(false) } } private val marginHoriz = 30 - private val margin = 22 + private val margin = 11 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val root = inflater.inflate(R.layout.fragment_buttons, container, false) + + // this is the actual list of the buttons items = listOf( - CardMenuItem(CardAction.NEARBY, getString(R.string.nearby_message_home_card), R.drawable.compass_3_fill), + CardMenuItem(CardAction.NEARBY, getString(R.string.near_me_title), R.drawable.compass_3_fill), CardMenuItem(CardAction.MAP, getString(R.string.map), R.drawable.map), CardMenuItem(CardAction.FAVORITES_STOPS, getString(R.string.action_favorites), R.drawable.ic_star_filled_white), CardMenuItem(CardAction.LINES, getString(R.string.lines), R.drawable.ic_moving_emph), CardMenuItem(CardAction.SETTINGS, getString(R.string.action_settings), R.drawable.ic_baseline_settings_24), CardMenuItem(CardAction.QR_SCAN, getString(R.string.scan_qr_code_stop), R.drawable.qr_code_scan) ) recyclerView = root.findViewById(R.id.buttonsRecyclerView) val gridLayoutManager = GridLayoutManager(requireContext(), 2) recyclerView.layoutManager = gridLayoutManager recyclerView.adapter = ActionsCardAdapter(items, this::onCardClicked) val margins = RecyclerViewMargin.makeMarginsDip(requireContext(), margin, 2) recyclerView.addItemDecoration(margins) /*gridLayout = root.findViewById(R.id.homeGridLayout) items.forEach { item -> // Inflate base layout val cardView = LayoutInflater.from(requireContext()) .inflate(R.layout.item_card_button, gridLayout, false) // Popola icona e testo cardView.findViewById(R.id.cardIcon).setImageResource(item.iconRes) cardView.findViewById(R.id.cardLabel).text = item.label // Parametri griglia: colonna flessibile + margini cardView.layoutParams = GridLayout.LayoutParams().apply { width = 0 height = GridLayout.LayoutParams.WRAP_CONTENT columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f) setMargins(marginHoriz, marginVer, marginHoriz, marginVer) // margini tra le card } // Click cardView.setOnClickListener { onCardClicked(item) } gridLayout.addView(cardView) } */ return root } private fun onCardClicked(item: CardMenuItem) { Log.d(DEBUG_TAG, "onCardClicked - item: ${item}, listener: ${listener}") // reagisci al tap val list = listener if(list == null){ Log.w(DEBUG_TAG, "onCardClicked - listener is null") } else when(item.action) { CardAction.NEARBY -> { list.openNearbyStopsFragment() } CardAction.MAP -> { list.showMapCenteredOnStop(null)} CardAction.LINES -> { list.openLinesFragment();} CardAction.SETTINGS -> { startActivity(Intent(requireContext(), ActivitySettings::class.java)) } CardAction.FAVORITES_STOPS -> { list.openFavoritesFragment() } CardAction.QR_SCAN -> { launchBarcodeScan() } } } override fun onQrScanSuccess(busIDToSearch: String) { listener?.let { it.requestArrivalsForStopID(busIDToSearch) } ?: Log.d(DEBUG_TAG, "onQrScanSuccess - listener is null") } override fun getBaseViewForSnackBar(): View? { return null } override fun onAttach(context: Context) { super.onAttach(context) if (context is CommonFragmentListener) { listener = context Log.d(DEBUG_TAG, "onAttach") } else{ throw RuntimeException("$context must implement CommonFragmentListener") } } override fun onDetach() { listener = null Log.d(DEBUG_TAG, "onDetach") super.onDetach() } override fun onResume() { super.onResume() listener?.readyGUIfor(FragmentKind.HOME_BUTTONS) } companion object { /** * @return A new instance of fragment ButtonsFragment. */ @JvmStatic fun newInstance() = ButtonsFragment().apply { arguments = Bundle().apply { } } const val DEBUG_TAG = "BusTO-ButtonsFragment" const val FRAGMENT_TAG = "HomeButtonsFragment" } data class CardMenuItem( val action: CardAction, val label: String, val iconRes: Int ) enum class CardAction { NEARBY, MAP, FAVORITES_STOPS, LINES, SETTINGS, QR_SCAN } } class ActionsCardAdapter(val actions: List, val listener: (ButtonsFragment.CardMenuItem) -> Unit) : RecyclerView.Adapter() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_card_button, parent, false) /* // Altezza match_parent per uniformare le card della stessa riga view.layoutParams = RecyclerView.LayoutParams( RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT ) */ return ViewHolder(view) } override fun onBindViewHolder( holder: ViewHolder, position: Int ) { val item = actions[position] holder.imgView.setImageResource(item.iconRes) holder.textView.text = item.label holder.cardView.setOnClickListener { listener(item) } } override fun getItemCount() = actions.size inner class ViewHolder(val view: View): RecyclerView.ViewHolder(view) { val textView = view.findViewById(R.id.cardLabel) val imgView: ImageView = view.findViewById(R.id.cardIcon) val cardView: MaterialCardView = view.findViewById(R.id.buttonCardView) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java index b52c78c..d5476b5 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,347 +1,348 @@ /* 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.app.AlertDialog; import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import androidx.work.WorkInfo; import it.reyboz.bustorino.*; import it.reyboz.bustorino.adapters.StopAdapterListener; import it.reyboz.bustorino.adapters.StopRecyclerAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.middleware.CoroutineFavoriteAction; import it.reyboz.bustorino.viewmodels.FavoritesViewModel; public class FavoritesFragment extends ScreenBaseFragment { private RecyclerView favoriteRecyclerView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; private boolean dbUpdateRunning = false; private FavoritesViewModel model; @Nullable private CommonFragmentListener mListener; public static final String FRAGMENT_TAG = "BusTOFavFragment"; private final static String DEBUG_TAG = FRAGMENT_TAG; private final StopAdapterListener adapterListener = new StopAdapterListener() { @Override public void onTappedStop(Stop stop) { mListener.requestArrivalsForStopID(stop.ID); } @Override public boolean onLongPressOnStop(Stop stop) { Log.d("BusTO-FavoritesFrag", "LongPressOnStop"); return true; } }; public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView); //favoriteListView = root.findViewById(R.id.favoriteListView); /*favoriteRecyclerView.setOn((parent, view, position, id) -> { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ /* Stop busStop = (Stop) parent.getItemAtPosition(position); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); */ LinearLayoutManager llManager = new LinearLayoutManager(getContext()); llManager.setOrientation(LinearLayoutManager.VERTICAL); favoriteRecyclerView.setLayoutManager(llManager); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(), llManager.getOrientation()); favoriteRecyclerView.addItemDecoration(dividerItemDecoration); angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); //register for the context menu registerForContextMenu(favoriteRecyclerView); model.getFavoritesWithStop().observe(getViewLifecycleOwner(), this::showStops); // watch the DB update DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, workInfos -> { if(workInfos.isEmpty()) return; WorkInfo wi = workInfos.get(0); if(wi.getState() == WorkInfo.State.RUNNING){ dbUpdateRunning = true; } else { //force reload if it was previously running if(model!=null && dbUpdateRunning) { Log.d(DEBUG_TAG,"DB Finished updating, reload favorites"); //model.getFavorites().forceReload(); } dbUpdateRunning = false; } }); showStops(new ArrayList<>()); return root; } + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } model = new ViewModelProvider(this).get(FavoritesViewModel.class); } @Override public void onDetach() { super.onDetach(); mListener = null; } /* This method is apparently NOT CALLED ANYMORE Called on Android 6 */ @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); Log.d("Favorites Fragment", "Creating context menu "); if (v.getId() == R.id.favoritesRecyclerView) { // if we aren't attached to activity, return null if (getActivity()==null) return; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public void onResume() { super.onResume(); if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter)) return false; StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter(); Stop busStop = adapter.getStops().get(adapter.getPosition()); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new CoroutineFavoriteAction(requireContext().getApplicationContext(), CoroutineFavoriteAction.Action.REMOVE, result -> {} ).execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: if (busStop.getLatitude() == null | busStop.getLongitude() == null | mListener==null ) { Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show(); return true; } //GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude()); mListener.showMapCenteredOnStop(busStop); return true; default: return super.onContextItemSelected(item); } } @Nullable @Override public View getBaseViewForSnackBar() { return favoriteRecyclerView; } void showStops(List busStops){ // If no data is found show a friendly message if(BuildConfig.DEBUG) Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list"); if (busStops.isEmpty()) { favoriteRecyclerView.setVisibility(View.INVISIBLE); // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); //assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } else { favoriteRecyclerView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * redrwaing everything. */ // Show results favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener, StopRecyclerAdapter.Use.FAVORITES)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> { String busStopUsername = busStopNameText.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.isEmpty()) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); } } else { // changed to something // something different? if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); } } launchUpdate(busStop); }); builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> { // delete user name from database busStop.setStopUserName(null); launchUpdate(busStop); }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new CoroutineFavoriteAction(requireContext().getApplicationContext(), CoroutineFavoriteAction.Action.UPDATE, result -> { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); /*new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, result -> { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); */ } /* THIS LOOKS TERRIBLE @Override public void setSnackbarPropertiesBeforeShowing(Snackbar snackbar) { final View view = snackbar.getView(); FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams(); params.gravity = Gravity.TOP; view.setLayoutParams(params); } */ } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java index 10d6acb..15ff058 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java @@ -1,23 +1,35 @@ /* 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public enum FragmentKind { STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS, MAP, MAIN_SCREEN_FRAGMENT, - LINES, HOME_BUTTONS + LINES, HOME_BUTTONS; + + @NonNull + public static FragmentKind getSuperKind(@NonNull FragmentKind kind){ + return switch (kind) { + case STOPS, ARRIVALS, NEARBY_STOPS, NEARBY_ARRIVALS, HOME_BUTTONS -> + MAIN_SCREEN_FRAGMENT; + default -> kind; + }; + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index 82fe686..59bbe00 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,1180 +1,1185 @@ /* BusTO - Fragments 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.fragments import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences 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.* import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.gson.JsonObject import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.map.* import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.viewmodels.LinesViewModel import it.reyboz.bustorino.viewmodels.MapStateViewModel import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel import kotlinx.coroutines.Runnable import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.style.expressions.Expression import org.maplibre.android.style.layers.LineLayer import org.maplibre.android.style.layers.Property import org.maplibre.android.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP import org.maplibre.android.style.layers.PropertyFactory import org.maplibre.android.style.layers.SymbolLayer import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.geojson.Feature import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.LineString import org.maplibre.geojson.Point class LinesDetailFragment() : GeneralMapLibreFragment() { private var lineID = "" // the GTFS line ID (e.g. "gtt:10U") private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null //private var isBottomSheetShowing = false private var shouldMapLocationBeReactivated = true private var toRunWhenMapReady : Runnable? = null //private var mapInitialized = AtomicBoolean(false) //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List //private lateinit var map: MapView private var patternShown: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() private val alertsViewModel: ServiceAlertsViewModel by activityViewModels() //private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton private lateinit var lineInfoButton: ImageButton private var favoritesButton: ImageButton? = null private var locationIcon: ImageButton? = null private var isLineInFavorite = false private var appContext: Context? = null private var isLocationPermissionOK = false private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged -> if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) newFavorites?.let {favorites-> isLineInFavorite = favorites.contains(lineID) //if the button has been intialized, change the icon accordingly favoritesButton?.let { button-> //avoid crashes if fragment not attached if(context==null) return@let if(isLineInFavorite) { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()} } else { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null)) appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()} } } } } private lateinit var stopsRecyclerView: RecyclerView private lateinit var descripTextView: TextView private var stopIDFromToShow = "" private var patternIdToShow = "" //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { fragmentListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(fragmentListener == null){ Log.e(DEBUG_TAG, "Fragment listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { TODO("Not yet implemented") } } private val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops -> if(p1.pattern.directionId != p2.pattern.directionId) return@Comparator p1.pattern.directionId - p2.pattern.directionId else return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size) } //map data //style and sources are in GeneralMapLibreFragment private lateinit var polylineSource: GeoJsonSource private lateinit var polyArrowSource: GeoJsonSource private var savedCameraPosition: CameraPosition? = null private var lastStopsSizeShown = 0 //BUS POSITIONS private var enablingPositionFromClick = false private var polyline: LineString? = null //private var stopPosList = ArrayList() //fragment actions private var showOnTopOfLine = false private var recyclerInitDone = false private var usingMQTTPositions = true private var restoredCameraInMap = false //position of live markers private val tripMarkersAnimators = HashMap() //extra items to use the LibreMap override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val args = requireArguments() lineID = args.getString(LINEID_KEY,"") stopIDFromToShow = args.getString(STOPID_FROM_KEY, "") //can be null patternIdToShow = args.getString(PATTERN_SHOW_KEY, "") } @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { //reset statuses //isBottomSheetShowing = false //stopsLayerStarted = false lastStopsSizeShown = 0 mapInitialized = false val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) //lineID = requireArguments().getString(LINEID_KEY, "") arguments?.let { lineID = it.getString(LINEID_KEY, "") stopIDFromToShow = it.getString(STOPID_FROM_KEY, "") //can be null patternIdToShow = it.getString(PATTERN_SHOW_KEY, "") Log.d(DEBUG_TAG, "LineID selected: $lineID, stopIDFromToShow: $stopIDFromToShow, patternIdToShow: $patternIdToShow") } switchButton = rootView.findViewById(R.id.switchImageButton) locationIcon = rootView.findViewById(R.id.locationEnableIcon) busPositionsIconButton = rootView.findViewById(R.id.busPositionsImageButton) lineInfoButton = rootView.findViewById(R.id.lineInfoWarningButton) favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) descripTextView = rootView.findViewById(R.id.lineDescripTextView) descripTextView.visibility = View.INVISIBLE //map stuff mapView = rootView.findViewById(R.id.lineMap) mapView!!.getMapAsync(this) // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { hideStopOrBusBottomSheet() } val titleTextView = rootView.findViewById(R.id.titleTextView) titleTextView.text = getString(R.string.line)+" "+ GtfsUtils.lineNameDisplayFromGtfsID(lineID) favoritesButton?.isClickable = true favoritesButton?.setOnClickListener { if(lineID.isNotEmpty()) PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite) } val preferences = PreferencesHolder.getMainSharedPreferences(requireContext()) val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) if(favorites!=null && favorites.contains(lineID)){ favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) isLineInFavorite = true } appContext = requireContext().applicationContext preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter initializeRecyclerView() switchButton.setOnClickListener{ if(mapView?.visibility == View.VISIBLE){ hideMapAndShowStopList() } else{ hideStopListAndShowMap() } } locationIcon?.let {view -> //set click Listener view.setOnClickListener(this::switchUserLocationStatus) } busPositionsIconButton.setOnClickListener { LivePositionsDialogFragment().show(parentFragmentManager, "LivePositionsDialog") } //set + + lineInfoButton.setOnClickListener { + AlertsDialogFragment(lineID).show(parentFragmentManager, "Alerts-Line$lineID") + } + /* + + */ + + Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") + + //listeners + patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { + val currentShownPattern = patternShown?.pattern + val patternWithStops = currentPatterns[position] + + Log.d(DEBUG_TAG, "request stops for pattern ${patternWithStops.pattern.code}") + setPatternAndReqStops(patternWithStops) + + if(mapView?.visibility == View.VISIBLE) { + //Clear buses if we are changing direction + currentShownPattern?.let { patt -> + if(patt.directionId != patternWithStops.pattern.directionId){ + stopAnimations() + updatesByVehDict.clear() + updatePositionsIcons(true) + livePositionsViewModel.retriggerPositionUpdate() + } + if (shownStopInBottomSheet!=null){ + //check if the stop is inside the new pattern + /*val s = shownStopInBottomSheet!! + val newPatternStops = patternWithStops.stopsIndices + val filterPStops = newPatternStops.filter { ps -> ps.stopGtfsId == "gtt:${s.ID}" } + if (filterPStops.isEmpty()){ + hideStopOrBusBottomSheet() + } + */ + // do another thing, just close the stop when the pattern is changed + if (patt.code != patternWithStops.pattern.code){ + hideStopOrBusBottomSheet() + } + } + } + } + livePositionsViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) + + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + } + } + Log.d(DEBUG_TAG, "Views created!") + + observeStatusLivePositions() + + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //reflect UI //INITIALIZE VIEW MODELS viewModel.setRouteIDQuery(lineID) livePositionsViewModel.setGtfsLineToFilterPos(lineID, null) //observe the change, clear buses when switching position livePositionsViewModel.useMQTTPositionsLiveData.observe(viewLifecycleOwner){ useMQTT-> //Log.d(DEBUG_TAG, "Changed MQTT positions, now have to use MQTT: $useMQTT") if (isResumed) { //Log.d(DEBUG_TAG, "Deciding to switch, the current source is using MQTT: $usingMQTTPositions") if(useMQTT!=usingMQTTPositions){ // we have to switch val clearPos = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("positions_clear_on_switch_pref", true) livePositionsViewModel.clearOldPositionsUpdates() if(useMQTT){ //switching to MQTT, the GTFS positions are disabled automatically livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) } else{ //switching to GTFS RT: stop Mato, launch first request livePositionsViewModel.stopMatoUpdates() livePositionsViewModel.requestGTFSUpdates() } Log.d(DEBUG_TAG, "Should clear positions: $clearPos") if (clearPos) { livePositionsViewModel.clearAllPositions() //force clear of the viewed data if(vehShowing.isNotEmpty()) hideStopOrBusBottomSheet() clearAllBusPositionsInMap() } } } usingMQTTPositions = useMQTT } val keySourcePositions = getString(R.string.pref_positions_source) usingMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner, this::savePatternsToShow) /* */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> val pattern = viewModel.selectedPatternLiveData.value if (pattern == null) { Log.w(DEBUG_TAG, "The selectedPattern is null!") return@observe } if(mapView?.visibility ==View.VISIBLE) { // We have the pattern and the stops here, time to display them //TODO: Decide if we should follow the camera view given by the previous screen (probably the map fragment) // use !restoredCameraInMap to do so - // val shouldZoom = (shownStopInBottomSheet == null) //use this if we want to avoid zoom when we're keeping the stop open + // val shouldZoom = (shownStopInBottomSheet == null) //use this if we want to avoid zoom when we're keeping the stop open displayPatternWithStopsOnMap(pattern, stops, true) } else { if(stopsRecyclerView.visibility==View.VISIBLE) { patternShown = pattern showStopsInRecyclerView(stops) } } } viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> if(route == null){ //need to close the fragment activity?.supportFragmentManager?.popBackStack() return@observe } - descripTextView.text = route.longName + descripTextView.text = route.longName descripTextView.visibility = View.VISIBLE } mapStateViewModel.locationUserActive.observe(viewLifecycleOwner) { setLocationIconEnabled(it) } // enable info button if there are alerts on the line alertsViewModel.setGtfsLineFilter(lineID) alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ list -> Log.d(DEBUG_TAG, "alerts for line $lineID: ${list.size}") if(list.isNotEmpty()){ lineInfoButton.visibility = View.VISIBLE //Log.d(DEBUG_TAG, "First alert is:\n ${list[0].longPrint()}") } else lineInfoButton.visibility = View.GONE } - lineInfoButton.setOnClickListener { - AlertsDialogFragment(lineID).show(parentFragmentManager, "Alerts-Line$lineID") - } - /* - - */ - - Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") - - //listeners - patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { - val currentShownPattern = patternShown?.pattern - val patternWithStops = currentPatterns[position] - - Log.d(DEBUG_TAG, "request stops for pattern ${patternWithStops.pattern.code}") - setPatternAndReqStops(patternWithStops) - - if(mapView?.visibility == View.VISIBLE) { - //Clear buses if we are changing direction - currentShownPattern?.let { patt -> - if(patt.directionId != patternWithStops.pattern.directionId){ - stopAnimations() - updatesByVehDict.clear() - updatePositionsIcons(true) - livePositionsViewModel.retriggerPositionUpdate() - } - if (shownStopInBottomSheet!=null){ - //check if the stop is inside the new pattern - /*val s = shownStopInBottomSheet!! - val newPatternStops = patternWithStops.stopsIndices - val filterPStops = newPatternStops.filter { ps -> ps.stopGtfsId == "gtt:${s.ID}" } - if (filterPStops.isEmpty()){ - hideStopOrBusBottomSheet() - } - */ - // do another thing, just close the stop when the pattern is changed - if (patt.code != patternWithStops.pattern.code){ - hideStopOrBusBottomSheet() - } - } - } - } - livePositionsViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) - - } - - override fun onNothingSelected(p0: AdapterView<*>?) { - } - } - Log.d(DEBUG_TAG, "Views created!") - - observeStatusLivePositions() - - return rootView } - // ------------- UI switch stuff --------- private fun hideMapAndShowStopList(){ mapView?.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE locationIcon?.visibility = View.GONE busPositionsIconButton?.visibility = View.GONE viewModel.setMapShowing(false) if(usingMQTTPositions) livePositionsViewModel.stopMatoUpdates() //map.overlayManager.remove(busPositionsOverlay) switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) hideStopOrBusBottomSheet() if(locationComponent.isLocationComponentEnabled){ setLocationComponentEnabled(false) shouldMapLocationBeReactivated = true } else shouldMapLocationBeReactivated = false } private fun hideStopListAndShowMap(){ stopsRecyclerView.visibility = View.GONE mapView?.visibility = View.VISIBLE locationIcon?.visibility = View.VISIBLE busPositionsIconButton.visibility = View.VISIBLE viewModel.setMapShowing(true) //map.overlayManager.add(busPositionsOverlay) //map. if(usingMQTTPositions) livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else livePositionsViewModel.requestGTFSUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) if(shouldMapLocationBeReactivated){ setLocationComponentEnabled(Permissions.bothLocationPermissionsGranted(requireContext())) } } override fun setLocationIconEnabled(enabled: Boolean){ if(enabled) { locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) } else { locationIcon?.setImageDrawable(ContextCompat.getDrawable( requireContext(), R.drawable.location_circlew_grey ) ) } } override fun onMapLocationEnabled(active: Boolean) { //extra thing: show the toast showToastLocation(active) } override fun onMapLocationComponentInitialized() { //enable the position after the first fix //onMapLocationEnabled(true) } @SuppressLint("MissingPermission") override fun onFirstReceivedLocation(location: Location) { if(mapInitialized){ val center = map!!.cameraPosition.target val newPos = LatLng(location.latitude, location.longitude) Log.d(DEBUG_TAG, "Center of the map : $center") val newStatus = if(center==null || newPos.distanceTo(center) > 20*1000){ Log.d(DEBUG_TAG, "Distance from center of map to location: "+center?.distanceTo(newPos)) if(!shownToastNoPosition) context?.let{ c-> Toast.makeText(c, R.string.too_far_not_showing_location, Toast.LENGTH_LONG).show() shownToastNoPosition = true } false } else{ true } if(!newStatus) setLocationComponentEnabled(newStatus) mapStateViewModel.locationUserActive.value = newStatus } } // ------------- Map Code ------------------------- /** * This method sets up the map and the layers */ override fun onMapReady(mapReady: MapLibreMap) { this.map = mapReady var setViewAlready = false val context = requireContext() val mjson = MapLibreStyles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") activity?.run { val builder = Style.Builder().fromJson(mjson!!) mapReady.setStyle(builder) { style -> addImagesStyle(style) mapStyle = style //setupLayers(style) //checkInitMapLocation(mapReady, style,requireContext()) //if(!stopsLayerStarted) initPolylineStopsLayers(style, null) setupBusLayer(style) initSymbolManager(mapReady, style) toRunWhenMapReady?.run() toRunWhenMapReady = null mapInitialized = true if(patternShown!=null){ viewModel.stopsForPatternLiveData.value?.let { Log.d(DEBUG_TAG, "Show stops from the cache") displayPatternWithStopsOnMap(patternShown!!, it, true) //Show stop from cache mapStateViewModel.lastOpenStopID.value?.let{ sID-> val s= it.filter { stop -> stop.ID==sID } if (s.isEmpty()) { if(sID.isNotEmpty()) Log.w(DEBUG_TAG,"Wanted to open stop $sID in map but it was not loaded!") } else openStopInBottomSheet(s[0]) } } } var restoredMapState = mapStateViewModel.restoreMapState(mapReady) arguments?.let { args -> // if there is a Camera State in the arguments, set it for the new camera (doesn't work yet!) if (!restoredMapState && MapCameraState.checkInBundle(args)) { val initCamState = MapCameraState.fromBundle(args) //map?.let{ MapStateViewModel.restoreMapState(mapReady, initCamState) setViewAlready = true restoredMapState = true } } restoredCameraInMap = restoredMapState } mapReady.addOnMapClickListener { point -> val screenPoint = mapReady.projection.toScreenLocation(point) val stopsNearby = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID) //Log.d(DEBUG_TAG, "onMapClick, stopsNearby: $stopsNearby \nstopShown: $shownStopInBottomSheet \nbusNearby: $busNearby,") if (stopsNearby.isNotEmpty()) { val feature = stopsNearby[0] val id = feature.getStringProperty("id") val stop = viewModel.getStopByID(id) stop?.let { if (isBottomSheetShowing() || vehShowing.isNotEmpty()) { hideStopOrBusBottomSheet() } openStopInBottomSheet(it) //move camera if(it.latitude!=null && it.longitude!=null) mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750) } return@addOnMapClickListener true } else if (busNearby.isNotEmpty()){ val feature = busNearby[0] openBusFromMapClick(feature) return@addOnMapClickListener true } false } // we start requesting the bus positions now observeBusPositionUpdates() } val zoom = 12.0 val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON) if(!setViewAlready) mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() savedCameraPosition = null if(shouldMapLocationBeReactivated) mapReady.style?.let{ checkInitMapLocation(mapReady,it, context)} } override fun showOpenStopWithSymbolLayer(): Boolean { return true } /** * Separate function to find the vehicle associated with a feature and display it */ private fun openBusFromMapClick(feature: Feature){ val vehid = feature.getStringProperty("veh") if(isBottomSheetShowing()) hideStopOrBusBottomSheet() showVehicleTripInBottomSheet(vehid) updatesByVehDict[vehid]?.let { map?.animateCamera( CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)), 750 ) } } private fun observeBusPositionUpdates(){ //live bus positions livePositionsViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair -> //Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions") val updates = pair.first val vehiclesNotOnCorrectDir = pair.second if(mapView?.visibility == View.GONE || patternShown ==null){ //DO NOTHING Log.w(DEBUG_TAG, "not doing anything because map is not visible") return@observe } //remove vehicles not on this direction removeVehiclesData(vehiclesNotOnCorrectDir) updateBusPositionsInMap(updates, hasVehicleTracking = true) { veh-> showVehicleTripInBottomSheet(veh) } //if not using MQTT positions if(!usingMQTTPositions){ livePositionsViewModel.requestDelayedGTFSUpdates(2000) } } //download missing tripIDs livePositionsViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.requestMatoTripsDownload( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } } private fun showVehicleTripInBottomSheet(veh: String) { super.showVehicleTripInBottomSheet(veh) { patternCode, veh -> //this is checked in @GeneralMapLibreFragment //val data = updatesByVehDict[veh] ?: return@showVehicleTripInBottomSheet if (patternCode.isEmpty()) return@showVehicleTripInBottomSheet if (patternShown?.pattern?.code == patternCode) { //center view on vehicle updatesByVehDict[veh]?.let { up-> map?.let{ /* val c = it.cameraPosition it.moveCamera(CameraUpdateFactory.CameraPositionUpdate(c.bearing, LatLng(up.posUpdate.latitude, up.posUpdate.longitude), c.tilt,c.zoom, c.padding) ) */ it.animateCamera(CameraUpdateFactory.newLatLng(LatLng(up.posUpdate.latitude, up.posUpdate.longitude))) } } ?: { Toast.makeText(context, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() } } else { showPatternWithCode(patternCode) } } } // ------- MAP LAYERS INITIALIZE ---- /** * Initialize the map layers for the stops */ private fun initPolylineStopsLayers(style: Style, arrowFeatures: FeatureCollection?){ Log.d(DEBUG_TAG, "INIT STOPS CALLED") stopsSource = GeoJsonSource(STOPS_SOURCE_ID) //val context = requireContext() val stopIcon = ResourcesCompat.getDrawable(resources,R.drawable.ball, activity?.theme)!! val imgStop = ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!! val polyIconArrow = ResourcesCompat.getDrawable(resources, R.drawable.arrow_up_box_fill, activity?.theme)!! //set the image tint //DrawableCompat.setTint(imgBus,ContextCompat.getColor(context,R.color.line_drawn_poly)) // add icons style.addImage(STOP_IMAGE_ID,stopIcon) style.addImage(POLY_ARROW, polyIconArrow) style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!) polylineSource = GeoJsonSource(POLYLINE_SOURCE) //lineFeature?.let { GeoJsonSource(POLYLINE_SOURCE, it) } ?: GeoJsonSource(POLYLINE_SOURCE) style.addSource(polylineSource) val color=ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) //paint.style = Paint.Style.FILL_AND_STROKE //paint.strokeJoin = Paint.Join.ROUND //paint.strokeCap = Paint.Cap.ROUND val lineLayer = LineLayer(POLYLINE_LAYER, POLYLINE_SOURCE).withProperties( PropertyFactory.lineColor(color), PropertyFactory.lineWidth(5.0f), //originally 13f PropertyFactory.lineOpacity(1.0f), PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND), PropertyFactory.lineCap(Property.LINE_CAP_ROUND) ) polyArrowSource = GeoJsonSource(POLY_ARROWS_SOURCE, arrowFeatures) style.addSource(polyArrowSource) val arrowsLayer = SymbolLayer(POLY_ARROWS_LAYER, POLY_ARROWS_SOURCE).withProperties( PropertyFactory.iconImage(POLY_ARROW), PropertyFactory.iconRotate(Expression.get("bearing")), PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP) ) val layers = style.layers val lastLayers = layers.filter { l-> l.id.contains("city") } //Log.d(DEBUG_TAG,"Layers:\n ${style.layers.map { l -> l.id }}") Log.d(DEBUG_TAG, "City layers: ${lastLayers.map { l-> l.id }}") if(lastLayers.isNotEmpty()) style.addLayerAbove(lineLayer,lastLayers[0].id) else style.addLayerBelow(lineLayer,"label_country_1") //style.addLayerAbove(stopsLayer, POLYLINE_LAYER) style.addLayerAbove(arrowsLayer, POLYLINE_LAYER) stopsLayerStarted = true initStopsLayer(style, null, POLY_ARROWS_LAYER) } private fun filterPatternFromArgs(patterns: List): MatoPatternWithStops?{ var p: MatoPatternWithStops? = null if (patternIdToShow.isNotEmpty()){ for (patt in patterns) { if (patt.pattern.code == patternIdToShow){ p = patt } } if(p==null) Log.w(DEBUG_TAG, "We had to show the pattern with code $patternIdToShow, but we didn't find it") else Log.d(DEBUG_TAG, "Requesting to show pattern with code $patternIdToShow, found pattern ${p.pattern.code}") } // if we are loading from a stop, find it else if(stopIDFromToShow.isNotEmpty()) { val stopGtfsID = "gtt:$stopIDFromToShow" var pLength = 0 for (patt in patterns) { for (pstop in patt.stopsIndices) { if (pstop.stopGtfsId == stopGtfsID) { //found if (patt.stopsIndices.size > pLength) { p = patt pLength = patt.stopsIndices.size } //break here, we have determined this pattern has the stop we're looking for break } } } if(p==null) Log.w(DEBUG_TAG, "We had to show the pattern from stop $stopIDFromToShow, but we didn't find it") else Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${p.pattern.code}") } // the flag of showing pattern is not necessary anymore, we have set the pattern patternIdToShow = "" // the flag of selecting from stop needs to be used again when displaying the pattern return p } /** * Save the loaded pattern data, without the stops! */ private fun savePatternsToShow(patterns: List){ currentPatterns = patterns.sortedWith(patternsSorter) patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } val patternToShow = filterPatternFromArgs(currentPatterns) if(patternToShow!=null) { //showPattern(patternToShow) patternShown = patternToShow } patternShown?.let { showPattern(it) } } /** * Called when the position of the spinner is updated */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.requestStopsForPatternWithStops(patternWithStops) } private fun showPattern(patternWs: MatoPatternWithStops){ //Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for (k in currentPatterns.indices) { if (currentPatterns[k].pattern.code.trim() == code) { pos = k break } } Log.d(DEBUG_TAG, "Requesting stops fro pattern $code in position: $pos") // this triggers the showing on the map / recyclerview if (pos !=-2) patternsSpinner.setSelection(pos) else Log.e(DEBUG_TAG, "Pattern with code $code not found!!") } /** * Zoom on the map to get the pattern */ private fun zoomToCurrentPattern(){ if(polyline==null) return val NULL_VALUE = -4000.0 var maxLat = NULL_VALUE var minLat = NULL_VALUE var minLong = NULL_VALUE var maxLong = NULL_VALUE polyline?.let { for(p in it.coordinates()){ val lat = p.latitude() val lon = p.longitude() // get max latitude if(maxLat == NULL_VALUE) maxLat =lat else if (maxLat < lat) maxLat = lat // find min latitude if (minLat ==NULL_VALUE) minLat = lat else if (minLat > lat) minLat = lat if(maxLong == NULL_VALUE || maxLong < lon ) maxLong = lon if (minLong == NULL_VALUE || minLong > lon) minLong = lon } val padding = 50 // Pixel di padding intorno ai limiti Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") val bbox = LatLngBounds.from(maxLat,maxLong, minLat, minLong) //map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) map?.animateCamera(CameraUpdateFactory.newLatLngBounds(bbox, padding)) } } private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stopsToSort: List, zoomToPattern: Boolean){ if(!mapInitialized){ //set the runnable and do nothing else Log.d(DEBUG_TAG, "Delaying pattern display to when map is Ready: ${patternWs.pattern.code}") toRunWhenMapReady = Runnable { displayPatternWithStopsOnMap(patternWs, stopsToSort, zoomToPattern) } return } Log.d(DEBUG_TAG, "Got the stops: ${stopsToSort.map { s->s.gtfsID }}}") patternShown = patternWs //Problem: stops are not sorted val stopOrderD = patternWs.stopsIndices.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stopsToSort.sortedBy { s-> stopOrderD[s.gtfsID] } val pattern = patternWs.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) val pointsToShow = pointsList.map { Point.fromLngLat(it.longitude, it.latitude) } Log.d(DEBUG_TAG, "The polyline has ${pointsToShow.size} points to display") polyline = LineString.fromLngLats(pointsToShow) val lineFeature = Feature.fromGeometry(polyline) //Log.d(DEBUG_TAG, "Polyline in JSON is: ${lineFeature.toJson()}") // --- STOPS--- val features = ArrayList() for (s in stopsSorted){ if (s.latitude!=null && s.longitude!=null) { val loc = if (showOnTopOfLine) findOptimalPosition(s, pointsList) else LatLng(s.latitude!!, s.longitude!!) features.add( Feature.fromGeometry( Point.fromLngLat(loc.longitude, loc.latitude), JsonObject().apply { addProperty("id", s.ID) addProperty("name", s.stopDefaultName) //addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object } ) ) } } // -- ARROWS -- //val splitPolyline = MapLibreUtils.splitPolyWhenDistanceTooBig(pointsList, 200.0) val arrowFeatures = ArrayList() val pointsIndexToShowIcon = MapLibreUtils.findPointsToPutDirectionMarkers(pointsList, stopsSorted, 750.0) for (idx in pointsIndexToShowIcon){ val pnow = pointsList[idx] val otherp = if(idx>1) pointsList[idx-1] else pointsList[idx+1] val bearing = if (idx>1) MapLibreUtils.getBearing(pointsList[idx-1], pnow) else MapLibreUtils.getBearing(pnow, pointsList[idx+1]) arrowFeatures.add(Feature.fromGeometry( Point.fromLngLat((pnow.longitude+otherp.longitude)/2, (pnow.latitude+otherp.latitude)/2 ), //average JsonObject().apply { addProperty("bearing", bearing) } )) } Log.d(DEBUG_TAG,"Have put ${features.size} stops to display") // if the layer is already started, substitute the stops inside, otherwise start it if (stopsLayerStarted) { stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) polylineSource.setGeoJson(lineFeature) polyArrowSource.setGeoJson(FeatureCollection.fromFeatures(arrowFeatures)) lastStopsSizeShown = features.size } else map?.let { Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer") initPolylineStopsLayers(mapStyle, FeatureCollection.fromFeatures(arrowFeatures)) Log.d(DEBUG_TAG,"Started stops layer on map") lastStopsSizeShown = features.size stopsLayerStarted = true } ?:{ Log.e(DEBUG_TAG, "Stops layer is not started!!") } var reallyZoomToPattern = zoomToPattern if(stopIDFromToShow.isNotEmpty()){ //open the stop val stopfilt = stopsSorted.filter { s -> s.ID == stopIDFromToShow } if (stopfilt.isEmpty()){ Log.e(DEBUG_TAG, "Tried to show stop but it's not in the selected pattern") } else{ val stop = stopfilt[0] openStopInBottomSheet(stop) if(stop.hasCoords()) { reallyZoomToPattern = false setCameraPosition(stop.latitude!!, stop.longitude!!, 13.5) } } // Reset this to avoid checking again when showing stopIDFromToShow = "" //camera set } if(reallyZoomToPattern) zoomToCurrentPattern() } private fun initializeRecyclerView(){ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager } private fun showStopsInRecyclerView(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") val setNewAdapter = true if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } /** * This method fixes the display of the pattern, to be used when clicking on a bus */ private fun showPatternWithCode(patternId: String){ //var index = 0 Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") for (i in currentPatterns.indices){ val pattStop = currentPatterns[i] if(pattStop.pattern.code == patternId){ Log.d(DEBUG_TAG, "Pattern found in position $i") //setPatternAndReqStops(pattStop) patternsSpinner.setSelection(i) break } } } override fun onResume() { super.onResume() Log.d(DEBUG_TAG, "Resetting paused from onResume") pausedFragment = false val keySourcePositions = getString(R.string.pref_positions_source) usingMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") //separate paths if(usingMQTTPositions) livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else livePositionsViewModel.requestGTFSUpdates() //initialize GUI here fragmentListener?.readyGUIfor(FragmentKind.LINES) } override fun onPause() { super.onPause() if(usingMQTTPositions) livePositionsViewModel.stopMatoUpdates() pausedFragment = true //save map map?.let{ //if map is initialized mapStateViewModel.saveMapState(it) } mapStateViewModel.lastOpenStopID.postValue(shownStopInBottomSheet?.ID) } override fun onStop() { super.onStop() if(locationInitialized) shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled else shouldMapLocationBeReactivated = false } override fun onDestroyView() { map?.run { Log.d(DEBUG_TAG, "Saving camera position") savedCameraPosition = cameraPosition } super.onDestroyView() Log.d(DEBUG_TAG, "Destroying the views") /*mapStyle.removeLayer(STOPS_LAYER_ID) mapStyle?.removeSource(STOPS_SOURCE_ID) mapStyle.removeLayer(POLYLINE_LAYER) mapStyle.removeSource(POLYLINE_SOURCE) */ //stopsLayerStarted = false } override fun onMapDestroy() { mapStyle.removeLayer(STOPS_LAYER_ID) mapStyle.removeSource(STOPS_SOURCE_ID) mapStyle.removeLayer(POLYLINE_LAYER) mapStyle.removeSource(POLYLINE_SOURCE) mapStyle.removeLayer(BUSES_LAYER_ID) mapStyle.removeSource(BUSES_SOURCE_ID) //map?.locationComponent?.isLocationComponentEnabled = false setLocationComponentEnabled(false) } override fun getBaseViewForSnackBar(): View? { return null } companion object { private const val LINEID_KEY="lineID" private const val STOPID_FROM_KEY="stopID" private const val PATTERN_SHOW_KEY ="patternIDShow" private const val DEBUG_TAG="BusTO-LineDetalFragment" fun makeArgs(lineID: String, stopIDFrom: String?): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) b.putString(STOPID_FROM_KEY, stopIDFrom) return b } fun makeArgsPattern(lineID: String, patternShow: String?, extraArgs: Bundle?): Bundle { val b= extraArgs ?: Bundle() b.putString(LINEID_KEY, lineID) b.putString(PATTERN_SHOW_KEY, patternShow) return b } fun newInstance(lineID: String?, stopIDFrom: String?) = LinesDetailFragment().apply { lineID?.let { arguments = makeArgs(it, stopIDFrom) } } @JvmStatic private fun findOptimalPosition(stop: Stop, pointsList: MutableList): LatLng{ if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) throw IllegalArgumentException() val sLat = stop.latitude!! val sLong = stop.longitude!! if(pointsList.size < 2) return pointsList[0] pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } val p1 = pointsList[0] val p2 = pointsList[1] if (p1.longitude == p2.longitude){ //Log.e(DEBUG_TAG, "Same longitude") return LatLng(sLat, p1.longitude) } else if (p1.latitude == p2.latitude){ //Log.d(DEBUG_TAG, "Same latitude") return LatLng(p2.latitude,sLong) } val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) val cR = p1.latitude - p1.longitude * m val longNew = (minv * sLong + sLat -cR ) / (m+minv) val latNew = (m*longNew + cR) //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") return LatLng(latNew,longNew) } private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt index 34a4fb4..dcabf16 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -1,418 +1,423 @@ /* BusTO - Fragments components Copyright (C) 2022 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.os.Parcelable import android.util.Log import android.view.* import android.widget.* import android.widget.AdapterView.INVALID_POSITION import android.widget.AdapterView.OnItemSelectedListener import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.PatternWithStopsSorter import it.reyboz.bustorino.viewmodels.LinesViewModel class LinesFragment : ScreenBaseFragment() { companion object { fun newInstance(){ LinesFragment() } private const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" val patternStopsComparator = PatternWithStopsSorter() } private lateinit var viewModel: LinesViewModel private lateinit var linesSpinner: Spinner private lateinit var patternsSpinner: Spinner private lateinit var currentRoutes: List private lateinit var selectedPatterns: List private lateinit var routeDescriptionTextView: TextView private lateinit var stopsRecyclerView: RecyclerView private var linesAdapter: ArrayAdapter? = null private var patternsAdapter: ArrayAdapter? = null private var mListener: CommonFragmentListener? = null private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private var firstClick = true private var recyclerViewState:Parcelable? = null private var patternsSpinnerState:Parcelable? = null private val adapterListener = object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { //var r = "" //stop?.let { r= it.stopDisplayName.toString() } if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { mListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(mListener == null){ Log.e(DEBUG_TAG, "Listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { Log.d(DEBUG_TAG, "LongPressOnStop") return true } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Log.d(DEBUG_TAG, "saveInstanceState bundle: $outState") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_lines, container, false) linesSpinner = rootView.findViewById(R.id.linesSpinner) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager //allow the context menu to be opened registerForContextMenu(stopsRecyclerView) Log.d(DEBUG_TAG, "Called onCreateView for LinesFragment") Log.d(DEBUG_TAG, "OnCreateView, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnCreateView, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") - //set requests - viewModel.routesGTTLiveData.observe(viewLifecycleOwner) { - setRoutes(it) - } - - viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ - patterns -> - run { - selectedPatterns = patterns.sortedBy { p-> p.pattern.code } - //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } - patternsAdapter?.let { - it.clear() - it.addAll(selectedPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) - it.notifyDataSetChanged() - } - viewModel.selectedPatternLiveData.value?.let { - setSelectedPattern(it) - } - val pos = patternsSpinner.selectedItemPosition - //might be possible that the selectedItem is different (larger than list size) - if(pos!= INVALID_POSITION && pos >= 0 && (pos < selectedPatterns.size)){ - val p = selectedPatterns[pos] - Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") - setPatternAndReqStops(selectedPatterns[pos]) - } - - } - } - - viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops-> - Log.d("BusTO-LinesFragment", "Setting stops from DB") - setCurrentStops(stops) - } if(context!=null) { patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter linesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) linesSpinner.adapter = linesAdapter if (linesSpinner.onItemSelectedListener != null){ Log.d(DEBUG_TAG, "linesSpinner listener != null") } //listener linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { val selRoute = currentRoutes.get(pos) routeDescriptionTextView.text = selRoute.longName val oldRoute = viewModel.getRouteIDQueried() val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim()) Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner, oldRoute: $oldRoute") //launch query for this gtfsID viewModel.setRouteIDQuery(selRoute.gtfsId) //reset spinner position if(resetSpinner) patternsSpinner.setSelection(0) } override fun onNothingSelected(p0: AdapterView<*>?) { } } patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = selectedPatterns.get(position) // setPatternAndReqStops(patternWithStops) //viewModel.currentPositionInPatterns.value = position } override fun onNothingSelected(p0: AdapterView<*>?) { } } } return rootView } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //set requests + viewModel.routesGTTLiveData.observe(viewLifecycleOwner) { + setRoutes(it) + } + + viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ + patterns -> + run { + selectedPatterns = patterns.sortedBy { p-> p.pattern.code } + //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } + patternsAdapter?.let { + it.clear() + it.addAll(selectedPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) + it.notifyDataSetChanged() + } + viewModel.selectedPatternLiveData.value?.let { + setSelectedPattern(it) + } + + val pos = patternsSpinner.selectedItemPosition + //might be possible that the selectedItem is different (larger than list size) + if(pos!= INVALID_POSITION && pos >= 0 && (pos < selectedPatterns.size)){ + val p = selectedPatterns[pos] + Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") + setPatternAndReqStops(selectedPatterns[pos]) + } + + } + } + + viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops-> + Log.d("BusTO-LinesFragment", "Setting stops from DB") + setCurrentStops(stops) + } + } + override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener) mListener = context else throw RuntimeException(context.toString() + " must implement CommonFragmentListener") } override fun onResume() { super.onResume() mListener?.readyGUIfor(FragmentKind.LINES) Log.d(DEBUG_TAG, "Resuming lines fragment") //Log.d(DEBUG_TAG, "OnResume, selected line spinner pos: ${linesSpinner.selectedItemPosition}") //Log.d(DEBUG_TAG, "OnResume, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) Log.d(DEBUG_TAG, "Fragment onCreate") } override fun getBaseViewForSnackBar(): View? { return null } private fun setSelectedPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in selectedPatterns.indices){ if(selectedPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0){ patternsSpinner.setSelection(pos) } } private fun setRoutes(routes: List){ Log.d(DEBUG_TAG, "Resetting routes") currentRoutes = routes.sortedWith(linesComparator) if (linesAdapter!=null){ var selGtfsRoute = viewModel.getRouteIDQueried() var selRouteIdx = 0 if(selGtfsRoute == null){ selGtfsRoute ="" } Log.d(DEBUG_TAG, "Setting routes, selected route gtfsID: $selGtfsRoute") val adapter = linesAdapter!! if (adapter.isEmpty) { Log.d(DEBUG_TAG, "Lines adapter is empty") } else{ adapter.clear() } adapter.addAll(currentRoutes.map { r -> r.shortName }) adapter.notifyDataSetChanged() for(j in currentRoutes.indices){ val route = currentRoutes[j] if (route.gtfsId == selGtfsRoute) { selRouteIdx = j Log.d(DEBUG_TAG, "Route $selGtfsRoute has index $j") } } linesSpinner.setSelection(selRouteIdx) // } /* linesAdapter?.clear() linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) linesAdapter?.notifyDataSetChanged() */ } private fun setCurrentStops(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") var setNewAdapter = true if(stopsRecyclerView.adapter is StopRecyclerAdapter){ val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) ){ Log.d(DEBUG_TAG, "Found same stops on recyclerview") setNewAdapter = false } /*else { Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") adapter.stops = stopsSorted adapter.notifyDataSetChanged() }*/ } if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") //currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.selectedPatternLiveData.value = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) Log.d("BusTO-LinesFragment", "Creating context menu ") if (v.id == R.id.patternStopsRecyclerView) { // if we aren't attached to activity, return null if (activity == null) return val inflater = requireActivity().menuInflater inflater.inflate(R.menu.menu_line_item, menu) } } override fun onContextItemSelected(item: MenuItem): Boolean { if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter val stop = adapter.stops.get(adapter.getPosition()) val acId = item.itemId if(acId == R.id.action_view_on_map){ // view on the map if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() return true } mListener!!.showMapCenteredOnStop(stop) return true } else if (acId == R.id.action_show_arrivals){ mListener?.requestArrivalsForStopID(stop.ID) return true } return false } override fun onStop() { super.onStop() Log.d(DEBUG_TAG, "Fragment stopped") recyclerViewState = stopsRecyclerView.layoutManager?.onSaveInstanceState() patternsSpinnerState = patternsSpinner.onSaveInstanceState() } override fun onStart() { super.onStart() Log.d(DEBUG_TAG, "OnStart, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnStart, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") if (recyclerViewState!=null){ stopsRecyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) } if(patternsSpinnerState!=null){ patternsSpinner.onRestoreInstanceState(patternsSpinnerState) } } /* override fun onDestroyView() { super.onDestroyView() Log.d(DEBUG_TAG, "Fragment view destroyed") } override fun onDestroy() { super.onDestroy() Log.d(DEBUG_TAG, "Fragment destroyed") } */ override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) Log.d(DEBUG_TAG, "OnViewStateRes, bundled saveinstancestate: $savedInstanceState") Log.d(DEBUG_TAG, "OnViewStateRes, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnViewStateRes, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt index 11c1a67..e5a3f91 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -1,440 +1,449 @@ +/* + BusTO - Fragments components + Copyright (C) 2018-2026 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.util.Log import android.view.* import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.ImageView import android.widget.TextView import androidx.appcompat.widget.SearchView import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.adapters.StringListAdapter import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.DBUpdateWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel class LinesGridShowingFragment : ScreenBaseFragment() { private val viewModel: LinesGridShowingViewModel by viewModels() //private lateinit var gridLayoutManager: AutoFitGridLayoutManager private lateinit var favoritesRecyclerView: RecyclerView private lateinit var urbanRecyclerView: RecyclerView private lateinit var extraurbanRecyclerView: RecyclerView private lateinit var touristRecyclerView: RecyclerView private lateinit var favoritesTitle: TextView private lateinit var urbanLinesTitle: TextView private lateinit var extrurbanLinesTitle: TextView private lateinit var touristLinesTitle: TextView private lateinit var updateMessageTextView: TextView //private lateinit var searchBar: SearchView private var routesByAgency = HashMap>() /*hashMapOf( AG_URBAN to ArrayList(), AG_EXTRAURB to ArrayList(), AG_TOUR to ArrayList() )*/ private lateinit var fragmentListener: CommonFragmentListener private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private val linesPriorityComparator = Comparator> { pa, pb -> if (pa.second != pb.second){ return@Comparator pa.second - pb.second } else{ return@Comparator linesNameSorter.compare(pa.first.shortName, pb.first.shortName) } } private val routeClickListener = RouteAdapter.ItemClicker { fragmentListener.openLineFromStop(it.gtfsId, null) } private val arrows = HashMap() private val durations = HashMap() //private val recyclerViewAdapters= HashMap() private val lastQueryEmptyForAgency = HashMap(3) private var openRecyclerView = "AG_URBAN" - private fun getFlexLayoutManager(context: Context): FlexboxLayoutManager{ - val layoutManager = FlexboxLayoutManager(context) - layoutManager.flexDirection = FlexDirection.ROW - layoutManager.justifyContent = JustifyContent.FLEX_START - - return layoutManager - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView) urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) updateMessageTextView = rootView.findViewById(R.id.updateMessageTextView) favoritesTitle = rootView.findViewById(R.id.favoritesTitleView) urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites) //show urban expanded by default val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) for (recyView in recViews) { val gridLayoutManager = AutoFitGridLayoutManager( requireContext().applicationContext, (utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt() ) recyView.layoutManager = gridLayoutManager } //init favorites recyclerview favoritesRecyclerView.layoutManager = getFlexLayoutManager(requireContext()) - viewModel.getLinesLiveData().observe(viewLifecycleOwner){ rL -> routesByAgency.clear() for (k in AGENCIES){ routesByAgency[k] = ArrayList() } val routesPrioByAg = HashMap>>() for (ag in AGENCIES){ routesPrioByAg[ag] = ArrayList() } for(p in rL){ val route = p.first val agency = route.agencyID if(agency !in routesByAgency.keys){ Log.e(DEBUG_TAG, "The agency $agency for route ${p.first.gtfsId} is not in the predefined agencies (${routesByAgency.keys})") } routesByAgency[agency]?.add(route) routesPrioByAg[agency]?.add(p) // I would print a debug here, but it's the same as above } //zip agencies and recyclerviews AGENCIES.zip(recViews) { ag, recView -> routesPrioByAg[ag]?.let { routePrioList -> if (routePrioList.isNotEmpty()) { routePrioList.sortWith(linesPriorityComparator) val adapter = RouteAdapter(routePrioList.map { it.first }, routeClickListener) val lastQueryEmpty = if(ag in lastQueryEmptyForAgency.keys) lastQueryEmptyForAgency[ag]!! else true if (lastQueryEmpty) recView.adapter = adapter else recView.swapAdapter(adapter, false) lastQueryEmptyForAgency[ag] = false } else { val messageString = if(viewModel.getLineQueryValue().isNotEmpty()) getString(R.string.no_lines_found_query) else getString(R.string.no_lines_found) val extraAdapter = StringListAdapter(listOf(messageString)) recView.adapter = extraAdapter lastQueryEmptyForAgency[ag] = true } durations[ag] = if(routePrioList.size < 20) ViewUtils.DEF_DURATION else 1000 } } } - viewModel.favoritesLines.observe(viewLifecycleOwner){ routes-> - val routesNames = routes.map { it.shortName } - //create new item click listener every time - val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> - val r = routes[pos] - fragmentListener.openLineFromStop(r.gtfsId, null) - } - favoritesRecyclerView.adapter = adapter - } - //onClicks urbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_URBAN) } extrurbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_EXTRAURB) } touristLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_TOUR) } favoritesTitle.setOnClickListener { closeOpenFavorites() } arrows[AG_FAV]?.setOnClickListener { closeOpenFavorites() } //arrows onClicks for(k in Companion.AGENCIES){ //k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) } } // watch for the db update DBUpdateWorker.getWorkInfoLiveData(requireContext()).observe(viewLifecycleOwner){ workInfoList -> if (workInfoList == null || workInfoList.isEmpty()) { return@observe } var showProgress = false for (workInfo in workInfoList) { if (workInfo.state == WorkInfo.State.RUNNING) { updateMessageTextView.visibility = View.VISIBLE } else{ updateMessageTextView.visibility = View.GONE } break } } return rootView } + fun setUserSearch(textSearch:String){ viewModel.setLineQuery(textSearch) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val menuHost: MenuHost = requireActivity() + viewModel.favoritesLines.observe(viewLifecycleOwner){ routes-> + val routesNames = routes.map { it.shortName } + //create new item click listener every time + val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> + val r = routes[pos] + fragmentListener.openLineFromStop(r.gtfsId, null) + } + favoritesRecyclerView.adapter = adapter + } + // Add menu items without using the Fragment Menu APIs // Note how we can tie the MenuProvider to the viewLifecycleOwner // and an optional Lifecycle.State (here, RESUMED) to indicate when // the menu should be visible menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { // Add menu items here menuInflater.inflate(R.menu.menu_search, menu) val search = menu.findItem(R.id.searchMenuItem).actionView as SearchView search.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ override fun onQueryTextSubmit(query: String?): Boolean { setUserSearch(query ?: "") return true } override fun onQueryTextChange(query: String?): Boolean { setUserSearch(query ?: "") return true } }) search.queryHint = getString(R.string.search_box_lines_suggestion_filter) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { // Handle the menu selection if (menuItem.itemId == R.id.searchMenuItem){ Log.d(DEBUG_TAG, "Clicked on search menu") } else{ Log.d(DEBUG_TAG, "Clicked on something else") } return false } }, viewLifecycleOwner, Lifecycle.State.RESUMED) } private fun closeOpenFavorites(){ if(favoritesRecyclerView.visibility == View.VISIBLE){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) viewModel.favoritesExpanded.value = false } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) viewModel.favoritesExpanded.value = true } } private fun openLinesAndCloseOthersIfNeeded(agency: String){ if(openRecyclerView!="" && openRecyclerView!= agency) { switchRecyclerViewStatus(openRecyclerView) } switchRecyclerViewStatus(agency) } private fun switchRecyclerViewStatus(agency: String){ val recyclerView = when(agency){ AG_TOUR -> touristRecyclerView AG_EXTRAURB -> extraurbanRecyclerView AG_URBAN -> urbanRecyclerView else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val expandedLiveData = when(agency){ AG_TOUR -> viewModel.isTouristExpanded AG_URBAN -> viewModel.isUrbanExpanded AG_EXTRAURB -> viewModel.isExtraUrbanExpanded else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val duration = durations[agency] val arrow = arrows[agency] val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration if(duration!=null&&arrow!=null) when (recyclerView.visibility){ View.GONE -> { Log.d(DEBUG_TAG, "Open recyclerview $agency") //val a =ViewUtils.expand(recyclerView, duration, 0) recyclerView.visibility = View.VISIBLE expandedLiveData.value = true Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}") setOpen(arrow, true) //arrow.startAnimation(rotateArrow(true,durArrow)) openRecyclerView = agency } View.VISIBLE -> { Log.d(DEBUG_TAG, "Close recyclerview $agency") //ViewUtils.collapse(recyclerView, duration) recyclerView.visibility = View.GONE expandedLiveData.value = false //arrow.rotation = 90f Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate") setOpen(arrow, false) //arrow.startAnimation(rotateArrow(false,durArrow)) openRecyclerView = "" } View.INVISIBLE -> { TODO() } } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } override fun getBaseViewForSnackBar(): View? { return null } override fun onResume() { super.onResume() val pref = PreferencesHolder.getMainSharedPreferences(requireContext()) val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))} //restore state viewModel.favoritesExpanded.value?.let { if(!it){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) } } viewModel.isUrbanExpanded.value?.let { if(it) { urbanRecyclerView.visibility = View.VISIBLE arrows[AG_URBAN]?.rotation= 90f openRecyclerView = AG_URBAN Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded") } else { urbanRecyclerView.visibility = View.GONE arrows[AG_URBAN]?.rotation= 0f } } viewModel.isTouristExpanded.value?.let { val recview = touristRecyclerView if(it) { recview.visibility = View.VISIBLE arrows[AG_TOUR]?.rotation=90f openRecyclerView = AG_TOUR } else { recview.visibility = View.GONE arrows[AG_TOUR]?.rotation= 0f } } viewModel.isExtraUrbanExpanded.value?.let { val recview = extraurbanRecyclerView if(it) { openRecyclerView = AG_EXTRAURB recview.visibility = View.VISIBLE arrows[AG_EXTRAURB]?.rotation=90f } else { recview.visibility = View.GONE arrows[AG_EXTRAURB]?.rotation=0f } } fragmentListener.readyGUIfor(FragmentKind.LINES) } companion object { private const val COLUMN_WIDTH_DP=250 private const val AG_FAV = "fav" private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" private const val AG_TOUR ="gtt:T" private const val DEBUG_TAG ="BusTO-LinesGridFragment" const val FRAGMENT_TAG = "LinesGridShowingFragment" private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR) fun newInstance() = LinesGridShowingFragment() @JvmStatic fun setOpen(imageView: ImageView, value: Boolean){ if(value) imageView.rotation = 90f else imageView.rotation = 0f } @JvmStatic fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{ val start = if (toOpen) 0f else 90f val stop = if(toOpen) 90f else 0f Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop") val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) rotate.duration = duration rotate.interpolator = LinearInterpolator() //rotate.fillAfter = true rotate.fillBefore = false return rotate } } } \ No newline at end of file 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 f98e7e5..1282296 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,886 +1,959 @@ /* 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.Intent; import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; 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.core.content.ContextCompat; 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.MenuItem; 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.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import java.security.InvalidParameterException; import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; -import it.reyboz.bustorino.middleware.BarcodeScanContract; -import it.reyboz.bustorino.middleware.BarcodeScanOptions; -import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.Permissions; import it.reyboz.bustorino.viewmodels.IntroViewModel; import org.jetbrains.annotations.NotNull; -import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; 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{ 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 InitialScreen { + public enum InternalScreen { HOME_BUTTONS(0), NEARBY_STOPS(1), ARRIVALS(2), - STOP_SEARCH(3); + STOP_SEARCH(3), + NEARBY_ARRIVALS(4); public final int code; - InitialScreen(int code) { this.code = code; } + InternalScreen(int code) { this.code = code; } @Nullable - public static InitialScreen fromCode(int code) { - for (InitialScreen c : values()) if (c.code == code) return c; + 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 MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; - private FrameLayout resultFrameLayout; + + /// VIEW MODELS in BaseFragment + private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; - //private Snackbar snackbar; - /* - * Search mode - */ - + private boolean initialScreenShown = false; private SearchMode searchMode = SearchMode.INITIAL; - //private ImageButton addToFavorites; - //// HIDDEN BUT IMPORTANT ELEMENTS //// private FragmentManager childFragMan; - private IntroViewModel introViewModel; + + /// 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 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"); } } - - /// LOCATION STUFF /// - boolean pendingIntroRun = false; - boolean pendingNearbyStopsFragmentRequest = false; - boolean pendingNearbyAddToBackStack = false; - boolean locationPermissionGranted, locationPermissionAsked = false; - //AppLocationManager locationManager; 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; } }); - - //// ACTIVITY ATTACHED (LISTENER /// - private CommonFragmentListener mListener; - - private String pendingStopID = null; - private String pendingSearchQuery = null; - private InitialScreen initialScreen = InitialScreen.HOME_BUTTONS; - private CoordinatorLayout coordLayout; - public MainScreenFragment() { // Required empty public constructor } - public static MainScreenFragment newInstance(@NonNull InitialScreen kind, + 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 InitialScreen kind, @Nullable Bundle args){ + 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 InitialScreen kind, @Nullable String stopId, @Nullable String query) { + 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(InitialScreen.ARRIVALS, stopID, null); + return makeArgs(InternalScreen.ARRIVALS, stopID, null); } public static Bundle makeArgsStops(@NonNull String query){ - return makeArgs(InitialScreen.STOP_SEARCH, query, null); + return makeArgs(InternalScreen.STOP_SEARCH, query, null); } public static Bundle makeArgsNearby(){ - return makeArgs(InitialScreen.NEARBY_STOPS, null, null); + return makeArgs(InternalScreen.NEARBY_STOPS, null, null); } public static Bundle makeArgsButtonsScreen(){ - return makeArgs(InitialScreen.HOME_BUTTONS, null, null); + 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, InitialScreen.HOME_BUTTONS.code); - InitialScreen parsed = InitialScreen.fromCode(code); - initialScreen = (parsed != null) ? parsed : InitialScreen.HOME_BUTTONS; + 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); } } @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); - resultFrameLayout = root.findViewById(R.id.resultFrame); 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 = new ViewModelProvider(requireActivity()).get(IntroViewModel.class); + IntroViewModel introViewModel = new ViewModelProvider(requireActivity()).get(IntroViewModel.class); introViewModel.getIntroIsRunning().observe(getViewLifecycleOwner(), isRunning -> { pendingIntroRun = isRunning; }); - Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); + // 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; - dispatchInitialContent(); + showDifferentSubFragments(internalScreen); } /** * Installs the initial child fragment based on the arguments supplied as arguments */ - private void dispatchInitialContent() { - switch (initialScreen) { + private void showDifferentSubFragments(@NonNull InternalScreen screen) { + boolean firstTime = !initialScreenShown; + switch (screen) { case NEARBY_STOPS: - showNearbyStopsFragmentChecking(false); + 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(true); + showButtonsFragment(firstTime); } pendingSearchQuery = null; break; case HOME_BUTTONS: default: - showButtonsFragment(true); + 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); + + //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); + //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 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"); 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() { - showNearbyStopsFragmentChecking(true); + 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 - final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); - + 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, fragment, NearbyStopsFragment.FRAGMENT_TAG); + 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/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt index 337bb10..30a0614 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -1,750 +1,756 @@ /* BusTO - Fragments components Copyright (C) 2025 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.location.LocationManager import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.room.concurrent.AtomicBoolean import com.google.android.material.bottomsheet.BottomSheetBehavior import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.mato.MQTTMatoClient import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.MapLibreLocationEngine import it.reyboz.bustorino.map.MapLibreStyles import it.reyboz.bustorino.viewmodels.StopsMapViewModel import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds import org.maplibre.android.location.engine.LocationEngineCallback import org.maplibre.android.location.engine.LocationEngineResult import org.maplibre.android.location.modes.CameraMode import org.maplibre.android.location.modes.RenderMode import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.plugins.annotation.Symbol import org.maplibre.geojson.Feature import org.maplibre.geojson.FeatureCollection /** * A simple [Fragment] subclass. * Use the [MapLibreFragment.newInstance] factory method to * create an instance of this fragment. */ class MapLibreFragment : GeneralMapLibreFragment() { private val stopsViewModel: StopsMapViewModel by viewModels() private var stopsShowing = ArrayList(0) // Sources for stops and buses are in GeneralMapLibreFragment private var isUserMovingCamera = false private var lastStopsSizeShown = 0 private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0) private var stopsRedrawnTimes = 0 //bottom Sheet behavior in GeneralMapLibreFragment //private var stopActiveSymbol: Symbol? = null // Location stuff private lateinit var locationManager: LocationManager private lateinit var userLocationButton: ImageButton private lateinit var centerUserButton: ImageButton private lateinit var followUserButton: ImageButton private var followingUserLocation = false private var ignoreCameraMovementForFollowing = true private var restoredMapCamera = AtomicBoolean() //BUS POSITIONS private var usingMQTTPositions = true // THIS IS INSIDE VIEW MODEL NOW private val symbolsToUpdate = ArrayList() private var initialStopToShow : Stop? = null private var initialStopShown = false private var waitingDelayedBusUpdate = false //shown stuff //private var savedStateOnStop : Bundle? = null private val showBusLayer = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { initialStopToShow = Stop.fromBundle(arguments) if (initialStopToShow==null){ } else if(!initialStopToShow!!.hasCoords()){ //null the stop if it doesn't have coordinates, we cannot find it initialStopToShow = null } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootView = inflater.inflate(R.layout.fragment_map_libre, container, false) //reset the counter lastStopsSizeShown = 0 stopsRedrawnTimes = 0 stopsLayerStarted = false symbolsToUpdate.clear() // Init layout view // Init the MapView mapView = rootView.findViewById(R.id.libreMapView) mapView!!.onCreate(savedInstanceState) mapView!!.getMapAsync(this) //init bottom sheet val bottomSheet = rootView.findViewById(R.id.bottom_sheet) bottomLayout = bottomSheet stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView) stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView) linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView) arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton) directionsCard = bottomSheet.findViewById(R.id.directionsCardButton) userLocationButton = rootView.findViewById(R.id.locationEnableIcon) userLocationButton.setOnClickListener(this::switchUserLocationStatus) followUserButton = rootView.findViewById(R.id.followUserImageButton) centerUserButton = rootView.findViewById(R.id.centerMapImageButton) busPositionsIconButton = rootView.findViewById(R.id.busPositionsImageButton) busPositionsIconButton.setOnClickListener { LivePositionsDialogFragment().show(parentFragmentManager, "LivePositionsDialog") } bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN arrivalsCard.setOnClickListener { if(context!=null){ Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show() } } centerUserButton.setOnClickListener { if(context!=null && locationComponent.isLocationComponentEnabled) { val location = locationComponent.lastKnownLocation location?.let { mapView?.getMapAsync { map -> map.animateCamera(CameraUpdateFactory.newCameraPosition( CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500) } } } } followUserButton.setOnClickListener { // onClick user following button if(context!=null && locationInitialized && locationComponent.isLocationComponentEnabled){ // CameraMode.TRACKING makes the camera move and jump to the location setFollowUserLocation(!followingUserLocation) } } //locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager /* if (Permissions.bothLocationPermissionsGranted(requireContext()) && deviceHasGpsProvider()) { requestInitialUserLocation() } else{ if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show() } // PERMISSIONS REQUESTED AFTER MAP SETUP } */ - - // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { hideStopOrBusBottomSheet() } - observeStatusLivePositions() + + + Log.d(DEBUG_TAG, "Fragment View Created!") + + //TODO: Reshow last open stop when switching back to the map fragment + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //observe change in source of the live positions livePositionsViewModel.useMQTTPositionsLiveData.observe(viewLifecycleOwner){ useMQTT-> //Log.d(DEBUG_TAG, "Changed MQTT positions, now have to use MQTT: $useMQTT") if (showBusLayer && isResumed) { //Log.d(DEBUG_TAG, "Deciding to switch, the current source is using MQTT: $usingMQTTPositions") if(useMQTT!=usingMQTTPositions){ // we have to switch val clearPos = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("positions_clear_on_switch_pref", true) livePositionsViewModel.clearOldPositionsUpdates() if(useMQTT){ //switching to MQTT, the GTFS positions are disabled automatically livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) } else{ //switching to GTFS RT: stop Mato, launch first request livePositionsViewModel.stopMatoUpdates() livePositionsViewModel.requestGTFSUpdates() } Log.d(DEBUG_TAG, "Should clear positions: $clearPos") if (clearPos) { livePositionsViewModel.clearAllPositions() //force clear of the viewed data if(vehShowing.isNotEmpty()) hideStopOrBusBottomSheet() clearAllBusPositionsInMap() } } } usingMQTTPositions = useMQTT } mapStateViewModel.locationUserActive.observe(viewLifecycleOwner){ setLocationIconEnabled(it)} mapStateViewModel.followingUserPosition.observe(viewLifecycleOwner){ updateFollowingIcon(it)} - Log.d(DEBUG_TAG, "Fragment View Created!") + observeStatusLivePositions() - //TODO: Reshow last open stop when switching back to the map fragment - return rootView } /** * This method sets up the map and the layers */ override fun onMapReady(mapReady: MapLibreMap) { this.map = mapReady val context = requireContext() val mjson = MapLibreStyles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) val builder = Style.Builder().fromJson(mjson!!) mapReady.setStyle(builder) { style -> mapStyle = style //setupLayers(style) addImagesStyle(style) //init stop layer with this val stopsInCache = stopsViewModel.stopsToShow.value if(stopsInCache.isNullOrEmpty()) initStopsLayer(style, null) else displayStops(stopsInCache) if(showBusLayer) setupBusLayer(style, withLabels = true, busIconsScale = 1.2f) // Start observing data now that everything is set up observeStops() checkInitMapLocation(mapReady,style, context) } mapReady.addOnCameraIdleListener { map?.let { val newBbox = it.projection.visibleRegion.latLngBounds stopsViewModel.loadStopsInLatLngBounds(newBbox) lastBBox = newBbox } } mapReady.addOnCameraMoveStartedListener { v-> if(v== MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE){ //the user is moving the map //isUserMovingCamera = true updateFollowingIcon(false) } } mapReady.addOnMapClickListener { point -> onMapClickReact(point) } // we start requesting the bus positions now observeBusPositionUpdates() //Restoring data if (initialStopToShow!=null && initialStopToShow?.hasCoords() == true){ val s = initialStopToShow!! if(s.hasCoords()){ mapReady.cameraPosition = CameraPosition.Builder().target( LatLng(s.latitude!!, s.longitude!!) ).zoom(DEFAULT_ZOOM).build() } restoredMapCamera.set(true) } else{ var boundsRestored = false //restore the map state here map?.let{ boundsRestored = mapStateViewModel.restoreMapState(it) mapStateViewModel.lastOpenStopID.value?.let{ sID-> val s= stopsViewModel.getStopByID(sID) if (s==null) { if(sID.isNotEmpty()) Log.w(DEBUG_TAG,"Wanted to open stop $sID in map but it was not loaded!") } else{ openStopInBottomSheet(s) } } } if(!boundsRestored){ // we have not restored the bounds, open normally in target location // TODO: check that the map is reopened in the same location val lastLoc = mapStateViewModel.locationToShow val defaultLoc = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) val proposedLoc = lastLoc?.let{ LatLng(lastLoc.latitude, lastLoc.longitude)} val targetLoc = if(proposedLoc == null || proposedLoc.distanceTo(defaultLoc) > MAX_DIST_KM*1000) defaultLoc else proposedLoc mapReady.cameraPosition = CameraPosition.Builder().target(targetLoc).zoom(DEFAULT_ZOOM).build() } restoredMapCamera.set(boundsRestored) } mapInitialized = true //pendingLocationActivation = true //positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) } private fun onMapClickReact(point: LatLng): Boolean{ map?.let { mapReady -> val screenPoint = mapReady.projection.toScreenLocation(point) val stopsFeatures = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID) Log.d(DEBUG_TAG, "Clicked on stops: $stopsFeatures \n and buses: $busNearby") if (stopsFeatures.isNotEmpty()) { val feature = stopsFeatures[0] val id = feature.getStringProperty("id") val name = feature.getStringProperty("name") //Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show() val stop = stopsViewModel.getStopByID(id) Log.d(DEBUG_TAG, "Decided click is on stop with id $id : $stop") stop?.let { newstop -> val sameStopClicked = shownStopInBottomSheet?.let { newstop.ID==it.ID } ?: false Log.d(DEBUG_TAG, "Hiding clicked stop: $sameStopClicked") if (isBottomSheetShowing()) { hideStopOrBusBottomSheet() } if(!sameStopClicked){ openStopInBottomSheet(newstop) //isBottomSheetShowing = true //move camera if (newstop.latitude != null && newstop.longitude != null) //mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(it.latitude!!, it.longitude!!)).build() mapReady.animateCamera( CameraUpdateFactory.newLatLng(LatLng(newstop.latitude!!, newstop.longitude!!)), 750 ) } } return true } else if (busNearby.isNotEmpty()) { val feature = busNearby[0] val vehid = feature.getStringProperty("veh") if (isBottomSheetShowing()) hideStopOrBusBottomSheet() showVehicleTripInBottomSheet(vehid) //move camera to center on vehicle updatesByVehDict[vehid]?.let { dat -> mapReady.animateCamera( CameraUpdateFactory.newLatLng(LatLng(dat.posUpdate.latitude, dat.posUpdate.longitude)), 750 ) } return true } } return false } override fun showOpenStopWithSymbolLayer(): Boolean { return false } override fun hideStopOrBusBottomSheet(){ if (shownStopInBottomSheet?.ID == initialStopToShow?.ID){ initialStopToShow = null } super.hideStopOrBusBottomSheet() } override fun onAttach(context: Context) { super.onAttach(context) fragmentListener = if (context is CommonFragmentListener) { context } else { throw RuntimeException( context.toString() + " must implement FragmentListenerMain" ) } } override fun onDetach() { super.onDetach() fragmentListener = null } override fun onStart() { super.onStart() } override fun onResume() { super.onResume() //mapView.onResume() handled in GeneralMapLibreFragment if(showBusLayer) { //first, clean up all the old positions livePositionsViewModel.clearOldPositionsUpdates() if (livePositionsViewModel.useMQTTPositionsLiveData.value!!){ livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) usingMQTTPositions = true } else { livePositionsViewModel.requestGTFSUpdates() usingMQTTPositions = false } livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean -> Log.d( DEBUG_TAG, "Last trip download result is $d" ) } livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List -> Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat") livePositionsViewModel.downloadTripsFromMato(dat) } } fragmentListener?.readyGUIfor(FragmentKind.MAP) } override fun onPause() { super.onPause() Log.d(DEBUG_TAG, "Fragment paused") map?.let{ //if map is initialized mapStateViewModel.saveMapState(it) } try{ //save last location map?.locationComponent?.let{ if(locationInitialized && it.isLocationComponentActivated){ stopsViewModel.lastUserLocation = it.lastKnownLocation } } }catch (e: Exception){ Log.w(DEBUG_TAG, "Cannot save lastKnowLocation from map location component,error: ${e.message}") } mapStateViewModel.lastOpenStopID.postValue(shownStopInBottomSheet?.ID) if (livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.stopMatoUpdates() } override fun onStop() { super.onStop() Log.d(DEBUG_TAG, "Fragment stopped!") } override fun onMapDestroy() { mapStyle.removeLayer(STOPS_LAYER_ID) mapStyle.removeSource(STOPS_SOURCE_ID) mapStyle.removeLayer(BUSES_LAYER_ID) mapStyle.removeSource(BUSES_SOURCE_ID) } override fun getBaseViewForSnackBar(): View? { return mapView } private fun showVehicleTripInBottomSheet(veh: String) { val data = updatesByVehDict[veh] ?: return super.showVehicleTripInBottomSheet(veh) { patternCode, _ -> map?.let { mapStateViewModel.saveMapState(it) } fragmentListener?.openLineFromVehicle( data.posUpdate.getLineGTFSFormat(), patternCode, mapStateViewModel.savedCameraState?.toBundle() ) } } private fun observeStops() { // Observe stops stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops -> stopsShowing = ArrayList(stops) displayStops(stopsShowing) initialStopToShow?.let{ s-> //show the stop in the bottom sheet if(!initialStopShown && (s.ID in stopsShowing.map { it.ID })) { val stopToShow = stopsShowing.first { it.ID == s.ID } openStopInBottomSheet(stopToShow) initialStopShown = true } } } } /** * Add the stops to the layers */ private fun displayStops(stops: List?) { if (stops.isNullOrEmpty()) return if (stops.size==lastStopsSizeShown){ Log.d(DEBUG_TAG, "Not updating, have same number of stops") return } /*if(stops.size> lastStopsSizeShown){ stopsRedrawnTimes = 0 } else{ stopsRedrawnTimes++ } */ val features = ArrayList()//stops.mapNotNull { stop -> //stop.latitude?.let { lat -> // stop.longitude?.let { lon -> for (s in stops){ if (s.latitude!=null && s.longitude!=null) features.add(stopToGeoJsonFeature(s)) } Log.d(DEBUG_TAG,"Displaying ${features.size} stops") // if the layer is already started, substitute the stops inside, otherwise start it if (stopsLayerStarted) { stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) lastStopsSizeShown = features.size } else map?.let { Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer") initStopsLayer(mapStyle, FeatureCollection.fromFeatures(features)) Log.d(DEBUG_TAG,"Started stops layer on map") lastStopsSizeShown = features.size stopsLayerStarted = true } } // --------------- BUS LOCATIONS STUFF -------------------------- /** * Start requesting position updates */ private fun observeBusPositionUpdates() { livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> Log.d( DEBUG_TAG, "Have " + data.size + " trip updates, has Map start finished: $mapInitialized" ) if (mapInitialized) updateBusPositionsInMap(data, hasVehicleTracking = true) { veh -> showVehicleTripInBottomSheet(veh) } if (!isDetached && !livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.requestDelayedGTFSUpdates( 3000 ) } } // ------ LOCATION STUFF ----- @SuppressLint("MissingPermission") override fun onMapLocationComponentInitialized() { //locationComponent.cameraMode = CameraMode.TRACKING locationComponent.renderMode = RenderMode.COMPASS locationComponent.locationEngine?.apply{ // this is only called once getLastLocation(object : LocationEngineCallback { override fun onSuccess(res: LocationEngineResult?) { Log.d(DEBUG_TAG, "Got the last location, ${res?.lastLocation}") res?.lastLocation?.let { loc -> if(mapInitialized){ val newLocation = LatLng(loc.latitude, loc.longitude) //center the position only if it is close enough if(newLocation.distanceTo(DEFAULT_LATLNG) < MAX_DIST_KM * 1000) map?.cameraPosition = CameraPosition.Builder().target(LatLng(loc.latitude, loc.longitude)).build() } else mapStateViewModel.locationToShow = loc } } override fun onFailure(p0: java.lang.Exception) { if( p0 is MapLibreLocationEngine.NoLocationException) Log.d(DEBUG_TAG, "Cannot find location: ${p0.message}") else Log.w(DEBUG_TAG, "Failed to get the last location, error: ${p0.message}",) } }) } if(locationEnabledOnDevice){ setFollowUserLocation(true) } } override fun onMapLocationEnabled(active: Boolean) { //Extra stuff to do setFollowUserLocation(active) } @SuppressLint("MissingPermission") override fun onFirstReceivedLocation(location: Location) { val it = location if(locationInitialized && !receivedFirstLocation) { //only zoom if the user position is close enough to the center val newPoint = LatLng(it.latitude, it.longitude) if(newPoint.distanceTo(DEFAULT_LATLNG) > MAX_DIST_KM * 1000){ //show Toast if(!shownToastNoPosition) context?.let{ c-> Toast.makeText(c, R.string.too_far_not_showing_location, Toast.LENGTH_LONG).show() shownToastNoPosition = true } setLocationComponentEnabled(false) //Update UI Status mapStateViewModel.locationUserActive.value = false mapStateViewModel.followingUserPosition.value = false } else { map?.apply { animateCamera( CameraUpdateFactory.newCameraPosition( CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build() ), 1000 ) setLocationComponentEnabled(true) locationComponent.cameraMode = CameraMode.TRACKING mapStateViewModel.locationUserActive.value = true } setFollowUserLocation(true) } } else{ //check for this is when the map is used mapStateViewModel.locationToShow = location } } override fun setLocationIconEnabled(enabled: Boolean){ if (enabled) userLocationButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) else userLocationButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) } private fun updateFollowingIcon(enabled: Boolean){ if(enabled) followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.walk_circle_active)) else followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.walk_circle_inactive)) } /** * This sets both the status on the component if it has been activated and the icon in the Fragment */ private fun setFollowUserLocation(enabled: Boolean){ if(locationInitialized) { if (enabled) locationComponent.cameraMode = CameraMode.TRACKING else locationComponent.cameraMode = CameraMode.NONE } //update the icon by updating the livedata mapStateViewModel.followingUserPosition.value = enabled } companion object { private const val STOPS_SOURCE_ID = "stops-source" private const val STOPS_LAYER_ID = "stops-layer" private const val LABELS_LAYER_ID = "bus-labels-layer" private const val LABELS_SOURCE = "labels-source" private const val STOP_IMAGE_ID ="bus-stop-icon" const val DEFAULT_CENTER_LAT = 45.0708 const val DEFAULT_CENTER_LON = 7.6858 private val DEFAULT_LATLNG = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) private val DEFAULT_ZOOM = 14.3 private const val POSITION_FOUND_ZOOM = 16.5 private const val NO_POSITION_ZOOM = 17.1 private const val DEBUG_TAG = "BusTO-MapLibreFrag" private const val STOP_ACTIVE_IMG = "Stop-active" const val FRAGMENT_TAG = "BusTOMapFragment" private const val LOCATION_PERMISSION_REQUEST_CODE = 981202 /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param stop Eventual stop to center the map into * @return A new instance of fragment MapLibreFragment. */ @JvmStatic fun newInstance(stop: Stop?) = MapLibreFragment().apply { arguments = Bundle().let { // Cannot use Parcelable as it requires higher version of Android //stop?.let{putParcelable(STOP_TO_SHOW, it)} stop?.toBundle(it) } } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 0800925..cc0eca8 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,714 +1,716 @@ /* 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/ResultBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java index 1a77b6c..11e05bf 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java @@ -1,33 +1,32 @@ package it.reyboz.bustorino.fragments; import android.content.Context; import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; public abstract class ResultBaseFragment extends ScreenBaseFragment { protected FragmentListenerMain mListener; protected static final String MESSAGE_TEXT_VIEW = "message_text_view"; public ResultBaseFragment() { } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { mListener.showFloatingActionButton(false); mListener = null; super.onDetach(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java index dd07dee..999c427 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -1,280 +1,279 @@ /* BusTO - Fragments components Copyright (C) 2016 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.UserDB; - /** * This is a generalized fragment that can be used both for * * */ public class ResultListFragment extends Fragment{ // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER static final String LIST_TYPE = "list-type"; protected static final String LIST_STATE = "list_state"; protected static final String MESSAGE_TEXT_VIEW = "message_text_view"; private FragmentKind adapterKind; protected FragmentListenerMain mListener; protected TextView messageTextView; protected ListView resultsListView; private ListAdapter mListAdapter = null; boolean listShown; private Parcelable mListInstanceState = null; public ResultListFragment() { // Required empty public constructor } public ListView getResultsListView() { return resultsListView; } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param listType whether the list is used for STOPS or LINES (Orari) * @return A new instance of fragment ResultListFragment. */ public static ResultListFragment newInstance(FragmentKind listType, String eventualStopTitle) { ResultListFragment fragment = new ResultListFragment(); Bundle args = new Bundle(); args.putSerializable(LIST_TYPE, listType); if (eventualStopTitle != null) { args.putString(ArrivalsFragment.STOP_TITLE, eventualStopTitle); } fragment.setArguments(args); return fragment; } public static ResultListFragment newInstance(FragmentKind listType) { return newInstance(listType, null); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { adapterKind = (FragmentKind) getArguments().getSerializable(LIST_TYPE); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_list_view, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); if (adapterKind != null) { resultsListView = (ListView) root.findViewById(R.id.resultsListView); switch (adapterKind) { case STOPS: resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ Stop busStop = (Stop) parent.getItemAtPosition(position); mListener.requestArrivalsForStopID(busStop.ID); } }); // set the textviewMessage setTextViewMessage(getString(R.string.results)); break; case ARRIVALS: resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { String routeName; Route r = (Route) parent.getItemAtPosition(position); routeName = FiveTNormalizer.routeInternalToDisplay(r.getName()); if (routeName == null) { routeName = r.getDisplayCode(); } if (r.destinazione == null || r.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show(); } } }); String displayName = getArguments().getString(ArrivalsFragment.STOP_TITLE); setTextViewMessage(String.format( getString(R.string.passages_fill), displayName)); break; default: throw new IllegalStateException("Argument passed was not of a supported type"); } String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } } else Log.d(getString(R.string.list_fragment_debug), "No content root for fragment"); return root; } public boolean isFragmentForTheSameStop(Palina p) { if (!adapterKind.equals(FragmentKind.ARRIVALS)) return false; if (getTag() != null) return getTag().equals(getFragmentTag(p)); else return false; } public static String getFragmentTag(Palina p) { return "palina_"+p.ID; } @Override public void onResume() { super.onResume(); //Log.d(getString(R.string.list_fragment_debug),"Fragment restored, saved listAdapter is "+(mListAdapter)); if (mListAdapter != null) { ListAdapter adapter = mListAdapter; mListAdapter = null; resetListAdapter(adapter); } if (mListInstanceState != null) { Log.d("resultsListView", "trying to restore instance state"); resultsListView.onRestoreInstanceState(mListInstanceState); } switch (adapterKind) { case ARRIVALS: resultsListView.setOnScrollListener(new CommonScrollListener(mListener, true)); mListener.showFloatingActionButton(true); break; case STOPS: resultsListView.setOnScrollListener(new CommonScrollListener(mListener, false)); break; default: //NONE } mListener.readyGUIfor(adapterKind); } @Override public void onPause() { if (adapterKind.equals(FragmentKind.ARRIVALS)) { SwipeRefreshLayout reflay = getActivity().findViewById(R.id.listRefreshLayout); reflay.setEnabled(false); Log.d("BusTO Fragment " + this.getTag(), "RefreshLayout disabled"); } super.onPause(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement ResultFragmentListener"); } } @Override public void onDetach() { mListener.showFloatingActionButton(false); mListener = null; super.onDetach(); } @Override public void onDestroyView() { resultsListView = null; //Log.d(getString(R.string.list_fragment_debug), "called onDestroyView"); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); super.onDestroyView(); } @Override public void onViewStateRestored(@Nullable Bundle savedInstanceState) { super.onViewStateRestored(savedInstanceState); Log.d("ResultListFragment", "onViewStateRestored"); if (savedInstanceState != null) { mListInstanceState = savedInstanceState.getParcelable(LIST_STATE); Log.d("ResultListFragment", "listInstanceStatePresent :" + mListInstanceState); } } protected void resetListAdapter(ListAdapter adapter) { boolean hadAdapter = mListAdapter != null; mListAdapter = adapter; if (resultsListView != null) { resultsListView.setAdapter(adapter); resultsListView.setVisibility(View.VISIBLE); } } /** * Set the message textView * @param message the whole message to write in the textView */ public void setTextViewMessage(String message) { messageTextView.setText(message); messageTextView.setVisibility(View.VISIBLE); } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index 6f5306f..24c3234 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -1,179 +1,216 @@ +/* + BusTO - Fragments components + Copyright (C) 2018-2026 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.SharedPreferences; +import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; +import com.google.android.flexbox.FlexDirection; +import com.google.android.flexbox.FlexboxLayoutManager; +import com.google.android.flexbox.JustifyContent; import com.google.android.material.snackbar.Snackbar; import it.reyboz.bustorino.BuildConfig; import java.util.Map; import static android.content.Context.MODE_PRIVATE; public abstract class ScreenBaseFragment extends Fragment { protected final static String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs"; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + + protected void setOption(String optionName, boolean value) { Context mContext = getContext(); assert mContext != null; SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { Context mContext = getContext(); assert mContext != null; return getOption(mContext, optionName, optDefault); } protected void showToastMessage(int messageID, boolean shortT) { final int length = shortT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; final Context context = getContext(); if(context!=null) Toast.makeText(context, messageID, length).show(); } protected void makeToast(String message){ Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); } protected void makeToast(int messageID){ Toast.makeText(getContext(), messageID, Toast.LENGTH_SHORT).show(); } public void hideKeyboard() { if (getActivity()==null) return; View view = getActivity().getCurrentFocus(); if (view != null) { ((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } /** * Find the view on which the snackbar should be shown * @return a view or null if you don't want the snackbar shown */ @Nullable public abstract View getBaseViewForSnackBar(); /** * Empty method to override properties of the Snackbar before showing it * @param snackbar the Snackbar to be possibly modified */ public void setSnackbarPropertiesBeforeShowing(Snackbar snackbar){ } public boolean showSnackbarOnDBUpdate() { return true; } public static boolean getOption(Context context, String optionName, boolean optDefault){ SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE); return preferences.getBoolean(optionName, optDefault); } public static void setOption(Context context,String optionName, boolean value) { SharedPreferences.Editor editor = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.apply(); } public ActivityResultLauncher getPositionRequestLauncher(LocationRequestListener listener){ return 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; final boolean coarseGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)); final boolean fineGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION)); if (coarseGranted != fineGranted){ Log.e("BusTO-ScreenBaseFragment", "the two permissions have different values, coarse "+ coarseGranted +", fineGranted "+fineGranted); } listener.onPermissionResult(coarseGranted || fineGranted); } }); } + + protected FlexboxLayoutManager getFlexLayoutManager(@NonNull Context context) { + var layoutManager = new FlexboxLayoutManager(context); + layoutManager.setFlexDirection(FlexDirection.ROW); + layoutManager.setJustifyContent(JustifyContent.FLEX_START); + + return layoutManager; + } /*protected void runActionFavorites(@NonNull Stop s, @NonNull FavoritesChangeWorker.Action action, @NonNull FavoritesChangeWorker.Companion.ResultListener resultListener){ Context mContext = requireContext(); WorkManager workManager = WorkManager.getInstance(mContext); WorkRequest req = FavoritesChangeWorker.makeRequest(s, action); workManager.enqueue(req); Context appContext = mContext.getApplicationContext(); //FavoritesChangeWorker.registerListener(mContext, getViewLifecycleOwner(), s, action, resultListener); workManager.getWorkInfosByTagLiveData(FavoritesChangeWorker.getTag(s, action)) .observe(getViewLifecycleOwner(), wi -> { Log.d("BusTO-BaseFragment", "workinfo for stop "+s.ID+" has arrived"); if(wi.isEmpty()){ return; } WorkInfo workInfo = wi.get(wi.size() - 1); Data progress = wi.get(wi.size()-1).getProgress(); int actvalue = progress.getInt(ACTION_ARG,-1); boolean done = progress.getBoolean(DONE_ARG, false); if (done) { // at this point the action should be just ADD or REMOVE if (actvalue == FavoritesChangeWorker.Action.ADD.getValue()) { // now added Toast.makeText(appContext, R.string.added_in_favorites, Toast.LENGTH_SHORT).show(); } else if (actvalue == FavoritesChangeWorker.Action.REMOVE.getValue()) { // now removed Toast.makeText(appContext, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show(); } } else { // wtf Toast.makeText(appContext, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show(); } Log.d("busTO-ScreenBaseFragm", "favorites action="+actvalue+ ",done="+done); // aggiorna UI resultListener.doStuffWithResult(done); }); } */ public static void applyBottomInsetAsPadding(ViewGroup scrollableView) { final int originalPaddingBottom = scrollableView.getPaddingBottom(); scrollableView.setClipToPadding(false); // ora lo trova ViewCompat.setOnApplyWindowInsetsListener(scrollableView, (v, insets) -> { Insets bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime() ); v.setPadding( v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), originalPaddingBottom + bars.bottom ); return insets; }); } + public interface LocationRequestListener{ void onPermissionResult(boolean locationGranted); } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java index 4ec5795..17f23db 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java @@ -1,150 +1,147 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; -import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.StopsTable; import it.reyboz.bustorino.adapters.StopAdapter; -import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.List; public class StopListFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks { private List stopList; private StopAdapter mListAdapter; private static final String[] dataProjection={StopsTable.COL_LINES_STOPPING,StopsTable.COL_PLACE,StopsTable.COL_TYPE,StopsTable.COL_LOCATION}; private static final String KEY_STOP_ID = "stopID"; private static final String WORDS_SEARCHED= "query"; private static final int EXTRA_ID=160; private String searchedWords; public StopListFragment(){ //required empty constructor } public static StopListFragment newInstance(String searchQuery) { Bundle args = new Bundle(); //TODO: search stops inside the DB args.putString(WORDS_SEARCHED,searchQuery); StopListFragment fragment = new StopListFragment(); args.putSerializable(LIST_TYPE,FragmentKind.STOPS); fragment.setArguments(args); return fragment; } public void setStopList(List stopList){ this.stopList = stopList; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); mListener.readyGUIfor(FragmentKind.STOPS); if(stopList!=null) { mListAdapter = new StopAdapter(getContext(),stopList); resetListAdapter(mListAdapter); for (int i = 0; i < stopList.size(); i++) { final Bundle b = new Bundle(); b.putString(KEY_STOP_ID, stopList.get(i).ID); loaderManager.restartLoader(i, b, this); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); searchedWords = getArguments().getString(WORDS_SEARCHED); } @Override public Loader onCreateLoader(int id, Bundle args) { //The id will be the position of the element in the list Uri.Builder builder = new Uri.Builder(); String stopID = args.getString(KEY_STOP_ID); //Log.d("StopListLoader","Creating loader for stop "+stopID+" in position: "+id); if(stopID!=null) { builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stop").appendPath(stopID); CursorLoader cursorLoader = new CursorLoader(getContext(),builder.build(),dataProjection,null,null,null); return cursorLoader; } else return null; } @Override public void onLoadFinished(Loader loader, Cursor data) { //check that we have valid data if(data==null) return; final int numRows = data.getCount(); final int elementIdx = loader.getId(); if (numRows==0) { Log.w(this.getClass().getName(),"No info for stop in position "+elementIdx); return; } else if(numRows>1){ Log.d("StopLoading","we have "+numRows+" rows, should only have 1. Taking the first..."); } final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); data.moveToFirst(); Stop stopToModify = stopList.get(elementIdx); final String linesStopping = data.getString(linesIndex); stopToModify.setRoutesThatStopHere(Arrays.asList(linesStopping.split(","))); try { final String possibleLocation = data.getString(data.getColumnIndexOrThrow(StopsTable.COL_LOCATION)); if (stopToModify.location == null && possibleLocation != null && !possibleLocation.isEmpty() && !possibleLocation.equals("_")) { stopToModify.location = possibleLocation; } if (stopToModify.type == null) { stopToModify.type = Route.Type.fromCode(data.getInt(data.getColumnIndex(StopsTable.COL_TYPE))); } }catch (IllegalArgumentException arg){ if(arg.getMessage().contains("'location' does not exist")) Log.w("StopLoading","stop with no location found"); } //Log.d("StopListFragmentLoader","Finished parsing data for stop in position "+elementIdx); mListAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader loader) { loader.abandon(); } } diff --git a/app/src/main/res/layout/round_line_header.xml b/app/src/main/res/layout/round_line_header.xml index 66159b4..3448368 100644 --- a/app/src/main/res/layout/round_line_header.xml +++ b/app/src/main/res/layout/round_line_header.xml @@ -1,37 +1,37 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/drawer_main.xml b/app/src/main/res/menu/drawer_main.xml index 0c2f4a4..ed03092 100644 --- a/app/src/main/res/menu/drawer_main.xml +++ b/app/src/main/res/menu/drawer_main.xml @@ -1,33 +1,38 @@ + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 01dc903..b4b6bb3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,260 +1,260 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca Codice QR Scansiona codice QR alla fermata Si No Prossimo Precedente Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Ricerca arrivi da %1$s Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Fermata: Linea Linee Linee urbane Linee extraurbane Linee turistiche Direzione: Nessuna linea in questa categoria Nessuna linea corrisponde alla ricerca Filtra per nome Linea %1$s Linee: %1$s Linea %1$s, direzione: Fermata %1$s Scegli la fermata… Matricola %1$s Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Vai alla wiki https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Mostra linea Vedi direzione Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari:\n 12:56* Orario in tempo reale\n 12:56 Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo OK! Benvenuto!\n \n

Grazie per aver scelto BusTO, un\'app open source e indipendente da GTT/5T, per spostarsi a Torino attraverso software libero!

\n

BusTO rispetta la tua privacy non raccogliendo nessun dato sull\'utilizzo, ed è leggera e senza pubblicità!

\n
\n

Qui puoi trovare più informazioni e link riguardo al progetto.

\n\t\t
\n

Schermata iniziale

\n

Se vuoi rivedere la schermata iniziale, usa il pulsante qui sotto:"]]> Notizie e aggiornamenti

Nel canale Telegram puoi trovare informazioni sugli ultimi aggiornamenti dell\'app

]]>
Ma come funziona?\n

Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito www.gtt.to.it, www.5t.torino.it, muoversiatorino.it \"per uso personale\" e altre fonti Open Data (aperto.comune.torino.it).

\n
\n\t\t

Ingredienti:
\n\t\t- Fabio Mazza attuale rockstar developer anziano.
\n\t\t- Andrea Ugo attuale rockstar developer in formazione.
\n\t\t- Silviu Chiriac designer del logo 2021.
\n\t\t- Marco M formidabile tester e cacciatore di bug.
\n\t\t- Ludovico Pavesi ex rockstar developer anziano asd.
\n\t\t- Valerio Bozzolan attuale manutentore.
\n\t\t- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
\n\t\t- JSoup libreria per \"web scaping\".
\n\t\t- Google icone e librerie di supporto e design.
\n\t\t- Altre icone da Bootstrap, Feather e Hero Icons
\n\t\t- Tutti i contributori e i beta tester!\n\t\t

\n
\n\t\tSe vuoi avere più informazioni o contribuire allo sviluppo, usa i pulsanti qui sotto!"]]>
Licenze\n\t\t

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3 (https://www.gnu.org/licenses/gpl-3.0.html).\n\t\tCiò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan e agli altri autori del codice dell\'app.\n\t\t

\n\n\t\t
\n\t\t

Note

\n\t\t

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.

\n\t\t

Tutti i dati utilizzati dall\'app provengono direttamente da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.

\n\t\t

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata - Vicino a me + Vicino a me Fermate vicine Ricerca della posizione Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Lancia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Consenti l\'accesso alla posizione per mostrare le fermate vicine Abilitare la posizione sul dispositivo arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Canale telegram Mostra introduzione La mia posizione Segui posizione Attiva o disattiva posizione Posizione attivata Posizione disattivata La posizione è disabilitata sul dispositivo Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Nessun passaggio per le linee: Canale default delle notifiche Operazioni sul database Informazioni sul database (aggiornamento) BusTO - posizioni in tempo reale Posizioni in tempo reale Attività del servizio delle posizioni in tempo reale Servizio posizioni MaTO in tempo reale attivo Download dei trip dal server MaTO Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio! di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Download dati dal server MaTO Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Fonte posizioni in tempo reale di bus e tram MaTO (aggiornate più spesso, può avere errori) GTFS RT (aggiornato meno frequentemente, posizioni più controllate) Linea aggiunta ai preferiti Linea rimossa dai preferiti Preferite Tocca a lungo la fermata per le opzioni Stile della mappa Versatiles (vettoriale) OSM Legacy (raster, più leggera) Rimuovi i dati dei trip (libera spazio) Tutti i trip GTFS sono rimossi dal database Mostra introduzione open source per il trasporto pubblico di Torino. Stai usando un\'app indipendente, senza pubblicità e senza nessun tracciamento.]]> Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]> preferiti toccando la stella a fianco del nome.]]> fermate più vicine a te direttamente nella schermata principale...]]> posizioni in tempo reale dei bus e tram (in blu)]]> Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Informazioni per sapere di più sull\'app e il team di sviluppo.]]> Capito, chiudi introduzione Chiudi introduzione Abilita accesso alla posizione Accesso alla posizione abilitato Accesso alla posizione non consentito dall\'utente Abilita notifiche Notifiche abilitate Backup e ripristino Dati salvati Backup Ripristino Backup completato Salva o ripristina i dati Salva backup Importa i dati dal backup Backup importato Seleziona almeno un elemento da importare! Importa preferiti dal backup Importa preferenze dal backup Nessuna app disponibile per mostrare la fermata! Destinazione sconosciuta Direzione già selezionata Sei troppo lontano, posizione nascosta Caricamento destinazione… Ciao fragment vuoto Il servizio delle posizioni funziona normalmente Nessuna posizione ricevuta Errore nella connessione al server Errore nel parsing della risposta Errore: risposta dal server inaspettata In connessione... Fonte posizioni in tempo reale: Cambia fonte Rimuovi posizioni sulla mappa quando si cambia fonte delle posizioni in tempo reale Aggiornato: %1$s Nessun avviso nella tua lingua, mostrati in %1$s Italiano Inglese Avvisi per la linea %1$s: Controllo degli avvisi disponibili in corso Servizio GPS non trovato sul dispositivo! Download dati avvisi in tempo reale Permesso mancante Per usare questa funzionalità, l\'app necessita dell\'accesso alla posizione, che ora può essere dato solo dalle impostazioni di sistema. Apri le impostazioni Premi ancora \"indietro\" per uscire dall\'app
diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index b6dfada..c54d6ee 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -1,41 +1,43 @@ layout_pref pref_update_db_now mqtt gtfsrt + arrivals + nearby favorites map lines matofetcher fivetapifetcher gttjsonfetcher fivetscraper matofetcher gttjsonfetcher pref_positions_source @string/positions_source_mqtt @string/positions_source_gtfsrt versatiles_c osm_legacy \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b11345d..56bce34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,405 +1,407 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop has this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Arrivals at: Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines No lines found in this category No lines match the searched name Destination: Lines: %1$s Line %1$s Line %1$s towards: Stop %1$s Vehicle %1$s No timetable found No QR code found, try using another app to scan Scan QR code at the bus stop Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Open the wiki https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, an open source and independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


Introductory tutorial

If you want to see the introduction again, use the button below:

]]>
News and Updates

On the Telegram channel, you can find information about the latest app updates

]]>
How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google for icons and support and design libraries.
- Other icons from Bootstrap, Feather, and Hero Icons.
- All the contributors, and the beta testers, too!


If you want more information or want to contribute to development, use the buttons below! ]]>
Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Show line details Show full direction Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating - - Near me + + + Near me Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device No GPS receiver found on the device! Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Show introduction Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s Loading arrivals from %1$s GTT App GTT Website 5T Torino website Muoversi a Torino Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database BusTO - live position service Live positions Showing activity related to the live positions service MaTO live bus positions service is running Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Home Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Filter by name Launching database update Downloading data from MaTO server Downloading realtime alerts data Capitalize directions @string/directions_capitalize_no_change @string/directions_capitalize_everything @string/directions_capitalize_first_letter Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_home_text + @string/near_me_title @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might have errors) GTFS RT (less frequently updated, more accurate) @string/positions_source_mato_descr @string/positions_source_gtfsrt_descr "You are too far, not showing your position Style of the map Versatiles (vector) OSM legacy (raster, lighter) @string/map_style_versatiles @string/map_style_legacy_raster Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted Location permission has not been granted Missing permission To use this functionality, the application needs access to the location, which now can only be granted in the system settings. Open settings OK, close the tutorial Close the tutorial Enable notifications Notifications enabled Backup and restore Backup Restore Backup or restore data Data saved Backup completed Backup to file Import data from backup Backup has been imported Check at least one item to import! Import favorites from backup Import preferences from backup Hello blank fragment No map app present to show the stop! Direction is already shown Loading destination… Destination unknown Service for positions working normally No positions received Error connecting to the server Error parsing the server response Error: network response is not as expected Connecting... MaTO GTFS RT Live positions source: Switch source Clear bus positions when switching live positions source Updated: %1$s Alerts for line %1$s: No alerts in your language, showing in %1$s Italian English Checking new alerts now Press back again to close the app