diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ef7a91..8da5a23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,148 +1,124 @@ - - - - - - - - - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index 517627b..a95a538 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,918 +1,924 @@ /* 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){ - 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(); - } + //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); - boolean showingArrivalsFromIntent = false; final Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); - mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - - } - - @Override - public void onDrawerOpened(@NonNull View drawerView) { - hideKeyboard(); - } - - @Override - public void onDrawerClosed(@NonNull View drawerView) { - - } - - @Override - public void onDrawerStateChanged(int newState) { - } - }); - + 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 busStopID = null; - Uri data = getIntent().getData(); - if (data != null) { + 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); - busStopID = getBusStopIDFromUri(data); - Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); - tryedFromIntent = true; - } + initialBusStopID = getBusStopIDFromUri(data); + tryedFromIntent = true; + Log.d(DEBUG_TAG, "Opening Intent: initialBusStopID: "+initialBusStopID); - // Intercept calls from other activities - if (!tryedFromIntent) { - Bundle b = getIntent().getExtras(); - if (b != null) { - busStopID = b.getString("bus-stop-ID"); - - /* - * I'm not very sure if you are coming from an Intent. - * Some launchers work in strange ways. - */ - tryedFromIntent = busStopID != null; + } + // 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 (busStopID == null) { + 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(); } - } else { - // If you are here an intent has worked successfully - //setBusStopSearchByIDEditText(busStopID); - //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); - requestArrivalsForStopID(busStopID); //this shows the fragment, too - showingArrivalsFromIntent = true; } - //database check + //save whether we started from intent + startedFromIntent = tryedFromIntent; - // DatabaseUpdate.requestDBUpdateWithWork(this, false, false); + //period database check DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false); - /* - Watch for database update - */ + + //Watch for database update DBUpdateWorker.getWorkInfoLiveData(this) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDatabaseUpdateSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); - if (showingArrivalsFromIntent){ - //do nothing but exclude a case + 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) { - var framan = getSupportFragmentManager(); //we are not restarting the activity from nothing switch (vl){ case "map" -> {requestMapFragment(false);} case "favorites" -> checkAndShowFavoritesFragment(framan, false); case "lines" -> showLinesFragment(framan, false, null); case "nearby" -> createShowMainFragment(framan, MainScreenFragment.makeArgsNearby(), false); default -> 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); } } /* 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); } } @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); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(@Nullable Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ final FragmentManager fm = getSupportFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); final MapLibreFragment fragment = MapLibreFragment.newInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapLibreFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } void startIntroductionActivity(){ Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); intent.putExtra(ActivityIntro.RESTART_MAIN, false); startActivity(intent); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if(id == R.id.action_about){ startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } else if (id == R.id.action_hack) { openIceweasel(getString(R.string.hack_url), activityContext); return true; } else if (id == R.id.action_source){ openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; } else if (id == R.id.action_licence){ openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; } else if (id == R.id.action_experiments) { startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); return true; } else if (id == R.id.action_tutorial) { startIntroductionActivity(); return true; } return false; } } @Override protected void onPause() { super.onPause(); // stop updating the alerts serviceAlertsViewModel.setRunningDownloadRequests(false); } @Override protected void onResume() { super.onResume(); serviceAlertsViewModel.launchAlertsPeriodCheck(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index b4724b3..82264ac 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,970 +1,973 @@ /* 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) pendingStopID = stopId; - pendingSearchQuery = args.getString(ARG_SEARCH_QUERY); + 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 && pendingStopID==null){ - //none of the following cases are true - // check if we are showing any fragment - /* - //TODO: check if this is needed - final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); - - if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ - //we are not showing anything - if(Permissions.anyLocationPermissionsGranted(getContext())){ - showNearbyFragmentIfPossible(); - } - } - - */ - } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } - - if(pendingStopID!=null){ + // 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) { 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); } - - /** - * Main method for stops requests - * @param ID the Stop ID - */ - @Override - public void requestArrivalsForStopID(String ID) { + private void requestsArrivalsInternal(String stopID, boolean addToBackStack) { if (!isResumed()){ - //defer request - pendingStopID = ID; - Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); + //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 (ID == null || ID.isEmpty()) { + 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{ - var palinaTrial = new Palina(ID); + // 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, true); + fragmentHelper.showArrivalsFragmentForStop(palinaTrial, addToBackStack); } } else { // this is not needed any more //prepareGUIForArrivals(); - fragmentHelper.showArrivalsFragmentForStop(palinaTrial, true); + 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/fragments/NearbyStopsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt index 458f800..523fc45 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt @@ -1,858 +1,861 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.content.res.ColorStateList import android.location.Location import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.AppCompatButton import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import com.google.android.material.button.MaterialButton import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.ArrivalsStopAdapter import it.reyboz.bustorino.adapters.SquareStopAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.DatabaseUpdate import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.middleware.FusedNativeLocationProvider import it.reyboz.bustorino.middleware.FusedNativeLocationProvider.LocationUpdateListener import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.util.Permissions.Companion.bothLocationPermissionsGranted import it.reyboz.bustorino.util.StopSorterByDistance import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel import java.util.* import java.util.concurrent.atomic.AtomicBoolean class NearbyStopsFragment : ScreenBaseFragment() { override fun getBaseViewForSnackBar(): View? { return null } enum class FragType(val num: Int) { STOPS(1), ARRIVALS(2); companion object { @JvmStatic fun fromNum(i: Int): FragType { when (i) { 1 -> return STOPS 2 -> return ARRIVALS else -> throw IllegalArgumentException("type not recognized") } } } } private enum class LocationShowingStatus { SEARCHING, LOCATION_FOUND, DISABLED, NO_PERMISSION //NO_STOPS_NEARBY } private var mListener: FragmentListenerMain? = null private var fragmentType = FragType.STOPS private lateinit var gridRecyclerView: RecyclerView private var dataAdapter: SquareStopAdapter? = null private var gridLayoutManager: AutoFitGridLayoutManager? = null private var lastPosition: GPSPoint? = null private var circlingProgressBar: ProgressBar? = null private lateinit var flatProgressBar: ProgressBar //protected SharedPreferences globalSharedPref; //private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private var messageTextView: TextView? = null private lateinit var enableLocationButton : MaterialButton private var titleTextView: TextView? = null private var loadingTextView: TextView? = null private var scrollListener: CommonScrollListener? = null private var switchButton: AppCompatButton? = null private var firstLocForStops = true private var firstLocForArrivals = true private var stopsMaxDistance = -3 private var stopsMinNumber = -1 //These are useful for the case of nearby arrivals private var arrivalsStopAdapter: ArrivalsStopAdapter? = null private var currentNearbyStops: ArrayList? = null private var showingStatus = LocationShowingStatus.NO_PERMISSION private var isLocationEnabled = false private var dataShownInAdapter = AtomicBoolean(false) private var noDataMessageId = R.string.no_stops_nearby private var loadingDataMessageId = R.string.position_searching_message private val locationUpdateListener: LocationUpdateListener = object : LocationUpdateListener { override fun onLocationUpdate(location: Location) { if (location.getAccuracy() < MIN_ACCURACY) { lastPosition = GPSPoint(location.getLatitude(), location.getLongitude()) viewModel.setLastLocation(location) } else{ Log.d(DEBUG_TAG, "Refusing location ${location.latitude},${location.longitude} because accuracy: ${location.accuracy} > MIN_ACCURACY=$MIN_ACCURACY") } } override fun onFusedStatusChanged(isEnabled: Boolean) { Log.d(DEBUG_TAG, "Location provider is enabled: " + isEnabled) isLocationEnabled = isEnabled if(!dataShownInAdapter.get()) if (isEnabled) { setShowingStatus(LocationShowingStatus.SEARCHING) } else { setShowingStatus(LocationShowingStatus.DISABLED) } } } private val locationPermissionLauncher = getPositionRequestLauncher(){ granted -> startLocationUpdatesByType() setShowingStatus(LocationShowingStatus.SEARCHING) } // Two different settings for the location provider private val locationOptionsArrivals = FusedNativeLocationProvider.Options(5 * 1000L, 25f) private val locationOptionsStops = FusedNativeLocationProvider.Options(1000L, 5f) private var locationProvider: FusedNativeLocationProvider? = null /*private val arrivalsListener: ArrivalsListener = object : ArrivalsListener { override fun setProgress(completedRequests: Int, pendingRequests: Int) { if (pendingRequests == 0) { flatProgressBar.setIndeterminate(true) flatProgressBar.setVisibility(View.GONE) } else { flatProgressBar.setIndeterminate(false) flatProgressBar.progress = completedRequests } } /*override fun onAllRequestsCancelled() { if (flatProgressBar != null) flatProgressBar!!.setVisibility(View.GONE) } */ override fun showCompletedArrivals(completedPalinas: ArrayList) { showArrivalsInRecycler(completedPalinas) } } */ //ViewModel private val viewModel : NearbyStopsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let{ setFragmentType(FragType.fromNum(it.getInt(FRAGMENT_TYPE_KEY))) } //locManager = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE); //fragmentLocationListener = new FragmentLocationListener(); if (getContext() != null) { //globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); //globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } //NearbyArrivalsDownloader nearbyArrivalsDownloader = new NearbyArrivalsDownloader(getContext().getApplicationContext(), arrivalsListener); locationProvider = FusedNativeLocationProvider(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Inflate the layout for this fragment val context = requireContext() val root = inflater.inflate(R.layout.fragment_nearby_stops, container, false) gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView) gridLayoutManager = AutoFitGridLayoutManager( context.getApplicationContext(), utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat()).toInt() ) gridRecyclerView.setLayoutManager(gridLayoutManager) gridRecyclerView.setHasFixedSize(false) circlingProgressBar = root.findViewById(R.id.circularProgressBar) flatProgressBar = root.findViewById(R.id.horizontalProgressBar) messageTextView = root.findViewById(R.id.messageTextView) enableLocationButton = root.findViewById(R.id.grantLocationButton) titleTextView = root.findViewById(R.id.titleTextView) loadingTextView = root.findViewById(R.id.positionLoadingTextView) switchButton = root.findViewById(R.id.switchButton) scrollListener = CommonScrollListener(mListener, false) switchButton!!.setOnClickListener { v: View? -> switchFragmentType() } if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "onCreateView") val appContext = requireContext().applicationContext DatabaseUpdate.watchUpdateWorkStatus(context, this){ workInfos -> if (workInfos.isEmpty()) { viewModel.setDBUpdateRunning(false) return@watchUpdateWorkStatus } val wi = workInfos.get(0) if (wi.state == WorkInfo.State.RUNNING && locationProvider!!.isRunning()) { locationProvider!!.stopUpdates() viewModel.setDBUpdateRunning(true) } else { //start the request checkPermissionLocationStart() viewModel.setDBUpdateRunning(false) //actually restart request } } //add location listener locationProvider!!.addListener(locationUpdateListener) enableLocationButton.setOnClickListener { locationPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS) } return root } private fun checkPermissionLocationStart(){ Log.d(DEBUG_TAG, "Check permission and start location updates") if (bothLocationPermissionsGranted(requireContext())) { if (!locationProvider!!.isRunning()) { startLocationUpdatesByType() setShowingStatus(LocationShowingStatus.SEARCHING) } else{ Log.w(DEBUG_TAG, "Asked to check and start location updates, but provider is already running") } } else { setShowingStatus(LocationShowingStatus.NO_PERMISSION) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) gridRecyclerView.setVisibility(View.INVISIBLE) gridRecyclerView.addOnScrollListener(scrollListener!!) //mListener?.readyGUIfor(FragmentKind.NEARBY_STOPS) //observe the livedata viewModel.stopsAtDistance.observe(getViewLifecycleOwner()) {stops -> Log.d(DEBUG_TAG, "Received " + stops.size + " stops nearby") var distance = viewModel.distanceMtLiveData.getValue() if (distance == null) { distance = 40 } if ((stops.size < stopsMinNumber && distance <= stopsMaxDistance)) { viewModel.setDistance(distance + 50) //viewModel.requestStopsAtDistance(distance, true); //Log.d(DEBUG_TAG, "Doubling distance now!"); return@observe // THIS WORKS AS AN `else` } displayStopsOrRequestArrivals(stops) } viewModel.locationLiveData.observe(getViewLifecycleOwner()) {loc -> if(loc!=null){ setShowingStatus(LocationShowingStatus.LOCATION_FOUND) } //the stops are updated manually } viewModel.downloadingArrivals.observe(viewLifecycleOwner){ running -> flatProgressBar.isIndeterminate = true if(!running) flatProgressBar.visibility = View.GONE else flatProgressBar.visibility = View.VISIBLE } /*viewModel.progressPerc.observe(viewLifecycleOwner){ progress -> flatProgressBar.isIndeterminate = false flatProgressBar.progress = progress flatProgressBar.max = 100 if (progress<100){ flatProgressBar.visibility = View.VISIBLE } } */ viewModel.arrivalsDecoupled.observe(viewLifecycleOwner){ stoprouteList -> if (getContext() == null) { Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached") return@observe } showArrivals(stoprouteList) //arrivalsStopAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage() //if (mListener != null) mListener!!.readyGUIfor(FragmentKind.NEARBY_ARRIVALS) } //added //checkPermissionLocationStart() } private fun displayStopsOrRequestArrivals(stops: ArrayList){ if (!stops.isEmpty()) { currentNearbyStops = stops //displayStopsOrLaunchArrivalsRequest(stops, lastPosition) viewModel.locationLiveData.value?.let{loc -> when(fragmentType){ FragType.STOPS->{ lastPosition?.let{loc -> Collections.sort(stops, StopSorterByDistance(loc)) showStopsInRecycler(stops,loc) } } FragType.ARRIVALS -> { + lastPosition?.let{ + Collections.sort(stops, StopSorterByDistance(loc)) + } viewModel.requestArrivalsForStops(stops) } } } } else{ showNoStopsMessage() viewModel.cancelAllArrivalsRequests() } } private fun showArrivals(stoprouteList: ArrayList){ val context = requireContext() if (firstLocForArrivals) { mListener?.let{ lastPosition?.let{ pos -> arrivalsStopAdapter = ArrivalsStopAdapter(stoprouteList, it, context, pos) gridRecyclerView.setAdapter(arrivalsStopAdapter) firstLocForArrivals = false } } } else { lastPosition?.let{ pos -> arrivalsStopAdapter?.setRoutesPairListAndPosition(stoprouteList, pos) } } dataShownInAdapter.set(true) } /** * Internal bit used to start location updates */ private fun startLocationUpdatesByType() { when (fragmentType) { FragType.STOPS -> locationProvider!!.startUpdates(locationOptionsStops) FragType.ARRIVALS -> locationProvider!!.startUpdates(locationOptionsArrivals) } } /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ private fun setFragmentType(type: FragType) { val isChanged = fragmentType != type this.fragmentType = type if (isChanged) { startLocationUpdatesByType() } } /** * Set the location in the view model if it is good * @param location new location */ /* private fun updateLocationViewModel(location: Location, accuracy: Float = 150f) { } */ private fun setShowingStatus(newStatus: LocationShowingStatus) { var newStatus = newStatus if (newStatus == showingStatus) { return } if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Changing showing status from $showingStatus to $newStatus") if (!isLocationEnabled && newStatus != LocationShowingStatus.NO_PERMISSION) { Log.d(DEBUG_TAG, "asked to show status: $newStatus but the position is disabled") newStatus = LocationShowingStatus.DISABLED } when (newStatus) { LocationShowingStatus.LOCATION_FOUND -> { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.VISIBLE) messageTextView!!.setVisibility(View.GONE) enableLocationButton.setVisibility(View.GONE) } LocationShowingStatus.NO_PERMISSION -> { circlingProgressBar?.setVisibility(View.GONE) flatProgressBar.setVisibility(View.GONE) loadingTextView?.setVisibility(View.GONE) messageTextView?.setText(R.string.enable_position_message_nearby) messageTextView?.setVisibility(View.VISIBLE) enableLocationButton.setVisibility(View.VISIBLE) } LocationShowingStatus.DISABLED -> { //if (showingStatus== LocationShowingStatus.SEARCHING){ circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) flatProgressBar.setVisibility(View.GONE) //} messageTextView!!.setText(R.string.enable_location_message) messageTextView!!.setVisibility(View.VISIBLE) enableLocationButton.setVisibility(View.GONE) } LocationShowingStatus.SEARCHING -> { circlingProgressBar!!.setVisibility(View.VISIBLE) flatProgressBar.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.GONE) messageTextView!!.setVisibility(View.GONE) enableLocationButton.setVisibility(View.GONE) loadingTextView?.apply { setText(loadingDataMessageId) visibility = View.VISIBLE } } } showingStatus = newStatus } override fun onAttach(context: Context) { super.onAttach(context) if (context is FragmentListenerMain) { mListener = context as FragmentListenerMain } else { throw RuntimeException( context .toString() + " must implement OnFragmentInteractionListener" ) } Log.d(DEBUG_TAG, "OnAttach called") //viewModel = ViewModelProvider(this).get(NearbyStopsViewModel::class.java) } override fun onPause() { super.onPause() //gridRecyclerView.setAdapter(null) Log.d(DEBUG_TAG, "On paused called") locationProvider!!.stopUpdates() } override fun onResume() { super.onResume() //fix view if we were showing the stops or the arrivals loadPreferencesStops() setGuiForFragmentType(fragmentType) //if(lastPosition == null){ viewModel.locationLiveData.value?.let{loc -> lastPosition = loc } //} if(bothLocationPermissionsGranted(requireContext())){ locationProvider?.apply { if(isLocationEnabled()){ //location is enabled, start updates startLocationUpdatesByType() if(lastPosition == null){ setShowingStatus(LocationShowingStatus.SEARCHING) } } else{ setShowingStatus(LocationShowingStatus.DISABLED) } } } else{ setShowingStatus(LocationShowingStatus.NO_PERMISSION) } //setupDataAndLayoutByFragmentType() mListener!!.enableRefreshLayout(false) if(fragmentType == FragType.ARRIVALS){ viewModel.arrivalsDecoupled.value?.let{ //re-do the adapter firstLocForArrivals = true showArrivals(it) } } else if(fragmentType == FragType.STOPS) { viewModel.stopsAtDistance.value?.let { //remake the adapter firstLocForStops = true displayStopsOrRequestArrivals(it) } } /* when (fragmentType) { FragType.STOPS -> if (dataAdapter != null) { //gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) } FragType.ARRIVALS -> if (arrivalsStopAdapter != null) { //gridRecyclerView.setAdapter(arrivalsStopAdapter); circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) } } */ Log.d(DEBUG_TAG, "OnResume called") if (getContext() == null) { Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now") stopsMinNumber = 5 stopsMaxDistance = 600 } //Re-read preferences } private fun loadPreferencesStops(){ val shpr = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext()) //For some reason, they are all saved as strings stopsMaxDistance = shpr.getInt(getString(R.string.pref_key_radius_recents), 600) var isMinStopInt = true try { stopsMinNumber = shpr.getInt(getString(R.string.pref_key_num_recents), 5) } catch (ex: ClassCastException) { isMinStopInt = false } if (!isMinStopInt) try { stopsMinNumber = shpr.getString(getString(R.string.pref_key_num_recents), "5")!!.toInt() } catch (ex: NumberFormatException) { stopsMinNumber = 5 } if (BuildConfig.DEBUG) Log.d( DEBUG_TAG, "Max distance for stops: $stopsMaxDistance, Min number of stops: $stopsMinNumber" ) } override fun onDetach() { super.onDetach() mListener = null //if (arrivalsManager != null) arrivalsManager!!.cancelAllRequests() } override fun onStart() { super.onStart() if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "onStart called") //checkPermissionLocationStart() } /** * Display the stops, or run new set of requests for arrivals */ /*private fun displayStopsOrLaunchArrivalsRequest(stops: ArrayList, location: GPSPoint) { if (stops.isEmpty()) { setNoStopsLayout() return } //quick trial to hopefully always get the stops in the correct order when (fragment_type) { FragType.STOPS -> { setShowingStatus(LocationShowingStatus.STOPS_FOUND) showStopsInRecycler(stops, location) } FragType.ARRIVALS -> { viewModel.requestArrivalsForStops(stops) //setShowingStatus(LocationShowingStatus.SEARCHING) viewModel.arrivalsDecoupled.value?.let{ } } } } */ /** * Call when you need to switch the type of fragment */ private fun switchFragmentType() { when (fragmentType) { FragType.ARRIVALS -> { viewModel.cancelAllArrivalsRequests() setFragmentType(FragType.STOPS) //when switching from arrivals firstLocForStops = true } FragType.STOPS -> { setFragmentType(FragType.ARRIVALS) firstLocForArrivals = true } } //now it's switched setGuiForFragmentType(fragmentType) viewModel.stopsAtDistance.value?.let{ //re-issue update, triggering chain viewModel.stopsAtDistance.value = it } /*if(fragmentType == FragType.ARRIVALS) { viewModel.stopsAtDistance.value?.let{ viewModel.requestArrivalsForStops(it) } } */ // //setupDataAndLayoutByFragmentType() } private fun setGuiForFragmentType(fragmentType: FragType) { when (fragmentType) { FragType.STOPS ->{ switchButton!!.text = getString(R.string.show_arrivals) titleTextView!!.text = getString(R.string.nearby_stops_message) noDataMessageId = R.string.no_stops_nearby loadingDataMessageId = R.string.position_searching_message //switchButton?.backgroundTintList = ColorStateList.valueOf( // ViewUtils.getColorFromTheme(requireContext(), R.attr.colorPrimaryDark)) } FragType.ARRIVALS ->{ titleTextView!!.text = getString(R.string.nearby_arrivals_message) switchButton!!.text = getString(R.string.show_stops) noDataMessageId = R.string.no_stops_nearby_arrivals loadingDataMessageId = R.string.searching_arrivals_indefinite //switchButton?.backgroundTintList = ColorStateList.valueOf( // ResourcesCompat.getColor(resources, R.color.light_blue_900, requireActivity().theme)) } } } /** * Prepare the views for the set fragment type */ /*private fun setupDataAndLayoutByFragmentType() { var dataAvailable = false if (fragmentType == FragType.STOPS) { switchButton!!.text = getString(R.string.show_arrivals) titleTextView!!.text = getString(R.string.nearby_stops_message) viewModel.stopsAtDistance.value?.let { stops-> // if data adapter is not null set stops, otherwise dataAdapter?.setStops(stops) ?: lastPosition?.let{ pos -> dataAdapter = SquareStopAdapter(stops, mListener, pos) } Log.d(DEBUG_TAG, "Found ${stops.size} stops") } dataAdapter?.let{ gridRecyclerView.adapter = it dataAvailable = true } mListener?.readyGUIfor(FragmentKind.NEARBY_STOPS) } else if (fragmentType == FragType.ARRIVALS) { titleTextView!!.text = getString(R.string.nearby_arrivals_message) switchButton!!.text = getString(R.string.show_stops) val arrivalsSorted = viewModel.arrivalsDecoupled.value arrivalsSorted?.let{ arrivalsStopAdapter?.setRoutesPairListAndPosition( it, lastPosition) ?: lastPosition?.let{pos -> arrivalsStopAdapter = ArrivalsStopAdapter(it, mListener!!, requireContext(), pos) } } arrivalsStopAdapter?.let{ gridRecyclerView.setAdapter(it) } mListener?.readyGUIfor(FragmentKind.NEARBY_ARRIVALS) } if(gridRecyclerView.adapter == null){ flatProgressBar.isIndeterminate = true flatProgressBar.visibility = View.VISIBLE } else{ flatProgressBar.visibility = View.GONE flatProgressBar.isIndeterminate = false } } */ //useful methods /**//// GUI METHODS //////// */ private fun showStopsInRecycler(stops: MutableList, location: GPSPoint) { // hide the progress bar flatProgressBar.visibility = View.GONE Collections.sort(stops, StopSorterByDistance(location)) if (dataAdapter == null) { dataAdapter = SquareStopAdapter(stops, mListener, lastPosition) firstLocForStops = false } else { dataAdapter!!.setUserPosition(lastPosition) dataAdapter!!.setStops(stops) } gridRecyclerView.setAdapter(dataAdapter) if(gridRecyclerView.visibility != View.VISIBLE){ if(showingStatus == LocationShowingStatus.LOCATION_FOUND) Log.e(DEBUG_TAG, "Visualization error: the recyclerView is not visible but location status is $showingStatus") else{ Log.w(DEBUG_TAG, "Grid recyclerView should be visible for the stops, setting status ${LocationShowingStatus.LOCATION_FOUND}") setShowingStatus(LocationShowingStatus.LOCATION_FOUND) } } dataShownInAdapter.set(true) //showRecyclerHidingLoadMessage(); /*if (gridRecyclerView!!.getVisibility() != View.VISIBLE) { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView!!.setVisibility(View.VISIBLE) } messageTextView!!.setVisibility(View.GONE) */ } /** * Does exactly what is says on the tin */ /*private fun showRecyclerHidingLoadMessage() { if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) gridRecyclerView.setVisibility(View.VISIBLE) } messageTextView!!.setVisibility(View.GONE) } */ private fun showNoStopsMessage(){ messageTextView!!.setVisibility(View.VISIBLE) messageTextView!!.setText(noDataMessageId) flatProgressBar.visibility = View.GONE circlingProgressBar!!.setVisibility(View.GONE) loadingTextView!!.setVisibility(View.GONE) enableLocationButton.setVisibility(View.GONE) } /* * Local locationListener, to use for the GPS */ /* class FragmentLocationListener implements LocationListenerCompat { private long lastUpdateTime = -1; public boolean isRegistered = false; @Override public void onLocationChanged(@NonNull Location location) { if(viewModel==null){ return; } if(location.getAccuracy()<200) { lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); //viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); viewModel.setLastLocation(location); } lastUpdateTime = System.currentTimeMillis(); //Log.d("BusTO:NearPositListen","can start request for stops: "+ !dbUpdateRunning); } @Override public void onProviderEnabled(@NonNull String provider) { Log.d(DEBUG_TAG, "Location provider "+provider+" enabled"); if(provider.equals(LocationManager.GPS_PROVIDER)){ setShowingStatus(LocationShowingStatus.SEARCHING); } } @Override public void onProviderDisabled(@NonNull String provider) { Log.d(DEBUG_TAG, "Location provider "+provider+" disabled"); if(provider.equals(LocationManager.GPS_PROVIDER)) { setShowingStatus(LocationShowingStatus.DISABLED); } } @Override public void onStatusChanged(@NonNull String provider, int status, @Nullable Bundle extras) { LocationListenerCompat.super.onStatusChanged(provider, status, extras); } } */ companion object { private const val DEBUG_TAG = "NearbyStopsFragment" private const val FRAGMENT_TYPE_KEY = "FragmentType" const val FRAGMENT_TAG: String = "NearbyStopsFrag" const val COLUMN_WIDTH_DP: Int = 250 const val MIN_ACCURACY = 200.0 /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ @JvmStatic fun newInstance(type: FragType): NearbyStopsFragment { //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); val fragment = NearbyStopsFragment() val args = Bundle(1) args.putInt(FRAGMENT_TYPE_KEY, type.num) fragment.setArguments(args) return fragment } } } diff --git a/app/src/main/res/layout/activity_principal.xml b/app/src/main/res/layout/activity_principal.xml index ea5afc4..a42b3c6 100644 --- a/app/src/main/res/layout/activity_principal.xml +++ b/app/src/main/res/layout/activity_principal.xml @@ -1,55 +1,55 @@ \ No newline at end of file diff --git a/app/src/main/res/values-night-v27/themes.xml b/app/src/main/res/values-night-v27/themes.xml new file mode 100644 index 0000000..7ebf668 --- /dev/null +++ b/app/src/main/res/values-night-v27/themes.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 9bb257f..606ca61 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,11 +1,16 @@ + + - + \ No newline at end of file diff --git a/app/src/main/res/values-v19/styles.xml b/app/src/main/res/values-v19/styles.xml deleted file mode 100644 index 5f8f81e..0000000 --- a/app/src/main/res/values-v19/styles.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..f521326 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v35/styles.xml b/app/src/main/res/values-v35/styles.xml deleted file mode 100644 index 1646d1b..0000000 --- a/app/src/main/res/values-v35/styles.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 83324ae..3c9bf34 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,101 +1,102 @@ #ff9800 #E77D13 #F57C00 #f57200 #e66b00 #cc6600 #994d00 + #b35900 #2196F3 #0b6fc1 #2a65e8 #2060dd #8A4247 #FFF3E0 #fff5e5 #e08700 #d16900 #2378e8 #0079f5 #2a968b #0067ff #2F59CC #CC5E43 #548017 #228b22 #0ABA34 #009688 #00a38d #009480 #00a887 #4DB6AC #80cbc4 #008175 #F5F5F5 #dddddd #f8f8f8 #bababa #acacac #757575 #444 #353535 #303030 #f2f2f2 #DE0908 #b30000 #dd441f #b30d0d #f15000 #f2621a #f47333 #2060DD #FFFFFF #000000 #1c1c1c #3f3f3f @color/blue_mid_2 @color/red_dark @color/blue_extra #3089e8 #FF039BE5 #FF01579B #FF40C4FF #FF00B0FF #66000000 #555 #cccccc @color/grey_500 #00000000 @color/orange_700 @color/grey_200 @color/grey_050 @color/grey_200 @color/orange_500 @color/blue_extraurbano @color/metro_red @color/orange_icons_10light @color/grey_400 @color/orange_750_l45 @color/grey_700 @color/blue_700 @color/light_blue_900 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 43597e3..3c0d23d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,106 +1,109 @@ + + \ No newline at end of file