diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index cf0a5e5..e573d42 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,104 +1,120 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.os.Bundle; import android.util.Log; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.FragmentTransaction; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { final static String DEBUG_TAG = "ExperimentsActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_container_fragment); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); actionBar.setIcon(R.drawable.ic_launcher); } if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) /* .add(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs("gtt:4U")) */ //.add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) //.add(R.id.fragment_container_view, IntroFragment.class, IntroFragment.makeArguments(0)) //.commit(); //.add(R.id.fragment_container_view, LinesDetailFragment.class, // LinesDetailFragment.Companion.makeArgs("gtt:4U")) .add(R.id.fragment_container_view, AlertsFragment.class, null) .commit(); } } @Override public void showFloatingActionButton(boolean yes) { Log.d(DEBUG_TAG, "Asked to show the action button"); } @Override public void readyGUIfor(FragmentKind fragmentType) { Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType); } @Override public void requestArrivalsForStopID(String ID) { } @Override - public void showMapCenteredOnStop(Stop stop) { + public void showMapCenteredOnStop(@Nullable Stop stop) { } + + @Override + public void openLinesFragment() { + Log.d(DEBUG_TAG, "Asked to open lines grid fragment"); + } + + @Override + public void openFavoritesFragment() { + + } + @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineonMap-"+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("Line-"+routeGtfsId); tr.commit(); } + @Override + public void openNearbyStopsFragment() { + Log.d(DEBUG_TAG, "Requested to open nearby stops fragment"); + } + } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index 817f8a4..c4ee229 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,861 +1,896 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.*; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.*; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateCheckWorker; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.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 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); /*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, ""); - //if (vl.length() == 0 || vl.equals("arrivals")) { - // showMainFragment(); + Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { + var framan = getSupportFragmentManager(); //we are not restarting the activity from nothing if (vl.equals("map")) { requestMapFragment(false); } else if (vl.equals("favorites")) { - checkAndShowFavoritesFragment(getSupportFragmentManager(), false); + checkAndShowFavoritesFragment(framan, false); } else if (vl.equals("lines")) { - showLinesFragment(getSupportFragmentManager(), false, null); + showLinesFragment(framan, false, null); } else { - showMainFragment(false); + showMainFragmentFromClick(false); } } onCreateComplete = true; //last but not least, set the good default values checkApplyDefaultSettingsValues(); // handle the device "insets" + /* ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rootRelativeLayout), (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Apply the insets as a margin to the view. This solution sets only the // bottom, left, and right dimensions, but you can apply whichever insets are // appropriate to your layout. You can also update the view padding if that's // more appropriate. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; mlp.rightMargin = insets.right; v.setLayoutParams(mlp); //set for toolbar //mlp = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams(); //mlp.topMargin = insets.top; //mToolbar.setLayoutParams(mlp); mToolbar.setPadding(0, insets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); - /* - ViewCompat.setOnApplyWindowInsetsListener(mToolbar, (v, windowInsets) -> { - Insets statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); - // Apply the insets as a margin to the view. - ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); - mlp.topMargin = statusBarInsets.top; - v.setLayoutParams(mlp); - v.setPadding(0, statusBarInsets.top, 0, 0); - // Return CONSUMED if you don't want the window insets to keep passing - // down to descendant views. - return WindowInsetsCompat.CONSUMED; - }); - - */ //to properly handle IME WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { insetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } + */ + // 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(); - showMainFragment(true); + showMainFragmentFromClick(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); showLinesFragment(getSupportFragmentManager(), true,null); return true; } else if(menuItem.getItemId() == R.id.drawer_action_info) { closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } //requesting permissions @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } /*@Override public void onBackPressed() { if (!activityCustomBackPressed()) super.onBackPressed(); } */ private boolean activityCustomBackPressed(){ boolean resolved = true; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStack(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); } else{ resolved = false; } return resolved; } /** * Create and show the SnackBar with the message * The fragment shown points to which view to attach the snackbar */ private void 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"); } } /** - * Show the fragment by adding it to the backstack + * 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(); } /** - * Show the fragment by adding it to the backstack + * 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 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(); } - private void showMainFragment(boolean addToBackStack){ - FragmentManager fraMan = getSupportFragmentManager(); - Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); - final MainScreenFragment mainScreenFragment; - if (fragment==null | !(fragment instanceof MainScreenFragment)){ - createShowMainFragment(fraMan, null, addToBackStack); - } - else if(!fragment.isVisible()){ - - - mainScreenFragment = (MainScreenFragment) fragment; - showMainFragment(fraMan, mainScreenFragment, addToBackStack); - Log.d(DEBUG_TAG, "Found the main fragment"); - } else{ - mainScreenFragment = (MainScreenFragment) fragment; - } - //return mainScreenFragment; - } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } + /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); if (mainFragmentIfVisible!=null){ mainFragmentIfVisible.readyGUIfor(fragmentType); } - int titleResId; + 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); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: + case HOME_BUTTONS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; - default: - titleResId = 0; } - if(getSupportActionBar()!=null && titleResId!=0) + 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){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment, true); probableFragment.requestArrivalsForStopID(ID); } else { // we have no fragment - final Bundle args = new Bundle(); - args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID); //if onCreate is complete, then we are not asking for the first showing fragment + 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); } @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); + } + 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(Stop stop) { + 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 new file mode 100644 index 0000000..d5a38b7 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RecyclerViewMargin.java @@ -0,0 +1,145 @@ +/* + 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 "); + } + 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/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java index ebd050b..1b83eaf 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -1,415 +1,414 @@ /* BusTO (backend components) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.text.Html; import android.text.Spanned; import android.util.Log; import android.util.TypedValue; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.fragments.SettingsFragment; public abstract class utils { private static final double EARTH_RADIUS = 6371.009e3; public static final String SOURCE_CODE_URL ="https://gitpull.it/source/libre-busto/"; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EARTH_RADIUS *c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/ EARTH_RADIUS); } - public static int convertDipToPixelsInt(Context con,double dips) - { - return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); + public static int convertDipToPixelInt(Context context, int dp) { + return Math.round(dp * context.getResources().getDisplayMetrics().density); } /** * Convert distance in meters on Earth in degrees of latitude, keeping the same longitude * @param distanceMeters distance in meters * @return angle in degrees */ public static Double latitudeDelta(Double distanceMeters){ final double angleRad = distanceMeters/EARTH_RADIUS; return Math.toDegrees(angleRad); } /** * Convert distance in meters on Earth in degrees of longitude, keeping the same latitude * @param distanceMeters distance in meters * @param latitude the latitude that is fixed * @return angle in degrees */ public static Double longitudeDelta(Double distanceMeters, Double latitude){ final double theta = Math.toRadians(latitude); final double denom = Math.abs(Math.cos(theta)); final double angleRad = 2*Math.asin(Math.sin(distanceMeters / EARTH_RADIUS) / denom); return Math.toDegrees(angleRad); } public static float convertDipToPixels(Context con, float dp){ return convertDipToPixels(con.getResources(), dp); } public static float convertDipToPixels(Resources res, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,res.getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } //SPLIT ON ', check if contains "D'" if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ sb.append("D'"); subs = subs.substring(2); } int index = 0; char c = subs.charAt(index); if(subs.length() > 1 && c=='('){ sb.append(c); index += 1; c = subs.charAt(index); } sb.append(Character.toUpperCase(c)); if (lowercaseRest) sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else sb.append(subs.substring(index+1)); } if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static List getDefaultArrivalsFetchers(Context context){ SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); final Set setSelected = new HashSet<>(defSharPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>())); if (setSelected.isEmpty()) { return Arrays.asList(new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()); }else{ ArrayList outFetchers = new ArrayList<>(4); /*for(String s: setSelected){ switch (s){ case "matofetcher": outFetchers.add(new MatoAPIFetcher()); break; case "fivetapifetcher": outFetchers.add(new FiveTAPIFetcher()); break; case "gttjsonfetcher": outFetchers.add(new GTTJSONFetcher()); break; case "fivetscraper": outFetchers.add(new FiveTScraperFetcher()); break; default: throw new IllegalArgumentException(); } }*/ if (setSelected.contains("matofetcher")) { outFetchers.add(new MatoAPIFetcher()); setSelected.remove("matofetcher"); } if (setSelected.contains("fivetapifetcher")) { outFetchers.add(new FiveTAPIFetcher()); setSelected.remove("fivetapifetcher"); } if (setSelected.contains("gttjsonfetcher")){ outFetchers.add(new GTTJSONFetcher()); setSelected.remove("gttjsonfetcher"); } if (setSelected.contains("fivetscraper")) { outFetchers.add(new FiveTScraperFetcher()); setSelected.remove("fivetscraper"); } if(!setSelected.isEmpty()){ Log.e("BusTO-Utils","Getting some fetchers values which are not contemplated: "+setSelected); } return outFetchers; } } /*public String getShorterDirection(String headSign){ String[] parts = headSign.split(","); if (parts.length<=1){ return headSign.trim(); } String first = parts[0].trim(); String second = parts[1].trim(); String firstLower = first.toLowerCase(Locale.ITALIAN); switch (firstLower){ case "circolare destra": case "circolare sinistra": case } }*/ /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i Set convertArrayToSet(T[] array) { // Create an empty Set Set set = new HashSet<>(); // Add each element into the set set.addAll(Arrays.asList(array)); // Return the converted Set return set; } public static String giveClassesForArray(T[] array){ StringBuilder sb = new StringBuilder(); for (T f: array){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } return sb.toString(); } public static Spanned convertHtml(String text) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT); } else { return Html.fromHtml(text); } } /** * Convert an integer (long) timestamp into a String * @param timestamp the timestamp in seconds (NOT milliseconds) * @return the formatted String */ public static String unixTimestampToLocalTime(long timestamp){ return unixTimestampToLocalTime(timestamp, "dd/MM/yyyy HH:mm:ss"); } /** * Convert an integer (long) timestamp into a String * @param timestamp the timestamp in seconds (NOT milliseconds) * @param patternFormat the format to convert it to * @return the formatted String */ public static String unixTimestampToLocalTime(long timestamp, String patternFormat) { Date date = new Date(timestamp * 1000L); // seconds to milliseconds SimpleDateFormat format = new SimpleDateFormat(patternFormat, Locale.getDefault()); return format.format(date); } public static Double roundDecimalUsingBigDecimal(Double value, int decimalPlace) { return new BigDecimal(value).setScale(decimalPlace, RoundingMode.HALF_UP).stripTrailingZeros().doubleValue(); } } 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 07a8ce5..6b7854d 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -1,836 +1,843 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.database.Cursor import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.middleware.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 fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName = route.routeLongDisplayName Log.d(DEBUG_TAG, "Make toast for line " + route.name) if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { Log.d( DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" ) if (route.gtfsId != null) { mListener.openLineFromStop(route.gtfsId, stopID) } else { val gtfsID = FiveTNormalizer.getGtfsRouteID(route) Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") mListener.openLineFromStop(gtfsID, stopID) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID arrivalsViewModel.setStopId(stopID) //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) preMessageTextView = root.findViewById(R.id.arrivalsTextView) addToFavorites = root.findViewById(R.id.addToFavorites) openInMapButton = root.findViewById(R.id.openInMapButton) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) arrivalsSourceTextView = root.findViewById(R.id.timesSourceTextView) arrivalsSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider arrivalsSourceTextView.setText(R.string.arrival_source_changing) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } arrivalsSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites toggleStopFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages_fill), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } noArrivalsRecyclerView.setLayoutManager(layoutManager) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times 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) } 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 if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) noArrivalsRecyclerView.adapter = noArrivalsAdapter noArrivalsRecyclerView.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) arrivalsSourceTextView.text = base_message arrivalsSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { arrivalsSourceTextView.visibility = View.VISIBLE } else { arrivalsSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the stop title in the fragment */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { //setTextViewMessage(getString(R.string.passages_fill, message)) setTextViewMessage(message) } } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader when (id) { loaderFavId -> { builder.appendPath("favorites").appendPath(stopID) cl = CursorLoader(requireContext(), builder.build(), UserDB.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(p: Palina): Boolean { - return if (tag != null) tag == getFragmentTag(p) + 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(p: Palina): String { - return "palina_" + p.ID - } + 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/BarcodeFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/BarcodeFragment.kt new file mode 100644 index 0000000..199a9e2 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/BarcodeFragment.kt @@ -0,0 +1,52 @@ +package it.reyboz.bustorino.fragments + +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.core.net.toUri +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.middleware.BarcodeScanContract +import it.reyboz.bustorino.middleware.BarcodeScanOptions +import it.reyboz.bustorino.middleware.BarcodeScanUtils + +//TODO: This might be probably implemented as interface +abstract class BarcodeFragment : ScreenBaseFragment(){ + + private val barcodeLauncher = registerForActivityResult(BarcodeScanContract(), ActivityResultCallback { + result -> + if (result != null && result.contents != null) { + //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); + val uri: Uri + try { + uri = result.contents.toUri() // this apparently prevents NullPointerException. Somehow. + } catch (e: Exception) { + Log.w("BusTO-BarcodeFragment","Cannot read QR code",e) + if (context != null) Toast.makeText( + requireContext(), + R.string.no_qrcode, Toast.LENGTH_SHORT + ).show() + return@ActivityResultCallback + } + val busStopID = utils.getBusStopIDFromUri(uri) + onQrScanSuccess(busStopID) + } else { + if (context != null) Toast.makeText( + requireContext(), R.string.no_qrcode, Toast.LENGTH_SHORT + ).show() + } + }) + + abstract fun onQrScanSuccess(busIDToSearch: String) + + protected fun launchBarcodeScan() { + val scanOptions = BarcodeScanOptions() + val intent = scanOptions.createScanIntent() + if (!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)) { + BarcodeScanUtils.showDownloadDialog(null, this) + } else { + barcodeLauncher.launch(scanOptions) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt new file mode 100644 index 0000000..3ae55e8 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt @@ -0,0 +1,218 @@ +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.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 + + 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) + items = listOf( + CardMenuItem(CardAction.NEARBY, getString(R.string.nearby_message_home_card), 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/CommonFragmentListener.java b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java index 7970bd1..c6a2f93 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -1,53 +1,65 @@ package it.reyboz.bustorino.fragments; import android.os.Bundle; import androidx.annotation.Nullable; import it.reyboz.bustorino.backend.Stop; public interface CommonFragmentListener { /** * Tell the activity that we need to disable/enable its floatingActionButton * @param yes or no */ void showFloatingActionButton(boolean yes); /** * Sends the message to the activity to adapt the GUI * to the fragment that has been attached * @param fragmentType the type of fragment attached */ void readyGUIfor(FragmentKind fragmentType); /** * Houston, we need another fragment! * * @param ID the Stop ID */ void requestArrivalsForStopID(String ID); /** * Method to call when we want to hide the keyboard */ void hideKeyboard(); /** * We want to open the map on the specified stop * @param stop needs to have location data (latitude, longitude) */ - void showMapCenteredOnStop(Stop stop); + void showMapCenteredOnStop(@Nullable Stop stop); /** * We want to show the line in detail for route coming from a stop * @param routeGtfsId the route gtfsID (eg, "gtt:10U") */ void openLineFromStop(String routeGtfsId, @Nullable String fromStopID); /** * Open the line screen on the line, from a live vehicle (optional pattern) * @param routeGtfsId the route gtfsID (eg, "gtt:10U") * @param optionalPatternId the pattern name (can be null) * @param args extra arguments given as Bundle */ void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args); + + /** + * Show the nearby stops fragment + */ + void openNearbyStopsFragment(); + + /** + * Show the lines + */ + void openLinesFragment(); + + void openFavoritesFragment(); } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java index 5d412b7..5dc985f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,287 +1,283 @@ /* BusTO (fragments) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import android.os.AsyncTask; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; private final FragmentListenerMain listenerMain; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; private final StopSearcher stopSearcher; - private boolean shouldHaltAllActivities=false; public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { this.listenerMain = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); stopSearcher = new StopSearcher(this); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ - public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){ - boolean sameFragment; + public void showArrivalsFragmentForStop(@NonNull Palina p, boolean addToBackStack){ + boolean sameFragment = false; ArrivalsFragment arrivalsFragment = null; + final FragmentManager fm = managerWeakRef.get(); + if(fm == null) return; - if(managerWeakRef.get()==null || shouldHaltAllActivities) { - //SOMETHING WENT VERY WRONG - Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); - return; - } - - FragmentManager fm = managerWeakRef.get(); - - if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) { - arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); - //Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?"); - if (arrivalsFragment == null) sameFragment = false; - else sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); - } else { - sameFragment = false; - Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); + if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment frag) { + sameFragment = frag.isFragmentForTheSameStop(p); + if(sameFragment) { + arrivalsFragment = frag; + Log.d("BusTO", "Same bus stop, accessing existing fragment"); + } } - setLastSuccessfullySearchedBusStop(p); - if (sameFragment){ - Log.d("BusTO", "Same bus stop, accessing existing fragment"); - arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); - if (arrivalsFragment == null) sameFragment = false; - } - if(!sameFragment) { - //set the String to be displayed on the fragment - String displayName = p.getStopDisplayName(); - if (displayName != null && displayName.length() > 0) { - arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); - } else { - arrivalsFragment = ArrivalsFragment.newInstance(p.ID); + if(!sameFragment) { + // get old fragment + var frag = fm.findFragmentByTag(ArrivalsFragment.getFragmentTag(p)); + if(frag instanceof ArrivalsFragment) { + attachFragmentToContainer(fm, frag, null, true, addToBackStack); + arrivalsFragment = (ArrivalsFragment) frag; + } else { // create new fragment + //set the String to be displayed on the fragment + String displayName = p.getStopDisplayName(); + if (displayName != null && !displayName.isEmpty()) { + arrivalsFragment = ArrivalsFragment.newInstance(p.ID, displayName); + } else { + arrivalsFragment = ArrivalsFragment.newInstance(p.ID); + } + String probableTag = ArrivalsFragment.getFragmentTag(p); + attachFragmentToContainer(fm, arrivalsFragment, probableTag, true, addToBackStack); } - String probableTag = ArrivalsFragment.getFragmentTag(p); - attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } - // DO NOT CALL `setListAdapter` ever on arrivals fragment - arrivalsFragment.updateFragmentData(p); + setLastSuccessfullySearchedBusStop(p); + // update the data only if I have information about the passaggi + if(p.getTotalNumberOfPassages() > 0) + arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); listenerMain.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ listenerMain.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); - if(managerWeakRef.get()==null || shouldHaltAllActivities) { + if(managerWeakRef.get()==null) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } - attachFragmentToContainer(managerWeakRef.get(),listfragment, - new AttachParameters("search_"+query, false,addToBackStack)); + attachFragmentToContainer(managerWeakRef.get(), + listfragment, "search_"+query, false, addToBackStack); listfragment.setStopList(resultList); //listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ listenerMain.toggleSpinner(on); } /** - * Attach a new fragment to a cointainer + * Attach a new fragment to the appropriate container * @param fm the FragmentManager * @param fragment the Fragment - * @param parameters attach parameters + * @param tagAttach attach tag (can be null, the fragment's own tag has preference) + * @param addToBackStack if the transaction is to be added to the stack + * @param toSecondaryFrame if the fragment goes to the secondary frame */ - protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){ - if(shouldHaltAllActivities) //nothing to do - return; + protected void attachFragmentToContainer(FragmentManager fm, Fragment fragment, @Nullable String tagAttach, boolean toSecondaryFrame, boolean addToBackStack){ + FragmentTransaction ft = fm.beginTransaction(); int frameID; - if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) - // ft.replace(secondaryFrameLayout,fragment,tag); + if(toSecondaryFrame && secondaryFrameLayout!=NO_FRAME) frameID = secondaryFrameLayout; - else frameID = primaryFrameLayout; - switch (parameters.transaction){ - case REPLACE: - ft.replace(frameID,fragment,parameters.tag); - - } - if (parameters.addToBackStack) - ft.addToBackStack("state_"+parameters.tag); + else + frameID = primaryFrameLayout; + var tag = fragment.getTag(); + if(tag == null) tag = tagAttach; + // there is only one case + //switch (pars.transaction){ + // case REPLACE: + ft.replace(frameID,fragment,tag); + //} + if (addToBackStack) + ft.addToBackStack("state_"+tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); - if(!fm.isDestroyed() && !shouldHaltAllActivities) - ft.commit(); + ft.commit(); //fm.executePendingTransactions(); } - public synchronized void setBlockAllActivities(boolean shouldI) { - this.shouldHaltAllActivities = shouldI; - } - public void stopLastRequestIfNeeded(){ /*if(lastTaskRef == null) return; AsyncTask task = lastTaskRef.get(); if(task!=null){ task.cancel(interruptIfRunning); } */ stopSearcher.cancelLastRequest(); } public void requestStopSearch(String query){ stopSearcher.cancelLastRequest(); stopSearcher.runRequest(query, new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); // run with the default fetchers } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.Result res, SearchRequestType type){ //TODO: implement a common set of errors for all fragments if (res==null){ Log.e(DEBUG_TAG, "Asked to show result with null result"); return; } Log.d(DEBUG_TAG, "Showing result for "+res); switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: if (type == SearchRequestType.STOPS) showShortToast(R.string.no_bus_stop_have_this_name); else if(type == SearchRequestType.ARRIVALS){ showShortToast(R.string.no_arrivals_stop); } break; case NOT_FOUND: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } - + /* + // 18/05/2026: Commenting, do not remove, might be useful later enum Transaction{ REPLACE, } - static final class AttachParameters { + private static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } + + */ } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentKind.java index 721b1ae..10d6acb 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,23 @@ /* 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; public enum FragmentKind { STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS, MAP, MAIN_SCREEN_FRAGMENT, - LINES + LINES, HOME_BUTTONS } 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 429830f..f98e7e5 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,773 +1,886 @@ /* 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.os.Handler; 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.util.List; +import java.security.InvalidParameterException; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; -import it.reyboz.bustorino.data.PreferencesHolder; 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 ScreenBaseFragment implements FragmentListenerMain{ +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 PENDING_STOP_SEARCH="PendingStopSearch"; + 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 { + HOME_BUTTONS(0), + NEARBY_STOPS(1), + ARRIVALS(2), + STOP_SEARCH(3); + + public final int code; + InitialScreen(int code) { this.code = code; } + + @Nullable + public static InitialScreen fromCode(int code) { + for (InitialScreen c : values()) if (c.code == code) return c; + return null; + } + } + private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ - private static final int SEARCH_BY_NAME = 0; - private static final int SEARCH_BY_ID = 1; - //private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE! - private int searchMode; + + private SearchMode searchMode = SearchMode.INITIAL; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// private FragmentManager childFragMan; - + private IntroViewModel introViewModel; private void refreshStop() { if(getContext() == null){ Log.w(DEBUG_TAG,"Asked to refresh stop but context is null"); return; } if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); } else{ //String stopName = fragment.getStopID(); fragment.requestArrivalsForTheFragment(); } } else { //we create a new fragment, which is WRONG Log.w(DEBUG_TAG, "Asked to refresh stop when there is no fragment"); } } - // - private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), - result -> { - if(result!=null && result.getContents()!=null) { - //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); - Uri uri; - try { - uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. - } catch (NullPointerException e) { - if (getContext()!=null) - Toast.makeText(getContext().getApplicationContext(), - R.string.no_qrcode, Toast.LENGTH_SHORT).show(); - return; - } - String busStopID = getBusStopIDFromUri(uri); - busStopSearchByIDEditText.setText(busStopID); - requestArrivalsForStopID(busStopID); - - } else { - //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); - if (getContext()!=null) - Toast.makeText(getContext().getApplicationContext(), - R.string.no_qrcode, Toast.LENGTH_SHORT).show(); - - - } - }); /// 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(); + 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() { - return new MainScreenFragment(); + public static MainScreenFragment newInstance(@NonNull InitialScreen 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){ + 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) { + 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); + } + public static Bundle makeArgsStops(@NonNull String query){ + return makeArgs(InitialScreen.STOP_SEARCH, query, null); + } + public static Bundle makeArgsNearby(){ + return makeArgs(InitialScreen.NEARBY_STOPS, null, null); + } + public static Bundle makeArgsButtonsScreen(){ + return makeArgs(InitialScreen.HOME_BUTTONS, null, null); + } + + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (getArguments() != null) { - //do nothing - Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); - if (getArguments().getString(PENDING_STOP_SEARCH)!=null) - pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); + 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; + } + 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); - setSearchModeBusStopID(); /* 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.getIntroIsRunning().observe(getViewLifecycleOwner(), isRunning -> { + pendingIntroRun = isRunning; + }); 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(); + } + + /** + * Installs the initial child fragment based on the arguments supplied as arguments + */ + private void dispatchInitialContent() { + switch (initialScreen) { + case NEARBY_STOPS: + showNearbyStopsFragmentChecking(false); + 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); + } + pendingSearchQuery = null; + break; + case HOME_BUTTONS: + default: + showButtonsFragment(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); + //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); if (setupOnStart) { if (pendingStopID==null){ - if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){ - Log.d(DEBUG_TAG, "Showing nearby stops"); - if(!checkLocationPermission()){ - requestLocationPermission(); - pendingNearbyStopsFragmentRequest = true; - } - else { - showNearbyFragmentIfPossible(); - } - } else { - //The Introductory Activity is about to be started, hence pause the request and show later - pendingIntroRun = true; + 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(); + } + + 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); - //if (locationManager == null) - // locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run - if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ - //request position permission if needed - if(!checkLocationPermission()){ - requestLocationPermission(); - pendingNearbyStopsFragmentRequest = true; - } - else { - showNearbyFragmentIfPossible(); - } - //deactivate flag - pendingIntroRun = false; - } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); - //if(!locationManager.isRequesterRegistered(requester)) - // locationManager.addLocationRequestFor(requester); + } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); - //TODO: if we come back to this from another fragment, and the user has given again the permission - // for the Location, we should show the Nearby Stops + if(!suppressArrivalsReload && pendingStopID==null){ //none of the following cases are true // check if we are showing any fragment + /* + //TODO: check if this is needed final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); + if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ //we are not showing anything if(Permissions.anyLocationPermissionsGranted(getContext())){ showNearbyFragmentIfPossible(); } } + + */ } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); - fragmentHelper.setBlockAllActivities(false); + //fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; //locationManager.removeLocationRequestFor(requester); - fragmentHelper.setBlockAllActivities(true); + //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) { - - BarcodeScanOptions scanOptions = new BarcodeScanOptions(); - Intent intent = scanOptions.createScanIntent(); - if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ - BarcodeScanUtils.showDownloadDialog(null, this); - }else { - barcodeLauncher.launch(scanOptions); - } + 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 == SEARCH_BY_ID) { + if (searchMode == SearchMode.SEARCH_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(); requestArrivalsForStopID(busStopID); - } else { // searchMode == SEARCH_BY_NAME + } 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) { - - if (searchMode == SEARCH_BY_NAME) { - setSearchModeBusStopID(); - if (busStopSearchByIDEditText.requestFocus()) { - showKeyboard(); - } - } else { // searchMode == SEARCH_BY_ID - setSearchModeBusStopName(); - if (busStopSearchByNameEditText.requestFocus()) { - showKeyboard(); - } + 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 = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; + 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 = SEARCH_BY_ID; + searchMode = SearchMode.SEARCH_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { - searchMode = SEARCH_BY_NAME; + 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); } - private void actuallyShowNearbyStopsFragment(){ - 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); - - FragmentTransaction ft = childFragMan.beginTransaction(); - - ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); - if (getActivity()!=null && !getActivity().isFinishing()) - ft.commit(); - else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); - } - } - - @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * 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 - mListener.openLineFromStop(routeGtfsId, stopIDFrom); + if(mListener!=null) mListener.openLineFromStop(routeGtfsId, stopIDFrom); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { - mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); + if(mListener!=null) mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); + } + + @Override + public void openNearbyStopsFragment() { + showNearbyStopsFragmentChecking(true); + } + + @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; } - ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); - if (ID == null || ID.length() <= 0) { + 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 if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { - ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); - if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ - // Run with previous fetchers - //fragment.getCurrentFetchers().toArray() - fragment.requestArrivalsForTheFragment(); - } else{ - //SHOW NEW ARRIVALS FRAGMENT - //new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); - fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true); + } 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 { - Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); - //prepareGUIForArrivals(); - //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); - fragmentHelper.createOrUpdateStopFragment(new Palina(ID), 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 isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; - final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; + final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED; - return isOldVersion || !noPermission; + 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() { + private void showNearbyFragmentIfPossible(boolean addToBackStack) { if (isNearbyFragmentShown()) { //nothing to do - Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); + 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 (fragmentHelper.getLastSuccessfullySearchedBusStop() == null - && !childFragMan.isDestroyed()) { + if (!childFragMan.isDestroyed()) { //Go ahead with the request - - actuallyShowNearbyStopsFragment(); + 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); + + FragmentTransaction ft = childFragMan.beginTransaction(); + + ft.replace(R.id.resultFrame, fragment, 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/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index c81589f..6f5306f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -1,159 +1,179 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.content.SharedPreferences; -import android.net.Uri; -import android.provider.Settings; 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.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"; 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 short_lenght) { - final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; + 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 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/middleware/StopSearcher.kt b/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt index 05727a4..89fdb21 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt +++ b/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt @@ -1,133 +1,130 @@ package it.reyboz.bustorino.middleware -import android.content.Context import android.util.Log import it.reyboz.bustorino.backend.Fetcher import it.reyboz.bustorino.backend.FiveTStopsFetcher import it.reyboz.bustorino.backend.GTTStopsFetcher import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.StopsFinderByName import it.reyboz.bustorino.fragments.FragmentHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.cancellation.CancellationException class StopSearcher( fragmentHelper: FragmentHelper, ) { private val helperRef = WeakReference(fragmentHelper) private val resultRef = AtomicReference(Fetcher.Result.PARSER_ERROR) private val finders = arrayOf(GTTStopsFetcher(), FiveTStopsFetcher()) private var lastJob : Job? = null private suspend fun getData(query: String, r: RecursionHelper): List? { if (helperRef.get() == null) return null if(query.isEmpty()) { resultRef.set(Fetcher.Result.QUERY_TOO_SHORT) return null } resultRef.set(Fetcher.Result.OK) Log.d(DEBUG_TAG, "Running with query " + query) //val results = ArrayList() //var resultsList: List val queryOk = query.trim { it <= ' ' } while (r.valid()) { val finder = r.getAndMoveForward() val resultsList = finder.FindByName(queryOk, resultRef) Log.d(DEBUG_TAG, "Result: " + resultRef.get() + ", " + resultsList.size + " stops") if (resultRef.get() == Fetcher.Result.OK) { return resultsList } //results.add(resultRef.get()) } /*var emptyResults = true for (re in results) { if (re != Fetcher.Result.EMPTY_RESULT_SET) { emptyResults = false break } } if (emptyResults) { showResultAsync(Fetcher.Result.EMPTY_RESULT_SET) } */ return listOf() } private fun showError(result: Fetcher.Result) { val helper = helperRef.get() ?: return helper.showErrorMessage(result, SearchRequestType.STOPS) } fun runRequest(query: String, fetchers: Array?) { //start spinner helperRef.get()?.toggleSpinner(true) lastJob = CoroutineScope(Dispatchers.IO).launch{ try { val r = RecursionHelper(fetchers ?: finders) val stopList = getData(query, r) if(stopList == null) { withContext(Dispatchers.Main) { showError(resultRef.get()) } } else if(stopList.isEmpty()) { withContext(Dispatchers.Main) { showError(Fetcher.Result.EMPTY_RESULT_SET) } } else{ //list of stops, non-null and not empty withContext(Dispatchers.Main) { showResult(stopList, query) } } }catch (e: CancellationException) { Log.d(DEBUG_TAG, "Request cancelled") /*withContext(Dispatchers.Main) { helperRef.get()?.toggleSpinner(false) } */ } withContext(Dispatchers.Main) { helperRef.get()?.toggleSpinner(false) } } } fun runRequest(query: String) { runRequest(query, null) } fun cancelLastRequest() { lastJob?.let{ if(!it.isCompleted) it.cancel() } } fun showResult(stops:List, query: String) { val helper = helperRef.get() ?: return helper.createStopListFragment(stops,query, true) } companion object { const val DEBUG_TAG = "BusTO-StopSearcher" } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt index 2164ae7..52406ff 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/FavoritesViewModel.kt @@ -1,95 +1,89 @@ package it.reyboz.bustorino.viewmodels import android.app.Application -import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.application import androidx.lifecycle.map import androidx.lifecycle.switchMap -import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.StopFavoritesData import it.reyboz.bustorino.data.DBUpdateWorker.Companion.getWorkInfoLiveData -import it.reyboz.bustorino.data.FavoritesLiveData import it.reyboz.bustorino.data.OldDataRepository -import it.reyboz.bustorino.data.QueryLiveData -import kotlinx.coroutines.launch import java.util.concurrent.Executors class FavoritesViewModel(application: Application) : AndroidViewModel(application) { val oldRepo: OldDataRepository init { val executor = Executors.newCachedThreadPool() oldRepo = OldDataRepository(executor, application) } /*var favoritesLiveData: FavoritesLiveData? = null override fun onCleared() { if (favoritesLiveData != null) favoritesLiveData!!.onClear() super.onCleared() } val favorites: FavoritesLiveData get() { if (favoritesLiveData == null) { favoritesLiveData = FavoritesLiveData(application, true) } return favoritesLiveData!! } */ val isDBUpdating = getWorkInfoLiveData(application).map { wilist -> var isUpdating = false if(wilist.isNotEmpty()){ val wi = wilist[0] isUpdating = wi.state == WorkInfo.State.RUNNING } isUpdating } // ---- NEW CODE ----- // this code is not active now, but it is gonna be useful for the day when the ContentObserver is gonna be dismissed //for all favorites val favoritesWithStop = MediatorLiveData>() val favoritesNoStop = oldRepo.getFavoritesLiveData() val stopsForFavorites = favoritesNoStop.switchMap { val sids = it.map { d-> d.stopID } oldRepo.getStopsForIdsLiveData(sids) } init{ // this fetches the stops when I have gotten the favorites favoritesWithStop.addSource(favoritesNoStop){ dat -> if(dat!=null) stopsForFavorites.value?.let{ stops -> matchFavoritesStopsAndUpdate(dat, stops) } } favoritesWithStop.addSource(stopsForFavorites) { stops -> favoritesNoStop.value?.let { fav -> if(stops!=null){ matchFavoritesStopsAndUpdate(fav, stops) } } } } fun matchFavoritesStopsAndUpdate(fav: List, stops: List) { //copy favorites info val stopsSave = ArrayList() for (f in fav) { stops.firstOrNull{ it.ID == f.stopID }?.let { s -> f.addToStop(s) stopsSave.add(s) } } favoritesWithStop.value = stopsSave } companion object { const val DEBUG_TAG = "BusTO-FavoritesViewM" } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/IntroViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/IntroViewModel.kt new file mode 100644 index 0000000..fc7d0a8 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/IntroViewModel.kt @@ -0,0 +1,11 @@ +package it.reyboz.bustorino.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class IntroViewModel: ViewModel() { + + + var introIsRunning = MutableLiveData(false) + +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt index 4750784..922a45a 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt @@ -1,131 +1,128 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* -import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository -import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository -import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import org.maplibre.android.geometry.LatLng import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() private var lastShownPatternStops = ArrayList() val currentPatternStops = MutableLiveData>() val selectedPatternLiveData = MutableLiveData() val stopsForPatternLiveData = MutableLiveData>() private val executor = Executors.newFixedThreadPool(2) val mapShowing = MutableLiveData(true) fun setMapShowing(yes: Boolean){ mapShowing.value = yes //retrigger redraw stopsForPatternLiveData.postValue(stopsForPatternLiveData.value) } init { gtfsRepo = GtfsRepository(application) oldRepo = OldDataRepository(executor, application) } val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } val gtfsRoute = routeIDToSearch.switchMap { gtfsRepo.getRouteFromGtfsId(it) } fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } fun getRouteIDQueried(): String?{ return routeIDToSearch.value } var shouldShowMessage = true fun setPatternToDisplay(patternStops: MatoPatternWithStops){ selectedPatternLiveData.value = patternStops } /** * Find the */ private fun requestStopsForGTFSIDs(gtfsIDs: List){ if (gtfsIDs.equals(lastShownPatternStops)){ //nothing to do return } oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } lastShownPatternStops.clear() for(id in gtfsIDs) lastShownPatternStops.add(id) } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } fun getStopByID(id:String) : Stop?{ //var stop : Stop? = null val stop = stopsForPatternLiveData.value?.let { stops -> for (s in stops){ if(s.ID == id) return@let s } return@let null } return stop } private var lastMapPos: Pair? = null fun saveMapPos(latLng: LatLng, zoom: Float){ lastMapPos = Pair(latLng, zoom) } fun getLastMapPos(): Pair? = lastMapPos /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_star.png b/app/src/main/res/drawable-hdpi/ic_star.png deleted file mode 100644 index 7337fed..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_star.png b/app/src/main/res/drawable-mdpi/ic_star.png deleted file mode 100644 index 0f91201..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star.png b/app/src/main/res/drawable-xhdpi/ic_star.png deleted file mode 100644 index 6994f15..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star.png b/app/src/main/res/drawable-xxhdpi/ic_star.png deleted file mode 100644 index bc99ac8..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_star.png b/app/src/main/res/drawable-xxxhdpi/ic_star.png deleted file mode 100644 index d0ff07e..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable/compass_3_fill.xml b/app/src/main/res/drawable/compass_3_fill.xml new file mode 100644 index 0000000..fe9bd72 --- /dev/null +++ b/app/src/main/res/drawable/compass_3_fill.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_moving_emph.xml b/app/src/main/res/drawable/ic_moving_emph.xml index 4dbee23..514b863 100644 --- a/app/src/main/res/drawable/ic_moving_emph.xml +++ b/app/src/main/res/drawable/ic_moving_emph.xml @@ -1,9 +1,9 @@ diff --git a/app/src/main/res/drawable/ic_star_filled_white.xml b/app/src/main/res/drawable/ic_star_filled_white.xml index 75d337d..87fb40f 100644 --- a/app/src/main/res/drawable/ic_star_filled_white.xml +++ b/app/src/main/res/drawable/ic_star_filled_white.xml @@ -1,9 +1,9 @@ diff --git a/app/src/main/res/drawable/magnifying_glass.xml b/app/src/main/res/drawable/magnifying_glass.xml index 630d02d..7fc5a10 100644 --- a/app/src/main/res/drawable/magnifying_glass.xml +++ b/app/src/main/res/drawable/magnifying_glass.xml @@ -1,8 +1,8 @@ + - diff --git a/app/src/main/res/drawable/magnifying_glass_larger.xml b/app/src/main/res/drawable/magnifying_glass_larger.xml new file mode 100644 index 0000000..105a295 --- /dev/null +++ b/app/src/main/res/drawable/magnifying_glass_larger.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/qr_code_scan.xml b/app/src/main/res/drawable/qr_code_scan.xml new file mode 100644 index 0000000..4f3559b --- /dev/null +++ b/app/src/main/res/drawable/qr_code_scan.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_principal.xml b/app/src/main/res/layout/activity_principal.xml index 6815296..e439b05 100644 --- a/app/src/main/res/layout/activity_principal.xml +++ b/app/src/main/res/layout/activity_principal.xml @@ -1,53 +1,55 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true" +> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_buttons.xml b/app/src/main/res/layout/fragment_buttons.xml new file mode 100644 index 0000000..dc65526 --- /dev/null +++ b/app/src/main/res/layout/fragment_buttons.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml index af18a3c..33c749c 100644 --- a/app/src/main/res/layout/fragment_main_screen.xml +++ b/app/src/main/res/layout/fragment_main_screen.xml @@ -1,145 +1,152 @@ - - > + android:contentDescription="@string/scan_qr_code_stop" + android:scaleType="fitCenter" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> + - - - - - + - - + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/searchButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_card_button.xml b/app/src/main/res/layout/item_card_button.xml new file mode 100644 index 0000000..4da1fa0 --- /dev/null +++ b/app/src/main/res/layout/item_card_button.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + \ 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 3a62823..0c2f4a4 100644 --- a/app/src/main/res/menu/drawer_main.xml +++ b/app/src/main/res/menu/drawer_main.xml @@ -1,33 +1,33 @@ + android:title="@string/nav_home_text" /> \ No newline at end of file diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml index 84a90f8..02c382b 100644 --- a/app/src/main/res/menu/menu_search.xml +++ b/app/src/main/res/menu/menu_search.xml @@ -1,11 +1,11 @@ \ 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 36176ca..01dc903 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,258 +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 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-v35/styles.xml b/app/src/main/res/values-v35/styles.xml index ae2e815..1646d1b 100644 --- a/app/src/main/res/values-v35/styles.xml +++ b/app/src/main/res/values-v35/styles.xml @@ -1,15 +1,4 @@ - - - \ 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 17942b4..b11345d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,402 +1,405 @@ 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 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_arrivals_text + @string/nav_home_text @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