diff --git a/build.gradle b/build.gradle index bfb2390..6344f9d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,117 +1,117 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } dependencies { classpath 'com.android.tools.build:gradle:4.0.2' } ext { //libraries versions fragment_version = "1.3.3" activity_version = "1.1.0" appcompat_version = "1.2.0" preference_version = "1.1.1" work_version = "2.5.0" acra_version = "5.7.0" lifecycle_version = "2.3.1" arch_version = "2.1.0" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() } } apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion '29.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 14 targetSdkVersion 29 versionCode 32 versionName "1.14.1" vectorDrawables.useSupportLibrary = true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { jcenter() mavenLocal() } dependencies { //new libraries implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation 'com.google.android.material:material:1.3.0' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.0' - implementation 'org.osmdroid:osmdroid-android:6.1.8' + implementation 'org.osmdroid:osmdroid-android:6.1.10' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.14.0' // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" } } diff --git a/src/it/reyboz/bustorino/ActivityMap.java b/src/it/reyboz/bustorino/ActivityMap.java index 0a79fcd..c8b010d 100644 --- a/src/it/reyboz/bustorino/ActivityMap.java +++ b/src/it/reyboz/bustorino/ActivityMap.java @@ -1,429 +1,451 @@ /* BusTO Activities Copyright (C) 2020 Andrea Ugo e 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.pm.PackageManager; import android.location.Location; import android.location.LocationManager; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ImageButton; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.preference.PreferenceManager; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.data.NextGenDB; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.Overlay; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; import java.util.*; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.map.CustomInfoWindow; public class ActivityMap extends GeneralActivity { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; private HashSet shownStops = null; private MapView map = null; public Context ctx; private MyLocationNewOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; + private final CustomInfoWindow.TouchResponder touchResponder = new CustomInfoWindow.TouchResponder() { + @Override + public void onActionUp(@NonNull String stopID, @Nullable String stopName) { + Intent intent = new Intent(ctx, ActivityMain.class); + Bundle b = new Bundle(); + b.putString("bus-stop-ID", stopID); + b.putString("bus-stop-display-name", stopName); + intent.putExtras(b); + intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + + // start ActivityMain with the previous intent + ctx.startActivity(intent); + } + }; + //@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //handle permissions first, before map is created. not depicted here //load/initialize the osmdroid configuration ctx = getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); //setting this before the layout is inflated is a good idea //it 'should' ensure that the map has a writable location for the map cache, even without permissions //if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath //see also StorageUtils //note, the load method also sets the HTTP User Agent to your application's package name, abusing osm's tile servers will get you banned based on this string //inflate and create the map setContentView(R.layout.activity_map); map = (MapView) findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = (ImageButton) findViewById(R.id.ic_center_map); btFollowMe = (ImageButton) findViewById(R.id.ic_follow_me); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); // take the parameters if it's called from other Activities Bundle b = getIntent().getExtras(); startMap(b, savedInstanceState); // on drag and zoom reload the markers map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { loadMarkers(); return true; } @Override public boolean onZoom(ZoomEvent event) { loadMarkers(); return true; } })); btCenterMap.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG, "centerMap clicked "); final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } }); btFollowMe.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG, "btFollowMe clicked "); if (!mLocationOverlay.isFollowLocationEnabled()) { mLocationOverlay.enableFollowLocation(); btFollowMe.setImageResource(R.drawable.ic_follow_me_on); } else { mLocationOverlay.disableFollowLocation(); btFollowMe.setImageResource(R.drawable.ic_follow_me); } } }); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; boolean havePositionPermission = true; if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, PERMISSION_REQUEST_POSITION); havePositionPermission = false; } if (marker != null) { startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); } else if (savedInstanceState != null || !havePositionPermission) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); } else { boolean found = false; LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); if (locationManager != null) { Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(16.0); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } // Location Overlay // from OpenBikeSharing (THANK GOD) GpsMyLocationProvider imlp = new GpsMyLocationProvider(this.getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); this.mLocationOverlay = new MyLocationNewOverlay(imlp,map); mLocationOverlay.enableMyLocation(); mLocationOverlay.enableFollowLocation(); btFollowMe.setImageResource(R.drawable.ic_follow_me_on); mLocationOverlay.setOptionsMenuEnabled(true); /* mLocationOverlay.runOnFirstFix(() -> { mapController.setCenter(mLocationOverlay.getMyLocation()); mapController.animateTo(mLocationOverlay.getMyLocation()); }); */ map.getOverlays().add(this.mLocationOverlay); //add stops overlay map.getOverlays().add(this.stopsFolderOverlay); loadMarkers(); if (marker != null) { // make a marker with the info window open for the searched marker makeMarker(startPoint, name , ID, true); } } public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) { // add a marker Marker marker = new Marker(map); // set custom info window as info window - CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName); + CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName, touchResponder); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click // create an intent with these extras Intent intent = new Intent(ActivityMap.this, ActivityMain.class); Bundle b = new Bundle(); b.putString("bus-stop-ID", ID); b.putString("bus-stop-display-name", stopName); intent.putExtras(b); intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); // start ActivityMain with the previous intent startActivity(intent); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); // add to it an icon marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(ID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); } return marker; } public void loadMarkers() { // get rid of the previous markers //map.getOverlays().clear(); //stopsFolderOverlay = new FolderOverlay(); List stopsOverlays = stopsFolderOverlay.getItems(); /*if (stopsOverlays != null){ stopsOverlays.clear(); }*/ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); // get the stops located in those coordinates /* StopsDB stopsDB = new StopsDB(ctx); stopsDB.openIfNeeded(); Stop[] stops = stopsDB.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo); stopsDB.closeIfNeeded(); */ NextGenDB dbHelper = new NextGenDB(ctx); Stop[] stops = dbHelper.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo); // add new markers of those stops for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } try{ stop.getLatitude(); stop.getLongitude(); } catch (NullPointerException e) { Log.e(TAG,"Stop "+stop.ID+ " gives null coordinates"); e.printStackTrace(); continue; } shownStops.add(stop.ID); GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false); stopsFolderOverlay.add(stopMarker); } } + @Override + protected void onPostResume() { + super.onPostResume(); + ctx = this; + } + protected boolean detachMapFromPosition(){ if (mLocationOverlay.isFollowLocationEnabled()) { mLocationOverlay.disableFollowLocation(); btFollowMe.setImageResource(R.drawable.ic_follow_me); return true; } return false; } public void onResume(){ super.onResume(); //this will refresh the osmdroid configuration on resuming. //if you make changes to the configuration, use //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); //Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this)); map.onResume(); //needed for compass, my location overlays, v6.0.0 and up mLocationOverlay.enableMyLocation(); } public void onPause(){ super.onPause(); //this will refresh the osmdroid configuration on resuming. //if you make changes to the configuration, use //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); //Configuration.getInstance().save(this, prefs); map.onPause(); //needed for compass, my location overlays, v6.0.0 and up mLocationOverlay.disableMyLocation(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); outState.putDouble(MAP_CENTER_LAT_KEY, map.getMapCenter().getLatitude()); outState.putDouble(MAP_CENTER_LON_KEY, map.getMapCenter().getLongitude()); } /** * PERMISSION STUFF **/ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case PERMISSION_REQUEST_POSITION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { setOption(LOCATION_PERMISSION_GIVEN, true); //if we sent a request for a new NearbyStopsFragment } else { //permission denied setOption(LOCATION_PERMISSION_GIVEN, false); } break; //add other cases for permissions } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java index fe85fe5..f900b2e 100644 --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,433 +1,480 @@ package it.reyboz.bustorino; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; 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.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; 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.concurrent.TimeUnit; +import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.fragments.FavoritesFragment; import it.reyboz.bustorino.fragments.FragmentKind; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.fragments.MainScreenFragment; +import it.reyboz.bustorino.fragments.MapFragment; 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; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); 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()); 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); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /// 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); 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); requestArrivalsForStopID(busStopID); } //Try (hopefully) database update PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); final WorkManager workManager = WorkManager.getInstance(this); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); if (version >= 0) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); /* Set database update */ 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; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment showMainFragment(); } 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); } 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 FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); FavoritesFragment fragment = FavoritesFragment.newInstance(); ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES); ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(); return true; + } else if(menuItem.getItemId() == R.id.nav_map_item){ + closeDrawerIfOpen(); + createAndShowMapFragment(null); + return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem.toString()); 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.extra_menu_items, menu); return super.onCreateOptionsMenu(menu); } @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 shownFrag.getChildFragmentManager().popBackStackImmediate(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); } else super.onBackPressed(); } private void createDefaultSnackbar() { if (snackbar == null) { snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); } snackbar.show(); } private MainScreenFragment createAndShowMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); MainScreenFragment fragment = MainScreenFragment.newInstance(); FragmentTransaction transaction = fraMan.beginTransaction(); transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG); transaction.commit(); return fragment; } /** * 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){ fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment) .setReorderingAllowed(true) .addToBackStack(null) /*.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) .commit(); } private MainScreenFragment showMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); MainScreenFragment mainScreenFragment = null; if (fragment==null | !(fragment instanceof MainScreenFragment)){ mainScreenFragment = createAndShowMainFragment(); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment); 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 probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.readyGUIfor(fragmentType); } + switch (fragmentType){ + case MAP: + mNavView.setCheckedItem(R.id.nav_map_item); + break; + case FAVORITES: + mNavView.setCheckedItem(R.id.nav_favorites_item); + break; + case ARRIVALS: + case NEARBY_STOPS: + case STOPS: + case MAIN_SCREEN_FRAGMENT: + case NEARBY_ARRIVALS: + mNavView.setCheckedItem(R.id.nav_arrivals); + break; + } } @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); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment); } else { // we have no fragment probableFragment = createAndShowMainFragment(); } } probableFragment.requestArrivalsForStopID(ID); mNavView.setCheckedItem(R.id.nav_arrivals); } @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); } } + //Map Fragment stuff + void createAndShowMapFragment(@Nullable Stop stop){ + 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); + ft.addToBackStack(null); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + ft.commit(); + } + class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ @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), getApplicationContext()); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", getApplicationContext()); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", getApplicationContext()); return true; default: } return false; } } } diff --git a/src/it/reyboz/bustorino/data/NextGenDB.java b/src/it/reyboz/bustorino/data/NextGenDB.java index c917312..a0781eb 100644 --- a/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/src/it/reyboz/bustorino/data/NextGenDB.java @@ -1,356 +1,355 @@ /* BusTO (middleware) 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.data; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import java.util.*; import static it.reyboz.bustorino.data.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; public static final int DATABASE_VERSION = 2; public static final String DEBUG_TAG = "NextGenDB-BusTO"; //NO Singleton instance //private static volatile NextGenDB instance = null; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public static final String[] QUERY_COLUMN_stops_all = { StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_LOCATION, StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING}; public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " + StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG + " >= ? AND "+ StopsTable.COL_LONG + " <= ?"; public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?"; private final Context appContext; public NextGenDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); appContext = context.getApplicationContext(); } @Override public void onCreate(SQLiteDatabase db) { Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion<2 && newVersion == 2){ //DROP ALL TABLES db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME); db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME); db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME); db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME); //RECREATE THE TABLES WITH THE NEW SCHEMA db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); DatabaseUpdateService.startDBUpdate(appContext,0,true); } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } public static String getSqlCreateStopsTable(String tableName){ return "CREATE TABLE "+tableName+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; } /** * Query some bus stops inside a map view * * You can obtain the coordinates from OSMDroid using something like this: * BoundingBoxE6 bb = mMapView.getBoundingBox(); * double latFrom = bb.getLatSouthE6() / 1E6; * double latTo = bb.getLatNorthE6() / 1E6; * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ public synchronized Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) { Stop[] stops = new Stop[0]; SQLiteDatabase db = this.getReadableDatabase(); - Cursor result; + //Cursor result=null; int count; // coordinates must be strings in the where condition String minLatRaw = String.valueOf(minLat); String maxLatRaw = String.valueOf(maxLat); String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); if(db == null) { return stops; } try { - result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, + final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null); stops = getStopsFromCursorAllFields(result); - + result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); return stops; + }finally { + db.close(); } - result.close(); - db.close(); - return stops; } /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} * @param result cursor from query * @return an Array of the stops found in the query */ public static Stop[] getStopsFromCursorAllFields(Cursor result){ int colID = result.getColumnIndex(StopsTable.COL_ID); int colName = result.getColumnIndex(StopsTable.COL_NAME); int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION); int colType = result.getColumnIndex(StopsTable.COL_TYPE); int colLat = result.getColumnIndex(StopsTable.COL_LAT); int colLon = result.getColumnIndex(StopsTable.COL_LONG); int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING); int count = result.getCount(); Stop[] stops = new Stop[count]; int i = 0; while(result.moveToNext()) { final String stopID = result.getString(colID).trim(); final Route.Type type = Route.getTypeFromSymbol(result.getString(colType)); String lines = result.getString(colLines).trim(); String locationSometimesEmpty = result.getString(colLocation); if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) { locationSometimesEmpty = null; } stops[i++] = new Stop(stopID, result.getString(colName), null, locationSometimesEmpty, type, splitLinesString(lines), result.getDouble(colLat), result.getDouble(colLon)); } return stops; } /** * Insert batch content, already prepared as * @param content ContentValues array * @return number of lines inserted */ public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException { final SQLiteDatabase db = this.getWritableDatabase(); int success = 0; db.beginTransaction(); for (final ContentValues cv : content) { try { db.replaceOrThrow(tableName, null, cv); success++; } catch (SQLiteConstraintException d){ Log.w("NextGenDB_Insert","Failed insert with FOREIGN KEY... \n"+d.getMessage()); } catch (Exception e) { Log.w("NextGenDB_Insert", e); } } db.setTransactionSuccessful(); db.endTransaction(); return success; } public static List splitLinesString(String linesStr){ return Arrays.asList(linesStr.split("\\s*,\\s*")); } public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public interface DataTables { String getTableName(); String[] getFields(); } public static final class LinesTable implements BaseColumns, DataTables { //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "line_name"; public static final String COLUMN_DESCRIPTION = "line_description"; public static final String COLUMN_TYPE = "line_bacino"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE}; } } public static final class BranchesTable implements BaseColumns, DataTables { public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "branch_direzione"; public static final String COL_FESTIVO = "branch_festivo"; public static final String COL_TYPE = "branch_type"; public static final String COL_LUN="runs_lun"; public static final String COL_MAR="runs_mar"; public static final String COL_MER="runs_mer"; public static final String COL_GIO="runs_gio"; public static final String COL_VEN="runs_ven"; public static final String COL_SAB="runs_sab"; public static final String COL_DOM="runs_dom"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION, COL_DIRECTION,COL_FESTIVO,COL_TYPE, COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM }; } } public static final class ConnectionsTable implements DataTables { public static final String TABLE_NAME = "connections"; public static final String COLUMN_BRANCH = "branchid"; public static final String COLUMN_STOP_ID = "stopid"; public static final String COLUMN_ORDER = "ordine"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER}; } } public static final class StopsTable implements DataTables { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "stopid"; //integer public static final String COL_TYPE = "stop_type"; public static final String COL_NAME = "stop_name"; public static final String COL_LAT = "stop_latitude"; public static final String COL_LONG = "stop_longitude"; public static final String COL_LOCATION = "stop_location"; public static final String COL_PLACE = "stop_placeName"; public static final String COL_LINES_STOPPING = "stop_lines"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; } } } public static final class DBUpdatingException extends Exception{ public DBUpdatingException(String message) { super(message); } } } diff --git a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java index f72703a..530d658 100644 --- a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java +++ b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java @@ -1,85 +1,85 @@ /* 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 androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.widget.AbsListView; import java.lang.ref.WeakReference; public class CommonScrollListener extends RecyclerView.OnScrollListener implements AbsListView.OnScrollListener{ WeakReference listenerWeakReference; //enable swipeRefreshLayout when scrolling down or not boolean enableRefreshLayout; int lastvisibleitem; public CommonScrollListener(FragmentListenerMain lis, boolean enableRefreshLayout){ listenerWeakReference = new WeakReference<>(lis); this.enableRefreshLayout = enableRefreshLayout; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { /* * This seems to be a totally useless method */ } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { FragmentListenerMain listener = listenerWeakReference.get(); if(listener==null){ //can't do anything, sorry Log.i(this.getClass().getName(),"called onScroll but FragmentListener is null"); return; } if (firstVisibleItem>=0) { if (lastvisibleitem < firstVisibleItem) { - Log.i("Busto", "Scrolling DOWN"); + //Log.i("Busto", "Scrolling DOWN"); listener.showFloatingActionButton(false); //lastScrollUp = true; } else if (lastvisibleitem > firstVisibleItem) { - Log.i("Busto", "Scrolling UP"); + //Log.i("Busto", "Scrolling UP"); listener.showFloatingActionButton(true); //lastScrollUp = false; } lastvisibleitem = firstVisibleItem; } if(enableRefreshLayout){ boolean enable = false; if(view != null && view.getChildCount() > 0){ // check if the first item of the list is visible boolean firstItemVisible = view.getFirstVisiblePosition() == 0; // check if the top of the first item is visible boolean topOfFirstItemVisible = view.getChildAt(0).getTop() == 0; // enabling or disabling the refresh layout enable = firstItemVisible && topOfFirstItemVisible; } listener.enableRefreshLayout(enable); //Log.d(getString(R.string.list_fragment_debug),"onScroll active, first item visible: "+firstVisibleItem+", refreshlayout enabled: "+enable); }} @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { FragmentListenerMain listener = listenerWeakReference.get(); if(newState!=SCROLL_STATE_IDLE) listener.showFloatingActionButton(false); else listener.showFloatingActionButton(true); } } diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java index 22e79ca..247f697 100644 --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,272 +1,281 @@ package it.reyboz.bustorino.fragments; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import java.util.List; import it.reyboz.bustorino.ActivityFavorites; import it.reyboz.bustorino.ActivityMain; import it.reyboz.bustorino.ActivityMap; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.StopAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.FavoritesViewModel; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class FavoritesFragment extends BaseFragment { private ListView favoriteListView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; @Nullable private CommonFragmentListener mListener; + public static final String FRAGMENT_TAG = "BusTOFavFragment"; + public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } - private FavoritesFragment(){ + public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteListView = root.findViewById(R.id.favoriteListView); favoriteListView.setOnItemClickListener((parent, view, position, id) -> { /** * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ Stop busStop = (Stop) parent.getItemAtPosition(position); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); registerForContextMenu(favoriteListView); FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context.toString() + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (v.getId() == R.id.favoriteListView) { // if we aren't attached to activity, return null if (getActivity()==null) return; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } + @Override + public void onResume() { + super.onResume(); + if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); + } + @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); Stop busStop = (Stop) favoriteListView.getItemAtPosition(info.position); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, result -> { }).execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: final String theGeoUrl = busStop.getGeoURL(); /* if(theGeoUrl==null){ //doesn't have a position Toast.makeText(getContext(),R.string.cannot_show_on_map_no_position,Toast.LENGTH_SHORT).show(); return true; } // start ActivityMap with these extras in intent Intent intent = new Intent(getContext(), ActivityMap.class); Bundle b = new Bundle(); double lat, lon; if (busStop.getLatitude()!=null) lat = busStop.getLatitude(); else lat = 200; if (busStop.getLongitude()!=null) lon = busStop.getLongitude(); else lon = 200; b.putDouble("lat", lat); b.putDouble("lon",lon); b.putString("name", busStop.getStopDefaultName()); b.putString("ID", busStop.ID); intent.putExtras(b); startActivity(intent); TODO: start map on button press */ return true; default: return super.onContextItemSelected(item); } } + void showStops(List busStops){ // If no data is found show a friendly message if (busStops.size() == 0) { favoriteListView.setVisibility(View.INVISIBLE); // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); //assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } else { favoriteListView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * redrwaing everything. */ // Show results favoriteListView.setAdapter(new StopAdapter(getContext(), busStops)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String busStopUsername = busStopNameText.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.length() == 0) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); } } else { // changed to something // something different? if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); } } launchUpdate(busStop); } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // delete user name from database busStop.setStopUserName(null); launchUpdate(busStop); } }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, new AsyncStopFavoriteAction.ResultListener() { @Override public void doStuffWithResult(Boolean result) { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); } }).execute(busStop); } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentKind.java b/src/it/reyboz/bustorino/fragments/FragmentKind.java index 07f5078..6aeac73 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentKind.java +++ b/src/it/reyboz/bustorino/fragments/FragmentKind.java @@ -1,22 +1,22 @@ /* BusTO (fragments) 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; public enum FragmentKind { - STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS + STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS, MAP, MAIN_SCREEN_FRAGMENT } diff --git a/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java b/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java index 4a3fd61..5f8a6b1 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java +++ b/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java @@ -1,40 +1,33 @@ /* 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 it.reyboz.bustorino.backend.Stop; public interface FragmentListenerMain extends CommonFragmentListener { void toggleSpinner(boolean state); - - /* - Unused method - * Add the last successfully searched stop to the favorites - */ - - //void toggleLastStopToFavorites(); - + //TODO: implement void showStopOnMap() /** * Tell activity that we need to enable/disable the refreshLayout * @param yes or no */ void enableRefreshLayout(boolean yes); } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index 7f5b2d6..5c82c99 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,636 +1,638 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.os.Build; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.zxing.integration.android.IntentIntegrator; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.FiveTStopsFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.GTTStopsFetcher; import it.reyboz.bustorino.backend.StopsFinderByName; import it.reyboz.bustorino.middleware.AsyncDataDownload; import it.reyboz.bustorino.util.Permissions; import static android.content.Context.LOCATION_SERVICE; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{ private static final String OPTION_SHOW_LEGEND = "show_legend"; private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private boolean setupOnAttached = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; private final ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; LocationManager locmgr; private final Criteria cr = new Criteria(); //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { MainScreenFragment fragment = new MainScreenFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Setup on attached: "+setupOnAttached); //Restore instance state if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnAttached = false; } } if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context.toString() + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener)); else{ ///TODO: if there is a stop displayed, we need to hold the update } //If there are no providers available, then, wait for them setupOnAttached = false; } else { } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); if (con != null) locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE); else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } suppressArrivalsReload = false; } if(pendingStopID!=null){ requestArrivalsForStopID(pendingStopID); pendingStopID = null; } + mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); } @Override public void onPause() { //mainHandler = null; locmgr = null; super.onPause(); } + /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query); } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if (getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) { locmgr.removeUpdates(locListener); pendingNearbyStopsRequest = false; } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: - Log.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment"); + Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncDataDownload(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { new AsyncDataDownload(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } /////////// LOCATION METHODS ////////// final LocationListener locListener = new LocationListener() { @Override public void onLocationChanged(Location location) { Log.d(DEBUG_TAG, "Location changed"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d(DEBUG_TAG, "Location provider status: " + status); if (status == LocationProvider.AVAILABLE) { resolveStopRequest(provider); } } @Override public void onProviderEnabled(String provider) { resolveStopRequest(provider); } @Override public void onProviderDisabled(String provider) { } }; private void resolveStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { pendingNearbyStopsRequest = false; mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener)); } } /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; LocationListener listener; public NearbyStopsRequester(Context appContext, Criteria criteria, LocationListener listener) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; this.listener = listener; } @Override public void run() { final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!canRunPosition) { pendingNearbyStopsRequest = true; Permissions.assertLocationPermissions(appContext,getActivity()); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); LocationManager locManager = (LocationManager) appContext.getSystemService(LOCATION_SERVICE); if (locManager == null) { Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); return; } if (Permissions.anyLocationProviderMatchesCriteria(locManager, cr, true) && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = fragMan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = fragMan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(R.id.resultFrame, fragment, "nearbyStop_correct"); ft.commit(); //fragMan.executePendingTransactions(); pendingNearbyStopsRequest = false; } else if (!Permissions.anyLocationProviderMatchesCriteria(locManager, cr, true)) { //Wait for the providers Log.d(DEBUG_TAG, "Queuing position request"); pendingNearbyStopsRequest = true; locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, listener); } } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/MapFragment.java b/src/it/reyboz/bustorino/fragments/MapFragment.java new file mode 100644 index 0000000..f7630e0 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -0,0 +1,532 @@ +package it.reyboz.bustorino.fragments; + +import android.Manifest; +import android.content.Context; + +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.preference.PreferenceManager; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.api.IMapController; +import org.osmdroid.config.Configuration; +import org.osmdroid.events.DelayedMapListener; +import org.osmdroid.events.MapListener; +import org.osmdroid.events.ScrollEvent; +import org.osmdroid.events.ZoomEvent; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.BoundingBox; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.FolderOverlay; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.infowindow.InfoWindow; +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; + +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.data.NextGenDB; +import it.reyboz.bustorino.map.CustomInfoWindow; +import it.reyboz.bustorino.map.LocationOverlay; +import it.reyboz.bustorino.middleware.GeneralActivity; + +import static it.reyboz.bustorino.util.Permissions.PERMISSION_REQUEST_POSITION; + +public class MapFragment extends BaseFragment { + + private static final String TAG = "Busto-MapActivity"; + private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; + private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; + private static final String MAP_CENTER_LON_KEY = "map-center-lon"; + private static final String FOLLOWING_LOCAT_KEY ="following"; + + public static final String BUNDLE_LATIT = "lat"; + public static final String BUNDLE_LONGIT = "lon"; + public static final String BUNDLE_NAME = "name"; + public static final String BUNDLE_ID = "ID"; + + public static final String FRAGMENT_TAG="BusTOMapFragment"; + + + private static final double DEFAULT_CENTER_LAT = 45.0708; + private static final double DEFAULT_CENTER_LON = 7.6858; + private static final double POSITION_FOUND_ZOOM = 18.3; + + private static final String DEBUG_TAG=FRAGMENT_TAG; + + protected FragmentListenerMain listenerMain; + + private HashSet shownStops = null; + + + private MapView map = null; + public Context ctx; + private LocationOverlay mLocationOverlay = null; + private FolderOverlay stopsFolderOverlay = null; + private Bundle savedMapState = null; + protected ImageButton btCenterMap; + protected ImageButton btFollowMe; + private boolean followingLocation = false; + + protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { + @Override + public void onActionUp(@NonNull String stopID, @Nullable String stopName) { + if (listenerMain!= null){ + listenerMain.requestArrivalsForStopID(stopID); + } + } + }; + protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { + @Override + public void onDisableFollowMyLocation() { + updateGUIForLocationFollowing(false); + followingLocation=false; + } + + @Override + public void onEnableFollowMyLocation() { + updateGUIForLocationFollowing(true); + followingLocation=true; + } + }; + + public MapFragment() { + } + public static MapFragment getInstance(){ + return new MapFragment(); + } + public static MapFragment getInstance(double stopLatit, double stopLong, String stopName, String stopID){ + MapFragment fragment= new MapFragment(); + Bundle args = new Bundle(); + args.putDouble(BUNDLE_LATIT, stopLatit); + args.putDouble(BUNDLE_LONGIT, stopLong); + args.putString(BUNDLE_NAME, stopName); + args.putString(BUNDLE_ID, stopID); + fragment.setArguments(args); + + return fragment; + } + public static MapFragment getInstance(Stop stop){ + return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); + } + + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + //use the same layout as the activity + View root = inflater.inflate(R.layout.activity_map, container, false); + if (getContext() == null){ + throw new IllegalStateException(); + } + ctx = getContext().getApplicationContext(); + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); + map = root.findViewById(R.id.map); + map.setTileSource(TileSourceFactory.MAPNIK); + //map.setTilesScaledToDpi(true); + map.setFlingEnabled(true); + + // add ability to zoom with 2 fingers + map.setMultiTouchControls(true); + + btCenterMap = root.findViewById(R.id.ic_center_map); + btFollowMe = root.findViewById(R.id.ic_follow_me); + + //setup FolderOverlay + stopsFolderOverlay = new FolderOverlay(); + + + //Start map from bundle + if (savedInstanceState !=null) + startMap(getArguments(), savedInstanceState); + else startMap(getArguments(), savedMapState); + //set listeners + map.addMapListener(new DelayedMapListener(new MapListener() { + + @Override + public boolean onScroll(ScrollEvent paramScrollEvent) { + requestStopsToShow(); + //Log.d(DEBUG_TAG, "Scrolling"); + //if (moveTriggeredByCode) moveTriggeredByCode =false; + //else setLocationFollowing(false); + return true; + } + + @Override + public boolean onZoom(ZoomEvent event) { + requestStopsToShow(); + return true; + } + + })); + + + btCenterMap.setOnClickListener(v -> { + //Log.i(TAG, "centerMap clicked "); + final GeoPoint myPosition = mLocationOverlay.getMyLocation(); + map.getController().animateTo(myPosition); + }); + + btFollowMe.setOnClickListener(v -> { + //Log.i(TAG, "btFollowMe clicked "); + switchLocationFollowing(!followingLocation); + }); + + return root; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof FragmentListenerMain) { + listenerMain = (FragmentListenerMain) context; + } else { + throw new RuntimeException(context.toString() + + " must implement FragmentListenerMain"); + } + } + @Override + public void onDetach() { + super.onDetach(); + listenerMain = null; + // setupOnAttached = true; + Log.w(DEBUG_TAG, "Fragment detached"); + } + + @Override + public void onPause() { + super.onPause(); + saveMapState(); + } + + /** + * Save the map state inside the fragment + * (calls saveMapState(bundle)) + */ + private void saveMapState(){ + savedMapState = new Bundle(); + saveMapState(savedMapState); + + } + + /** + * Save the state of the map to restore it to a later time + * @param bundle the bundle in which to save the data + */ + private void saveMapState(Bundle bundle){ + final IGeoPoint loc = map.getMapCenter(); + bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); + bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); + bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); + Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); + bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); + + } + + @Override + public void onResume() { + super.onResume(); + if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + saveMapState(outState); + + super.onSaveInstanceState(outState); + } + + //own methods + + /** + * Switch following the location on and off + * @param value true if we want to follow location + */ + public void switchLocationFollowing(Boolean value){ + followingLocation = value; + if (value){ + mLocationOverlay.enableFollowLocation(); + } else { + mLocationOverlay.disableFollowLocation(); + } + } + + /** + * Do all the stuff you need to do on the gui, when parameter is changed to value + * @param following value + */ + protected void updateGUIForLocationFollowing(boolean following){ + if (following) + btFollowMe.setImageResource(R.drawable.ic_follow_me_on); + else + btFollowMe.setImageResource(R.drawable.ic_follow_me); + + } + + public void startMap(Bundle incoming, Bundle savedInstanceState) { + //Check that we're attached + GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; + if(getContext()==null|| activity==null){ + //we are not attached + Log.e(DEBUG_TAG, "Calling startMap when not attached"); + return; + }else{ + Log.d(DEBUG_TAG, "Starting map from scratch"); + } + + + //parse incoming bundle + GeoPoint marker = null; + String name = null; + String ID = null; + if (incoming != null) { + double lat = incoming.getDouble(BUNDLE_LATIT); + double lon = incoming.getDouble(BUNDLE_LONGIT); + marker = new GeoPoint(lat, lon); + name = incoming.getString(BUNDLE_NAME); + ID = incoming.getString(BUNDLE_ID); + } + + shownStops = new HashSet<>(); + // move the map on the marker position or on a default view point: Turin, Piazza Castello + // and set the start zoom + IMapController mapController = map.getController(); + GeoPoint startPoint = null; + + boolean havePositionPermission = true; + + if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + activity.askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, PERMISSION_REQUEST_POSITION); + havePositionPermission = false; + } + // Location Overlay + // from OpenBikeSharing (THANK GOD) + GpsMyLocationProvider imlp = new GpsMyLocationProvider(activity.getBaseContext()); + imlp.setLocationUpdateMinDistance(5); + imlp.setLocationUpdateMinTime(2000); + this.mLocationOverlay = new LocationOverlay(imlp,map, locationCallbacks); + mLocationOverlay.enableMyLocation(); + mLocationOverlay.setOptionsMenuEnabled(true); + + if (marker != null) { + startPoint = marker; + mapController.setZoom(POSITION_FOUND_ZOOM); + switchLocationFollowing(false); + } else if (savedInstanceState != null) { + mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); + mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), + savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); + Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); + switchLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); + } else { + Log.d(DEBUG_TAG, "No position found from intent or saved state"); + boolean found = false; + LocationManager locationManager = + (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); + if (locationManager != null) { + + Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (userLocation != null) { + mapController.setZoom(POSITION_FOUND_ZOOM); + startPoint = new GeoPoint(userLocation); + found = true; + switchLocationFollowing(true); + } + } + if(!found){ + startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); + mapController.setZoom(16.0); + switchLocationFollowing(false); + } + } + + // set the minimum zoom level + map.setMinZoomLevel(15.0); + //add contingency check (shouldn't happen..., but) + if (startPoint != null) { + mapController.setCenter(startPoint); + } + + + + map.getOverlays().add(this.mLocationOverlay); + + //add stops overlay + map.getOverlays().add(this.stopsFolderOverlay); + + Log.d(DEBUG_TAG, "Requesting stops load"); + // This is not necessary, by setting the center we already move + // the map and we trigger a stop request + //requestStopsToShow(); + if (marker != null) { + // make a marker with the info window open for the searched marker + makeMarker(startPoint, name , ID, true); + } + + } + + /** + * Start a request to load the stops that are in the current view + * from the database + */ + private void requestStopsToShow(){ + // get the top, bottom, left and right screen's coordinate + BoundingBox bb = map.getBoundingBox(); + double latFrom = bb.getLatSouth(); + double latTo = bb.getLatNorth(); + double lngFrom = bb.getLonWest(); + double lngTo = bb.getLonEast(); + + new AsyncStopFetcher(this).execute( + new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); + } + + /** + * Add stops as Markers on the map + * @param stops the list of stops that must be included + */ + protected void showStopsMarkers(List stops){ + + for (Stop stop : stops) { + if (shownStops.contains(stop.ID)){ + continue; + } + if(stop.getLongitude()==null || stop.getLatitude()==null) + continue; + + shownStops.add(stop.ID); + GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); + Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false); + stopsFolderOverlay.add(stopMarker); + if (!map.getOverlays().contains(stopsFolderOverlay)) { + Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); + } + } + //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); + //force redraw of markers + map.invalidate(); + } + + public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) { + + // add a marker + Marker marker = new Marker(map); + + // set custom info window as info window + CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName, responder); + marker.setInfoWindow(popup); + + // make the marker clickable + marker.setOnMarkerClickListener((thisMarker, mapView) -> { + if (thisMarker.isInfoWindowOpen()) { + // on second click + //TODO: show the arrivals for the stop + Log.w(DEBUG_TAG, "Pressed on the click marker"); + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map); + // show this particular info window + thisMarker.showInfoWindow(); + // move the map to its position + map.getController().animateTo(thisMarker.getPosition()); + } + + return true; + }); + + // set its position + marker.setPosition(geoPoint); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + // add to it an icon + //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + + marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme())); + // add to it a title + marker.setTitle(stopName); + // set the description as the ID + marker.setSnippet(ID); + + // show popup info window of the searched marker + if (isStartMarker) { + marker.showInfoWindow(); + } + + return marker; + } + + /** + * Simple asyncTask class to load the stops in the background + * Holds a weak reference to the fragment to do callbacks + */ + static class AsyncStopFetcher extends AsyncTask>{ + + final WeakReference fragmentWeakReference; + + public AsyncStopFetcher(MapFragment fragment) { + this.fragmentWeakReference = new WeakReference<>(fragment); + } + + @Override + protected List doInBackground(BoundingBoxLimit... limits) { + if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ + Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); + + return null; + + } + final BoundingBoxLimit limit = limits[0]; + //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); + + NextGenDB dbHelper = new NextGenDB(fragmentWeakReference.get().getContext()); + Stop[] stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, + limit.longFrom, limit.latitTo); + dbHelper.close(); + return Arrays.asList(stops); + } + + @Override + protected void onPostExecute(List stops) { + super.onPostExecute(stops); + //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); + if(fragmentWeakReference.get()==null) { + Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); + return; + } + Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); + fragmentWeakReference.get().showStopsMarkers(stops); + } + + private static class BoundingBoxLimit{ + final double longFrom, longTo, latitFrom, latitTo; + + public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { + this.longFrom = longFrom; + this.longTo = longTo; + this.latitFrom = latitFrom; + this.latitTo = latitTo; + } + } + + } +} diff --git a/src/it/reyboz/bustorino/map/CustomInfoWindow.java b/src/it/reyboz/bustorino/map/CustomInfoWindow.java index 8c4a9c7..57442cd 100644 --- a/src/it/reyboz/bustorino/map/CustomInfoWindow.java +++ b/src/it/reyboz/bustorino/map/CustomInfoWindow.java @@ -1,60 +1,64 @@ package it.reyboz.bustorino.map; import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; +import android.os.Build; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; -import org.osmdroid.api.IMapView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.infowindow.BasicInfoWindow; -import it.reyboz.bustorino.ActivityMain; import it.reyboz.bustorino.R; public class CustomInfoWindow extends BasicInfoWindow { + //TODO: Make the action on the Click customizable + private final TouchResponder touchResponder; + private final String stopID, name; @Override public void onOpen(Object item) { super.onOpen(item); - TextView descr_textView = (TextView) mView.findViewById(R.id.bubble_description); CharSequence text = descr_textView.getText(); if (text==null || !text.toString().isEmpty()){ descr_textView.setVisibility(View.VISIBLE); } else descr_textView.setVisibility(View.GONE); - mView.setElevation(3.2f); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mView.setElevation(3.2f); + } } @SuppressLint("ClickableViewAccessibility") - public CustomInfoWindow(MapView mapView, String ID, String stopName) { + public CustomInfoWindow(MapView mapView, String stopID, String name, TouchResponder responder) { // get the personalized layout super(R.layout.map_popup, mapView); + touchResponder =responder; + this.stopID = stopID; + this.name = name; // make clickable mView.setOnTouchListener((View v, MotionEvent e) -> { if (e.getAction() == MotionEvent.ACTION_UP) { // on click - - // create an intent with these extras - Intent intent = new Intent(mapView.getContext(), ActivityMain.class); - Bundle b = new Bundle(); - b.putString("bus-stop-ID", ID); - b.putString("bus-stop-display-name", stopName); - intent.putExtras(b); - intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); - - // start ActivityMain with the previous intent - mapView.getContext().startActivity(intent); + touchResponder.onActionUp(stopID, name); } return true; }); } + public interface TouchResponder{ + /** + * React to a click on the stop View + * @param stopID the stop id + * @param stopName the stop name + */ + void onActionUp(@NonNull String stopID, @Nullable String stopName); + } } diff --git a/src/it/reyboz/bustorino/map/LocationOverlay.java b/src/it/reyboz/bustorino/map/LocationOverlay.java new file mode 100644 index 0000000..483a87f --- /dev/null +++ b/src/it/reyboz/bustorino/map/LocationOverlay.java @@ -0,0 +1,62 @@ +/* + BusTO (middleware) + 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.map; + + +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.mylocation.IMyLocationProvider; +import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; + +public class LocationOverlay extends MyLocationNewOverlay { + + final OverlayCallbacks callbacks; + + public LocationOverlay(MapView mapView, OverlayCallbacks callbacks) { + super(mapView); + this.callbacks = callbacks; + } + + public LocationOverlay(IMyLocationProvider myLocationProvider, MapView mapView, OverlayCallbacks callbacks) { + super(myLocationProvider, mapView); + this.callbacks = callbacks; + } + + @Override + public void enableFollowLocation() { + super.enableFollowLocation(); + callbacks.onEnableFollowMyLocation(); + } + + @Override + public void disableFollowLocation() { + super.disableFollowLocation(); + callbacks.onDisableFollowMyLocation(); + } + + public interface OverlayCallbacks{ + /** + * Called right after disableFollowMyLocation + */ + void onDisableFollowMyLocation(); + + /** + * Called right after enableFollowMyLocation + */ + void onEnableFollowMyLocation(); + } +} diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index 047e62e..cb33819 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,329 +1,347 @@ /* BusTO (middleware) 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.middleware; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; import android.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; import android.util.Log; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.Calendar; /** * This should be used to download data, but not to display it */ public class AsyncDataDownload extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; + private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference res; private final RequestType t; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final Fetcher[] theFetchers; private Context context; private final boolean replaceFragment; public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) { RequestType type; helperRef = new WeakReference<>(fh); fh.setLastTaskRef(new WeakReference<>(this)); res = new AtomicReference<>(); this.context = context.getApplicationContext(); this.replaceFragment = true; theFetchers = fetchers; if (theFetchers.length < 1){ throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); } if (theFetchers[0] instanceof ArrivalsFetcher){ type = RequestType.ARRIVALS; } else if (theFetchers[0] instanceof StopsFinderByName){ type = RequestType.STOPS; } else{ type = null; } t = type; } @Override protected Object doInBackground(String... params) { RecursionHelper r = new RecursionHelper<>(theFetchers); boolean success=false; Object result; FragmentHelper fh = helperRef.get(); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ return null; } //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); while(r.valid()) { if(this.isCancelled()) { return null; } //get the data from the fetcher switch (t){ case ARRIVALS: ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); Palina p; String stopID; if(params.length>0) stopID=params[0]; //(it's a Palina) else if(lastSearchedBusStop!=null) stopID = lastSearchedBusStop.ID; //(it's a Palina) else { publishProgress(Fetcher.result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times if(f instanceof FiveTAPIFetcher && Integer.parseInt(stopID)>= 8200) continue; p= f.ReadArrivalTimesAll(stopID,res); publishProgress(res.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); if(gres.get() == Fetcher.result.OK){ p.addInfoFromRoutes(branches); Thread t = new Thread(new BranchInserter(branches, context)); t.start(); otherActivities.add(t); } //put updated values into Database } if(lastSearchedBusStop != null && res.get()== Fetcher.result.OK) { // check that we don't have the same stop if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } } result = p; //TODO: find a way to avoid overloading the user with toasts break; case STOPS: StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward(); List resultList= finder.FindByName(params[0], this.res); //it's a List Log.d(TAG,"Using the StopFinderByName: "+finder.getClass()); query =params[0]; result = resultList; //dummy result break; default: result = null; } //find if it went well if(res.get()== Fetcher.result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return result; } } //at this point, we are sure that the result has been negative failedAll=true; return null; } @Override protected void onProgressUpdate(Fetcher.result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.result r : values){ //TODO: make Toast fh.showErrorMessage(r); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Object o) { FragmentHelper fh = helperRef.get(); if(failedAll || o == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; switch (t){ case ARRIVALS: Palina palina = (Palina) o; fh.createOrUpdateStopFragment(palina, replaceFragment); break; case STOPS: //this should never be a problem - List stopList = (List) o; + if(!(o instanceof List)){ + throw new IllegalStateException(); + } + List list = (List) o; + if (list.size() ==0) return; + Object firstItem = list.get(0); + if(!(firstItem instanceof Stop)) return; + ArrayList stops = new ArrayList<>(); + for(Object x: list){ + if(x instanceof Stop) stops.add((Stop) x); + } + if(list.size() != stops.size()){ + Log.w(DEBUG_TAG, "Wrong stop list size:\n incoming: "+ + list.size()+" out: "+stops.size()); + } + //List stopList = (List) list; if(query!=null && !isCancelled()) { - fh.createStopListFragment(stopList,query, replaceFragment); + fh.createStopListFragment(stops,query, replaceFragment); } else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT"); break; case DBUPDATE: break; } } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } public enum RequestType { ARRIVALS,STOPS,DBUPDATE } - public class BranchInserter implements Runnable{ + public static class BranchInserter implements Runnable{ private final List routesToInsert; private final Context context; - private final NextGenDB nextGenDB; + //private final NextGenDB nextGenDB; public BranchInserter(List routesToInsert,@NonNull Context con) { this.routesToInsert = routesToInsert; - this.context = con; - nextGenDB = new NextGenDB(context); + this.context = con.getApplicationContext(); + //nextGenDB = new NextGenDB(context); } @Override public void run() { + final NextGenDB nextGenDB = new NextGenDB(context); ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); values[routesToInsert.indexOf(r)] = cv; for(int i=0; i