diff --git a/app/build.gradle b/app/build.gradle index 67d5dd3..90c1337 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,193 +1,186 @@ plugins { id 'com.google.protobuf' id 'org.jetbrains.kotlin.android' id 'com.android.application' id 'com.google.devtools.ksp' } android { compileSdk 36 namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 24 //noinspection EditedTargetSdkVersion targetSdkVersion 36 buildToolsVersion = '36.0.0' versionCode 72 versionName "2.4.9" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } testOptions { unitTests.returnDefaultValues = true } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlin { jvmToolchain 17 } lint { abortOnError false } androidResources { generateLocaleConfig true } buildFeatures{ buildConfig = true } splits{ abi{ enable true reset() // Specifies a list of ABIs for Gradle to create APKs for. include "x86", "x86_64","armeabi-v7a", "arm64-v8a" universalApk true } } packagingOptions { resources.excludes.add("META-INF/*") } } protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.22.3' } generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } } ksp { arg("room.schemaLocation", "$projectDir/assets/schemas") } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Guava implementation for DBUpdateWorker implementation 'com.google.guava:guava:33.5.0-android' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity-ktx:$activity_version" implementation "androidx.annotation:annotation:1.9.1" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference-ktx:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.13.0" implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.coordinatorlayout:coordinatorlayout:1.3.0" - implementation 'org.jsoup:jsoup:1.21.2' + implementation 'org.jsoup:jsoup:1.22.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' //maplibre implementation 'org.maplibre.gl:android-sdk:12.0.1' implementation 'org.maplibre.gl:android-sdk-turf:6.0.1' implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2' // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime - implementation 'com.google.protobuf:protoc:4.33.0' - - implementation 'com.google.protobuf:protobuf-javalite:4.33.0' + implementation 'com.google.protobuf:protoc:4.34.1' + implementation 'com.google.protobuf:protobuf-javalite:4.34.1' // mqtt library //implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' //implementation 'com.github.hannesa2:paho.mqtt.android:4.4' - implementation("com.hivemq:hivemq-mqtt-client:1.3.10") - implementation(platform("com.hivemq:hivemq-mqtt-client-websocket:1.3.10")) + implementation("com.hivemq:hivemq-mqtt-client:1.3.13") + implementation(platform("com.hivemq:hivemq-mqtt-client-websocket:1.3.13")) // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" ksp "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" - implementation 'de.siegmar:fastcsv:2.2.2' + implementation 'de.siegmar:fastcsv:4.2.0' testImplementation 'junit:junit:4.13.2' - implementation 'junit:junit:4.13.2' - - implementation "androidx.test.ext:junit:1.3.0" - implementation "androidx.test:core:$androidXTestVersion" - implementation "androidx.test:runner:$androidXTestVersion" - implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.3.0" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index b32915d..304081f 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,94 +1,104 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.os.Bundle; import android.util.Log; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.FragmentTransaction; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { final static String DEBUG_TAG = "ExperimentsActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_container_fragment); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); actionBar.setIcon(R.drawable.ic_launcher); } if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) /* .add(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs("gtt:4U")) */ //.add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) //.add(R.id.fragment_container_view, IntroFragment.class, IntroFragment.makeArguments(0)) //.commit(); //.add(R.id.fragment_container_view, LinesDetailFragment.class, // LinesDetailFragment.Companion.makeArgs("gtt:4U")) .add(R.id.fragment_container_view, MapLibreFragment.class, null) .commit(); } } @Override public void showFloatingActionButton(boolean yes) { Log.d(DEBUG_TAG, "Asked to show the action button"); } @Override public void readyGUIfor(FragmentKind fragmentType) { Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType); } @Override public void requestArrivalsForStopID(String ID) { } @Override public void showMapCenteredOnStop(Stop stop) { } @Override - public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){ + public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); + } + @Override + public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); + tr.addToBackStack("Line-"+routeGtfsId); + tr.commit(); } + } \ 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 5294dcc..8677da1 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,859 +1,865 @@ /* 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.*; -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.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.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); /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getWindow().setNavigationBarContrastEnforced(false); } */ boolean showingArrivalsFromIntent = false; final Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } //database check 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 setDefaultSettingsValuesWhenMissing(); // handle the device "insets" ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rootRelativeLayout), (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); // Apply the insets as a margin to the view. This solution sets only the // bottom, left, and right dimensions, but you can apply whichever insets are // appropriate to your layout. You can also update the view padding if that's // more appropriate. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; mlp.rightMargin = insets.right; v.setLayoutParams(mlp); //set for toolbar //mlp = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams(); //mlp.topMargin = insets.top; //mToolbar.setLayoutParams(mlp); mToolbar.setPadding(0, insets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); /* ViewCompat.setOnApplyWindowInsetsListener(mToolbar, (v, windowInsets) -> { Insets statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); // Apply the insets as a margin to the view. ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.topMargin = statusBarInsets.top; v.setLayoutParams(mlp); v.setPadding(0, statusBarInsets.top, 0, 0); // Return CONSUMED if you don't want the window insets to keep passing // down to descendant views. return WindowInsetsCompat.CONSUMED; }); */ //to properly handle IME WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { insetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } //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"); 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(); } */ //The permissions are handled in the MapLibreFragment instead createAndShowMapFragment(null, allowReturn); } 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, @Nullable String stopIDFrom){ + public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); - tr.addToBackStack("LineonMap-"+routeGtfsId); + tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); + } + @Override + public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); + tr.addToBackStack("LineFromOther-"+routeGtfsId); + tr.commit(); } @Override public void 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){ 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; } } /** * Adjust setting to match the default ones */ private void setDefaultSettingsValuesWhenMissing(){ 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; } //Map style final String mapStylePref = mainSharedPref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, ""); if(mapStylePref.isEmpty()){ final String[] defaultVals = getResources().getStringArray(R.array.map_style_pref_values); editor.putString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, defaultVals[0]); edit=true; } if (edit){ editor.commit(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index ab65954..dc192c6 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,294 +1,294 @@ /* BusTO - UI components Copyright (C) 2017 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.adapters; import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.util.RoutePositionSorter; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; private @NonNull GPSPoint userPosition; private FragmentListenerMain listener; private List< Pair > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable private String KEY_CAPITALIZE; private NameCapitalize capit; public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @NonNull GPSPoint pos) { listener = fragmentListener; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); this.capit = NameCapitalize.getCapitalize(capitalizeKey); } @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS final Pair stopRoutePair = routesPairList.get(position); if(stopRoutePair!=null && stopRoutePair.first!=null){ final Stop stop = stopRoutePair.first; final Route r = stopRoutePair.second; final Double distance = stop.getDistanceFromLocation(userPosition.getLatitude(), userPosition.longitude); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getDisplayCode()); holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return routesPairList.size(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); capit = NameCapitalize.getCapitalize(k); notifyDataSetChanged(); } } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } } public void setUserPosition(@Nullable GPSPoint userPosition) { this.userPosition = userPosition; } public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable GPSPoint pos) { if(pos!=null){ this.userPosition = pos; } if(mRoutesPairList!=null){ //this.routesPairList = routesPairList; //remove duplicates sortAndRemoveDuplicates(mRoutesPairList, this.userPosition); //routesPairList = mRoutesPairList; //STUPID CODE if (this.routesPairList == null || routesPairList.size() == 0){ routesPairList = mRoutesPairList; notifyDataSetChanged(); } else{ final HashMap, Integer> indexMapIn = getRouteIndexMap(mRoutesPairList); final HashMap, Integer> indexMapExisting = getRouteIndexMap(routesPairList); //List> oldList = routesPairList; routesPairList = mRoutesPairList; /* for (Pair pair: indexMapIn.keySet()){ final Integer posIn = indexMapIn.get(pair); if (posIn == null) continue; if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemMoved(posExisting, posIn); indexMapExisting.remove(pair); } else{ //INSERT IT //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemInserted(posIn); } }// //REMOVE OLD STOPS for (Pair pair: indexMapExisting.keySet()) { final Integer posExisting = indexMapExisting.get(pair); if (posExisting == null) continue; //routesPairList.remove(posExisting.intValue()); notifyItemRemoved(posExisting); } //*/notifyDataSetChanged(); } //remove and join the } } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } /** * Sort and remove the repetitions in the list */ private static void sortAndRemoveDuplicates(List< Pair > routesPairList, GPSPoint positionToSort ){ Collections.sort(routesPairList,new RoutePositionSorter(positionToSort)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } private static HashMap, Integer> getRouteIndexMap(List> routesPairList){ final HashMap, Integer> myMap = new HashMap<>(); for (int i=0; i(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); + myMap.put(new Pair<>(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); } return myMap; } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java index edb78b3..416f3ca 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -1,272 +1,272 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.adapters; import android.content.Context; import android.content.res.Resources; import androidx.annotation.NonNull; import androidx.cardview.widget.CardView; import androidx.core.content.res.ResourcesCompat; import androidx.preference.PreferenceManager; import android.content.SharedPreferences; import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.*; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.util.PassaggiSorter; import it.reyboz.bustorino.util.RouteSorterByArrivalTime; import org.jetbrains.annotations.NotNull; /** * This once was a ListView Adapter for BusLine[]. * * Thanks to Framentos developers for the guide: * http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/# * * @author Valerio Bozzolan * @author Ludovico Pavesi * @author Fabio Mazza */ public class PalinaAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private static final int ROW_LAYOUT = R.layout.entry_bus_line_passage; private static final int metroBg = R.drawable.route_background_metro; private static final int busBg = R.drawable.route_background_bus; private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance; private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; private final String KEY_CAPITALIZE; private Capitalize capit; private final List mRoutes; private final PalinaClickListener mRouteListener; @NonNull @NotNull @Override public PalinaViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(ROW_LAYOUT, parent, false); return new PalinaViewHolder(view); } @Override public void onBindViewHolder(@NonNull @NotNull PalinaViewHolder vh, int position) { final Route route = mRoutes.get(position); final Context con = vh.itemView.getContext(); final Resources res = con.getResources(); vh.routeIDTextView.setText(route.getDisplayCode()); vh.routeCard.setOnClickListener(view -> mRouteListener.requestShowingRoute(route)); if(route.destinazione==null || route.destinazione.length() == 0) { vh.rowRouteDestination.setVisibility(View.GONE); // move around the route timetable final ViewGroup.MarginLayoutParams pars = (ViewGroup.MarginLayoutParams) vh.rowRouteTimetable.getLayoutParams(); if (pars!=null){ pars.topMargin = 16; if(Build.VERSION.SDK_INT >= 17) pars.setMarginStart(20); pars.leftMargin = 20; } } else { // View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible. vh.rowRouteDestination.setVisibility(View.VISIBLE); String dest = route.destinazione; switch (capit){ case ALL: dest = route.destinazione.toUpperCase(Locale.ROOT); break; case FIRST: dest = utils.toTitleCase(route.destinazione, true); break; case DO_NOTHING: default: } vh.rowRouteDestination.setText(dest); //set click listener vh.itemView.setOnClickListener(view -> { mRouteListener.showRouteFullDirection(route); }); } switch (route.type) { //UNKNOWN = BUS for the moment case UNKNOWN: case BUS: default: // convertView could contain another background, reset it //vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: //vh.rowStopIcon.setBackgroundResource(extraurbanoBg); vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.extraurban_bus_bg, null)); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: //vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); //vh.rowStopIcon.setBackgroundResource(metroBg); vh.routeIDTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.metro_bg, null)); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case RAILWAY: //vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: // never used but whatever. //vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; } List passaggi = route.passaggi; //TODO: Sort the passaggi with realtime first if source is GTTJSONFetcher if(passaggi.size() == 0) { vh.rowRouteTimetable.setText(R.string.no_passages); } else { vh.rowRouteTimetable.setText(route.getPassaggiToString()); } } @Override public int getItemCount() { return mRoutes.size(); } //private static final int cityIcon = R.drawable.city; // hey look, a pattern! public static class PalinaViewHolder extends RecyclerView.ViewHolder { //final TextView rowStopIcon; final TextView routeIDTextView; final CardView routeCard; final TextView rowRouteDestination; final TextView rowRouteTimetable; public PalinaViewHolder(@NonNull @NotNull View view) { super(view); /* convertView.findViewById(R.id.routeID); vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination); vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere); */ //rowStopIcon = view.findViewById(R.id.routeID); routeIDTextView = view.findViewById(R.id.routeNameTextView); routeCard = view.findViewById(R.id.routeCard); rowRouteDestination = view.findViewById(R.id.routeDestination); rowRouteTimetable = view.findViewById(R.id.routesThatStopHere); } } private static Capitalize getCapitalize(SharedPreferences shPr, String key){ String capitalize = shPr.getString(key, ""); switch (capitalize.trim()){ case "KEEP": return Capitalize.DO_NOTHING; case "CAPITALIZE_ALL": return Capitalize.ALL; case "CAPITALIZE_FIRST": return Capitalize.FIRST; } return Capitalize.DO_NOTHING; } public PalinaAdapter(Context context, Palina p, PalinaClickListener listener, boolean hideEmptyRoutes) { Comparator sorter = null; if (p.getPassaggiSourceIfAny()== Passaggio.Source.GTTJSON){ sorter = new PassaggiSorter(); } final List routes; if (hideEmptyRoutes){ // build the routes by filtering them routes = new ArrayList<>(); for(Route r: p.queryAllRoutes()){ //add only if there is at least one passage if (r.numPassaggi()>0){ routes.add(r); } } } else routes = p.queryAllRoutes(); for(Route r: routes){ if (sorter==null) Collections.sort(r.passaggi); else Collections.sort(r.passaggi, sorter); } Collections.sort(routes,new RouteSorterByArrivalTime()); mRoutes = routes; KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE); this.mRouteListener = listener; } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ capit = getCapitalize(sharedPreferences, KEY_CAPITALIZE); notifyDataSetChanged(); } } enum Capitalize{ DO_NOTHING, ALL, FIRST } public interface PalinaClickListener{ /** * Simple click listener for the whole line (show info) * @param route for toast */ void showRouteFullDirection(Route route); /** - * Show the line with all the stops in the app + * Show the line with all the stops in the line screen * @param route partial line info */ void requestShowingRoute(Route route); } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java index ac72fa3..c95696d 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -1,378 +1,383 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.util.Log; /** * Converts some weird stop IDs found on the 5T website to the form used everywhere else (including GTT website). *

* A stop ID in normalized form is:
* - a string containing a number without leading zeros
* - a string beginning with "ST" and then a number
* - whatever the GTT website uses.
*

* A bus route ID in normalized form is:
* - a string containing a number and optionally: "B", "CS", "CD", "N", "S", "C" at the end
* - the string "METRO"
* - "ST1" or "ST2" (Star 1 and Star 2)
* - a string beginning with E, N, S or W, followed by two digits (with a leading zero)
* - "RV2", "OB1"
* - "CAN" or "FTC" (railway lines)
* - ...screw it, let's just hope all the websites and APIs return something sane as route IDs.
*

* This class exists because Java doesn't support traits.
*
* Note: this class also just became useless, as 5T now uses the same format as GTT website. */ public abstract class FiveTNormalizer { public static String FiveTNormalizeRoute(String RouteID) { while (RouteID.startsWith("0")) { RouteID = RouteID.substring(1); } return RouteID; } // public static String FiveTNormalizeStop(String StopID) { // StopID = FiveTNormalizeRoute(StopID); // // is this faster than a regex? // if (StopID.length() == 5 && StopID.startsWith("ST") && Character.isLetter(StopID.charAt(2)) && Character.isLetter(StopID.charAt(3)) && Character.isLetter(StopID.charAt(4))) { // switch (StopID) { // case "STFER": // return "8210"; // case "STPAR": // return "8211"; // case "STMAR": // return "8212"; // case "STMAS": // return "8213"; // case "STPOS": // return "8214"; // case "STMGR": // return "8215"; // case "STRIV": // return "8216"; // case "STRAC": // return "8217"; // case "STBER": // return "8218"; // case "STPDA": // return "8219"; // case "STDOD": // return "8220"; // case "STPSU": // return "8221"; // case "STVIN": // return "8222"; // case "STREU": // return "8223"; // case "STPNU": // return "8224"; // case "STMCI": // return "8225"; // case "STNIZ": // return "8226"; // case "STDAN": // return "8227"; // case "STCAR": // return "8228"; // case "STSPE": // return "8229"; // case "STLGO": // return "8230"; // } // } // return StopID; // } // // public static String NormalizedToFiveT(final String StopID) { // if(StopID.startsWith("82") && StopID.length() == 4) { // switch (StopID) { // case "8230": // return "STLGO"; // case "8229": // return "STSPE"; // case "8228": // return "STCAR"; // case "8227": // return "STDAN"; // case "8226": // return "STNIZ"; // case "8225": // return "STMCI"; // case "8224": // return "STPNU"; // case "8223": // return "STREU"; // case "8222": // return "STVIN"; // case "8221": // return "STPSU"; // case "8220": // return "STDOD"; // case "8219": // return "STPDA"; // case "8218": // return "STBER"; // case "8217": // return "STRAC"; // case "8216": // return "STRIV"; // case "8215": // return "STMGR"; // case "8214": // return "STPOS"; // case "8213": // return "STMAS"; // case "8212": // return "STMAR"; // case "8211": // return "STPAR"; // case "8210": // return "STFER"; // } // } // // return StopID; // } public static Route.Type decodeType(final String routename, final String bacino) { if(routename.equals("METRO")) { return Route.Type.METRO; } else if(routename.equals("79")) { return Route.Type.RAILWAY; } switch (bacino) { case "U": return Route.Type.BUS; case "F": return Route.Type.RAILWAY; case "E": return Route.Type.LONG_DISTANCE_BUS; default: return Route.Type.BUS; } } /** * Converts a route ID from internal format to display format, returns null if it has the same name. * * @param routeID ID in "internal" and normalized format * @return string with display name, null if unchanged */ public static String routeInternalToDisplay(final String routeID) { if(routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0,2).concat("/"); } switch(routeID) { case "1C": return "1 Chieri"; case "1N": return "1 Nichelino"; case "OB1": return "1 Orbassano"; case "2C": return "2 Chieri"; case "RV2": return "2 Rivalta"; case "CO1": return "Circolare Collegno"; case "SE1": // I wonder why GTT calls this "SE1" while other absurd names have a human readable name too. return "1 Settimo"; case "16CD": return "16 CD"; case "16CS": return "16 CS"; case "79": return "Cremagliera Sassi-Superga"; case "W01": return "Night Buster 1 Arancio"; case "N10": return "Night Buster 10 Gialla"; case "W15": return "Night Buster 15 Rosa"; case "S18": return "Night Buster 18 Blu"; case "S04": return "Night Buster 4 Azzurra"; case "N4": return "Night Buster 4 Rossa"; case "N57": return "Night Buster 57 Oro"; case "W60": return "Night Buster 60 Argento"; case "E68": return "Night Buster 68 Verde"; case "S05": return "Night Buster 5 Viola"; case "ST1": return "Star 1"; case "ST2": return "Star 2"; case "4N": return "4 Navetta"; case "10N": return "10 Navetta"; case "13N": return "13 Navetta"; case "35N": return "35 Navetta"; case "36N": return "36 Navetta"; case "36S": return "36 Speciale"; case "38S": return "38 Speciale"; case "44S": return "44 Scolastico"; case "46N": return "46 Navetta"; case "M1S": return "MetroBus sostitutivo"; default: return null; } } public static String fixShortNameForDisplay(String routeID, boolean withBarratoSpace) { /*if (routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0, 2).concat("/"); } else if (routeID.charAt(routeID.length() - 1) == '/' && routeID.charAt(routeID.length() - 2) == ' ') { //remove last space return routeID.substring(0, routeID.length() - 2).concat("/"); } else return routeID; */ int len = routeID.length(); final boolean isBarrato = (routeID.charAt(len-1) == 'B') || (routeID.charAt(len-1) == '/'); if(isBarrato) { String output; if ((routeID.charAt(len - 2) == ' ')) output = routeID.substring(0, len - 2); else output = routeID.substring(0, len - 1); if(withBarratoSpace) output = output.concat(" /"); else output = output.concat("/"); return output; } else return routeID; } public static String fixShortNameForDisplay(String routeID){ return fixShortNameForDisplay(routeID, false); } public static String routeDisplayToInternal(String displayName){ String name = displayName.trim(); if(name.charAt(displayName.length()-1)=='/'){ return displayName.replace(" ","").replace("/","B"); } switch (name.toLowerCase()){ //DEFAULT CASES case "star 1": return "ST1"; case "star 2": return "ST2"; case "night buster 1 arancio": return "W01"; case "night buster 10 gialla": return "N10"; case "night buster 15 rosa": return "W15"; case "night buster 18 blu": return "S18"; case "night buster 4 azzurra": return "S04"; case "night buster 4 rossa": return "N4"; case "night buster 57 oro": return "N57"; case "night buster 60 argento": return "W60"; case "night buster 68 verde": return "E68"; case "night buster 5 viola": return "S05"; case "1 nichelino": return "1N"; case "1 chieri": return "1C"; case "1 orbassano": return "OB1"; case "2 chieri": return "2C"; case "2 rivalta": return "RV2"; default: // return displayName.trim(); } String[] arr = name.toLowerCase().split("\\s+"); try { if (arr.length == 2 && arr[1].trim().equals("navetta") && Integer.decode(arr[0]) > 0) return arr[0].trim().concat("N"); } catch (NumberFormatException e){ //It's not "# navetta" Log.w("FivetNorm","checking number when it's not"); } if(name.toLowerCase().contains("night buster")){ if(name.toLowerCase().contains("viola")) return "S05"; else if(name.toLowerCase().contains("verde")) return "E68"; } //Everything failed, let's at least compact the the (probable) code return name.replace(" ",""); } + /** + * Create the line name in GTFS format (e.g., "gtt:10U") from a more human readable name ("10") + * @param route the route object + * @return the code for the line in GTFS format + */ public static String getGtfsRouteID(Route route){ String routeName = route.getName(); String cutName = routeName.replace("\\s", ""); int len = cutName.length(); StringBuilder sb = new StringBuilder("gtt:"); if (cutName.charAt(len-1) == '/'){ sb.append(cutName.substring(0, len-2)); sb.append("B"); //cutName = cutName.substring(0, len-2).concat("B"); } else { sb.append(cutName); } //determine service kind switch (route.type){ case UNKNOWN: case BUS: case TRAM: //tourist lines have "U" in the routeid sb.append("U"); break; case RAILWAY: sb.append("F"); break; case LONG_DISTANCE_BUS: sb.append("E"); } return sb.toString(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java index b72ec4c..a3dd28f 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java @@ -1,302 +1,307 @@ /* BusTO - Backend 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.backend.gtfs; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import de.siegmar.fastcsv.reader.CloseableIterator; -import de.siegmar.fastcsv.reader.NamedCsvReader; -import de.siegmar.fastcsv.reader.NamedCsvRow; +import de.siegmar.fastcsv.reader.CsvReader; +import de.siegmar.fastcsv.reader.NamedCsvRecord; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.networkTools; import it.reyboz.bustorino.data.gtfs.CsvTableInserter; import org.jsoup.Jsoup; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; abstract public class GtfsDataParser { public static final String GTFS_ADDRESS="https://www.gtt.to.it/open_data/gtt_gtfs.zip"; public static final String GTFS_PAGE_ADDRESS="http://aperto.comune.torino.it/dataset/feed-gtfs-trasporti-gtt"; private static final String DEBUG_TAG = "BusTO-GTFSDataParser"; private static final Pattern quotePattern = Pattern.compile("^\\s*\"((?:[^\"]|(?:\"\"))*?)\"\\s*,"); /** * First trial for a function to download the zip * @param res Fetcher.result * @return the list of files inside the ziè */ public static ArrayList readFilesList(AtomicReference res){ HttpURLConnection urlConnection; InputStream in; ArrayList result = new ArrayList<>(); try { final URL gtfsUrl = new URL(GTFS_ADDRESS); urlConnection = (HttpURLConnection) gtfsUrl.openConnection(); } catch(IOException e) { //e.printStackTrace(); res.set(Fetcher.Result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. return null; } urlConnection.setConnectTimeout(4000); urlConnection.setReadTimeout(50*1000); try { in = urlConnection.getInputStream(); } catch (Exception e) { try { if(urlConnection.getResponseCode()==404) res.set(Fetcher.Result.SERVER_ERROR_404); } catch (IOException e2) { e2.printStackTrace(); } return null; } try (ZipInputStream stream = new ZipInputStream(in)) { // now iterate through each item in the stream. The get next // entry call will return a ZipEntry for each file in the // stream ZipEntry entry; while ((entry = stream.getNextEntry()) != null) { String s = String.format(Locale.ENGLISH, "Entry: %s len %d added", entry.getName(), entry.getSize() ); System.out.println(s); // Once we get the entry from the stream, the stream is // positioned read to read the raw data, and we keep // reading until read returns 0 or less. result.add(entry.getName()); } } catch (IOException e) { e.printStackTrace(); } // we must always close the zip file. return result; } public static Date getLastGTFSUpdateDate(AtomicReference res) { URL theURL; final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH); //final Date baseDate = dateFormat.parse("1970-00-00T00:00:00+0000"); final Date nullDate = new Date(0); try{ theURL = new URL(GTFS_PAGE_ADDRESS); } catch (IOException ex){ Log.e(DEBUG_TAG, "Fixed URL is null, this is a real issue"); return nullDate; } res.set(Fetcher.Result.OK); final String fullPageDOM = networkTools.getDOM(theURL, res); if(fullPageDOM== null){ //Something wrong happend Log.e(DEBUG_TAG, "Cannot get URL"); return nullDate; } res.set(Fetcher.Result.OK); Document doc = Jsoup.parse(fullPageDOM); Elements sections = doc.select("section.additional-info"); Date finalDate = new Date(0); for (Element sec: sections){ Element head = sec.select("h3").first(); String headTitle = head.text(); if(!headTitle.trim().toLowerCase(Locale.ITALIAN).equals("informazioni supplementari")) continue; for (Element row: sec.select("tr")){ if(!row.selectFirst("th").text().trim() .toLowerCase(Locale.ITALIAN).equals("ultimo aggiornamento")) continue; Attributes spanAttributes = row.selectFirst("td > span").attributes(); String dateAsString = spanAttributes.get("data-datetime"); try { finalDate = dateFormat.parse(dateAsString); return finalDate; }catch (ParseException ex){ Log.e(DEBUG_TAG, "Wrong date for the last update of GTFS Data: "+dateAsString); res.set(Fetcher.Result.PARSER_ERROR); ex.printStackTrace(); } break; } } res.set(Fetcher.Result.PARSER_ERROR); return finalDate; } public static void readGtfsZipEntry(ZipEntry entry, ZipFile zipFile, Context con) throws IOException{ String tableName = entry.getName().split("\\.")[0].trim(); InputStream stream = zipFile.getInputStream(entry); String s = String.format(Locale.ENGLISH, "Entry: %s len %d added", entry.getName(), entry.getSize() ); Log.d(DEBUG_TAG, s); final BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); GtfsDataParser.readCSVWithColumns(reader, tableName, con); stream.close(); } public static void readCSVWithColumns(BufferedReader reader, String tableName, Context con) { //String[] elements; List lineElements; String line; /*final String header = reader.readLine(); if (header == null){ throw new IOException(); }*/ //elements = header.split("\n")[0].split(","); //System.out.println(Arrays.toString(elements)); //lineElements = readCsvLine(header); - NamedCsvReader csvReader = NamedCsvReader.builder().build(reader); - CloseableIterator iterator = csvReader.iterator(); + CsvReader csvReader = CsvReader.builder().ofNamedCsvRecord(reader); + CloseableIterator iterator = csvReader.iterator(); final CsvTableInserter inserter = new CsvTableInserter(tableName,con); /*final HashMap columnMap = new HashMap<>(); for (int i=0; i< lineElements.size(); i++){ //columnMap.put(i, fixStringIfItHasQuotes(elements[i].trim()) ); columnMap.put(i, lineElements.get(i).trim() ); } Log.d(DEBUG_TAG, "Columns for the file: "+columnMap); boolean first = true; while((line = reader.readLine())!=null){ //there is a line of data //elements = line.split("\n")[0].split(","); if(first) Log.d(DEBUG_TAG, "Element line: "+line); lineElements = readCsvLine(line); final Map rowsMap = getColumnsAsString(lineElements.toArray(new String[0]), columnMap); if (first){ Log.d(DEBUG_TAG, " in map:"+rowsMap); first=false; } inserter.addElement(rowsMap); }*/ int c = 0; while (iterator.hasNext()){ - final Map rowsMap = iterator.next().getFields(); + //final Map rowsMap = iterator.next().getFields(); + final NamedCsvRecord record = iterator.next(); + final Map rowsMap = new HashMap<>(); + for (String col: record.getHeader()){ + rowsMap.put(col, record.getField(col)); + } if (c < 1){ Log.d(DEBUG_TAG, " in map:"+rowsMap); c++; } inserter.addElement(rowsMap); } //commit data inserter.finishInsert(); } @NonNull private static Map getColumnsAsString(@NonNull String[] lineElements, Map colsIndices) { final HashMap theMap = new HashMap<>(); for(int l=0; l1){ //if(elements.length<3) throw new IllegalArgumentException("Malformed string"); return elements[1]; } else if(elements.length > 0) return elements[0]; else return item; } //https://stackoverflow.com/questions/7800494/parse-csv-with-double-quote-in-some-cases#7800519 public static List readCsvLine(String line) throws IllegalArgumentException { List list = new ArrayList<>(); line += ","; for (int x = 0; x < line.length(); x++) { String s = line.substring(x); if (s.trim().startsWith("\"")) { Matcher m = quotePattern.matcher(s); if (!m.find()) { Log.e(DEBUG_TAG, "Cannot find pattern, "+s+" , line: "+line); throw new IllegalArgumentException("CSV is malformed"); } list.add(m.group(1).replace("\"\"", "\"")); x += m.end() - 1; } else { int y = s.indexOf(","); if (y == -1) throw new IllegalArgumentException("CSV is malformed"); list.add(s.substring(0, y)); x += y; } } return list; } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt index 86f30e5..d6728e5 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt @@ -1,75 +1,66 @@ /* BusTO - Backend 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.backend.gtfs import com.google.transit.realtime.GtfsRealtime.VehiclePosition +/** + * General data class for the live position update + * Used in both the GTFS and MaTO services + */ data class LivePositionUpdate( val tripID: String, //tripID WITHOUT THE "gtt:" prefix val startTime: String?, val startDate: String?, - val routeID: String, + val routeID: String, // routeID DOES NOT HAVE THE "gtt:" PREFIX val vehicle: String, var latitude: Double, var longitude: Double, var bearing: Float?, //the timestamp IN SECONDS val timestamp: Long, val nextStop: String?, /*val vehicleInfo: VehicleInfo, val occupancyStatus: OccupancyStatus?, val scheduleRelationship: ScheduleRelationship? */ //var tripInfo: TripAndPatternWithStops?, ){ constructor(position: VehiclePosition) : this( position.trip.tripId, position.trip.startTime, position.trip.startDate, position.trip.routeId, position.vehicle.label, position.position.latitude.toDouble(), position.position.longitude.toDouble(), position.position.bearing, position.timestamp, null ) - /*data class VehicleInfo( - val id: String, - val label:String - ) - - */ - /*fun withNewPositionAndBearing(latitude: Double, longitude: Double, bearing: Float) = - LivePositionUpdate(this.tripID, this.startTime, this.startTime, - this.routeID, this.vehicle, latitude, longitude, bearing, - this.timestamp,this.nextStop) - fun withNewPosition(latitude: Double, longitude: Double) = - LivePositionUpdate(this.tripID, this.startTime, this.startTime, - this.routeID, this.vehicle, latitude, longitude, this.bearing, - this.timestamp,this.nextStop) - - */ + fun getLineGTFSFormat(): String{ + return "gtt:$routeID" + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java index 9586f2a..a70cd80 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -1,393 +1,408 @@ /* BusTO (backend components) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.text.Html; import android.text.Spanned; import android.util.Log; import android.util.TypedValue; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; +import java.text.SimpleDateFormat; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.fragments.SettingsFragment; public abstract class utils { private static final double EARTH_RADIUS = 6371.009e3; public static final String SOURCE_CODE_URL ="https://gitpull.it/source/libre-busto/"; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EARTH_RADIUS *c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/ EARTH_RADIUS); } public static int convertDipToPixelsInt(Context con,double dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } /** * Convert distance in meters on Earth in degrees of latitude, keeping the same longitude * @param distanceMeters distance in meters * @return angle in degrees */ public static Double latitudeDelta(Double distanceMeters){ final double angleRad = distanceMeters/EARTH_RADIUS; return Math.toDegrees(angleRad); } /** * Convert distance in meters on Earth in degrees of longitude, keeping the same latitude * @param distanceMeters distance in meters * @param latitude the latitude that is fixed * @return angle in degrees */ public static Double longitudeDelta(Double distanceMeters, Double latitude){ final double theta = Math.toRadians(latitude); final double denom = Math.abs(Math.cos(theta)); final double angleRad = 2*Math.asin(Math.sin(distanceMeters / EARTH_RADIUS) / denom); return Math.toDegrees(angleRad); } public static float convertDipToPixels(Context con, float dp){ return convertDipToPixels(con.getResources(), dp); } public static float convertDipToPixels(Resources res, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,res.getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } //SPLIT ON ', check if contains "D'" if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ sb.append("D'"); subs = subs.substring(2); } int index = 0; char c = subs.charAt(index); if(subs.length() > 1 && c=='('){ sb.append(c); index += 1; c = subs.charAt(index); } sb.append(Character.toUpperCase(c)); if (lowercaseRest) sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else sb.append(subs.substring(index+1)); } if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static List getDefaultArrivalsFetchers(Context context){ SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); final Set setSelected = new HashSet<>(defSharPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>())); if (setSelected.isEmpty()) { return Arrays.asList(new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()); }else{ ArrayList outFetchers = new ArrayList<>(4); /*for(String s: setSelected){ switch (s){ case "matofetcher": outFetchers.add(new MatoAPIFetcher()); break; case "fivetapifetcher": outFetchers.add(new FiveTAPIFetcher()); break; case "gttjsonfetcher": outFetchers.add(new GTTJSONFetcher()); break; case "fivetscraper": outFetchers.add(new FiveTScraperFetcher()); break; default: throw new IllegalArgumentException(); } }*/ if (setSelected.contains("matofetcher")) { outFetchers.add(new MatoAPIFetcher()); setSelected.remove("matofetcher"); } if (setSelected.contains("fivetapifetcher")) { outFetchers.add(new FiveTAPIFetcher()); setSelected.remove("fivetapifetcher"); } if (setSelected.contains("gttjsonfetcher")){ outFetchers.add(new GTTJSONFetcher()); setSelected.remove("gttjsonfetcher"); } if (setSelected.contains("fivetscraper")) { outFetchers.add(new FiveTScraperFetcher()); setSelected.remove("fivetscraper"); } if(!setSelected.isEmpty()){ Log.e("BusTO-Utils","Getting some fetchers values which are not contemplated: "+setSelected); } return outFetchers; } } /*public String getShorterDirection(String headSign){ String[] parts = headSign.split(","); if (parts.length<=1){ return headSign.trim(); } String first = parts[0].trim(); String second = parts[1].trim(); String firstLower = first.toLowerCase(Locale.ITALIAN); switch (firstLower){ case "circolare destra": case "circolare sinistra": case } }*/ /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i Set convertArrayToSet(T[] array) { // Create an empty Set Set set = new HashSet<>(); // Add each element into the set set.addAll(Arrays.asList(array)); // Return the converted Set return set; } public static String giveClassesForArray(T[] array){ StringBuilder sb = new StringBuilder(); for (T f: array){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } return sb.toString(); } public static Spanned convertHtml(String text) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT); } else { return Html.fromHtml(text); } } + /** + * Convert an integer (long) timestamp into a String + * @param timestamp the timestamp in seconds (NOT milliseconds) + * @return the formatted String + */ + public static String unixTimestampToLocalTime(long timestamp){ + return unixTimestampToLocalTime(timestamp, "dd/MM/yyyy HH:mm:ss"); + } + /** + * Convert an integer (long) timestamp into a String + * @param timestamp the timestamp in seconds (NOT milliseconds) + * @param patternFormat the format to convert it to + * @return the formatted String + */ + public static String unixTimestampToLocalTime(long timestamp, String patternFormat) { + Date date = new Date(timestamp * 1000L); // seconds to milliseconds + SimpleDateFormat format = new SimpleDateFormat(patternFormat, Locale.getDefault()); + return format.format(date); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java index 7f9908c..b8faf60 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java @@ -1,397 +1,397 @@ /* BusTO ("backend" components) Copyright (C) 2016 Ludovico Pavesi 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.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.content.Context; import android.net.Uri; import android.util.Log; import java.io.IOException; import java.util.*; import androidx.annotation.Nullable; import de.siegmar.fastcsv.reader.CloseableIterator; import de.siegmar.fastcsv.reader.CsvReader; -import de.siegmar.fastcsv.reader.CsvRow; +import de.siegmar.fastcsv.reader.CsvRecord; import de.siegmar.fastcsv.writer.CsvWriter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsDBInterface; public class UserDB extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "user.db"; static final String TABLE_NAME = "favorites"; private final Context c; // needed during upgrade public final static String COL_ID = "ID"; public final static String COL_USERNAME="username"; public static final int FILE_INVALID=-10; private final static String[] usernameColumnNameAsArray = {"username"}; public final static String[] getFavoritesColumnNamesAsArray = {COL_ID, COL_USERNAME}; private static final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath( AppDataProvider.FAVORITES).build(); public UserDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.c = context; } @Override public void onCreate(SQLiteDatabase db) { // exception intentionally left unhandled db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)"); if(OldDB.doesItExist(this.c)) { upgradeFromOldDatabase(db); } } private void upgradeFromOldDatabase(SQLiteDatabase newdb) { OldDB old; try { old = new OldDB(this.c); } catch(IllegalStateException e) { // can't create database => it doesn't really exist, no matter what doesItExist() says return; } int ver = old.getOldVersion(); /* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app * has crashed midway through the upgrade and the user is retrying, that should never show * up here. And if it does, try to recover favorites anyway. * Versions < 8 already got dropped during the update process, so let's do the same. * * Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it * more complicated. Workaround added in OldDB. */ if(ver >= 8) { ArrayList ID = new ArrayList<>(); ArrayList username = new ArrayList<>(); int len; int len2; try { Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {}); int zero = c.getColumnIndex("busstop_ID"); int one = c.getColumnIndex("busstop_username"); while(c.moveToNext()) { try { ID.add(c.getString(zero)); } catch(Exception e) { // no ID = can't add this continue; } if(c.getString(one) == null || c.getString(one).length() <= 0) { username.add(null); } else { username.add(c.getString(one)); } } c.close(); old.close(); } catch(Exception ignored) { // there's no hope, go ahead and nuke old database. } len = ID.size(); len2 = username.size(); if(len2 < len) { len = len2; } if (len > 0) { try { for (int i = 0; i < len; i++) { final Stop mStop = new Stop(ID.get(i)); mStop.setStopUserName(username.get(i)); addOrUpdateStop(mStop, newdb); } } catch(Exception ignored) { // partial data is better than no data at all, no transactions here } } } if(!OldDB.destroy(this.c)) { // TODO: notify user somehow? Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user."); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } /** * Check if a stop ID is in the favorites * * @param db readable database * @param stopId stop ID * @return boolean */ public static boolean isStopInFavorites(SQLiteDatabase db, String stopId) { boolean found = false; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopId}, null, null, null); if(c.moveToNext()) { found = true; } c.close(); } catch(SQLiteException ignored) { // don't care } return found; } /** * Gets stop name set by the user. * * @param db readable database * @param stopID stop ID * @return name set by user, or null if not set\not found */ public static @Nullable String getStopUserName(SQLiteDatabase db, String stopID) { String username = null; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null); if(c.moveToNext()) { int userNameIndex = c.getColumnIndex("username"); if (userNameIndex>=0) username = c.getString(userNameIndex); } c.close(); } catch(SQLiteException e) { Log.e("BusTO-UserDB","Cannot get stop User name for stop "+stopID+":\n"+e); } return username; } /** * Get all the bus stops marked as favorites * * @param db * @param dbi * @return */ public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) { List l = new ArrayList<>(); Stop s; String stopID, stopUserName; try { Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null); int colID = c.getColumnIndex("ID"); int colUser = c.getColumnIndex("username"); while(c.moveToNext()) { stopUserName = c.getString(colUser); stopID = c.getString(colID); s = dbi.getAllFromID(stopID); if(s == null) { // can't find it in database l.add(new Stop(stopUserName, stopID, null, null, null)); } else { // setStopName() already does sanity checks s.setStopUserName(stopUserName); l.add(s); } } c.close(); } catch(SQLiteException ignored) {} // comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database Collections.sort(l); return l; } public static void notifyContentProvider(Context context){ context. getContentResolver(). notifyChange(FAVORITES_URI, null); } public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){ List colsList = Arrays.asList(columns); if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){ throw new IllegalArgumentException(); } ArrayList l = new ArrayList<>(); if (cursor==null){ Log.e("UserDB-BusTO", "Null cursor given in getFavoritesFromCursor"); return l; } final int colID = cursor.getColumnIndex("ID"); final int colUser = cursor.getColumnIndex("username"); while(cursor.moveToNext()) { final String stopUserName = cursor.getString(colUser); final String stopID = cursor.getString(colID); final Stop s = new Stop(stopID.trim()); if (stopUserName!=null) s.setStopUserName(stopUserName); l.add(s); } return l; } public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { ContentValues cv = new ContentValues(); long result = -1; String un = s.getStopUserName(); cv.put("ID", s.ID); // is there an username? if(un == null) { // no: see if it's in the database cv.put("username", getStopUserName(db, s.ID)); } else { // yes: use it cv.put("username", un); } try { //ignore and throw -1 if the row is already in the DB result = db.insertWithOnConflict(TABLE_NAME, null, cv,SQLiteDatabase.CONFLICT_IGNORE); } catch (SQLiteException ignored) {} // Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return return (result != -1) || updateStop(s, db); } public static boolean updateStop(Stop s, SQLiteDatabase db) { try { ContentValues cv = new ContentValues(); cv.put("username", s.getStopUserName()); db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean deleteStop(Stop s, SQLiteDatabase db) { try { db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean checkStopInFavorites(String stopID, Context con){ boolean found = false; // no stop no party if (stopID != null) { SQLiteDatabase userDB = new UserDB(con).getReadableDatabase(); found = UserDB.isStopInFavorites(userDB, stopID); } return found; } //extract rows into CSV public boolean writeFavoritesToCsv(CsvWriter writer){ SQLiteDatabase db = this.getReadableDatabase(); String sortOrder = COL_ID + " DESC"; Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder); final int nCols = 2;//cursor.getColumnCount(); - writer.writeRow(cursor.getColumnNames()); + writer.writeRecord(cursor.getColumnNames()); while (cursor.moveToNext()){ String[] arr = {cursor.getString(0), cursor.getString(1)}; - writer.writeRow(arr); + writer.writeRecord(arr); } cursor.close(); return true; } - public int insertRowsFromCSV(CsvReader reader){ + public int insertRowsFromCSV(CsvReader reader){ SQLiteDatabase db = this.getWritableDatabase(); boolean firstrow = true; final HashMap colIndexByRows = new HashMap<>(); - final CloseableIterator rowsIter = reader.iterator(); + final CloseableIterator rowsIter = reader.iterator(); if (!rowsIter.hasNext()){ //nothing to do, it's an empty file return -1; } - final CsvRow firstRow = rowsIter.next(); + final CsvRecord firstRow = rowsIter.next(); // close if there isn't another rows if(!rowsIter.hasNext()) return -2; for (int i =0; i= 0) updated +=1; } db.setTransactionSuccessful(); db.endTransaction(); db.close(); return updated; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt index 5572e7c..a6ef86e 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -1,792 +1,792 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.database.Cursor import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.viewmodels.ArrivalsViewModel import java.util.* class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { private var DEBUG_TAG = DEBUG_TAG_ALL private lateinit var stopID: String //private set private var stopName: String? = null private var prefs: DBStatusManager? = null private var listener: OnDBUpdateStatusChangeListener? = null private var justCreated = false private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false private var stopIsInFavorites = false //Views protected lateinit var addToFavorites: ImageButton protected lateinit var timesSourceTextView: TextView protected lateinit var messageTextView: TextView protected lateinit var arrivalsRecyclerView: RecyclerView private var mListAdapter: PalinaAdapter? = null private lateinit var resultsLayout : LinearLayout private lateinit var loadingMessageTextView: TextView private lateinit var progressBar: ProgressBar private lateinit var howDoesItWorkTextView: TextView private lateinit var hideHintButton: Button //private NestedScrollView theScrollView; protected lateinit var noArrivalsRecyclerView: RecyclerView private var noArrivalsAdapter: RouteOnlyLineAdapter? = null private var noArrivalsTitleView: TextView? = null private var layoutManager: GridLayoutManager? = null //private View canaryEndView; private var fetchers: List = ArrayList() private val arrivalsViewModel : ArrivalsViewModel by viewModels() private var reloadOnResume = true fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName: String? Log.d(DEBUG_TAG, "Make toast for line " + route.name) routeName = FiveTNormalizer.routeInternalToDisplay(route.name) if (routeName == null) { routeName = route.displayCode } if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { Log.d( DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" ) if (route.gtfsId != null) { - mListener.showLineOnMap(route.gtfsId, stopID) + mListener.openLineFromStop(route.gtfsId, stopID) } else { val gtfsID = FiveTNormalizer.getGtfsRouteID(route) Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") - mListener.showLineOnMap(gtfsID, stopID) + mListener.openLineFromStop(gtfsID, stopID) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) addToFavorites = root.findViewById(R.id.addToFavorites) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) timesSourceTextView = root.findViewById(R.id.timesSourceTextView) timesSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing) //mListener.requestArrivalsForStopID(stopID) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } timesSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites toggleLastStopToFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } noArrivalsRecyclerView.setLayoutManager(layoutManager) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times lastUpdatedPalina?.let { showArrivalsSources(it) } /*if (lastUpdatedPalina?.queryAllRoutes() != null && lastUpdatedPalina!!.queryAllRoutes()!!.size >0){ showArrivalsSources(lastUpdatedPalina!!) } else{ Log.d(DEBUG_TAG, "No routes names") } */ arrivalsViewModel.palinaLiveData.observe(viewLifecycleOwner){ mListener.toggleSpinner(false) if(arrivalsViewModel.resultLiveData.value==Fetcher.Result.OK){ //the result is true changeUIFirstSearchActive(false) updateFragmentData(it) } else{ progressBar.visibility=View.INVISIBLE // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times if (stopName==null) loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) else loadingMessageTextView.text = getString(R.string.no_arrivals_stop) } } arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "Using arrivals source: $it") val srcString = getDisplayArrivalsSource(it,requireContext()) loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString) } arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res -> when (res) { Fetcher.Result.OK -> {} Fetcher.Result.CLIENT_OFFLINE -> showToastMessage(R.string.network_error, true) Fetcher.Result.SERVER_ERROR -> { if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true) } else { showToastMessage(R.string.network_error, true) } showToastMessage(R.string.internal_error,true) } Fetcher.Result.PARSER_ERROR -> showShortToast(R.string.internal_error) Fetcher.Result.QUERY_TOO_SHORT -> showShortToast(R.string.query_too_short) Fetcher.Result.EMPTY_RESULT_SET -> showShortToast(R.string.no_arrivals_stop) Fetcher.Result.NOT_FOUND -> showShortToast(R.string.no_bus_stop_have_this_name) else -> showShortToast(R.string.internal_error) } } return root } private fun showShortToast(id: Int) = showToastMessage(id,true) private fun changeUIFirstSearchActive(yes: Boolean){ if(yes){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } else{ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } } override fun onResume() { super.onResume() val loaderManager = loaderManager Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina") /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ /*if(lastUpdatedPalina!=null){ updateFragmentData(null); showArrivalsSources(lastUpdatedPalina); }*/ mListener.readyGUIfor(FragmentKind.ARRIVALS) //fix bug when the list adapter is null mListAdapter?.let { resetListAdapter(it) } if (noArrivalsAdapter != null) { noArrivalsRecyclerView.adapter = noArrivalsAdapter } if (stopID.isNotEmpty()) { if (!justCreated) { fetchers = utils.getDefaultArrivalsFetchers(context) adjustFetchersToSource() if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID) } else { //start first search requestArrivalsForTheFragment() changeUIFirstSearchActive(true) justCreated = false } //start the loader if (prefs!!.isDBUpdating(true)) { prefs!!.registerListener() } else { Log.d(DEBUG_TAG, "Restarting loader for stop") loaderManager.restartLoader( loaderFavId, arguments, this ) } updateMessage() } if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) { showHints() } } override fun onStart() { super.onStart() if (needUpdateOnAttach) { updateFragmentData(null) needUpdateOnAttach = false } } override fun onPause() { if (listener != null) prefs!!.unregisterListener() super.onPause() val loaderManager = loaderManager Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders()) loaderManager.destroyLoader(loaderFavId) } override fun onAttach(context: Context) { super.onAttach(context) //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context) } fun reloadsOnResume(): Boolean { return reloadOnResume } fun setReloadOnResume(reloadOnResume: Boolean) { this.reloadOnResume = reloadOnResume } // HINT "HOW TO USE" private fun showHints() { howDoesItWorkTextView.visibility = View.VISIBLE hideHintButton.visibility = View.VISIBLE //actionHelpMenuItem.setVisible(false); } private fun hideHints() { howDoesItWorkTextView.visibility = View.GONE hideHintButton.visibility = View.GONE //actionHelpMenuItem.setVisible(true); } fun onHideHint(v: View?) { hideHints() setOption(requireContext(), OPTION_SHOW_LEGEND, false) } /*val currentFetchersAsArray: Array get() { val arr = arrayOfNulls(fetchers!!.size) fetchers!!.toArray(arr) return arr } */ fun getCurrentFetchersAsArray(): Array { val r= fetchers.toTypedArray() //?: emptyArray() return r } private fun rotateFetchers() { Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers") fetchers?.let { Collections.rotate(it, -1) } Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers") } /** * Update the UI with the new data * @param p the full Palina */ fun updateFragmentData(p: Palina?) { if (p != null) lastUpdatedPalina = p if (!isAdded) { //defer update at next show if (p == null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null") else needUpdateOnAttach = true } else { val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) showArrivalsSources(lastUpdatedPalina!!) resetListAdapter(adapter) val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView!!.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) noArrivalsRecyclerView!!.adapter = noArrivalsAdapter noArrivalsRecyclerView!!.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) timesSourceTextView.text = base_message timesSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { timesSourceTextView.visibility = View.VISIBLE } else { timesSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { setTextViewMessage(getString(R.string.passages, message)) } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader when (id) { loaderFavId -> { builder.appendPath("favorites").appendPath(stopID) cl = CursorLoader(requireContext(), builder.build(), UserDB.getFavoritesColumnNamesAsArray, null, null, null) } loaderStopId -> { builder.appendPath("stop").appendPath(stopID) cl = CursorLoader( requireContext(), builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME), null, null, null ) } else -> { cl = CursorLoader(requireContext(), builder.build(), null, null,null,null) Log.d(DEBUG_TAG, "This is probably going to crash") } } cl.setUpdateThrottle(500) return cl } override fun onLoadFinished(loader: Loader, data: Cursor) { when (loader.id) { loaderFavId -> { val colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]) if (data.count > 0) { // IT'S IN FAVORITES data.moveToFirst() val probableName = data.getString(colUserName) stopIsInFavorites = true if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop //update the message in the textview updateMessage() } else { stopIsInFavorites = false } updateStarIcon() if (stopName == null) { //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment$tag", "Stop wasn't in the favorites and has no name, looking in the DB") loaderManager.restartLoader( loaderStopId, arguments, this ) } } loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ) if (index == -1) { Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...") } stopName = data.getString(index) updateMessage() } else { Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } } } override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } protected fun resetListAdapter(adapter: PalinaAdapter) { mListAdapter = adapter arrivalsRecyclerView.adapter = adapter arrivalsRecyclerView.visibility = View.VISIBLE } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } fun toggleLastStopToFavorites() { val stop: Stop? = lastUpdatedPalina if (stop != null) { // toggle the status in background AsyncStopFavoriteAction( requireContext().applicationContext, AsyncStopFavoriteAction.Action.TOGGLE ) { v: Boolean -> updateStarIconFromLastBusStop(v) }.execute(stop) } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true) } } /** * Update the star "Add to favorite" icon */ fun updateStarIconFromLastBusStop(toggleDone: Boolean) { stopIsInFavorites = if (stopIsInFavorites) !toggleDone else toggleDone updateStarIcon() // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ fun updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID.isEmpty()) { addToFavorites.visibility = View.INVISIBLE } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled) } else { addToFavorites.setImageResource(R.drawable.ic_star_outline) } addToFavorites.visibility = View.VISIBLE } } override fun onDestroyView() { //arrivalsRecyclerView = null if (arguments != null) { requireArguments().putString(SOURCES_TEXT, timesSourceTextView.text.toString()) requireArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.text.toString()) } super.onDestroyView() } override fun getBaseViewForSnackBar(): View? { return null } fun isFragmentForTheSameStop(p: Palina): Boolean { return if (tag != null) tag == getFragmentTag(p) else false } /** * Request arrivals in the fragment */ fun requestArrivalsForTheFragment(){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() //AsyncArrivalsSearcher(, getCurrentFetchersAsArray(), context).execute(stopID) context?.let { mListener.toggleSpinner(true) val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" } //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray()) //val workManager = WorkManager.getInstance(it) //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest) arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray()) //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID") } } companion object { private const val OPTION_SHOW_LEGEND = "show_legend" private const val KEY_STOP_ID = "stopid" private const val KEY_STOP_NAME = "stopname" private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment" private const val loaderFavId = 2 private const val loaderStopId = 1 const val STOP_TITLE: String = "messageExtra" private const val SOURCES_TEXT = "sources_textview_message" @JvmStatic @JvmOverloads fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment { val fragment = ArrivalsFragment() val args = Bundle() args.putString(KEY_STOP_ID, stopID) //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null) { args.putString(KEY_STOP_NAME, stopName) } fragment.arguments = args return fragment } @JvmStatic fun getFragmentTag(p: Palina): String { return "palina_" + p.ID } @JvmStatic fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID" @JvmStatic fun getDisplayArrivalsSource(source: Source, context: Context): String{ return when (source) { Source.GTTJSON -> context.getString(R.string.gttjsonfetcher) Source.FiveTAPI -> context.getString(R.string.fivetapifetcher) Source.FiveTScraper -> context.getString(R.string.fivetscraper) Source.MatoAPI -> context.getString(R.string.source_mato) Source.UNDETERMINED -> //Don't show the view context.getString(R.string.undetermined_source) } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt index 47468b2..01dcc9d 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/BackupImportFragment.kt @@ -1,298 +1,296 @@ package it.reyboz.bustorino.fragments import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CheckBox import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import de.siegmar.fastcsv.reader.CsvReader import de.siegmar.fastcsv.writer.CsvWriter import it.reyboz.bustorino.R import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.util.ImportExport import java.io.* import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream /** * A simple [Fragment] subclass. * Use the [BackupImportFragment.newInstance] factory method to * create an instance of this fragment. */ class BackupImportFragment : Fragment() { private val saveFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.also { uri -> writeDataZip(uri) } } } private val openFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (!(loadFavorites|| loadPreferences)){ Toast.makeText(context, R.string.message_check_at_least_one, Toast.LENGTH_SHORT).show() } else if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.also { uri -> - loadZipData(uri,loadFavorites, loadPreferences) } } } private lateinit var saveButton: Button private var loadFavorites = true private var loadPreferences = true private lateinit var checkFavorites: CheckBox private lateinit var checkPreferences: CheckBox override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /*arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) }*/ } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootview= inflater.inflate(R.layout.fragment_test_saving, container, false) saveButton = rootview.findViewById(R.id.saveButton) saveButton.setOnClickListener { startFileSaveIntent() } checkFavorites = rootview.findViewById(R.id.favoritesCheckBox) checkFavorites.setOnCheckedChangeListener { _, isChecked -> loadFavorites = isChecked } checkPreferences = rootview.findViewById(R.id.preferencesCheckBox) checkPreferences.setOnCheckedChangeListener { _, isChecked -> loadPreferences = isChecked } val readFavoritesButton = rootview.findViewById