diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5d81892..ef7d7a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,122 +1,126 @@ + + + - - + \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityAbout.java b/app/src/main/java/it/reyboz/bustorino/ActivityAbout.java index 8199608..cbed07c 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityAbout.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityAbout.java @@ -1,83 +1,84 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan 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.content.Intent; import android.net.Uri; import android.util.Log; import androidx.appcompat.widget.Toolbar; import androidx.appcompat.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.middleware.BarcodeScanUtils; public class ActivityAbout extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); - Spanned htmlText = Html.fromHtml(getResources().getString( + Spanned htmlText = utils.convertHtml(getResources().getString( R.string.about_history)); TextView aboutTextView = findViewById(R.id.aboutTextView); assert aboutTextView != null; aboutTextView.setText(htmlText); aboutTextView.setMovementMethod(LinkMovementMethod.getInstance()); Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); TextView versionText = findViewById(R.id.versionTextView); Log.d("BusTO About", "The version text view is: "+versionText); versionText.setText(getResources().getText(R.string.app_version)+": "+BuildConfig.VERSION_NAME); Button openTelegramButton = findViewById(R.id.openTelegramButton); openTelegramButton.setOnClickListener(view -> { Intent trueIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("tg://resolve?domain=busto_fdroid")); if(BarcodeScanUtils.checkTargetPackageExists(this, trueIntent)) startActivity(trueIntent); else{ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/busto_fdroid")); startActivity(intent); // Toast.makeText(this, "Install Telegram and retry", Toast.LENGTH_SHORT).show(); } }); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Respond to the action bar's Up/Home button if (item.getItemId() == android.R.id.home) {//NavUtils.navigateUpFromSameTask(this); onBackPressed(); return true; } return super.onOptionsItemSelected(item); } } diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index bb21d62..1b32665 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,92 +1,93 @@ /* 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.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 = "ExperimentsGTFS"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_experiments); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); + 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) - .commit(); + //.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, TestRealtimeGtfsFragment.class, null) - //.commit(); + .add(R.id.fragment_container_view, TestRealtimeGtfsFragment.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){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt b/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt new file mode 100644 index 0000000..237d070 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/ActivityIntro.kt @@ -0,0 +1,107 @@ +package it.reyboz.bustorino + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageButton +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import it.reyboz.bustorino.data.PreferencesHolder +import it.reyboz.bustorino.fragments.IntroFragment + +class ActivityIntro : AppCompatActivity(), IntroFragment.IntroListener { + + private lateinit var viewPager : ViewPager2 + private lateinit var btnForward: ImageButton + private lateinit var btnBackward: ImageButton + + private var restartMain = true + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_intro) + + viewPager = findViewById(R.id.viewPager) + btnBackward = findViewById(R.id.btnPrevious) + btnForward = findViewById(R.id.btnNext) + + val extras = intent.extras + if(extras!=null){ + restartMain = extras.getBoolean(RESTART_MAIN) + } + + + val adapter = IntroPagerAdapter(this) + viewPager.adapter = adapter + + val tabLayout = findViewById(R.id.tab_layout) + val tabLayoutMediator = TabLayoutMediator(tabLayout, viewPager) { tab, pos -> + Log.d(DEBUG_TAG, "tabview on position $pos") + + } + tabLayoutMediator.attach() + + + btnForward.setOnClickListener { + viewPager.setCurrentItem(viewPager.currentItem+1,true) + } + btnBackward.setOnClickListener { + viewPager.setCurrentItem(viewPager.currentItem-1, true) + } + + viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + + override fun onPageSelected(position: Int) { + if(position == 0){ + btnBackward.visibility = View.INVISIBLE + } else{ + btnBackward.visibility = View.VISIBLE + } + if(position == NUM_ITEMS-1){ + btnForward.visibility = View.INVISIBLE + } else{ + btnForward.visibility = View.VISIBLE + } + } + + }) + } + + + + /** + * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in + * sequence. + */ + private inner class IntroPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { + override fun getItemCount(): Int = NUM_ITEMS + + override fun createFragment(position: Int): Fragment = IntroFragment.newInstance(position) + } + + companion object{ + const private val DEBUG_TAG = "BusTO-IntroActivity" + const val RESTART_MAIN = "restartMainActivity" + + const val NUM_ITEMS = 7 + } + + override fun closeIntroduction() { + if(restartMain) startActivity(Intent(this, ActivityPrincipal::class.java)) + val pref = PreferencesHolder.getMainSharedPreferences(this) + val editor = pref.edit() + editor.putBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN, true) + //use commit so we don't "lose" info + editor.commit() + finish() + } + +} \ 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 cb3ea6f..3905f5a 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,762 +1,777 @@ /* 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.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; private boolean onCreateComplete = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); - final SharedPreferences theShPr = getMainSharedPreferences(); boolean showingArrivalsFromIntent = false; - //database check - GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); - - final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); - boolean dataUpdateRequested = false; - 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); - } - - Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } + //database check + GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); + + final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); + boolean dataUpdateRequested = false; + final SharedPreferences theShPr = getMainSharedPreferences(); + + final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); + Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); + if (old_version < db_version){ + //decide update conditions in the future + if(old_version < 2 && db_version >= 2) { + dataUpdateRequested = true; + DatabaseUpdate.requestDBUpdateWithWork(this, true, true); + } + PreferencesHolder.setGtfsDBVersion(theShPr, db_version); + } //Try (hopefully) database update if(!dataUpdateRequested) DatabaseUpdate.requestDBUpdateWithWork(this, false, false); /* Watch for database update */ final WorkManager workManager = WorkManager.getInstance(this); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); //if (vl.length() == 0 || vl.equals("arrivals")) { // showMainFragment(); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { //we are not restarting the activity from nothing if (vl.equals("map")) { requestMapFragment(false); } else if (vl.equals("favorites")) { checkAndShowFavoritesFragment(getSupportFragmentManager(), false); } else if (vl.equals("lines")) { showLinesFragment(getSupportFragmentManager(), false, null); } else { showMainFragment(false); } } onCreateComplete = true; //last but not least, set the good default values manageDefaultValuesForSettings(); + + //check if first run activity (IntroActivity) has been started once or not + boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false); + if(!hasIntroRun){ + startIntroductionActivity(); + } } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment checkAndShowFavoritesFragment(getSupportFragmentManager(), true); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); showLinesFragment(getSupportFragmentManager(), true,null); return true; } else if(menuItem.getItemId() == R.id.drawer_action_info) { closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } @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; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } private void requestMapFragment(final boolean allowReturn){ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ //nothing to do Log.d(DEBUG_TAG, "Build codes allow the showing of the map"); createAndShowMapFragment(null, allowReturn); return; } final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); Log.d(DEBUG_TAG, "Permission for storage: "+result); switch (result) { case PERMISSION_OK: createAndShowMapFragment(null, allowReturn); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null, allowReturn)); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = getString(R.string.storage_permission); String text = getString(R.string.too_many_permission_asks, storage_perm); Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } } private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); if(fragment!=null){ ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); }else{ //use new method ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); } if (addToBackStack) ft.addToBackStack("favorites_main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setReorderingAllowed(false); ft.commit(); } private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack("linesGrid"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private void showMainFragment(boolean addToBackStack){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ createShowMainFragment(fraMan, null, addToBackStack); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment, addToBackStack); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } //return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); if (mainFragmentIfVisible!=null){ mainFragmentIfVisible.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; default: titleResId = 0; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment, true); probableFragment.requestArrivalsForStopID(ID); } else { // we have no fragment final Bundle args = new Bundle(); args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID); //if onCreate is complete, then we are not asking for the first showing fragment boolean addtobackstack = onCreateComplete; createShowMainFragment(fraMan, args ,addtobackstack); } } else { //the MainScreeFragment is shown, nothing to do probableFragment.requestArrivalsForStopID(ID); } mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void showLineOnMap(String routeGtfsId){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } + void startIntroductionActivity(){ + Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); + intent.putExtra(ActivityIntro.RESTART_MAIN, false); + startActivity(intent); + } + class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_about: - startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); - return true; - case R.id.action_hack: - openIceweasel(getString(R.string.hack_url), activityContext); - return true; - case R.id.action_source: - openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); - return true; - case R.id.action_licence: - openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); - return true; - case R.id.action_experiments: - startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); - default: + final int id = item.getItemId(); + if(id == R.id.action_about){ + startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); + return true; + } else if (id == R.id.action_hack) { + openIceweasel(getString(R.string.hack_url), activityContext); + return true; + } else if (id == R.id.action_source){ + openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); + return true; + } else if (id == R.id.action_licence){ + openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); + return true; + } else if (id == R.id.action_experiments) { + startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); + return true; + } else if (id == R.id.action_tutorial) { + startIntroductionActivity(); + return true; } + return false; } } /** * Adjust setting to match the default ones */ private void manageDefaultValuesForSettings(){ SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = mainSharedPref.edit(); //Main fragment to show String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); boolean edit = false; if (screen.isEmpty()){ editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals"); edit=true; } //Fetchers final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>()); if (setSelected.isEmpty()){ String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default); editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals)); edit=true; } if (edit){ editor.commit(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/BustoApp.java b/app/src/main/java/it/reyboz/bustorino/BustoApp.java index 1b534d5..a80c054 100644 --- a/app/src/main/java/it/reyboz/bustorino/BustoApp.java +++ b/app/src/main/java/it/reyboz/bustorino/BustoApp.java @@ -1,59 +1,60 @@ /* 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.content.Context; import androidx.multidex.MultiDexApplication; import org.acra.ACRA; import org.acra.BuildConfig; import org.acra.ReportField; import org.acra.config.CoreConfigurationBuilder; import org.acra.config.DialogConfigurationBuilder; import org.acra.config.MailSenderConfigurationBuilder; import org.acra.data.StringFormat; import static org.acra.ReportField.*; public class BustoApp extends MultiDexApplication { private static final ReportField[] REPORT_FIELDS = {REPORT_ID, APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME, PHONE_MODEL, BRAND, PRODUCT, ANDROID_VERSION, BUILD_CONFIG, CUSTOM_DATA, IS_SILENT, STACK_TRACE, INITIAL_CONFIGURATION, CRASH_CONFIGURATION, DISPLAY, USER_COMMENT, USER_APP_START_DATE, USER_CRASH_DATE, LOGCAT, SHARED_PREFERENCES}; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); CoreConfigurationBuilder builder = new CoreConfigurationBuilder(this); builder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.JSON) .setDeleteUnapprovedReportsOnApplicationStart(true); builder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setMailTo("gtt@succhia.cz") .setReportFileName(it.reyboz.bustorino.BuildConfig.VERSION_NAME +"_report.json") .setResBody(R.string.acra_email_message) .setEnabled(true); builder.getPluginConfigurationBuilder(DialogConfigurationBuilder.class).setResText(R.string.message_crash) .setResTheme(R.style.AppTheme) .setEnabled(true); builder.setReportContent(REPORT_FIELDS); if (!it.reyboz.bustorino.BuildConfig.DEBUG) ACRA.init(this, builder); } + } 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 00efda3..026b969 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -1,375 +1,390 @@ /* 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.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 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 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); + 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); + } + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java index a0c551a..477ec9b 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java +++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java @@ -1,51 +1,62 @@ /* 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.data; import android.content.Context; import android.content.SharedPreferences; import it.reyboz.bustorino.R; import static android.content.Context.MODE_PRIVATE; import androidx.preference.PreferenceManager; /** * Static class for commonly used SharedPreference operations */ public abstract class PreferencesHolder { public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version"; + public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run"; public static SharedPreferences getMainSharedPreferences(Context context){ return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE); } public static SharedPreferences getAppPreferences(Context con){ return PreferenceManager.getDefaultSharedPreferences(con); } public static int getGtfsDBVersion(SharedPreferences pref){ return pref.getInt(PREF_GTFS_DB_VERSION,-1); } public static void setGtfsDBVersion(SharedPreferences pref,int version){ SharedPreferences.Editor ed = pref.edit(); ed.putInt(PREF_GTFS_DB_VERSION,version); ed.apply(); } + + /** + * Check if the introduction activity has been run at least one + * @param con the context needed + * @return true if it has been run + */ + public static boolean hasIntroFinishedOneShot(Context con){ + final SharedPreferences pref = getMainSharedPreferences(con); + return pref.getBoolean(PREF_INTRO_ACTIVITY_RUN, false); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt new file mode 100644 index 0000000..5006d75 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/IntroFragment.kt @@ -0,0 +1,151 @@ +package it.reyboz.bustorino.fragments + +import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.text.LineBreaker +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.utils +import java.lang.IllegalStateException + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val SCREEN_INDEX = "screenindex" + +/** + * A simple [Fragment] subclass. + * Use the [IntroFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class IntroFragment : Fragment() { + // TODO: Rename and change types of parameters + private var screenIndex = 1 + private lateinit var imageHolder: ImageView + private lateinit var textView: TextView + + + private lateinit var listener: IntroListener + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + screenIndex = it.getInt(SCREEN_INDEX) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if(context !is IntroListener){ + throw IllegalStateException("Context must implement IntroListener") + } + listener = context + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val root= inflater.inflate(R.layout.fragment_intro, container, false) + imageHolder = root.findViewById(R.id.image_tutorial) + textView = root.findViewById(R.id.tutorialTextView) + + + when(screenIndex){ + 0 -> { + setImageBitmap(imageHolder, R.drawable.tuto_busto, 300f) + textView.text = utils.convertHtml(getString(R.string.tutorial_first)) + } + + 1->{ + setImageBitmap(imageHolder, R.drawable.tuto_search) + setTextHtmlDescription(R.string.tutorial_search) + } + 2 ->{ + setImageBitmap(imageHolder, R.drawable.tuto_arrivals) + textView.text = utils.convertHtml(getString(R.string.tutorial_arrivals)) + } + 3 ->{ + setImageBitmap(imageHolder, R.drawable.tuto_stops) + textView.text = utils.convertHtml(getString(R.string.tutorial_stops)) + } + 4 ->{ + setImageBitmap(imageHolder, R.drawable.tuto_map) + textView.text = utils.convertHtml(getString(R.string.tutorial_map)) + } + 5 ->{ + setImageBitmap(imageHolder, R.drawable.tuto_line_det) + textView.text = utils.convertHtml(getString(R.string.tutorial_line)) + } + 6-> { + setImageBitmap(imageHolder,R.drawable.tuto_menu) + setTextHtmlDescription(R.string.tutorial_menu) + val closeButton = root.findViewById