diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
index e593268..7a9f1fb 100644
--- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
@@ -1,786 +1,799 @@
/*
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.Build;
import android.os.Bundle;
import android.util.Log;
+import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.DBUpdateWorker;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.PreferencesHolder;
import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
import it.reyboz.bustorino.fragments.*;
import it.reyboz.bustorino.middleware.GeneralActivity;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.backend.utils.openIceweasel;
public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain {
private DrawerLayout mDrawer;
private NavigationView mNavView;
private ActionBarDrawerToggle drawerToggle;
private final static String DEBUG_TAG="BusTO Act Principal";
private final static String TAG_FAVORITES="favorites_frag";
private Snackbar snackbar;
private boolean showingMainFragmentFromOther = false;
private boolean onCreateComplete = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState);
setContentView(R.layout.activity_principal);
boolean showingArrivalsFromIntent = false;
Toolbar mToolbar = findViewById(R.id.default_toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar()!=null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
else Log.w(DEBUG_TAG, "NO ACTION BAR");
mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this));
mDrawer = findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle(mToolbar);
// Setup toggle to display hamburger icon with nice animation
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
mDrawer.addDrawerListener(drawerToggle);
mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
}
@Override
public void onDrawerOpened(@NonNull View drawerView) {
hideKeyboard();
}
@Override
public void onDrawerClosed(@NonNull View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
});
mNavView = findViewById(R.id.nvView);
setupDrawerContent(mNavView);
/*View header = mNavView.getHeaderView(0);
*/
//mNavView.getMenu().findItem(R.id.versionFooter).
/// LEGACY CODE
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
Uri data = getIntent().getData();
if (data != null) {
busStopID = getBusStopIDFromUri(data);
Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
/*
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
//setBusStopSearchByIDEditText(busStopID);
//Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent");
requestArrivalsForStopID(busStopID); //this shows the fragment, too
showingArrivalsFromIntent = true;
}
//database check
GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this);
final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion();
boolean dataUpdateRequested = false;
final SharedPreferences theShPr = getMainSharedPreferences();
final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr);
Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version);
if (old_version < db_version){
//decide update conditions in the future
if(old_version < 2 && db_version >= 2) {
dataUpdateRequested = true;
DatabaseUpdate.requestDBUpdateWithWork(this, true, true);
}
PreferencesHolder.setGtfsDBVersion(theShPr, db_version);
}
//Try (hopefully) database update
if(!dataUpdateRequested)
DatabaseUpdate.requestDBUpdateWithWork(this, false, false);
/*
Watch for database update
*/
final WorkManager workManager = WorkManager.getInstance(this);
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
.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) {
createDefaultSnackbar();
} else {
if(snackbar!=null) {
snackbar.dismiss();
snackbar = null;
}
}
});
// show the main fragment
Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
Log.d(DEBUG_TAG, "OnCreate the fragment is "+f);
String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "");
//if (vl.length() == 0 || vl.equals("arrivals")) {
// showMainFragment();
Log.d(DEBUG_TAG, "The default screen to open is: "+vl);
if (showingArrivalsFromIntent){
//do nothing but exclude a case
}else if (savedInstanceState==null) {
//we are not restarting the activity from nothing
if (vl.equals("map")) {
requestMapFragment(false);
} else if (vl.equals("favorites")) {
checkAndShowFavoritesFragment(getSupportFragmentManager(), false);
} else if (vl.equals("lines")) {
showLinesFragment(getSupportFragmentManager(), false, null);
} else {
showMainFragment(false);
}
}
onCreateComplete = true;
//last but not least, set the good default values
manageDefaultValuesForSettings();
//check if first run activity (IntroActivity) has been started once or not
boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false);
if(!hasIntroRun){
startIntroductionActivity();
}
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
// and will not render the hamburger icon without it.
return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}
/**
* Setup drawer actions
* @param navigationView the navigation view on which to set the callbacks
*/
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
menuItem -> {
if (menuItem.getItemId() == R.id.drawer_action_settings) {
Log.d("MAINBusTO", "Pressed button preferences");
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class));
return true;
} else if(menuItem.getItemId() == R.id.nav_favorites_item){
closeDrawerIfOpen();
//get Fragment
checkAndShowFavoritesFragment(getSupportFragmentManager(), true);
return true;
} else if(menuItem.getItemId() == R.id.nav_arrivals){
closeDrawerIfOpen();
showMainFragment(true);
return true;
} else if(menuItem.getItemId() == R.id.nav_map_item){
closeDrawerIfOpen();
requestMapFragment(true);
return true;
} else if (menuItem.getItemId() == R.id.nav_lines_item) {
closeDrawerIfOpen();
showLinesFragment(getSupportFragmentManager(), true,null);
return true;
} else if(menuItem.getItemId() == R.id.drawer_action_info) {
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
}
//selectDrawerItem(menuItem);
Log.d(DEBUG_TAG, "pressed item "+menuItem);
return true;
});
}
private void closeDrawerIfOpen(){
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
}
// `onPostCreate` called when activity start-up is complete after `onStart()`
// NOTE 1: Make sure to override the method with only a single `Bundle` argument
// Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method.
// There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon.
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.principal_menu, menu);
MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments);
SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
experimentsMenuItem.setVisible(exper_On);
return super.onCreateOptionsMenu(menu);
}
//requesting permissions
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode==STORAGE_PERMISSION_REQ){
final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE;
if (permissionDoneRunnables.containsKey(storagePerm)) {
Runnable toRun = permissionDoneRunnables.get(storagePerm);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storagePerm);
}
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions));
if (permissionDoneRunnables.containsKey(storagePerm)) {
Runnable toRun = permissionDoneRunnables.get(storagePerm);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storagePerm);
}
} else {
//permission denied
showToastMessage(R.string.permission_storage_maps_msg, false);
}
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item};
Log.d(DEBUG_TAG, "Item pressed");
if (item.getItemId() == android.R.id.home) {
mDrawer.openDrawer(GravityCompat.START);
return true;
}
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
boolean foundFragment = false;
Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){
//if we have been asked to show a stop from another fragment, we should go back even in the main
if(shownFrag instanceof MainScreenFragment){
//we have to stop the arrivals reload
((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded();
}
shownFrag.getChildFragmentManager().popBackStack();
if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main back stack also");
}
}
else if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main frame backstack for fragments");
}
else
super.onBackPressed();
}
/**
* Create and show the SnackBar with the message
*/
private void createDefaultSnackbar() {
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");
- snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE);
- snackbar.show();
+ //if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now");
+ if (baseView !=null && showSnackbar) {
+ this.snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE);
+ if (frag instanceof ScreenBaseFragment){
+ ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(this.snackbar);
+ }
+ this.snackbar.show();
+
+ } else{
+ Log.e(DEBUG_TAG, "Asked to show the snackbar but the baseView is null");
+ }
}
/**
* Show the fragment by adding it to the backstack
* @param fraMan the fragmentManager
* @param fragment the fragment
*/
private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){
FragmentTransaction ft = fraMan.beginTransaction()
.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG)
.setReorderingAllowed(false)
/*.setCustomAnimations(
R.anim.slide_in, // enter
R.anim.fade_out, // exit
R.anim.fade_in, // popEnter
R.anim.slide_out // popExit
)*/
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToBackStack) ft.addToBackStack(null);
ft.commit();
}
/**
* Show the fragment by adding it to the backstack
* @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 requestMapFragment(final boolean allowReturn){
// starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
//nothing to do
Log.d(DEBUG_TAG, "Build codes allow the showing of the map");
createAndShowMapFragment(null, allowReturn);
return;
}
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
Log.d(DEBUG_TAG, "Permission for storage: "+result);
switch (result) {
case PERMISSION_OK:
createAndShowMapFragment(null, allowReturn);
break;
case PERMISSION_ASKING:
permissionDoneRunnables.put(permission,
() -> createAndShowMapFragment(null, allowReturn));
break;
case PERMISSION_NEG_CANNOT_ASK:
String storage_perm = getString(R.string.storage_permission);
String text = getString(R.string.too_many_permission_asks, storage_perm);
Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show();
}
}
private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){
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 static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){
FragmentTransaction ft = fragmentManager.beginTransaction();
Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG);
if(f!=null){
ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG);
}else{
//use new method
ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs,
LinesGridShowingFragment.FRAGMENT_TAG);
}
if (addToBackStack)
ft.addToBackStack("linesGrid");
ft.setReorderingAllowed(true)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
private void showMainFragment(boolean addToBackStack){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
final MainScreenFragment mainScreenFragment;
if (fragment==null | !(fragment instanceof MainScreenFragment)){
createShowMainFragment(fraMan, null, addToBackStack);
}
else if(!fragment.isVisible()){
mainScreenFragment = (MainScreenFragment) fragment;
showMainFragment(fraMan, mainScreenFragment, addToBackStack);
Log.d(DEBUG_TAG, "Found the main fragment");
} else{
mainScreenFragment = (MainScreenFragment) fragment;
}
//return mainScreenFragment;
}
@Nullable
private MainScreenFragment getMainFragmentIfVisible(){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment;
else return null;
}
@Override
public void showFloatingActionButton(boolean yes) {
//TODO
}
/*
public void setDrawerSelectedItem(String fragmentTag){
switch (fragmentTag){
case MainScreenFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MapFragment.FRAGMENT_TAG:
break;
case FavoritesFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_favorites_item);
break;
}
}*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible();
if (mainFragmentIfVisible!=null){
mainFragmentIfVisible.readyGUIfor(fragmentType);
}
int titleResId;
switch (fragmentType){
case MAP:
mNavView.setCheckedItem(R.id.nav_map_item);
titleResId = R.string.map;
break;
case FAVORITES:
mNavView.setCheckedItem(R.id.nav_favorites_item);
titleResId = R.string.nav_favorites_text;
break;
case ARRIVALS:
titleResId = R.string.nav_arrivals_text;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case STOPS:
titleResId = R.string.stop_search_view_title;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MAIN_SCREEN_FRAGMENT:
case NEARBY_STOPS:
case NEARBY_ARRIVALS:
titleResId=R.string.app_name_full;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case LINES:
titleResId=R.string.lines;
mNavView.setCheckedItem(R.id.nav_lines_item);
break;
default:
titleResId = 0;
}
if(getSupportActionBar()!=null && titleResId!=0)
getSupportActionBar().setTitle(titleResId);
}
@Override
public void requestArrivalsForStopID(String ID) {
//register if the request came from the main fragment or not
MainScreenFragment probableFragment = getMainFragmentIfVisible();
showingMainFragmentFromOther = (probableFragment==null);
if (showingMainFragmentFromOther){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment);
if(fragment!=null){
//the fragment is there but not shown
probableFragment = (MainScreenFragment) fragment;
// set the flag
probableFragment.setSuppressArrivalsReload(true);
showMainFragment(fraMan, probableFragment, true);
probableFragment.requestArrivalsForStopID(ID);
} else {
// we have no fragment
final Bundle args = new Bundle();
args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID);
//if onCreate is complete, then we are not asking for the first showing fragment
boolean addtobackstack = onCreateComplete;
createShowMainFragment(fraMan, args ,addtobackstack);
}
} else {
//the MainScreeFragment is shown, nothing to do
probableFragment.requestArrivalsForStopID(ID);
}
mNavView.setCheckedItem(R.id.nav_arrivals);
}
@Override
public void showLineOnMap(String routeGtfsId){
readyGUIfor(FragmentKind.LINES);
FragmentTransaction tr = getSupportFragmentManager().beginTransaction();
tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class,
LinesDetailFragment.Companion.makeArgs(routeGtfsId));
tr.addToBackStack("LineonMap-"+routeGtfsId);
tr.commit();
}
@Override
public void toggleSpinner(boolean state) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.toggleSpinner(state);
}
}
@Override
public void enableRefreshLayout(boolean yes) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.enableRefreshLayout(yes);
}
}
@Override
public void showMapCenteredOnStop(Stop stop) {
createAndShowMapFragment(stop, true);
}
//Map Fragment stuff
void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop);
ft.replace(R.id.mainActContentFrame, fragment, MapFragment.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;
}
}
/**
* Adjust setting to match the default ones
*/
private void manageDefaultValuesForSettings(){
SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = mainSharedPref.edit();
//Main fragment to show
String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "");
boolean edit = false;
if (screen.isEmpty()){
editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals");
edit=true;
}
//Fetchers
final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>());
if (setSelected.isEmpty()){
String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default);
editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals));
edit=true;
}
//Live bus positions
final String keySourcePositions=getString(R.string.pref_positions_source);
final String positionsSource = mainSharedPref.getString(keySourcePositions, "");
if(positionsSource.isEmpty()){
String[] defaultVals = getResources().getStringArray(R.array.positions_source_values);
editor.putString(keySourcePositions, defaultVals[0]);
edit=true;
}
if (edit){
editor.commit();
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
index 0f03a28..ba37750 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
@@ -1,140 +1,140 @@
/*
BusTO - Data components
Copyright (C) 2023 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.work.*
import it.reyboz.bustorino.backend.Notifications
import it.reyboz.bustorino.data.gtfs.GtfsTrip
import java.util.concurrent.CountDownLatch
class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameters)
: CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
return downloadGtfsTrips()
}
/**
* Download GTFS Trips from Mato
*/
private fun downloadGtfsTrips():Result{
val tripsList = inputData.getStringArray(TRIPS_KEYS)
if (tripsList== null){
Log.e(DEBUG_TAG,"trips list given is null")
return Result.failure()
}
val gtfsRepository = GtfsRepository(applicationContext)
val matoRepository = MatoRepository(applicationContext)
//clear the matoTrips
val queriedMatoTrips = HashSet()
val downloadedMatoTrips = ArrayList()
val failedMatoTripsDownload = HashSet()
Log.i(DEBUG_TAG, "Requesting download for the trips")
val requestCountDown = CountDownLatch(tripsList.size);
for(trip in tripsList){
queriedMatoTrips.add(trip)
matoRepository.requestTripUpdate(trip,{error->
- Log.e(DEBUG_TAG, "Cannot download Gtfs Trip $trip", error)
+ Log.e(DEBUG_TAG, "Cannot download Gtfs Trip $trip, error: $error")
//val stacktrace = error.stackTrace.take(5)
//Log.w(DEBUG_TAG, "Stacktrace:\n$stacktrace")
failedMatoTripsDownload.add(trip)
requestCountDown.countDown()
}){
if(it.isSuccess){
if (it.result == null){
Log.e(DEBUG_TAG, "Got null result");
}
downloadedMatoTrips.add(it.result!!)
} else{
failedMatoTripsDownload.add(trip)
}
Log.i(
DEBUG_TAG,"Result download, so far, trips: ${queriedMatoTrips.size}, failed: ${failedMatoTripsDownload.size}," +
" succeded: ${downloadedMatoTrips.size}")
//check if we can insert the trips
requestCountDown.countDown()
}
}
requestCountDown.await()
val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID }
if (tripsIDsCompleted.isEmpty()){
Log.d(DEBUG_TAG, "No trips have been downloaded, set work to fail")
return Result.failure()
} else {
val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted)
Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert")
if (doInsert) {
gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips)
}
return Result.success()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val context = applicationContext
Notifications.createDBNotificationChannel(context)
return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context))
}
companion object{
const val TRIPS_KEYS = "tripsToDownload"
const val DEBUG_TAG="BusTO:MatoTripDownWRK"
const val NOTIFICATION_ID=42424221
const val TAG_TRIPS ="gtfsTripsDownload"
fun requestMatoTripsDownload(trips: List, context: Context, debugTag: String): OneTimeWorkRequest? {
if (trips.isEmpty()) return null
val workManager = WorkManager.getInstance(context)
val info = workManager.getWorkInfosForUniqueWork(TAG_TRIPS).get()
val runNewWork = if(info.isEmpty()) true
else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED
val addDat = if(info.isEmpty())
null else info[0].state
Log.d(debugTag, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat")
if(runNewWork) {
val tripsArr = trips.toTypedArray()
val dataBuilder = Data.Builder().putStringArray(TRIPS_KEYS, tripsArr)
//build()
val requ = OneTimeWorkRequest.Builder(MatoTripsDownloadWorker::class.java)
.setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag(TAG_TRIPS)
.build()
workManager.enqueueUniqueWork(TAG_TRIPS, ExistingWorkPolicy.KEEP, requ)
return requ
} else return null;
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
index 900e401..a7117ba 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
@@ -1,333 +1,343 @@
/*
BusTO - Fragments components
Copyright (C) 2021 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import androidx.work.WorkInfo;
import it.reyboz.bustorino.*;
import it.reyboz.bustorino.adapters.StopAdapterListener;
import it.reyboz.bustorino.adapters.StopRecyclerAdapter;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.FavoritesViewModel;
import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction;
public class FavoritesFragment extends ScreenBaseFragment {
private RecyclerView favoriteRecyclerView;
private EditText busStopNameText;
private TextView favoriteTipTextView;
private ImageView angeryBusImageView;
private boolean dbUpdateRunning = false;
private FavoritesViewModel model;
@Nullable
private CommonFragmentListener mListener;
public static final String FRAGMENT_TAG = "BusTOFavFragment";
private final static String DEBUG_TAG = FRAGMENT_TAG;
private final StopAdapterListener adapterListener = new StopAdapterListener() {
@Override
public void onTappedStop(Stop stop) {
mListener.requestArrivalsForStopID(stop.ID);
}
@Override
public boolean onLongPressOnStop(Stop stop) {
Log.d("BusTO-FavoritesFrag", "LongPressOnStop");
return true;
}
};
public static FavoritesFragment newInstance() {
FavoritesFragment fragment = new FavoritesFragment();
Bundle args = new Bundle();
//args.putString(ARG_PARAM1, param1);
//args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
public FavoritesFragment(){
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
//do nothing
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_favorites, container, false);
favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView);
//favoriteListView = root.findViewById(R.id.favoriteListView);
/*favoriteRecyclerView.setOn((parent, view, position, id) -> {
/*
* Casting because of Javamerda
* @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener
*/
/*
Stop busStop = (Stop) parent.getItemAtPosition(position);
if(mListener!=null){
mListener.requestArrivalsForStopID(busStop.ID);
}
});
*/
LinearLayoutManager llManager = new LinearLayoutManager(getContext());
llManager.setOrientation(LinearLayoutManager.VERTICAL);
favoriteRecyclerView.setLayoutManager(llManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(),
llManager.getOrientation());
favoriteRecyclerView.addItemDecoration(dividerItemDecoration);
angeryBusImageView = root.findViewById(R.id.angeryBusImageView);
favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView);
//register for the context menu
registerForContextMenu(favoriteRecyclerView);
model.getFavorites().observe(getViewLifecycleOwner(), this::showStops);
// watch the DB update
DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, workInfos -> {
if(workInfos.isEmpty()) return;
WorkInfo wi = workInfos.get(0);
if(wi.getState() == WorkInfo.State.RUNNING){
dbUpdateRunning = true;
} else {
//force reload if it was previously running
if(model!=null && dbUpdateRunning) {
Log.d(DEBUG_TAG,"DB Finished updating, reload favorites");
model.getFavorites().forceReload();
}
dbUpdateRunning = false;
}
});
showStops(new ArrayList<>());
return root;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof CommonFragmentListener) {
mListener = (CommonFragmentListener) context;
} else {
throw new RuntimeException(context
+ " must implement CommonFragmentListener");
}
model = new ViewModelProvider(this).get(FavoritesViewModel.class);
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
/*
This method is apparently NOT CALLED ANYMORE
Called on Android 6
*/
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v,
ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
Log.d("Favorites Fragment", "Creating context menu ");
if (v.getId() == R.id.favoritesRecyclerView) {
// if we aren't attached to activity, return null
if (getActivity()==null) return;
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.menu_favourites_entry, menu);
}
}
@Override
public void onResume() {
super.onResume();
if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item
.getMenuInfo();
if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter))
return false;
StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter();
Stop busStop = adapter.getStops().get(adapter.getPosition());
switch (item.getItemId()) {
case R.id.action_favourite_entry_delete:
if (getContext()!=null)
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE,
result -> {
}).execute(busStop);
return true;
case R.id.action_rename_bus_stop_username:
showBusStopUsernameInputDialog(busStop);
return true;
case R.id.action_view_on_map:
if (busStop.getLatitude() == null | busStop.getLongitude() == null |
mListener==null
) {
Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show();
return true;
}
//GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude());
mListener.showMapCenteredOnStop(busStop);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Nullable
@Override
public View getBaseViewForSnackBar() {
return favoriteRecyclerView;
}
void showStops(List busStops){
// If no data is found show a friendly message
if(BuildConfig.DEBUG)
Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list");
- if (busStops.size() == 0) {
+ if (busStops.isEmpty()) {
favoriteRecyclerView.setVisibility(View.INVISIBLE);
// TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView);
//assert favoriteTipTextView != null;
favoriteTipTextView.setVisibility(View.VISIBLE);
//ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView);
angeryBusImageView.setVisibility(View.VISIBLE);
} else {
favoriteRecyclerView.setVisibility(View.VISIBLE);
favoriteTipTextView.setVisibility(View.INVISIBLE);
angeryBusImageView.setVisibility(View.INVISIBLE);
}
/* There's a nice method called notifyDataSetChanged() to avoid building the ListView
* all over again. This method exists in a billion answers on Stack Overflow, but
* it's nowhere to be seen around here, Android Studio can't find it no matter what.
* Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I
* guess) and requires to modify the list with .add() and .clear() and some other
* methods, so to update a single stop we need to completely rebuild the list for no
* reason. It would probably end up as "slow" as throwing away the old ListView and
* redrwaing everything.
*/
// Show results
favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener, StopRecyclerAdapter.Use.FAVORITES));
}
public void showBusStopUsernameInputDialog(final Stop busStop) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
LayoutInflater inflater = this.getLayoutInflater();
View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null);
busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name);
busStopNameText.setText(busStop.getStopDisplayName());
busStopNameText.setHint(busStop.getStopDefaultName());
builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title));
builder.setView(renameDialogLayout);
builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> {
String busStopUsername = busStopNameText.getText().toString();
String oldUserName = busStop.getStopUserName();
// changed to none
- if(busStopUsername.length() == 0) {
+ if(busStopUsername.isEmpty()) {
// unless it was already empty, set new
if(oldUserName != null) {
busStop.setStopUserName(null);
}
} else { // changed to something
// something different?
if(!busStopUsername.equals(oldUserName)) {
busStop.setStopUserName(busStopUsername);
}
}
launchUpdate(busStop);
});
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> {
// delete user name from database
busStop.setStopUserName(null);
launchUpdate(busStop);
});
builder.show();
}
private void launchUpdate(Stop busStop){
if (getContext()!=null)
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE,
result -> {
//Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show();
}).execute(busStop);
}
+ /*
+ THIS LOOKS TERRIBLE
+ @Override
+ public void setSnackbarPropertiesBeforeShowing(Snackbar snackbar) {
+ final View view = snackbar.getView();
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
+ params.gravity = Gravity.TOP;
+ view.setLayoutParams(params);
+ }
+ */
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
index e234f2e..f6a9cb9 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
@@ -1,403 +1,430 @@
package it.reyboz.bustorino.fragments
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.RecyclerView
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
import it.reyboz.bustorino.R
import it.reyboz.bustorino.adapters.RouteAdapter
import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter
import it.reyboz.bustorino.adapters.StringListAdapter
import it.reyboz.bustorino.backend.utils
+import it.reyboz.bustorino.data.DBUpdateWorker
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager
import it.reyboz.bustorino.util.LinesNameSorter
import it.reyboz.bustorino.util.ViewUtils
import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel
class LinesGridShowingFragment : ScreenBaseFragment() {
private val viewModel: LinesGridShowingViewModel by viewModels()
//private lateinit var gridLayoutManager: AutoFitGridLayoutManager
private lateinit var favoritesRecyclerView: RecyclerView
private lateinit var urbanRecyclerView: RecyclerView
private lateinit var extraurbanRecyclerView: RecyclerView
private lateinit var touristRecyclerView: RecyclerView
private lateinit var favoritesTitle: TextView
private lateinit var urbanLinesTitle: TextView
private lateinit var extrurbanLinesTitle: TextView
private lateinit var touristLinesTitle: TextView
+ private lateinit var updateMessageTextView: TextView
//private lateinit var searchBar: SearchView
private var routesByAgency = HashMap>()
/*hashMapOf(
AG_URBAN to ArrayList(),
AG_EXTRAURB to ArrayList(),
AG_TOUR to ArrayList()
)*/
private lateinit var fragmentListener: CommonFragmentListener
private val linesNameSorter = LinesNameSorter()
private val linesComparator = Comparator { a,b ->
return@Comparator linesNameSorter.compare(a.shortName, b.shortName)
}
private val routeClickListener = RouteAdapter.ItemClicker {
fragmentListener.showLineOnMap(it.gtfsId)
}
private val arrows = HashMap()
private val durations = HashMap()
//private val recyclerViewAdapters= HashMap()
private val lastQueryEmptyForAgency = HashMap(3)
private var openRecyclerView = "AG_URBAN"
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false)
favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView)
urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView)
extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView)
touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView)
+ updateMessageTextView = rootView.findViewById(R.id.updateMessageTextView)
favoritesTitle = rootView.findViewById(R.id.favoritesTitleView)
urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView)
extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView)
touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView)
arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb)
arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist)
arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban)
arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites)
//show urban expanded by default
val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView)
for (recyView in recViews) {
val gridLayoutManager = AutoFitGridLayoutManager(
requireContext().applicationContext,
(utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt()
)
recyView.layoutManager = gridLayoutManager
}
//init favorites recyclerview
val gridLayoutManager = AutoFitGridLayoutManager(
requireContext().applicationContext,
(utils.convertDipToPixels(context, 70f)).toInt()
)
favoritesRecyclerView.layoutManager = gridLayoutManager
viewModel.getLinesLiveData().observe(viewLifecycleOwner){
//routesList = ArrayList(it)
//routesList.sortWith(linesComparator)
routesByAgency.clear()
for (k in AGENCIES){
routesByAgency[k] = ArrayList()
}
for(route in it){
val agency = route.agencyID
if(agency !in routesByAgency.keys){
Log.e(DEBUG_TAG, "The agency $agency is not present in the predefined agencies (${routesByAgency.keys})")
}
routesByAgency[agency]?.add(route)
}
//zip agencies and recyclerviews
Companion.AGENCIES.zip(recViews) { ag, recView ->
routesByAgency[ag]?.let { routeList ->
if (routeList.size > 0) {
routeList.sortWith(linesComparator)
//val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName })
val adapter = RouteAdapter(routeList, routeClickListener)
val lastQueryEmpty = if(ag in lastQueryEmptyForAgency.keys) lastQueryEmptyForAgency[ag]!! else true
if (lastQueryEmpty)
recView.adapter = adapter
else recView.swapAdapter(adapter, false)
lastQueryEmptyForAgency[ag] = false
} else {
val messageString = if(viewModel.getLineQueryValue().isNotEmpty()) getString(R.string.no_lines_found_query) else getString(R.string.no_lines_found)
val extraAdapter = StringListAdapter(listOf(messageString))
recView.adapter = extraAdapter
lastQueryEmptyForAgency[ag] = true
}
durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000
}
}
}
viewModel.favoritesLines.observe(viewLifecycleOwner){ routes->
val routesNames = routes.map { it.shortName }
//create new item click listener every time
val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ ->
val r = routes[pos]
fragmentListener.showLineOnMap(r.gtfsId)
}
favoritesRecyclerView.adapter = adapter
}
//onClicks
urbanLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_URBAN)
}
extrurbanLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_EXTRAURB)
}
touristLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_TOUR)
}
favoritesTitle.setOnClickListener {
closeOpenFavorites()
}
arrows[AG_FAV]?.setOnClickListener {
closeOpenFavorites()
}
//arrows onClicks
for(k in Companion.AGENCIES){
//k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN
arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) }
}
+ // watch for the db update
+ WorkManager.getInstance(requireContext()).getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe(viewLifecycleOwner){
+ workInfoList ->
+ if (workInfoList == null || workInfoList.isEmpty()) {
+ return@observe
+ }
+
+ var showProgress = false
+ for (workInfo in workInfoList) {
+ if (workInfo.state == WorkInfo.State.RUNNING) {
+ updateMessageTextView.visibility = View.VISIBLE
+
+ } else{
+ updateMessageTextView.visibility = View.GONE
+ }
+ break
+ }
+ }
return rootView
}
fun setUserSearch(textSearch:String){
viewModel.setLineQuery(textSearch)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val menuHost: MenuHost = requireActivity()
// Add menu items without using the Fragment Menu APIs
// Note how we can tie the MenuProvider to the viewLifecycleOwner
// and an optional Lifecycle.State (here, RESUMED) to indicate when
// the menu should be visible
menuHost.addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// Add menu items here
menuInflater.inflate(R.menu.menu_search, menu)
val search = menu.findItem(R.id.searchMenuItem).actionView as SearchView
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
setUserSearch(query ?: "")
return true
}
override fun onQueryTextChange(query: String?): Boolean {
setUserSearch(query ?: "")
return true
}
})
search.queryHint = getString(R.string.search_box_lines_suggestion_filter)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
// Handle the menu selection
if (menuItem.itemId == R.id.searchMenuItem){
Log.d(DEBUG_TAG, "Clicked on search menu")
}
else{
Log.d(DEBUG_TAG, "Clicked on something else")
}
return false
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
private fun closeOpenFavorites(){
if(favoritesRecyclerView.visibility == View.VISIBLE){
//close it
favoritesRecyclerView.visibility = View.GONE
setOpen(arrows[AG_FAV]!!, false)
viewModel.favoritesExpanded.value = false
} else{
favoritesRecyclerView.visibility = View.VISIBLE
setOpen(arrows[AG_FAV]!!, true)
viewModel.favoritesExpanded.value = true
}
}
private fun openLinesAndCloseOthersIfNeeded(agency: String){
if(openRecyclerView!="" && openRecyclerView!= agency) {
switchRecyclerViewStatus(openRecyclerView)
}
switchRecyclerViewStatus(agency)
}
private fun switchRecyclerViewStatus(agency: String){
val recyclerView = when(agency){
AG_TOUR -> touristRecyclerView
AG_EXTRAURB -> extraurbanRecyclerView
AG_URBAN -> urbanRecyclerView
else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid")
}
val expandedLiveData = when(agency){
AG_TOUR -> viewModel.isTouristExpanded
AG_URBAN -> viewModel.isUrbanExpanded
AG_EXTRAURB -> viewModel.isExtraUrbanExpanded
else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid")
}
val duration = durations[agency]
val arrow = arrows[agency]
val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration
if(duration!=null&&arrow!=null)
when (recyclerView.visibility){
View.GONE -> {
Log.d(DEBUG_TAG, "Open recyclerview $agency")
//val a =ViewUtils.expand(recyclerView, duration, 0)
recyclerView.visibility = View.VISIBLE
expandedLiveData.value = true
Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}")
setOpen(arrow, true)
//arrow.startAnimation(rotateArrow(true,durArrow))
openRecyclerView = agency
}
View.VISIBLE -> {
Log.d(DEBUG_TAG, "Close recyclerview $agency")
//ViewUtils.collapse(recyclerView, duration)
recyclerView.visibility = View.GONE
expandedLiveData.value = false
//arrow.rotation = 90f
Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate")
setOpen(arrow, false)
//arrow.startAnimation(rotateArrow(false,durArrow))
openRecyclerView = ""
}
View.INVISIBLE -> {
TODO()
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is CommonFragmentListener){
fragmentListener = context
} else throw RuntimeException("$context must implement CommonFragmentListener")
}
override fun getBaseViewForSnackBar(): View? {
return null
}
override fun onResume() {
super.onResume()
val pref = PreferencesHolder.getMainSharedPreferences(requireContext())
val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))}
//restore state
viewModel.favoritesExpanded.value?.let {
if(!it){
//close it
favoritesRecyclerView.visibility = View.GONE
setOpen(arrows[AG_FAV]!!, false)
} else{
favoritesRecyclerView.visibility = View.VISIBLE
setOpen(arrows[AG_FAV]!!, true)
}
}
viewModel.isUrbanExpanded.value?.let {
if(it) {
urbanRecyclerView.visibility = View.VISIBLE
arrows[AG_URBAN]?.rotation= 90f
openRecyclerView = AG_URBAN
Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded")
}
else {
urbanRecyclerView.visibility = View.GONE
arrows[AG_URBAN]?.rotation= 0f
}
}
viewModel.isTouristExpanded.value?.let {
val recview = touristRecyclerView
if(it) {
recview.visibility = View.VISIBLE
arrows[AG_TOUR]?.rotation=90f
openRecyclerView = AG_TOUR
} else {
recview.visibility = View.GONE
arrows[AG_TOUR]?.rotation= 0f
}
}
viewModel.isExtraUrbanExpanded.value?.let {
val recview = extraurbanRecyclerView
if(it) {
openRecyclerView = AG_EXTRAURB
recview.visibility = View.VISIBLE
arrows[AG_EXTRAURB]?.rotation=90f
} else {
recview.visibility = View.GONE
arrows[AG_EXTRAURB]?.rotation=0f
}
}
fragmentListener.readyGUIfor(FragmentKind.LINES)
}
companion object {
private const val COLUMN_WIDTH_DP=200
private const val AG_FAV = "fav"
private const val AG_URBAN = "gtt:U"
private const val AG_EXTRAURB ="gtt:E"
private const val AG_TOUR ="gtt:T"
private const val DEBUG_TAG ="BusTO-LinesGridFragment"
const val FRAGMENT_TAG = "LinesGridShowingFragment"
private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR)
fun newInstance() = LinesGridShowingFragment()
@JvmStatic
fun setOpen(imageView: ImageView, value: Boolean){
if(value)
imageView.rotation = 90f
else
imageView.rotation = 0f
}
@JvmStatic
fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{
val start = if (toOpen) 0f else 90f
val stop = if(toOpen) 90f else 0f
Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop")
val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF,
0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotate.duration = duration
rotate.interpolator = LinearInterpolator()
//rotate.fillAfter = true
rotate.fillBefore = false
return rotate
}
}
+ override fun showSnackbarOnDBUpdate(): Boolean {
+ return false
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
index b99ff4f..70b262c 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
@@ -1,91 +1,104 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
+import android.view.Gravity;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import com.google.android.material.snackbar.Snackbar;
import it.reyboz.bustorino.BuildConfig;
import java.util.Map;
import static android.content.Context.MODE_PRIVATE;
public abstract class ScreenBaseFragment extends Fragment {
protected final static String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs";
protected void setOption(String optionName, boolean value) {
Context mContext = getContext();
SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit();
editor.putBoolean(optionName, value);
editor.commit();
}
protected boolean getOption(String optionName, boolean optDefault) {
Context mContext = getContext();
assert mContext != null;
return getOption(mContext, optionName, optDefault);
}
protected void showToastMessage(int messageID, boolean short_lenght) {
final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
Toast.makeText(getContext(), messageID, length).show();
}
public void hideKeyboard() {
if (getActivity()==null) return;
View view = getActivity().getCurrentFocus();
if (view != null) {
((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(view.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}
/**
* Find the view on which the snackbar should be shown
* @return a view or null if you don't want the snackbar shown
*/
@Nullable
public abstract View getBaseViewForSnackBar();
+ /**
+ * Empty method to override properties of the Snackbar before showing it
+ * @param snackbar the Snackbar to be possibly modified
+ */
+ public void setSnackbarPropertiesBeforeShowing(Snackbar snackbar){
+
+ }
+ public boolean showSnackbarOnDBUpdate() {
+ return true;
+ }
+
public static boolean getOption(Context context, String optionName, boolean optDefault){
SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE);
return preferences.getBoolean(optionName, optDefault);
}
public static void setOption(Context context,String optionName, boolean value) {
SharedPreferences.Editor editor = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit();
editor.putBoolean(optionName, value);
editor.apply();
}
public ActivityResultLauncher getPositionRequestLauncher(LocationRequestListener listener){
return registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() {
@Override
public void onActivityResult(Map result) {
if (result == null) return;
if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||
result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null)
return;
final boolean coarseGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION));
final boolean fineGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION));
listener.onPermissionResult(coarseGranted, fineGranted);
}
});
}
public interface LocationRequestListener{
void onPermissionResult(boolean isCoarseGranted, boolean isFineGranted);
}
}
diff --git a/app/src/main/res/drawable/backgroud_box_round.xml b/app/src/main/res/drawable/backgroud_box_round.xml
new file mode 100644
index 0000000..9218f3f
--- /dev/null
+++ b/app/src/main/res/drawable/backgroud_box_round.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_lines_grid.xml b/app/src/main/res/layout/fragment_lines_grid.xml
index 99df4c5..bb14f8e 100644
--- a/app/src/main/res/layout/fragment_lines_grid.xml
+++ b/app/src/main/res/layout/fragment_lines_grid.xml
@@ -1,177 +1,197 @@
+
+
\ No newline at end of file