diff --git a/app/src/main/java/org/y20k/trackbook/Database.kt b/app/src/main/java/org/y20k/trackbook/Database.kt index f5e3197..8b17f54 100644 --- a/app/src/main/java/org/y20k/trackbook/Database.kt +++ b/app/src/main/java/org/y20k/trackbook/Database.kt @@ -53,6 +53,14 @@ class Database(val trackbook: Trackbook) this.connection.endTransaction() } + fun delete_trkpt(device_id: String, time: Long) + { + Log.i("VOUSSOIR", "Database.delete_trkpt") + begin_transaction() + connection.delete("trkpt", "device_id = ? AND time = ?", arrayOf(device_id, time.toString())) + commit() + } + fun insert_trkpt(trkpt: Trkpt) { Log.i("VOUSSOIR", "Database.insert_trkpt") @@ -60,7 +68,7 @@ class Database(val trackbook: Trackbook) put("device_id", trkpt.device_id) put("lat", trkpt.latitude) put("lon", trkpt.longitude) - put("time", GregorianCalendar.getInstance().time.time) + put("time", trkpt.time) put("accuracy", trkpt.accuracy) put("sat", trkpt.numberSatellites) put("ele", trkpt.altitude) @@ -108,8 +116,9 @@ class Database(val trackbook: Trackbook) { begin_transaction() this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)") - this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(lat, lon, time, device_id))") + this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(device_id, time))") this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(id INTEGER PRIMARY KEY, lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT)") + this.connection.execSQL("CREATE INDEX IF NOT EXISTS index_trkpt_device_id_time on trkpt(device_id, time)") // The pragmas don't seem to execute unless you call moveToNext. var cursor: Cursor cursor = this.connection.rawQuery("PRAGMA journal_mode = DELETE", null) diff --git a/app/src/main/java/org/y20k/trackbook/Keys.kt b/app/src/main/java/org/y20k/trackbook/Keys.kt index c79f7c9..83df516 100644 --- a/app/src/main/java/org/y20k/trackbook/Keys.kt +++ b/app/src/main/java/org/y20k/trackbook/Keys.kt @@ -16,6 +16,7 @@ package org.y20k.trackbook +import android.graphics.Color import java.util.* /* @@ -109,4 +110,7 @@ object Keys { // notification const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1 const val NOTIFICATION_CHANNEL_RECORDING: String = "notificationChannelIdRecordingChannel" + + const val POLYLINE_THICKNESS = 4F + val POLYLINE_COLOR = Color.argb(255, 255, 0, 255) } diff --git a/app/src/main/java/org/y20k/trackbook/MapFragment.kt b/app/src/main/java/org/y20k/trackbook/MapFragment.kt index ea9e82d..b6cc877 100644 --- a/app/src/main/java/org/y20k/trackbook/MapFragment.kt +++ b/app/src/main/java/org/y20k/trackbook/MapFragment.kt @@ -40,8 +40,6 @@ import androidx.fragment.app.Fragment import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar -import org.osmdroid.api.IGeoPoint -import org.osmdroid.api.IMapController import org.osmdroid.events.MapEventsReceiver import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint @@ -51,46 +49,46 @@ import org.osmdroid.views.overlay.MapEventsOverlay import org.osmdroid.views.overlay.Overlay import org.osmdroid.views.overlay.OverlayItem import org.osmdroid.views.overlay.Polygon +import org.osmdroid.views.overlay.Polyline import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.compass.CompassOverlay import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider -import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay import org.y20k.trackbook.helpers.* -/* - * MapFragment class - */ class MapFragment : Fragment() { - /* Main class variables */ + private lateinit var trackbook: Trackbook + private var bound: Boolean = false - private val handler: Handler = Handler(Looper.getMainLooper()) + val handler: Handler = Handler(Looper.getMainLooper()) private var trackingState: Int = Keys.STATE_TRACKING_STOPPED private var gpsProviderActive: Boolean = false private var networkProviderActive: Boolean = false private lateinit var currentBestLocation: Location - private lateinit var trackerService: TrackerService - private lateinit var trackbook: Trackbook - lateinit var rootView: View var continuous_auto_center: Boolean = true - lateinit var currentLocationButton: FloatingActionButton + private lateinit var trackerService: TrackerService + private lateinit var database_changed_listener: DatabaseChangedListener + + var thismapfragment: MapFragment? = null + lateinit var rootView: View + private lateinit var mapView: MapView + lateinit var mainButton: ExtendedFloatingActionButton + lateinit var zoom_in_button: FloatingActionButton lateinit var zoom_out_button: FloatingActionButton - lateinit var mainButton: ExtendedFloatingActionButton - private lateinit var mapView: MapView + lateinit var currentLocationButton: FloatingActionButton + private var current_track_overlay: Polyline? = null private var current_position_overlays = ArrayList() - private var currentTrackOverlay: SimpleFastPointOverlay? = null - private lateinit var locationErrorBar: Snackbar - private var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL private var homepoints_overlays = ArrayList() - private lateinit var database_changed_listener: DatabaseChangedListener + private lateinit var locationErrorBar: Snackbar /* Overrides onCreate from Fragment */ override fun onCreate(savedInstanceState: Bundle?) { Log.i("VOUSSOIR", "MapFragment.onCreate") super.onCreate(savedInstanceState) + thismapfragment = this this.trackbook = (requireContext().applicationContext as Trackbook) database_changed_listener = object: DatabaseChangedListener { @@ -99,7 +97,7 @@ class MapFragment : Fragment() Log.i("VOUSSOIR", "MapFragment database_ready_changed to ${trackbook.database.ready}") if (trackbook.database.ready) { - create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) + create_homepoint_overlays() } else { @@ -116,7 +114,7 @@ class MapFragment : Fragment() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Log.i("VOUSSOIR", "MapFragment.onCreateView") - // find views + rootView = inflater.inflate(R.layout.fragment_map, container, false) mapView = rootView.findViewById(R.id.map) currentLocationButton = rootView.findViewById(R.id.location_button) @@ -130,8 +128,6 @@ class MapFragment : Fragment() true } mapView.isLongClickable = true - - // basic map setup mapView.isTilesScaledToDpi = true mapView.isVerticalMapRepetitionEnabled = false mapView.setTileSource(TileSourceFactory.MAPNIK) @@ -145,10 +141,8 @@ class MapFragment : Fragment() } val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(requireContext()) - val compassOverlay = CompassOverlay(requireContext(), InternalCompassOrientationProvider(requireContext()), mapView) compassOverlay.enableCompass() - // compassOverlay.setCompassCenter(36f, 36f + (statusBarHeight / densityScalingFactor)) // TODO uncomment when transparent status bar is re-implemented val screen_width = Resources.getSystem().displayMetrics.widthPixels compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f) mapView.overlays.add(compassOverlay) @@ -187,7 +181,7 @@ class MapFragment : Fragment() radius=radius, ) trackbook.load_homepoints() - create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) + create_homepoint_overlays() dialog.dismiss() } @@ -198,28 +192,34 @@ class MapFragment : Fragment() mapView.overlays.add(MapEventsOverlay(receiver)) trackbook.load_homepoints() - create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) + create_homepoint_overlays() if (database_changed_listener !in trackbook.database_changed_listeners) { trackbook.database_changed_listeners.add(database_changed_listener) } + create_current_position_overlays(currentBestLocation, trackingState) + centerMap(currentBestLocation) - // initialize track overlays - currentTrackOverlay = null - - // initialize main button state - update_main_button() + current_track_overlay = null mapView.setOnTouchListener { v, event -> continuous_auto_center = false false } - // set up buttons + update_main_button() mainButton.setOnClickListener { - handleTrackingManagementMenu() + if (trackingState == Keys.STATE_TRACKING_ACTIVE) + { + trackerService.stopTracking() + } + else + { + startTracking() + } + handler.post(location_update_redraw) } currentLocationButton.setOnClickListener { centerMap(currentBestLocation, animated=true) @@ -238,6 +238,7 @@ class MapFragment : Fragment() /* Overrides onStart from Fragment */ override fun onStart() { + Log.i("VOUSSOIR", "MapFragment.onStart") super.onStart() // request location permission if denied if (ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) @@ -265,6 +266,10 @@ class MapFragment : Fragment() { Log.i("VOUSSOIR", "MapFragment.onPause") super.onPause() + if (::trackerService.isInitialized) + { + trackerService.mapfragment = null + } saveBestLocationState(currentBestLocation) if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) { trackerService.removeGpsLocationListener() @@ -278,6 +283,10 @@ class MapFragment : Fragment() override fun onStop() { super.onStop() + if (::trackerService.isInitialized) + { + trackerService.mapfragment = null + } // unbind from TrackerService if (bound) { @@ -290,6 +299,7 @@ class MapFragment : Fragment() { Log.i("VOUSSOIR", "MapFragment.onDestroy") super.onDestroy() + trackerService.mapfragment = null requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (database_changed_listener in trackbook.database_changed_listeners) @@ -314,14 +324,17 @@ class MapFragment : Fragment() toggleLocationErrorBar(gpsProviderActive, networkProviderActive) } - /* Start recording waypoints */ - private fun startTracking() { + private fun startTracking() + { // start service via intent so that it keeps running after unbind val intent = Intent(activity, TrackerService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { // ... start service in foreground to prevent it being killed on Oreo activity?.startForegroundService(intent) - } else { + } + else + { activity?.startService(intent) } trackerService.startTracking() @@ -334,16 +347,9 @@ class MapFragment : Fragment() bound = false // unregister listener for changes in shared preferences PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener) - // stop receiving location updates - handler.removeCallbacks(periodicLocationRequestRunnable) - } - - /* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */ - private fun handleTrackingManagementMenu() - { - when (trackingState) { - Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking() - Keys.STATE_TRACKING_STOPPED -> startTracking() + if (::trackerService.isInitialized) + { + trackerService.mapfragment = null } } @@ -374,7 +380,8 @@ class MapFragment : Fragment() continuous_auto_center = true } - fun saveBestLocationState(currentBestLocation: Location) { + fun saveBestLocationState(currentBestLocation: Location) + { PreferencesHelper.saveCurrentBestLocation(currentBestLocation) PreferencesHelper.saveZoomLevel(mapView.zoomLevelDouble) continuous_auto_center = true @@ -395,38 +402,26 @@ class MapFragment : Fragment() /* Mark current position on map */ fun create_current_position_overlays(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED) { - // Log.i("VOUSSOIR", "MapFragmentLayoutHolder.markCurrentPosition") - clear_current_position_overlays() val locationIsOld: Boolean = !(isRecentEnough(location)) - // create marker val newMarker: Drawable val fillcolor: Int - if (trackingState == Keys.STATE_TRACKING_ACTIVE) + if (locationIsOld) + { + fillcolor = Color.argb(64, 0, 0, 0) + newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!! + } + else if (trackingState == Keys.STATE_TRACKING_ACTIVE) { fillcolor = Color.argb(64, 220, 61, 51) - if (locationIsOld) - { - newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!! - } - else - { - newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!! - } + newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!! } else { fillcolor = Color.argb(64, 60, 152, 219) - if(locationIsOld) - { - newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!! - } - else - { - newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!! - } + newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!! } val current_location_radius = Polygon() @@ -447,6 +442,21 @@ class MapFragment : Fragment() } } + fun clear_track_overlay() + { + mapView.overlays.remove(current_track_overlay) + } + + fun create_track_overlay() + { + clear_track_overlay() + val pl = Polyline(mapView) + pl.outlinePaint.strokeWidth = Keys.POLYLINE_THICKNESS + pl.outlinePaint.color = Keys.POLYLINE_COLOR + mapView.overlays.add(pl) + current_track_overlay = pl + } + fun clear_homepoint_overlays() { for (ov in homepoints_overlays) @@ -459,15 +469,16 @@ class MapFragment : Fragment() homepoints_overlays.clear() } - fun create_homepoint_overlays(context: Context, map_view: MapView, homepoints: List) + fun create_homepoint_overlays() { Log.i("VOUSSOIR", "MapFragmentLayoutHolder.createHomepointOverlays") + val context = requireContext() val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!! clear_homepoint_overlays() - for (homepoint in homepoints) + for (homepoint in trackbook.homepoints) { val p = Polygon() p.points = Polygon.pointsAsCircle(GeoPoint(homepoint.location.latitude, homepoint.location.longitude), homepoint.location.accuracy.toDouble()) @@ -476,7 +487,14 @@ class MapFragment : Fragment() homepoints_overlays.add(p) val overlayItems: java.util.ArrayList = java.util.ArrayList() - val overlayItem: OverlayItem = createOverlayItem(context, homepoint.location.latitude, homepoint.location.longitude, homepoint.location.accuracy, homepoint.location.provider.toString(), homepoint.location.time) + val overlayItem: OverlayItem = createOverlayItem( + context, + homepoint.location.latitude, + homepoint.location.longitude, + homepoint.location.accuracy, + homepoint.location.provider.toString(), + homepoint.location.time + ) overlayItem.setMarker(newMarker) overlayItems.add(overlayItem) val homepoint_overlay = ItemizedIconOverlay(context, overlayItems, @@ -504,14 +522,14 @@ class MapFragment : Fragment() delete_button.setOnClickListener { trackbook.database.delete_homepoint(homepoint.id) trackbook.load_homepoints() - create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) + create_homepoint_overlays() dialog.dismiss() } save_button.setOnClickListener { val radius = radius_input.text.toString().toDoubleOrNull() ?: 25.0 trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius) trackbook.load_homepoints() - create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) + create_homepoint_overlays() dialog.dismiss() } @@ -529,19 +547,6 @@ class MapFragment : Fragment() } } - /* Overlay current track on map */ - fun create_current_track_overlay(geopoints: MutableList, trackingState: Int) - { - if (currentTrackOverlay != null) - { - mapView.overlays.remove(currentTrackOverlay) - } - if (geopoints.isNotEmpty()) - { - currentTrackOverlay = createTrackOverlay(requireContext(), mapView, geopoints, trackingState) - } - } - fun update_main_button() { mainButton.isEnabled = trackbook.database.ready @@ -592,14 +597,13 @@ class MapFragment : Fragment() // get reference to tracker service val binder = service as TrackerService.LocalBinder trackerService = binder.service + trackerService.mapfragment = thismapfragment // get state of tracking and update button if necessary trackingState = trackerService.trackingState update_main_button() // register listener for changes in shared preferences PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) // start listening for location updates - handler.removeCallbacks(periodicLocationRequestRunnable) - handler.postDelayed(periodicLocationRequestRunnable, 0) } override fun onServiceDisconnected(arg0: ComponentName) { @@ -608,22 +612,27 @@ class MapFragment : Fragment() } } - private val periodicLocationRequestRunnable: Runnable = object : Runnable { + val location_update_redraw: Runnable = object : Runnable + { override fun run() { + Log.i("VOUSSOIR", "MapFragment.location_update_redraw") currentBestLocation = trackerService.currentBestLocation gpsProviderActive = trackerService.gpsProviderActive networkProviderActive = trackerService.networkProviderActive trackingState = trackerService.trackingState - // update location and track + create_current_position_overlays(currentBestLocation, trackingState) - create_current_track_overlay(trackerService.recent_trackpoints_for_mapview, trackingState) - // center map, if it had not been dragged/zoomed before + if (current_track_overlay == null) + { + create_track_overlay() + } + current_track_overlay!!.setPoints(trackerService.recent_trackpoints_for_mapview) + if (continuous_auto_center) { centerMap(currentBestLocation, animated=false) } - handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL) } } } diff --git a/app/src/main/java/org/y20k/trackbook/Track.kt b/app/src/main/java/org/y20k/trackbook/Track.kt index 7553d90..5aa6c8d 100644 --- a/app/src/main/java/org/y20k/trackbook/Track.kt +++ b/app/src/main/java/org/y20k/trackbook/Track.kt @@ -18,12 +18,13 @@ package org.y20k.trackbook import android.content.Context import android.database.Cursor +import android.database.DatabaseUtils.dumpCursorToString import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log import android.widget.Toast -import org.y20k.trackbook.helpers.iso8601_format +import org.y20k.trackbook.helpers.iso8601 import java.text.SimpleDateFormat import java.util.* @@ -33,7 +34,7 @@ data class Track ( var start_time: Date, var end_time: Date, var name: String = "", - val trkpts: ArrayDeque = ArrayDeque(), + val trkpts: ArrayList = ArrayList(), var view_latitude: Double = Keys.DEFAULT_LATITUDE, var view_longitude: Double = Keys.DEFAULT_LONGITUDE, var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, @@ -73,7 +74,7 @@ data class Track ( > """.trimIndent()) write("\t") - write("\t\tTrackbook Recording: ${this.name}") + write("\t\t${this.name}") write("\t\t${this.device_id}") write("\t") @@ -81,7 +82,6 @@ data class Track ( val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) dateFormat.timeZone = TimeZone.getTimeZone("UTC") write("\t") - write("\t\t${this.name}") write("\t\t") var previous: Trkpt? = null @@ -94,7 +94,8 @@ data class Track ( } write("\t\t\t") write("\t\t\t\t${trkpt.altitude}") - write("\t\t\t\t") + write("\t\t\t\t") + write("\t\t\t\t${trkpt.time}") write("\t\t\t\t${trkpt.numberSatellites}") write("\t\t\t") previous = trkpt @@ -170,11 +171,11 @@ data class Track ( fun trkpt_generator() = iterator { - val cursor: Cursor = database.connection.rawQuery( + var cursor: Cursor = database.connection.rawQuery( "SELECT lat, lon, time, ele, accuracy, sat FROM trkpt WHERE device_id = ? AND time > ? AND time < ? ORDER BY time ASC", arrayOf(device_id, start_time.time.toString(), end_time.time.toString()) ) - Log.i("VOUSSOIR", "Track.trkpt_generator: Querying points between ${start_time} -- ${end_time}") + Log.i("VOUSSOIR", "Track.trkpt_generator: Querying points between ${start_time} -- ${end_time}, ${cursor.count} results") val COLUMN_LAT = cursor.getColumnIndex("lat") val COLUMN_LON = cursor.getColumnIndex("lon") val COLUMN_ELE = cursor.getColumnIndex("ele") diff --git a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt index b472e8a..3cd60c5 100644 --- a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt +++ b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt @@ -20,6 +20,7 @@ import YesNoDialog import android.app.Activity import android.content.Context import android.content.Intent +import android.graphics.Paint import android.net.Uri import android.os.Bundle import android.os.Handler @@ -48,32 +49,38 @@ import org.osmdroid.events.ZoomEvent import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView -import org.osmdroid.views.overlay.ItemizedIconOverlay -import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.Polyline import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay +import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions +import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme import org.y20k.trackbook.helpers.AppThemeHelper import org.y20k.trackbook.helpers.DateTimeHelper import org.y20k.trackbook.helpers.LengthUnitHelper import org.y20k.trackbook.helpers.PreferencesHelper -import org.y20k.trackbook.helpers.createTrackOverlay +import org.y20k.trackbook.helpers.UiHelper import org.y20k.trackbook.helpers.create_start_end_markers -import org.y20k.trackbook.helpers.iso8601_format +import org.y20k.trackbook.helpers.iso8601 +import org.y20k.trackbook.helpers.iso8601_parse import java.text.SimpleDateFormat import java.util.* class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener { + private lateinit var trackbook: Trackbook + lateinit var rootView: View lateinit var save_track_button: ImageButton lateinit var deleteButton: ImageButton lateinit var zoom_in_button: FloatingActionButton lateinit var zoom_out_button: FloatingActionButton lateinit var trackNameView: MaterialTextView + lateinit var selected_trkpt_info: MaterialTextView lateinit var track_query_start_date: DatePicker lateinit var track_query_start_time: TimePicker lateinit var track_query_end_date: DatePicker lateinit var track_query_end_time: TimePicker + lateinit var delete_selected_trkpt_button: ImageButton var track_query_start_time_previous: Int = 0 var track_query_end_time_previous: Int = 0 private lateinit var mapView: MapView @@ -95,22 +102,25 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener private lateinit var negativeElevationView: MaterialTextView private lateinit var elevationDataViews: Group private lateinit var track: Track + private lateinit var track_segment_overlays: ArrayDeque + private var track_geopoints: MutableList = mutableListOf() + private var track_points_overlay: SimpleFastPointOverlay? = null + // private lateinit var trkpt_infowindow: InfoWindow private var useImperialUnits: Boolean = false private val handler: Handler = Handler(Looper.getMainLooper()) - private var special_points_overlay: ItemizedIconOverlay? = null - private var track_overlay: SimpleFastPointOverlay? = null val RERENDER_DELAY: Long = 1000 /* Overrides onCreateView from Fragment */ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + this.trackbook = (requireContext().applicationContext as Trackbook) val database: Database = (requireActivity().applicationContext as Trackbook).database track = Track( database=database, name=this.requireArguments().getString(Keys.ARG_TRACK_TITLE, ""), device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""), - start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!), - end_time=iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!), + start_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!), + end_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!), ) track.load_trkpts() rootView = inflater.inflate(R.layout.fragment_track, container, false) @@ -131,6 +141,8 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude)) controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL) + // trkpt_infowindow = MarkerInfoWindow(R.layout.trkpt_infowindow, mapView) + statisticsSheet = rootView.findViewById(R.id.statistics_sheet) statisticsView = rootView.findViewById(R.id.statistics_view) distanceView = rootView.findViewById(R.id.statistics_data_distance) @@ -227,6 +239,27 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener } }) + selected_trkpt_info = rootView.findViewById(R.id.selected_trkpt_info) + delete_selected_trkpt_button = rootView.findViewById(R.id.delete_selected_trkpt_button) + delete_selected_trkpt_button.setOnClickListener { + Log.i("VOUSSOIR", "delete selected trkpt button.") + if (track_points_overlay != null) + { + val selected = (track_geopoints[track_points_overlay!!.selectedPoint] as Trkpt) + track_geopoints.remove(selected) + track_points_overlay!!.selectedPoint = null + Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString()) + selected.rendered_by_polyline?.actualPoints?.remove(selected) + Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString()) + selected.rendered_by_polyline?.setPoints(ArrayList(selected.rendered_by_polyline?.actualPoints)) + Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString()) + trackbook.database.delete_trkpt(selected.device_id, selected.time) + delete_selected_trkpt_button.visibility = View.GONE + selected_trkpt_info.text = "" + mapView.invalidate() + } + } + save_track_button.setOnClickListener { openSaveGpxDialog() } @@ -251,6 +284,7 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener statisticsSheetBehavior = BottomSheetBehavior.from(statisticsSheet) statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + track_segment_overlays = ArrayDeque(10) render_track() return rootView @@ -265,25 +299,81 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener fun render_track() { Log.i("VOUSSOIR", "TrackFragment.render_track") - if (special_points_overlay != null) + mapView.overlays.clear() + track_segment_overlays.clear() + delete_selected_trkpt_button.visibility = View.GONE + + setupStatisticsViews() + + if (track.trkpts.isEmpty()) { - mapView.overlays.remove(special_points_overlay) + return } - if (track_overlay != null) - { - mapView.overlays.remove(track_overlay) - } - val geopoints: MutableList = mutableListOf() + Log.i("VOUSSOIR", "MapOverlayHelper.createTrackOverlay") + track_geopoints = mutableListOf() for (trkpt in track.trkpts) { - geopoints.add(trkpt) + track_geopoints.add(trkpt) } - if (track.trkpts.isNotEmpty()) + + var pl = new_track_segment_overlay() + var previous_time: Long = 0 + for (trkpt in track.trkpts) { - track_overlay = createTrackOverlay(requireContext(), mapView, geopoints, Keys.STATE_TRACKING_STOPPED) - special_points_overlay = create_start_end_markers(requireContext(), mapView, track.trkpts) + if (previous_time > 0 && (trkpt.time - previous_time) > Keys.STOP_OVER_THRESHOLD) + { + pl = new_track_segment_overlay() + } + pl.addPoint(trkpt) + trkpt.rendered_by_polyline = pl + previous_time = trkpt.time } - setupStatisticsViews() + + for (pl in track_segment_overlays) + { + create_start_end_markers(requireContext(), mapView, pl.actualPoints.first() as Trkpt, pl.actualPoints.last() as Trkpt) + } + + val pointTheme = SimplePointTheme(track_geopoints, false) + val style = Paint() + style.style = Paint.Style.FILL + style.color = Keys.POLYLINE_COLOR + style.flags = Paint.ANTI_ALIAS_FLAG + val overlayOptions: SimpleFastPointOverlayOptions = SimpleFastPointOverlayOptions.getDefaultStyle() + .setAlgorithm(SimpleFastPointOverlayOptions.RenderingAlgorithm.MEDIUM_OPTIMIZATION) + .setSymbol(SimpleFastPointOverlayOptions.Shape.CIRCLE) + .setPointStyle(style) + .setRadius(((Keys.POLYLINE_THICKNESS + 1 ) / 2) * UiHelper.getDensityScalingFactor(requireContext())) + .setIsClickable(true) + .setCellSize(12) + track_points_overlay = SimpleFastPointOverlay(pointTheme, overlayOptions) + mapView.overlays.add(track_points_overlay) + + track_points_overlay!!.setOnClickListener(object : SimpleFastPointOverlay.OnClickListener { + override fun onClick(points: SimpleFastPointOverlay.PointAdapter?, point: Int?) + { + if (points == null || point == null || point == 0) + { + return + } + val trkpt = (points[point]) as Trkpt + Log.i("VOUSSOIR", "Clicked ${trkpt.device_id} ${trkpt.time}") + selected_trkpt_info.text = "${trkpt.time}\n${iso8601(trkpt.time)}\n${trkpt.latitude}\n${trkpt.longitude}" + delete_selected_trkpt_button.visibility = View.VISIBLE + return + } + }) + } + + fun new_track_segment_overlay(): Polyline + { + var pl = Polyline(mapView) + pl.outlinePaint.strokeWidth = Keys.POLYLINE_THICKNESS + pl.outlinePaint.color = Keys.POLYLINE_COLOR + pl.infoWindow = null + track_segment_overlays.add(pl) + mapView.overlays.add(pl) + return pl } fun get_datetime(datepicker: DatePicker, timepicker: TimePicker, seconds: Int): Date @@ -348,7 +438,16 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener /* Overrides onZoom from MapListener */ override fun onZoom(event: ZoomEvent?): Boolean { - return (event != null) + if (event == null) + { + return false + } + if (track_points_overlay == null) + { + return false + } + track_points_overlay!!.isEnabled = event.zoomLevel >= 16 + return true } /* Overrides onScroll from MapListener */ diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt index 3c7b0f4..90eeadc 100644 --- a/app/src/main/java/org/y20k/trackbook/TrackerService.kt +++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt @@ -30,9 +30,7 @@ import android.Manifest import android.os.* import android.util.Log import androidx.core.content.ContextCompat -import org.osmdroid.api.IGeoPoint import org.osmdroid.util.GeoPoint -import org.osmdroid.views.overlay.simplefastpoint.LabelledGeoPoint import java.util.* import org.y20k.trackbook.helpers.* @@ -55,8 +53,8 @@ class TrackerService: Service() var location_min_time_ms: Long = 0 private val RECENT_TRKPT_COUNT = 7200 lateinit var recent_trkpts: Deque - lateinit var recent_displacement_trkpts: Deque - var recent_trackpoints_for_mapview: MutableList = mutableListOf() + lateinit var recent_displacement_locations: Deque + var recent_trackpoints_for_mapview: MutableList = mutableListOf() var gpsLocationListenerRegistered: Boolean = false var networkLocationListenerRegistered: Boolean = false var bound: Boolean = false @@ -67,6 +65,7 @@ class TrackerService: Service() private lateinit var notificationHelper: NotificationHelper private lateinit var gpsLocationListener: LocationListener private lateinit var networkLocationListener: LocationListener + var mapfragment: MapFragment? = null private fun addGpsLocationListener() { @@ -160,6 +159,11 @@ class TrackerService: Service() currentBestLocation = location + if (mapfragment != null) + { + mapfragment!!.handler.postDelayed(mapfragment!!.location_update_redraw, 0) + } + if (trackingState != Keys.STATE_TRACKING_ACTIVE) { return @@ -195,7 +199,7 @@ class TrackerService: Service() return } } - if (! (recent_displacement_trkpts.isEmpty() || isDifferentEnough(recent_displacement_trkpts.first().toLocation(), location, omitRests))) + if (! (recent_displacement_locations.isEmpty() || isDifferentEnough(recent_displacement_locations.first(), location, omitRests))) { Log.i("VOUSSOIR", "Omitting due to too close to previous.") return @@ -215,10 +219,10 @@ class TrackerService: Service() recent_trackpoints_for_mapview.removeFirst() } - recent_displacement_trkpts.add(trkpt) - while (recent_displacement_trkpts.size > 5) + recent_displacement_locations.add(location) + while (recent_displacement_locations.size > 5) { - recent_displacement_trkpts.removeFirst() + recent_displacement_locations.removeFirst() } if (location.time - lastCommit > Keys.COMMIT_INTERVAL) @@ -275,7 +279,7 @@ class TrackerService: Service() trackbook = (applicationContext as Trackbook) trackbook.load_homepoints() recent_trkpts = ArrayDeque(RECENT_TRKPT_COUNT) - recent_displacement_trkpts = ArrayDeque(5) + recent_displacement_locations = ArrayDeque(5) recent_trackpoints_for_mapview = mutableListOf() use_gps_location = PreferencesHelper.load_location_gps() use_network_location = PreferencesHelper.load_location_network() @@ -392,7 +396,7 @@ class TrackerService: Service() addNetworkLocationListener() trackingState = Keys.STATE_TRACKING_ACTIVE PreferencesHelper.saveTrackingState(trackingState) - recent_displacement_trkpts.clear() + recent_displacement_locations.clear() startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification()) } @@ -402,7 +406,7 @@ class TrackerService: Service() trackingState = Keys.STATE_TRACKING_STOPPED PreferencesHelper.saveTrackingState(trackingState) - recent_displacement_trkpts.clear() + recent_displacement_locations.clear() displayNotification() stopForeground(STOP_FOREGROUND_DETACH) } diff --git a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt index 999a645..3ed4aa6 100644 --- a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt +++ b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt @@ -33,7 +33,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.Main import org.y20k.trackbook.helpers.UiHelper -import org.y20k.trackbook.helpers.iso8601_format +import org.y20k.trackbook.helpers.iso8601 import org.y20k.trackbook.tracklist.TracklistAdapter /* @@ -46,7 +46,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, private lateinit var trackElementList: RecyclerView private lateinit var tracklistOnboarding: ConstraintLayout - /* Overrides onCreateView from Fragment */ + /* Overrides onCreate from Fragment */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -67,18 +67,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, trackElementList.itemAnimator = DefaultItemAnimator() trackElementList.adapter = tracklistAdapter - // enable swipe to delete - val swipeHandler = object : UiHelper.SwipeToDeleteCallback(activity as Context) { - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - // ask user - val adapterPosition: Int = viewHolder.adapterPosition // first position in list is reserved for statistics - val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${tracklistAdapter.getTrackName(adapterPosition)}" - YesNoDialog(this@TracklistFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_delete_recording, payload = adapterPosition) - } - } - val itemTouchHelper = ItemTouchHelper(swipeHandler) - itemTouchHelper.attachToRecyclerView(trackElementList) - // toggle onboarding layout toggleOnboardingLayout() @@ -90,40 +78,15 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, val bundle: Bundle = bundleOf( Keys.ARG_TRACK_TITLE to track.name, Keys.ARG_TRACK_DEVICE_ID to track.device_id, - Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time), - Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.end_time), + Keys.ARG_TRACK_START_TIME to iso8601(track.start_time), + Keys.ARG_TRACK_STOP_TIME to iso8601(track.end_time), ) findNavController().navigate(R.id.fragment_track, bundle) } - /* Overrides onYesNoDialog from YesNoDialogListener */ - override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) - { - when (type) - { - Keys.DIALOG_DELETE_TRACK -> - { - when (dialogResult) { - // user tapped remove track - true -> - { - tracklistAdapter.delete_track_at_position(activity as Context, payload) - toggleOnboardingLayout() - } - // user tapped cancel - false -> - { - // The user slid the track over to the side and turned it red, we have to - // bring it back. - tracklistAdapter.notifyItemChanged(payload) - } - } - } - } - } - // toggle onboarding layout - private fun toggleOnboardingLayout() { + private fun toggleOnboardingLayout() + { when (tracklistAdapter.isEmpty()) { true -> { // show onboarding layout @@ -143,11 +106,13 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, */ inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) { - override fun supportsPredictiveItemAnimations(): Boolean { + override fun supportsPredictiveItemAnimations(): Boolean + { return true } - override fun onLayoutCompleted(state: RecyclerView.State?) { + override fun onLayoutCompleted(state: RecyclerView.State?) + { super.onLayoutCompleted(state) // handle delete request from TrackFragment - after layout calculations are complete val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L diff --git a/app/src/main/java/org/y20k/trackbook/Trkpt.kt b/app/src/main/java/org/y20k/trackbook/Trkpt.kt index 1594ae9..d92fda4 100644 --- a/app/src/main/java/org/y20k/trackbook/Trkpt.kt +++ b/app/src/main/java/org/y20k/trackbook/Trkpt.kt @@ -19,6 +19,7 @@ package org.y20k.trackbook import android.location.Location import org.osmdroid.api.IGeoPoint import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.Polyline import org.y20k.trackbook.helpers.getNumberOfSatellites class Trkpt( @@ -30,6 +31,7 @@ class Trkpt( val accuracy: Float, val time: Long, val numberSatellites: Int = 0, + var rendered_by_polyline: Polyline? = null ) : GeoPoint(latitude, longitude, altitude) { constructor(device_id: String, location: Location) : this( diff --git a/app/src/main/java/org/y20k/trackbook/functions.kt b/app/src/main/java/org/y20k/trackbook/functions.kt index da61f7e..6331248 100644 --- a/app/src/main/java/org/y20k/trackbook/functions.kt +++ b/app/src/main/java/org/y20k/trackbook/functions.kt @@ -15,12 +15,25 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.random.Random.Default.nextBits -val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US) private val RNG = SecureRandom() +fun iso8601(timestamp: Long): String +{ + val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + iso8601_format.timeZone = TimeZone.getTimeZone("UTC") + return iso8601_format.format(timestamp) +} + fun iso8601(datetime: Date): String { - return iso8601_format.format(datetime) + return iso8601(datetime.time) +} + +fun iso8601_parse(datetime: String): Date +{ + val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + iso8601_format.timeZone = TimeZone.getTimeZone("UTC") + return iso8601_format.parse(datetime) } fun random_int(): Int diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt index 179c5da..c9ed53a 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt @@ -83,15 +83,4 @@ object DateTimeHelper { fun convertToReadableDateAndTime(date: Date, dateStyle: Int = DateFormat.SHORT, timeStyle: Int = DateFormat.SHORT): String { return "${DateFormat.getDateInstance(dateStyle, Locale.getDefault()).format(date)} ${DateFormat.getTimeInstance(timeStyle, Locale.getDefault()).format(date)}" } - - /* Calculates time difference between two locations */ - fun calculateTimeDistance(previousLocation: Location?, location: Location): Long { - var timeDifference: Long = 0L - // two data points needed to calculate time difference - if (previousLocation != null) { - // get time difference - timeDifference = location.time - previousLocation.time - } - return timeDifference - } } diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt index 54a4d75..f067da5 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt @@ -17,81 +17,27 @@ package org.y20k.trackbook.helpers import android.content.Context -import android.graphics.Paint import android.util.Log import android.widget.Toast import androidx.core.content.ContextCompat -import org.osmdroid.api.IGeoPoint import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.ItemizedIconOverlay import org.osmdroid.views.overlay.OverlayItem -import org.osmdroid.views.overlay.simplefastpoint.LabelledGeoPoint -import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay -import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions -import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme -import org.y20k.trackbook.Keys import org.y20k.trackbook.R import org.y20k.trackbook.Trkpt import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.* -fun createTrackOverlay(context: Context, map_view: MapView, geopoints: MutableList, trackingState: Int): SimpleFastPointOverlay -{ - Log.i("VOUSSOIR", "MapOverlayHelper.createTrackOverlay") - val pointTheme = SimplePointTheme(geopoints, false) - val style = Paint() - style.style = Paint.Style.FILL - style.color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red) else context.getColor(R.color.default_blue) - style.flags = Paint.ANTI_ALIAS_FLAG - val overlayOptions: SimpleFastPointOverlayOptions = SimpleFastPointOverlayOptions.getDefaultStyle() - .setAlgorithm(SimpleFastPointOverlayOptions.RenderingAlgorithm.MAXIMUM_OPTIMIZATION) - .setSymbol(SimpleFastPointOverlayOptions.Shape.CIRCLE) - .setPointStyle(style) - .setRadius(6F * UiHelper.getDensityScalingFactor(context)) // radius is set in px - scaling factor makes that display density independent (= dp) - .setIsClickable(true) - .setCellSize(12) // Sets the grid cell size used for indexing, in pixels. Larger cells result in faster rendering speed, but worse fidelity. Default is 10 pixels, for large datasets (>10k points), use 15. - var overlay = SimpleFastPointOverlay(pointTheme, overlayOptions) - - overlay.setOnClickListener(object : SimpleFastPointOverlay.OnClickListener { - override fun onClick(points: SimpleFastPointOverlay.PointAdapter?, point: Int?) - { - if (points == null || point == null || point == 0) - { - return - } - val trkpt = (points[point]) as Trkpt - Log.i("VOUSSOIR", "Clicked ${trkpt.device_id} ${trkpt.time}") - // trackpoints.remove(points[point]) - // map_view.overlays.remove(overlay) - // overlay = SimpleFastPointOverlay(pointTheme, overlayOptions) - // overlay.setOnClickListener(this) - // map_view.overlays.add(overlay) - // map_view.postInvalidate() - return - } - }) - - map_view.overlays.add(overlay) - return overlay -} - -fun create_start_end_markers(context: Context, map_view: MapView, trkpts: Collection): ItemizedIconOverlay? +fun create_start_end_markers(context: Context, map_view: MapView, startpoint: Trkpt, endpoint: Trkpt): ItemizedIconOverlay? { Log.i("VOUSSOIR", "MapOverlayHelper.create_start_end_markers") - if (trkpts.size == 0) - { - return null - } - val overlayItems: ArrayList = ArrayList() - val startpoint = trkpts.first() - val endpoint = trkpts.last() val startmarker: OverlayItem = createOverlayItem(context, startpoint.latitude, startpoint.longitude, startpoint.accuracy, startpoint.provider, startpoint.time) startmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!) overlayItems.add(startmarker) - if (trkpts.size > 1) + if (startpoint != endpoint) { val endmarker: OverlayItem = createOverlayItem(context, endpoint.latitude, endpoint.longitude, endpoint.accuracy, endpoint.provider, endpoint.time) endmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!) diff --git a/app/src/main/res/layout/fragment_track.xml b/app/src/main/res/layout/fragment_track.xml index 36a35f0..d7a6c6d 100755 --- a/app/src/main/res/layout/fragment_track.xml +++ b/app/src/main/res/layout/fragment_track.xml @@ -12,19 +12,42 @@ android:layout_height="match_parent" > + + + + + + app:layout_constraintTop_toTopOf="parent" />