diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index a95a538..2a1379a 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,924 +1,932 @@ /* 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.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.*; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.concurrent.LinkedBlockingDeque; import java.util.function.Consumer; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateCheckWorker; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean startedFromIntent = false; private ServiceAlertsViewModel serviceAlertsViewModel; private final DrawerLayout.DrawerListener drawerListener = 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) { } }; //private FragmentKind showingFragmentKind; private final Map menuActions = Map.of( R.id.drawer_action_settings, () -> { Log.d("MAINBusTO", "Pressed button preferences"); startActivity(new Intent(this, ActivitySettings.class)); }, R.id.nav_favorites_item, () -> checkAndShowFavoritesFragment(getSupportFragmentManager(), true), R.id.nav_home, this::showHomeMainFragmentFromClick, R.id.nav_map_item, () -> requestMapFragment(true), R.id.nav_lines_item, () -> showLinesFragment(getSupportFragmentManager(), true, null), R.id.drawer_action_info, () -> startActivity(new Intent(this, ActivityAbout.class)), R.id.nav_nearby, this::openNearbyStopsFragment ); private long lastClosingAttempt = -1L; private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { boolean isResolved = activityCustomBackPressed(); Log.d(DEBUG_TAG, "backpress resolved: " + isResolved); if(!isResolved){ //if(startedFromIntent){ // finish(); //} else{ long currentTime = System.currentTimeMillis(); if (currentTime - lastClosingAttempt < 2000) finish(); else { lastClosingAttempt = currentTime; Toast.makeText(getApplicationContext(), R.string.back_again_to_close, Toast.LENGTH_SHORT).show(); } //} } } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); serviceAlertsViewModel = new ViewModelProvider(this).get(ServiceAlertsViewModel.class); //Use LiveModel to sync fragment state //onBackPressed solution required from Android 16 backPressedCallback.setEnabled(true); this.getOnBackPressedDispatcher().addCallback(backPressedCallback); 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(drawerListener); 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 initialBusStopID = null; var intent = getIntent(); if(intent != null){ var data = intent.getData(); if(data != null){ //var rest = data.getSchemeSpecificPart(); //Log.d(DEBUG_TAG, "the rest is: "+rest); initialBusStopID = getBusStopIDFromUri(data); tryedFromIntent = true; Log.d(DEBUG_TAG, "Opening Intent: initialBusStopID: "+initialBusStopID); } // Intercept calls from other activities if(!tryedFromIntent){ Bundle b =intent.getExtras(); if (b != null) { initialBusStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = initialBusStopID != null; } } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (initialBusStopID == 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(); } } //save whether we started from intent startedFromIntent = tryedFromIntent; //period database check DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false); //Watch for database update DBUpdateWorker.getWorkInfoLiveData(this) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); var framan = getSupportFragmentManager(); if (initialBusStopID!=null) { Log.d(DEBUG_TAG, "Opening Main Fragment on arrivals, bus Stop: "+initialBusStopID); createShowMainFragment(framan, MainScreenFragment.makeArgsArrivals(initialBusStopID), false); }else if (savedInstanceState==null) { //we are not restarting the activity from nothing switch (vl){ case "map" -> {requestMapFragment(false);} case "favorites" -> checkAndShowFavoritesFragment(framan, false); case "lines" -> showLinesFragment(framan, false, null); case "nearby" -> createShowMainFragment(framan, MainScreenFragment.makeArgsNearby(), false); default -> createShowMainFragment(framan, MainScreenFragment.makeArgsButtonsScreen(), false); } } //boolean onCreateComplete = true; //default values are set in the BustoApp //checkApplyDefaultSettingsValues(); // handle the device "insets" /* ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rootRelativeLayout), (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Apply the insets as a margin to the view. This solution sets only the // bottom, left, and right dimensions, but you can apply whichever insets are // appropriate to your layout. You can also update the view padding if that's // more appropriate. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; mlp.rightMargin = insets.right; v.setLayoutParams(mlp); //set for toolbar //mlp = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams(); //mlp.topMargin = insets.top; //mToolbar.setLayoutParams(mlp); mToolbar.setPadding(0, insets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); //to properly handle IME WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { insetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } */ // Toolbar: solo inset superiore (status bar) ViewCompat.setOnApplyWindowInsetsListener(mToolbar, (v, windowInsets) -> { Insets statusBar = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); v.setPadding(0, statusBar.top, 0, 0); return windowInsets; // NON consumare: passa gli insets ai figli }); // Content frame: insets laterali e inferiori (navigation bar) // I fragment figli riceveranno gli insets e potranno gestirli a loro volta ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.mainActContentFrame), (v, windowInsets) -> { Insets systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Solo left/right, bottom lo gestisce ogni fragment v.setPadding(systemBars.left, 0, systemBars.right, 0); return windowInsets; //WindowInsetsCompat.CONSUMED; // NON consumare: passa ai fragment }); //check if first run activity (IntroActivity) has been started once or not final SharedPreferences theShPr = getMainSharedPreferences(); boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false); if(!hasIntroRun){ startIntroductionActivity(); } serviceAlertsViewModel.getLastTimeRunningDownload().observe(this, (timeRunning) -> { if (timeRunning != null) { Log.d(DEBUG_TAG, "requested alerts download at time: "+timeRunning); } }); serviceAlertsViewModel.launchAlertsPeriodCheck(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { int menuId = menuItem.getItemId(); if( menuActions.containsKey(menuId)){ closeDrawerIfOpen(); var runnable = menuActions.get(menuId); if(runnable!=null) runnable.run(); return true; } else{ return false; } }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } //requesting permissions @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } /*@Override public void onBackPressed() { if (!activityCustomBackPressed()) super.onBackPressed(); } */ private boolean activityCustomBackPressed(){ var mainFragManager = getSupportFragmentManager(); boolean resolved = mainFragManager.getBackStackEntryCount() > 0; Fragment shownFrag = mainFragManager.findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) { mDrawer.closeDrawer(GravityCompat.START); resolved = true; } else if (shownFrag instanceof MainScreenFragment mainFrag) { //the only case when we have to pop both stacks mainFrag.cancelReloadArrivalsIfNeeded(); var chManager = mainFrag.getChildFragmentManager(); if(chManager.getBackStackEntryCount() > 0){ chManager.popBackStack(); Log.d(DEBUG_TAG, "Child back stack popped"); resolved = true; } else{ Log.d(DEBUG_TAG, "No back stack on child fragment manager"); } boolean haveToPopMainFromFragment = mainFrag.needToPopMainStackOnBack(); //mainFrag.getChildFragmentManager().popBackStack(); if(haveToPopMainFromFragment){ // pops the stack mainFragManager.popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { mainFragManager.popBackStack(); var newFrag = mainFragManager.findFragmentById(R.id.mainActContentFrame); if(newFrag != null) { int backStackCount = newFrag.getChildFragmentManager().getBackStackEntryCount(); Log.d(DEBUG_TAG, "new fragment is "+newFrag.getClass().getSimpleName() +", count of the backstack: "+backStackCount); } Log.d(DEBUG_TAG, "Popping main backstack"); } else{ resolved = false; } return resolved; } /** * Create and show the SnackBar with the message * The fragment shown points to which view to attach the snackbar */ private void createDatabaseUpdateSnackbar() { View baseView = null; boolean showSnackbar = true; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); showSnackbar = ((ScreenBaseFragment) frag).showSnackbarOnDBUpdate(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); //if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); if (baseView !=null && showSnackbar) { snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.setTextColor(getColor(android.R.color.white)); snackbar.setBackgroundTint(getColor(R.color.grey_800)); if (frag instanceof ScreenBaseFragment){ ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(snackbar); } snackbar.show(); } else{ Log.e(DEBUG_TAG, "Asked to show the snackbar but the baseView is null"); } } /* private void updateShowingFragmentKindInternal(@NonNull FragmentKind newKind){ if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Updating fragment kind, new: "+newKind+", current: "+showingFragmentKind); boolean showingMainFragmentFromOther = false; if(showingFragmentKind == null){ showingFragmentKind = newKind; showingMainFragmentFromOther = false; } else if(newKind != showingFragmentKind) { showingMainFragmentFromOther = ( FragmentKind.getSuperKind(newKind) != FragmentKind.getSuperKind(showingFragmentKind)); showingFragmentKind = newKind; } } */ /** * Show the actual fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) { ft.addToBackStack(null); } ft.commit(); } /** * Create a new MainFragment for the arguments provided and show it in the layout * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ 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 showHomeMainFragmentFromClick(){ FragmentManager fraMan = getSupportFragmentManager(); var fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if(fragment instanceof MainScreenFragment mainFrag){ var visible = mainFrag.isVisible(); if(!mainFrag.isVisible()){ showMainFragment(fraMan, mainFrag, true); } mainFrag.showButtonsFragmentIfNotNearby(true); } else{ createShowMainFragment(fraMan, MainScreenFragment.makeArgsButtonsScreen(), true); } } private void requestMapFragment(final boolean allowReturn){ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.mainActContentFrame); if(fragment instanceof MapLibreFragment){ Log.d(DEBUG_TAG, "Requested map fragment, but it is already open"); } else { fragment = fm.findFragmentByTag(MapLibreFragment.FRAGMENT_TAG); if(fragment != null){ Log.d(DEBUG_TAG, "Found map fragment, reopening it"); var ft = fm.beginTransaction(); ft.replace(R.id.mainActContentFrame,fragment, MapLibreFragment.FRAGMENT_TAG); if(allowReturn) ft.addToBackStack(null); ft.commit(); } else { //create from scratch //The permissions are handled in the MapLibreFragment instead createAndShowMapFragment(null, allowReturn); } } } private void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ if(getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame) instanceof FavoritesFragment){ Log.d(DEBUG_TAG, "Requested favorites fragment, but it is already open"); return; } FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); if(fragment!=null){ ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); }else{ //use new method ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); } if (addToBackStack) ft.addToBackStack("favorites_main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setReorderingAllowed(false); ft.commit(); } private void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ if(getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame) instanceof LinesGridShowingFragment){ Log.d(DEBUG_TAG, "Requested lines grid fragment, but it is already open"); return; } FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack("linesGrid"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } - + /* @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } + */ + @Override public void showFloatingActionButton(boolean yes) { - var frag = getMainFragmentIfVisible(); - if(frag!=null){ - frag.showFloatingActionButton(yes); + var framan = getSupportFragmentManager(); + var frag = framan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if(frag instanceof MainScreenFragment mainFrag){ + mainFrag.showFloatingActionButton(yes); + } else{ + Log.d(DEBUG_TAG, "No main screen fragment found to set showFloatingActionButton"); } } /* 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) { //updateShowingFragmentKindInternal(fragmentType); if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "readyGUIfor fragment kind: " + fragmentType); Integer titleResId = null; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_home); //TODO: Figure out way to change title //mNavView.getCheckedItem().setTitle(R.string.nav_arrivals_text); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_home); break; case MAIN_SCREEN_FRAGMENT: case HOME_BUTTONS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_home); //mNavView.getCheckedItem().setTitle(R.string.nav_home_text); break; case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_nearby); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; } if(getSupportActionBar()!=null && titleResId!=null) getSupportActionBar().setTitle(titleResId); - MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); - if (mainFragmentIfVisible!=null){ - mainFragmentIfVisible.readyGUIfor(fragmentType); + //MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); + var frag = getSupportFragmentManager().findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if (frag instanceof MainScreenFragment mainFrag){ + mainFrag.readyGUIfor(fragmentType); } } @Override public void requestArrivalsForStopID(String ID) { Consumer consumer = fragment -> { fragment.setSuppressArrivalsReload(true); fragment.requestArrivalsForStopID(ID); }; boolean done = getMainFragmentAndDoStuff(consumer); if(!done){ //create the fragment final Bundle args = MainScreenFragment.makeArgsArrivals(ID); createShowMainFragment(getSupportFragmentManager(), args ,true); } } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); } private boolean getMainFragmentAndDoStuff(Consumer consumer){ FragmentManager fraMan = getSupportFragmentManager(); var frag = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if( frag instanceof MainScreenFragment mainFrag){ if(!mainFrag.isVisible()) { showMainFragment(fraMan, mainFrag, true); mainFrag.setMainFragmentManagerTransition(true); } else{ mainFrag.setMainFragmentManagerTransition(false); } consumer.accept(mainFrag); return true; } else{ return false; } } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); tr.addToBackStack("LineFromOther-"+routeGtfsId); tr.commit(); } @Override public void openNearbyStopsFragment() { boolean done = getMainFragmentAndDoStuff(MainScreenFragment::openNearbyStopsFragment); if(!done){ createShowMainFragment(getSupportFragmentManager(), MainScreenFragment.makeArgsNearby(), true); } } @Override public void openLinesFragment() { showLinesFragment(getSupportFragmentManager(), true, null); } @Override public void openFavoritesFragment() { checkAndShowFavoritesFragment(getSupportFragmentManager(), true); } @Override public void toggleSpinner(boolean state) { - MainScreenFragment probableFragment = getMainFragmentIfVisible(); - if (probableFragment!=null){ - probableFragment.toggleSpinner(state); + var frag = getSupportFragmentManager().findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if (frag instanceof MainScreenFragment mainFrag) { + mainFrag.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { - MainScreenFragment probableFragment = getMainFragmentIfVisible(); - if (probableFragment!=null){ - probableFragment.enableRefreshLayout(yes); + Log.d(DEBUG_TAG, "enableRefreshLayout: "+yes); + var framan = getSupportFragmentManager(); + var frag = framan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); + if (frag instanceof MainScreenFragment mainFrag){ + mainFrag.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(@Nullable Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ final FragmentManager fm = getSupportFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); final MapLibreFragment fragment = MapLibreFragment.newInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapLibreFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } void startIntroductionActivity(){ Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); intent.putExtra(ActivityIntro.RESTART_MAIN, false); startActivity(intent); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if(id == R.id.action_about){ startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } else if (id == R.id.action_hack) { openIceweasel(getString(R.string.hack_url), activityContext); return true; } else if (id == R.id.action_source){ openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; } else if (id == R.id.action_licence){ openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; } else if (id == R.id.action_experiments) { startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); return true; } else if (id == R.id.action_tutorial) { startIntroductionActivity(); return true; } return false; } } @Override protected void onPause() { super.onPause(); // stop updating the alerts serviceAlertsViewModel.setRunningDownloadRequests(false); } @Override protected void onResume() { super.onResume(); serviceAlertsViewModel.launchAlertsPeriodCheck(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt index 8ae3d11..33760ff 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ButtonsFragment.kt @@ -1,238 +1,220 @@ /* BusTO - Fragments components Copyright (C) 2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView +import it.reyboz.bustorino.ActivityAbout 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 = 11 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val root = inflater.inflate(R.layout.fragment_buttons, container, false) // this is the actual list of the buttons items = listOf( CardMenuItem(CardAction.NEARBY, getString(R.string.near_me_title), R.drawable.compass_3_fill), CardMenuItem(CardAction.MAP, getString(R.string.map), R.drawable.map), CardMenuItem(CardAction.FAVORITES_STOPS, getString(R.string.action_favorites), R.drawable.ic_star_filled_white), CardMenuItem(CardAction.LINES, getString(R.string.lines), R.drawable.ic_moving_emph), CardMenuItem(CardAction.SETTINGS, getString(R.string.action_settings), R.drawable.ic_baseline_settings_24), - CardMenuItem(CardAction.QR_SCAN, getString(R.string.scan_qr_code_stop), R.drawable.qr_code_scan) - ) + CardMenuItem(CardAction.QR_SCAN, getString(R.string.scan_qr_code_stop), + R.drawable.qr_code_scan), + CardMenuItem(CardAction.INFO, + getString(R.string.action_about), R.drawable.ic_baseline_info_24 + ), + + ) 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() } + CardAction.INFO ->{ + startActivity(Intent(requireContext(), ActivityAbout::class.java)) + } } } 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) + if(listener is FragmentListenerMain){ + val ll = listener as FragmentListenerMain + ll.enableRefreshLayout(false) + } } 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 + NEARBY, MAP, FAVORITES_STOPS, LINES, SETTINGS, QR_SCAN, INFO } } 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) + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_card_button_home, 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/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 82264ac..f8d3020 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,973 +1,974 @@ /* BusTO - Fragments components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.Map; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.util.Permissions; import it.reyboz.bustorino.viewmodels.IntroViewModel; import org.jetbrains.annotations.NotNull; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends BarcodeFragment implements FragmentListenerMain, ParentFragmentManagerFromChild{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String ARG_INITIAL_CONTENT = "initial_content"; public static final String ARG_STOP_ID = "pending_stop_id"; public static final String ARG_SEARCH_QUERY = "pending_search_query"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private enum SearchMode {SEARCH_ID,SEARCH_NAME,INITIAL} public enum InternalScreen { HOME_BUTTONS(0), NEARBY_STOPS(1), ARRIVALS(2), STOP_SEARCH(3), NEARBY_ARRIVALS(4); public final int code; InternalScreen(int code) { this.code = code; } @Nullable public static InternalScreen fromCode(int code) { for (InternalScreen c : values()) if (c.code == code) return c; return null; } @NonNull public static InternalScreen fromFragmentKind(@NonNull FragmentKind kind){ switch (kind){ case HOME_BUTTONS -> { return InternalScreen.HOME_BUTTONS; } case NEARBY_STOPS -> { return InternalScreen.NEARBY_STOPS; } case FragmentKind.ARRIVALS -> { return InternalScreen.ARRIVALS; } case FragmentKind.STOPS -> { return InternalScreen.STOP_SEARCH; } case FragmentKind.NEARBY_ARRIVALS -> { return InternalScreen.NEARBY_ARRIVALS; } default -> { throw new IllegalArgumentException("Unknown fragment kind"); } } } } private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private FloatingActionButton floatingActionButton; /// VIEW MODELS in BaseFragment private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean initialScreenShown = false; private SearchMode searchMode = SearchMode.INITIAL; private FragmentManager childFragMan; /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean pendingNearbyAddToBackStack = false; boolean locationPermissionGranted, locationPermissionAsked = false; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private String pendingSearchQuery = null; private InternalScreen internalScreen = InternalScreen.HOME_BUTTONS; private CoordinatorLayout coordLayout; //this is really a hackish thing, but it works private final LinkedBlockingQueue thingsToDoOnStart = new LinkedBlockingQueue<>(); private void refreshStop() { if(getContext() == null){ Log.w(DEBUG_TAG,"Asked to refresh stop but context is null"); return; } if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); } else{ //String stopName = fragment.getStopID(); fragment.requestArrivalsForTheFragment(); } } else { //we create a new fragment, which is WRONG Log.w(DEBUG_TAG, "Asked to refresh stop when there is no fragment"); } } private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() { @Override public void onActivityResult(Map result) { if (result == null) return; if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: " + result); if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) { locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); /*if (mListener != null && getContext() != null) { if (locationManager == null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } */ // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if (pendingNearbyStopsFragmentRequest) { showNearbyFragmentIfPossible(pendingNearbyAddToBackStack); pendingNearbyStopsFragmentRequest = false; } } if (pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest = false; } }); public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance(@NonNull InternalScreen kind, @Nullable String stopId, @Nullable String query) { MainScreenFragment f = new MainScreenFragment(); f.setArguments(makeArgs(kind, stopId, query)); return f; } public static MainScreenFragment newInstance(@NonNull InternalScreen kind, @Nullable Bundle args){ MainScreenFragment f = new MainScreenFragment(); if (args != null) { f.setArguments(args); } return f; } /** * Create the bundle for the arguments of the fragment * @param kind the kind of initial screen * @param stopId * @param query * @return */ public static Bundle makeArgs(@NonNull InternalScreen kind, @Nullable String stopId, @Nullable String query) { Bundle b = new Bundle(); b.putInt(ARG_INITIAL_CONTENT, kind.code); if (stopId != null) b.putString(ARG_STOP_ID, stopId); if (query != null) b.putString(ARG_SEARCH_QUERY, query); return b; } public static Bundle makeArgsArrivals(@NonNull String stopID){ return makeArgs(InternalScreen.ARRIVALS, stopID, null); } public static Bundle makeArgsStops(@NonNull String query){ return makeArgs(InternalScreen.STOP_SEARCH, query, null); } public static Bundle makeArgsNearby(){ return makeArgs(InternalScreen.NEARBY_STOPS, null, null); } public static Bundle makeArgsButtonsScreen(){ return makeArgs(InternalScreen.HOME_BUTTONS, null, null); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); if (args != null) { Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+ args); if (args.containsKey(ARG_INITIAL_CONTENT)) { int code = args.getInt(ARG_INITIAL_CONTENT, InternalScreen.HOME_BUTTONS.code); InternalScreen parsed = InternalScreen.fromCode(code); internalScreen = (parsed != null) ? parsed : InternalScreen.HOME_BUTTONS; } String stopId = args.getString(ARG_STOP_ID); if (stopId != null) pendingSearchQuery = stopId; else pendingSearchQuery = args.getString(ARG_SEARCH_QUERY); } fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); } @Override public boolean needToPopMainStackOnBack() { return fragmentHelper.needToPopMainStackOnBack(); } @Override public void setMainFragmentManagerTransition(boolean yes) { fragmentHelper.setMainFragmentManagerTransition(yes); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setImageResource(R.drawable.magnifying_glass_larger); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); busStopSearchByIDEditText.setOnFocusChangeListener((v, hasFocus) -> { //Log.d(DEBUG_TAG, "stop search by ID has focus: " + hasFocus); if(hasFocus) setSearchModeBusStopID(); }); busStopSearchByNameEditText.setOnFocusChangeListener((v, hasFocus) -> { //Log.d(DEBUG_TAG, "stop search by Name has focus: " + hasFocus); if(hasFocus) setSearchModeBusStopName(); }); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff childFragMan = getChildFragmentManager(); childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); /* cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); */ //locationManager = AppLocationManager.getInstance(requireContext()); IntroViewModel introViewModel = new ViewModelProvider(requireActivity()).get(IntroViewModel.class); introViewModel.getIntroIsRunning().observe(getViewLifecycleOwner(), isRunning -> { pendingIntroRun = isRunning; }); // TODO: Figure out how to go back to home when pressing home in the nav side bar /*fragShowingViewModel.getKindShowingFragment().observe(getViewLifecycleOwner(), kind -> { Log.w(DEBUG_TAG, "showing fragment kind: " + kind); try { var screenType = InternalScreen.fromFragmentKind(kind); if(screenType != internalScreen) { showDifferentSubFragments(screenType); internalScreen = screenType; } } catch (IllegalArgumentException e) { //ignored Log.d(DEBUG_TAG, "no update from fragment kind"); } }); */ Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); // The child FragmentManager has restored its content — don't dispatch again return; } if (savedInstanceState != null) return; showDifferentSubFragments(internalScreen); } /** * Installs the initial child fragment based on the arguments supplied as arguments */ private void showDifferentSubFragments(@NonNull InternalScreen screen) { boolean firstTime = !initialScreenShown; switch (screen) { case NEARBY_STOPS: case NEARBY_ARRIVALS: // TODO differentiate later //add to back stack if it is not just created showNearbyStopsFragmentChecking(!firstTime); break; case ARRIVALS: // pendingStopID is consumed in onResume → requestArrivalsForStopID if(pendingSearchQuery != null && isResumed()) { swipeRefreshLayout.setVisibility(View.VISIBLE); Log.d(DEBUG_TAG, "Searching arrivals for initial stop: "+pendingSearchQuery); requestsArrivalsInternal(pendingSearchQuery, false); pendingSearchQuery = null; } break; case STOP_SEARCH: if (pendingSearchQuery != null && pendingSearchQuery.length() >= 2) { fragmentHelper.requestStopSearch(pendingSearchQuery); } else { showButtonsFragment(firstTime); } pendingSearchQuery = null; break; case HOME_BUTTONS: default: showButtonsFragment(firstTime); } if(!initialScreenShown){ initialScreenShown = true; } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); //if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); try { while (!thingsToDoOnStart.isEmpty()) { var task = thingsToDoOnStart.take(); task.run(); } } catch (InterruptedException e) { Log.w(DEBUG_TAG, "Interrupted while doing task for start"); thingsToDoOnStart.clear(); } if (setupOnStart) { if (pendingStopID==null){ if(!pendingIntroRun){ //show the fragment //showButtonsFragment(); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } private void showButtonsFragment(boolean addInsteadOfReplace){ swipeRefreshLayout.setVisibility(View.VISIBLE); var ft = childFragMan.beginTransaction(); var frag = ButtonsFragment.newInstance(); if(addInsteadOfReplace) ft.add(R.id.resultFrame,frag, ButtonsFragment.FRAGMENT_TAG); else{ ft.replace(R.id.resultFrame, frag, ButtonsFragment.FRAGMENT_TAG); ft.addToBackStack(null); } ft.commit(); } public void showButtonsFragmentIfNotNearby(boolean addToBackStack){ if(isAdded()) { var framan = getChildFragmentManager(); var showingFrag = framan.findFragmentById(R.id.resultFrame); if (showingFrag == null || showingFrag instanceof NearbyStopsFragment) { var fragHome = ButtonsFragment.newInstance(); var ft = framan.beginTransaction(); if (showingFrag == null) { ft.add(R.id.resultFrame, fragHome, ButtonsFragment.FRAGMENT_TAG); } else { ft.replace(R.id.resultFrame, fragHome, ButtonsFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack(null); ft.commit(); } else { Log.d(DEBUG_TAG, "attempting to show buttons home fragment but have other types (different than nearby)"); } } else{ Log.d(DEBUG_TAG, "Fragment is not added, putting in queue of things to do"); try { thingsToDoOnStart.put(() -> { showButtonsFragmentIfNotNearby(addToBackStack); }); } catch (InterruptedException e) { Log.e(DEBUG_TAG,"Cannot add task"); } } } private void showNearbyStopsFragmentChecking(boolean addToBackStack){ if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; pendingNearbyAddToBackStack = addToBackStack; Log.d(DEBUG_TAG, "requesting location permission for nearby fragment"); } else { Log.d(DEBUG_TAG, "Showing nearby stops fragment"); showNearbyFragmentIfPossible(addToBackStack); } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); //recheck the introduction activity has been run if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // 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; } // check if the fragment start query is null if(pendingSearchQuery!=null) { requestsArrivalsInternal(pendingSearchQuery, false); pendingSearchQuery = null; } else if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } //mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); //fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; //locationManager.removeLocationRequestFor(requester); //fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(); super.onPause(); } /* GUI METHODS */ @Override public void onQrScanSuccess(@NotNull String busIDToSearch) { busStopSearchByIDEditText.setText(busIDToSearch); requestArrivalsForStopID(busIDToSearch); } /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { launchBarcodeScan(); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { //final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SearchMode.SEARCH_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(); requestArrivalsForStopID(busStopID); } else if (searchMode == SearchMode.SEARCH_NAME) { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.requestStopSearch(query); } } } } public void onToggleKeyboardLayout(View v) { switch (searchMode){ case SEARCH_ID: setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } break; case SEARCH_NAME: case INITIAL: setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { + Log.d(DEBUG_TAG, "Enabling refresh layout: " + yes); swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view; if(searchMode == SearchMode.SEARCH_ID) view= busStopSearchByIDEditText; else if(searchMode == SearchMode.SEARCH_NAME) view = busStopSearchByNameEditText; else{ Log.e(DEBUG_TAG, "Asking to show keyboard but SearchMode is "+searchMode+", ignoring"); return; } imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SearchMode.SEARCH_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SearchMode.SEARCH_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForArrivals() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } @Override public void showFloatingActionButton(boolean yes) { //mListener.showFloatingActionButton(yes); if(yes) floatingActionButton.setVisibility(View.VISIBLE); else floatingActionButton.setVisibility(View.GONE); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Readying main fragment for type "+fragmentType); //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { //locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForArrivals(); break; case STOPS: prepareGUIForBusStops(); break; default: //Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity if(mListener!=null) mListener.openLineFromStop(routeGtfsId, stopIDFrom); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { if(mListener!=null) mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); } @Override public void openNearbyStopsFragment() { if(isAdded()) showNearbyStopsFragmentChecking(true); else try{ thingsToDoOnStart.put(() -> showNearbyStopsFragmentChecking(true)); } catch (InterruptedException e) { Log.e(DEBUG_TAG, "trying to put open nearby in task but was interrupted"); } } @Override public void openLinesFragment() { if(mListener!=null) mListener.openLinesFragment(); } @Override public void openFavoritesFragment() { if(mListener!=null) mListener.openFavoritesFragment(); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } private void requestsArrivalsInternal(String stopID, boolean addToBackStack) { if (!isResumed()){ //defer request to onResume - it will be added to the backstack pendingStopID = stopID; Log.d(DEBUG_TAG, "Deferring update for stop "+stopID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (stopID == null || stopID.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{ // ensure that the new sub-fragment is gonna be visible swipeRefreshLayout.setVisibility(View.VISIBLE); var palinaTrial = new Palina(stopID); 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, addToBackStack); } } else { // this is not needed any more //prepareGUIForArrivals(); fragmentHelper.showArrivalsFragmentForStop(palinaTrial, addToBackStack); } } } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { requestsArrivalsInternal(ID, true); } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED; return !noPermission; } private void requestLocationPermission(){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ makeToast(R.string.enable_position_message_nearby); } requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfPossible(boolean addToBackStack) { if (isNearbyFragmentShown()) { //nothing to do Log.d(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } if (!childFragMan.isDestroyed()) { //Go ahead with the request swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing var nearbyFrag = (NearbyStopsFragment) childFragMan.findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); if(nearbyFrag==null){ nearbyFrag = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); } FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, nearbyFrag, NearbyStopsFragment.FRAGMENT_TAG); if(addToBackStack) ft.addToBackStack(null); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } pendingNearbyStopsFragmentRequest = false; } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt deleted file mode 100644 index 25c5db5..0000000 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AppLocationManager.kt +++ /dev/null @@ -1,273 +0,0 @@ -/* - BusTO (middleware) - 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.middleware - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.location.* -import android.os.Bundle -import android.util.Log -import androidx.core.content.ContextCompat -import it.reyboz.bustorino.util.LocationCriteria -import java.lang.ref.WeakReference -import kotlin.math.min - -/** - * Singleton class used to access location. Possibly extended with other location sources. - * - * 2024: This is far too much. We need to simplify the whole mechanism (no more singleton) - */ -class AppLocationManager private constructor(context: Context) : LocationListener { - private val appContext: Context - private val locMan: LocationManager - private val BUNDLE_LOCATION = "location" - private var oldGPSLocStatus = LOCATION_UNAVAILABLE - private var minimum_time_milli = -1 - private val requestersRef = ArrayList>() - - init { - appContext = context.applicationContext - locMan = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - } - - @Throws(SecurityException::class) - private fun requestGPSPositionUpdates(): Boolean { - val timeinterval = - if (minimum_time_milli > 0 && minimum_time_milli < Int.MAX_VALUE) minimum_time_milli else 2000 - locMan.removeUpdates(this) - if (!checkLocationPermission(appContext)){ - Log.e(DEBUG_TAG, "No location permission!!") - return false - } - if (locMan.allProviders.contains("gps")) locMan.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - timeinterval.toLong(), - 5f, - this - ) - /*LocationManagerCompat.requestLocationUpdates(locMan, LocationManager.GPS_PROVIDER, - new LocationRequestCompat.Builder(timeinterval).setMinUpdateDistanceMeters(5.F).build(),this, ); - TODO: find a way to do this - */ - return true - } - - private fun cleanAndUpdateRequesters() { - minimum_time_milli = Int.MAX_VALUE - val iter = requestersRef.listIterator() - while (iter.hasNext()) { - val cReq = iter.next().get() - if (cReq == null) iter.remove() else { - minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) - .toInt() - } - } - Log.d( - DEBUG_TAG, - "Updated requesters, got " + requestersRef.size + " listeners to update every " + minimum_time_milli + " ms at least" - ) - } - - fun addLocationRequestFor(req: LocationRequester) { - var present = false - minimum_time_milli = Int.MAX_VALUE - var countNull = 0 - val iter = requestersRef.listIterator() - while (iter.hasNext()) { - val cReq = iter.next().get() - if (cReq == null) { - countNull++ - iter.remove() - } else if (cReq == req) { - present = true - minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) - .toInt() - } - } - Log.d(DEBUG_TAG, "$countNull listeners have been removed because null") - if (!present) { - val newref = WeakReference(req) - requestersRef.add(newref) - minimum_time_milli = min(req.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) - .toInt() - Log.d(DEBUG_TAG, "Added new stop requester, instance of " + req.javaClass.simpleName) - } - if (requestersRef.size > 0) { - Log.d(DEBUG_TAG, "Requesting location updates") - requestGPSPositionUpdates() - } - } - - fun removeLocationRequestFor(req: LocationRequester) { - minimum_time_milli = Int.MAX_VALUE - val iter = requestersRef.listIterator() - while (iter.hasNext()) { - val cReq = iter.next().get() - if (cReq == null || cReq == req) iter.remove() else { - minimum_time_milli = min(cReq.locationCriteria.timeInterval.toDouble(), minimum_time_milli.toDouble()) - .toInt() - } - } - if (requestersRef.size <= 0) { - locMan.removeUpdates(this) - } - } - - private fun sendLocationStatusToAll(status: Int) { - val iter = requestersRef.listIterator() - while (iter.hasNext()) { - val cReq = iter.next().get() - if (cReq == null) iter.remove() else cReq.onLocationStatusChanged(status) - } - } - - fun isRequesterRegistered(requester: LocationRequester): Boolean { - for (regRef in requestersRef) { - if (regRef.get() != null && regRef.get() === requester) return true - } - return false - } - - override fun onLocationChanged(location: Location) { - Log.d( - DEBUG_TAG, "found location: \nlat: ${location.latitude} lon: ${location.longitude} accuracy: ${location.accuracy}" - ) - val iter = requestersRef.listIterator() - var new_min_interval = Int.MAX_VALUE - while (iter.hasNext()) { - val requester = iter.next().get() - if (requester == null) iter.remove() else { - val timeNow = System.currentTimeMillis() - val criteria = requester.locationCriteria - if (location.accuracy < criteria.minAccuracy && - timeNow - requester.lastUpdateTimeMillis > criteria.timeInterval - ) { - requester.onLocationChanged(location) - Log.d( - "AppLocationManager", - "Updating position for instance of requester " + requester.javaClass.simpleName - ) - } - //update minimum time interval - new_min_interval = min(requester.locationCriteria.timeInterval.toDouble(), new_min_interval.toDouble()) - .toInt() - } - } - minimum_time_milli = new_min_interval - if (requestersRef.size == 0) { - //stop requesting the position - locMan.removeUpdates(this) - } - } - - @Deprecated("Deprecated in Java") - override fun onStatusChanged(provider: String, status: Int, extras: Bundle) { - //IF ANOTHER LOCATION SOURCE IS READY, USE IT - //OTHERWISE, SIGNAL THAT WE HAVE NO LOCATION - if (oldGPSLocStatus != status) { - if (status == LocationProvider.OUT_OF_SERVICE || status == LocationProvider.TEMPORARILY_UNAVAILABLE) { - sendLocationStatusToAll(LOCATION_UNAVAILABLE) - } else if (status == LocationProvider.AVAILABLE) { - sendLocationStatusToAll(LOCATION_GPS_AVAILABLE) - } - oldGPSLocStatus = status - } - Log.d(DEBUG_TAG, "Provider status changed: $provider status: $status") - } - - override fun onProviderEnabled(provider: String) { - cleanAndUpdateRequesters() - requestGPSPositionUpdates() - Log.d(DEBUG_TAG, "Provider: $provider enabled") - for (req in requestersRef) { - if (req.get() == null) continue - req.get()!!.onLocationProviderAvailable() - } - } - - override fun onProviderDisabled(provider: String) { - cleanAndUpdateRequesters() - for (req in requestersRef) { - if (req.get() == null) continue - req.get()!!.onLocationDisabled() - } - //locMan.removeUpdates(this); - Log.d(DEBUG_TAG, "Provider: $provider disabled") - } - - - /** - * Interface to be implemented to get the location request - */ - interface LocationRequester { - /** - * Do something with the newly obtained location - * @param loc the obtained location - */ - fun onLocationChanged(loc: Location?) - - /** - * Inform the requester that the GPS status has changed - * @param status new status - */ - fun onLocationStatusChanged(status: Int) - - /** - * We have a location provider available - */ - fun onLocationProviderAvailable() - - /** - * Called when location is disabled - */ - fun onLocationDisabled() - - /** - * Give the last time of update the requester has - * Set it to -1 in order to receive each new location - * @return the time for update in milliseconds since epoch - */ - val lastUpdateTimeMillis: Long - - /** - * Get the specifications for the location - * @return fully parsed LocationCriteria - */ - val locationCriteria: LocationCriteria - } - - companion object { - const val LOCATION_GPS_AVAILABLE = 22 - const val LOCATION_UNAVAILABLE = -22 - private const val DEBUG_TAG = "BUSTO LocAdapter" - private var instance: AppLocationManager? = null - @JvmStatic - fun getInstance(con: Context): AppLocationManager { - if (instance == null) instance = AppLocationManager(con) - return instance!! - } - - fun checkLocationPermission(context: Context?): Boolean { - return ContextCompat.checkSelfPermission( - context!!, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - } - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt b/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt index 2ec7efc..2dc9894 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt +++ b/app/src/main/java/it/reyboz/bustorino/util/Permissions.kt @@ -1,156 +1,147 @@ package it.reyboz.bustorino.util import android.Manifest import android.app.Activity import android.content.Context import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.location.Criteria import android.location.LocationManager import android.net.Uri import android.os.Build import android.provider.Settings import android.util.Log import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.startActivity import it.reyboz.bustorino.R import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt class Permissions private constructor(private val appContext: Context) { /* @get:RequiresApi(api = Build.VERSION_CODES.TIRAMISU) val notificationPermissions: Array //final static public String[] NOTIFICATION_PERMISSION={Manifest.permission.POST_NOTIFICATIONS}; get() = arrayOf(Manifest.permission.POST_NOTIFICATIONS) */ private var askedTimesLocation = AtomicInteger(0) - fun anyLocationProviderMatchesCriteria(mng: LocationManager, cr: Criteria, enabled: Boolean): Boolean { - val providers = mng.getProviders(cr, enabled) - Log.d(DEBUG_TAG, "Getting enabled location providers: ") - for (s in providers) { - Log.d(DEBUG_TAG, "Provider " + s) - } - return !providers.isEmpty() - } - fun checkRequestLocationPermissions(activity: Activity, launcher: ActivityResultLauncher>): Boolean { //activity.getSharedPreferences(, Context.MODE_PRIVATE) var launched = false if(shouldShowRequestPermissionRationale(activity,Manifest.permission.ACCESS_FINE_LOCATION)){ Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_LONG).show() } /*else{ //cannot show the dialog anymore, go to the settings openShowAppSettingsLocationDialog() } */ val reqTimes = askedTimesLocation.getAndIncrement() Log.d(DEBUG_TAG, "Requesting location permissions, asked ${reqTimes} times ") if(reqTimes > 4){ openShowAppSettingsLocationDialog() } else{ launcher.launch(LOCATION_PERMISSIONS) launched = true } return launched } /** * Show alert dialog to enable location permission */ fun openShowAppSettingsLocationDialog() { val context = appContext val builder = AlertDialog.Builder(context) builder.setTitle(R.string.no_permission_dialog_title) builder.setMessage(R.string.no_permission_dialog_text_location) builder.setPositiveButton( R.string.no_permission_dialog_open, DialogInterface.OnClickListener { dialogInterface: DialogInterface?, i: Int -> val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.setData(Uri.fromParts("package", context.getPackageName(), null)) context.startActivity(intent) }) builder.setNegativeButton(android.R.string.cancel, null) builder.show() } fun assertLocationPermissions(con: Context, activity: Activity) { if (!isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) || !isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION) ) { ActivityCompat.requestPermissions( activity, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_POSITION ) } } companion object{ const val DEBUG_TAG: String = "BusTO -Permissions" const val PERMISSION_REQUEST_POSITION: Int = 33 const val LOCATION_PERMISSION_GIVEN: String = "loc_permission" const val STORAGE_PERMISSION_REQ: Int = 291 const val PERMISSION_OK: Int = 0 const val PERMISSION_ASKING: Int = 11 const val PERMISSION_NEG_CANNOT_ASK: Int = -3 @JvmField val LOCATION_PERMISSIONS: Array = arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION ) @JvmStatic fun isPermissionGranted(con: Context, permission: String): Boolean { return ContextCompat.checkSelfPermission(con, permission) == PackageManager.PERMISSION_GRANTED } @JvmStatic fun bothLocationPermissionsGranted(con: Context): Boolean { return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) && isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION) } @JvmStatic fun anyLocationPermissionsGranted(con: Context): Boolean { return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) || isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION) } /** * Check if the system requires the POST_NOTIFICATION permission to send notifications * @return true if required */ @JvmStatic fun isNotificationPermissionNeeded() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) @Volatile private var instance: Permissions? = null fun getInstance(context: Context) = instance ?: synchronized(this) { instance ?: Permissions(context.applicationContext).also { instance = it } } } } diff --git a/app/src/main/res/drawable/ic_outline_info_24.xml b/app/src/main/res/drawable/ic_outline_info_24.xml index dca49ae..23d190e 100644 --- a/app/src/main/res/drawable/ic_outline_info_24.xml +++ b/app/src/main/res/drawable/ic_outline_info_24.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/layout/item_card_button.xml b/app/src/main/res/layout/item_card_button_home.xml similarity index 96% rename from app/src/main/res/layout/item_card_button.xml rename to app/src/main/res/layout/item_card_button_home.xml index 976eaf8..e2755df 100644 --- a/app/src/main/res/layout/item_card_button.xml +++ b/app/src/main/res/layout/item_card_button_home.xml @@ -1,53 +1,53 @@ + android:padding="18dp"> \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1e22f96..63edc15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,269 +1,269 @@ Oui Paramètres Rechercher Numéro de l\'arrêt de bus Insérer le numéro de l\'arrêt de bus Cette application nécessite une autre application pour scanner les codes QR. Souhaitez-vous installer Barcode Scanner maintenant ? Insérer le nom de l\'arrêt de bus %1$s vers %2$s %s (destination inconnue) Vérifiez votre connexion Internet ! Nom trop court, tapez davantage de caractères et réessayez Erreur lors de l\'analyse du site 5T/GTT (foutu site!) Sélectionner l\'arrêt de bus… Ligne Lignes: %1$s Lignes interurbaines Destination: Aucun planning trouvé https://gitpull.it/w/librebusto/en/ Code source Licence11 L\'arrêt de bus est désormais dans vos favoris Favoris À propos de l\\\'application Fermer le tutoriel Majuscules partout Appuyez pour modifier Afficher les arrivées à l\'appui sur un arrêt Activer les fonctions expérimentales MaTO (le plus fréquemment mis à jour, parfois avec erreurs) Supprimer les données des trajets (libère de l\'espace) Autoriser l\'accès à la localisation Permission d\'accès à la localisation accordée Permission d\'accès à la localisation refusée OK, fermer le tutoriel Sauvegarder et restaurer La sauvegarde a été importée Vérifiez cocher au moins un élément à importer ! Importer les favoris depuis une sauvegarde Importer les préférences depuis une sauvegarde Arrivées à&nbsp;: %1$s En savoir plus Rencontrer l\'auteur Aide Lignes Lignes urbaines Lignes touristiques Aucun code QR trouvé, essayez d\'utiliser une autre application pour le scanner Ouvrez la wiki Ligne retirée de vos favoris Heures d\'arrivée Arrêt de bus retiré de vos favoris Ligne ajoutée à vos favoris Favoris Aucune arrivée trouvée pour les lignes : Renommer Impossible de trouver la position de l\'arrêt Distance maximale (en mètres) Autorisez l\'accès à la localisation pour l\'afficher sur la carte Mise à jour de la base de données en cours… Lancer la mise à jour manuelle de la base de données Veuillez activer la localisation sur l\'appareil Mise à jour de la base de données Forcer la mise à jour de la base de données à l\'arrêt Afficher les arrivées Muoversi a Torino Le service de localisation en temps réel MaTO live bus est en cours d\'exécution stockage Rechercher par arrêt L\'application a planté en raison d\'un bug.\nSi vous le souhaitez, vous pouvez aidez les développeurs en envoyant le rapport de plantage par e-mail.\nVeuillez noter que ce rapport ne comporte aucune donnée sensible, seulement quelques informations sur votre téléphone et la configuration/l\'état de l\'application. Ouvrir le menu de navigation Fonctions expérimentales Lancement de la mise à jour de la base de données Filtrer par nom Ne pas modifier la direction des arrivées Majuscules sur la première lettre uniquement Section à afficher au démarrage "Source de la localisation en temps réel pour les bus et les trams" Appui long sur l\'arrêt pour afficher les options GTFS RT (moins fréquemment mis à jour, mais plus précis) Tous les trajets GTFS ont été supprimés de la base de données Sauvegarde dans un fichier Activer les notifications Notifications activées Importer depuis une sauvegarde Installer Barcode Scanner ? Appuyez sur l\\\'étoile pour ajouter l\'arrêt de bus à vos favoris\n\nPour lire les fiches horaires:\n 12:56* Heures d\'arrivée en temps réel\n 12:56 Heures d\'arrivée programmées\n\nTirez vers le bas pour actualiser la fiche\n Appui long sur la source des arrivées pour la modifier Actualités et mises à jour

Sur le canal Telegram, vous pouvez retrouver des informations sur les dernières mises à jour de l\'application

]]>
Précédent Scanner le code QR Suivant Nom de l\'arrêt de bus Aucune arrivée prévue pour cette arrêt - Il semble qu\'il n\\\'y a aucun arrêt de bus avec ce nom - À propos de l\\\'application + Il semble qu\'il n\'y a aucun arrêt de bus avec ce nom + À propos de l\'application Aucune ligne trouvée dans cette catégorie Aucune ligne ne correspond à la recherche Ligne %1$s Ligne %1$s, direction: Erreur interne inattendue, impossible d\'extraire les données depuis le site GTT/5T Favoris Carte Supprimer Renommer l\'arrêt de bus Aucun favori ? Ah ! Appuyez sur l\'étoile au niveau d\'un arrêt de bus pour en ajouter ! Réinitialiser J\'AI COMPRIS ! Voir sur la carte Arrêts à proximité Version de l\'application Le nombre d\'arrêts à afficher dans les arrêts récents est invalide Valeur invalide, veuillez saisir un nombre valide Recherche de l\'emplacement Aucun arrêt à proximité Nombre minmum d\'arrêts Préférences Paramètres Général Fonctionnalités expérimentales Arrêts récents Paramètres généraux Gestion de la base de données Autorisez l\'accès à la localisation pour afficher les arrêts à proximité Appuyez pour mettre à jour la base de données maintenant arrive à Afficher les arrêts Rejoindre le canal Telegram Afficher l\'introduction Recentrer sur ma position Me suivre Activer ou désactiver la localisation Localisation activée Localisation désactivée La localisation est désactivée sur l\'appareil Source des arrivées : %1$s Application GTT Site Web de GTT Site Web de 5T Torino Non défini Modification de la source des heures d\'arrivée… Appui long pour modifier la source des arrivées Source des heures d\'arrivée Sélectionnez les sources d\'heures d\'arrivée à utiliser Canal par défaut pour les notifications Opérations sur la base de données Mises à jour de la base de données de l\'application BusTO - Service de localisation en temps réel Localisation en temps réel Affichage de l\'activité associée au service de localisation en temps réel Téléchargement des trajets depuis le serveur de MaTO Permission pour %1$s demandée à de trop nombreuses reprises Impossible d\'utiliser la carte sans la permission d\'accès au stockage ! L\'application a planté et le rapport de plantage se trouve en pièce-jointe. Veuillez décrire ce que vous faisiez avant le plantage : Arrivées Carte Favoris Fermer le menu de navigation Offrir un café Carte Téléchargement des données depuis le serveur MaTO Majuscules pour les directions Afficher le tutoriel Données sauvegardées Non Vous bénéficiez de la dernière technologie en matière de respect de votre vie privée. Arrêt %1$s Licences

L\'application et le code source associé sont publiés par Valerio Bozzolan et les autres auteurs sous les termes de la licence GNU General Public License v3+. Tout le monde est donc autorisé à utiliser, étudier, améliorer et partager cette application par tout moyen et à toutes fins : à condition de respecter ces droits et d\attribuer l\'œuvre originale à Valerio Bozzolan.


Remarques

Cette application a été développée dans l\'espoir d\'être utile à tous, mais elle est fournie sans AUCUNE garantie d\'aucune sorte.

Les données utilisées par l\'application proviennent directement de GTT et d\'autres organismes publics : si vous constatez des erreurs, veuillez vous adresser à eux, et non à nous.

Cette traduction est gracieusement fournie par Ludovico Pavesi et Fabio Mazza.

Bonne utilisation ! :)

]]>
Impossible d\'ajouter aux favoris ( stockage plein ou base de données corrompue ?) ! Impossible de trouver une application où l\'afficher Arrivées à proximité Recherche des arrivées depuis %1$s Vous êtes trop loin, position non affichée Style de la carte Versatiles (vectoriel) OSM legacy (raster, plus légèr) open source pour les transports publics de Turin. Il s\'agit d\'une application indépendante, sans publicité ni traceurs d\'aucune sorte.]]> Si vous vous trouvez à un arrêt, vous pouvez scanner le code QR présent sur le panneau en appuyant sur l\'icône à gauche de la barre de recherche.]]> favoris en touchant l\'étoile à côté de son nom]]>> bleu)]]> Paramètres pour personnaliser l\'application, et la section À propos de l\'application si vous souhaitez en savoir plus sur l\'application et ses développeurs.]]> Notifications pour afficher les informations relatives au fonctionnement en arrière-plan. Appuyez sur le bouton ci-dessous pour l\'autoriser]]> Bonjour fragment vide Aucune application trouvée pour afficher l\'arrêt ! Direction déjà sélectionnée Chargement de la destination… Destination inconnue Le service des positions fonctionne normalement Aucune position reçue Erreur de connexion au serveur Erreur lors de la lecture de la réponse du serveur Erreur : réponse du serveur de type inattendu Connexion en cours... Source des positions en temps réel : Changer de source Supprimer les positions sur la carte lorsque la source des positions en temps réel est modifiée Véhicule %1$s Bienvenue !

Merci d\'avoir choisi BusTO, une application open source et indépendante utile pour se déplacer dans la ville de Turin avec un logiciel libre !

BusTO respecte votre vie privée en ne collectant aucune donnée sur votre utilisation. Elle est légère et ne contient aucune publicité !


Ici, vous trouverez plus d\'informations et des liens concernant le projet.


Tutoriel

Si vous souhaitez consulter à nouveau l\'introduction, utilisez le bouton ci-dessous :

]]>
How does it work?

Cette application est capable d\'accomplir toutes ces choses incroyables en extrayant des données de www.gtt.to.it, www.5t.torino.it ou muoversiatorino.it "pour usage personnel", ainsi que les données ouvertes du site web AperTO (aperto.comune.torino.it) .


Le travail de plusieurs personnes est à l\'origine de cette application, en particulier :
- Fabio Mazza, développeur rockstar senior actuel.
- Andrea Ugo, développeur junior rockstar actuel.
- Silviu Chiriac, créateur du logo du 2021.
- Marco M, testeur rockstar et chasseur de bugs.
- Ludovico Pavesi, ancien développeur rockstar senior (asd).
- Valerio Bozzolan, mainteneur et infrastructures (sponsor).
- Marco Gagino, contributeur et créateur de la première icône.
- JSoup bibliothèque pour le scraping web.
- makovkastar boutons flottants
- Google pour les icônes et les bibliothèques de support et de design.
- Autres icônes provenant de Bootstrap, Feather et Hero Icons.
- Tous les contributeurs, ainsi que les bêta-testeurs !


Si vous souhaitez obtenir plus d\'informations ou contribuer au développement, utilisez les boutons ci-dessous ! ]]>
Arrivées à: Afficher détails de la ligne Afficher la direction complète Aucun récepteur GPS n\'a été détecté sur l\'appareil ! Téléchargement des alertes en temps réel Autorisation manquante Pour utiliser cette fonctionnalité, l\'application nécessite d\'accéder à la localisation, ce qui ne peut désormais être autorisé que dans les paramètres système. Ouvrir les paramètres Sauvegarde Restauration Sauvegarder ou restaurer les données Sauvegarde terminée Alertes pour la ligne %1$s: Les alertes ne sont pas disponibles en français et sont donc affichées en %1$s italien anglais Vérification des nouvelles alertes en cours Appuyez à nouveau sur le bouton de retour pour fermer l\'application Mise à jour le : %1$s
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ae15f99..3866111 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,267 +1,267 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca Codice QR Scansiona codice QR alla fermata Si No Prossimo Precedente Necessaria app per leggere i codici QR Questa azione richiede un\'altra app per scansionare i codici QR, ma non è stata trovata sul dispositivo. Vuoi installare Binary Eye? 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, riprova Preferiti Aiuto - Informazioni + Informazioni sull\'app 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 Chiaro Scuro Segui il sistema Imposta tema scuro o chiaro