checkpoint

master
voussoir 2023-03-10 19:35:27 -08:00
parent 5dad3d6209
commit 1f695958e7
5 changed files with 183 additions and 134 deletions

View File

@ -1,11 +1,14 @@
package org.y20k.trackbook
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.openOrCreateDatabase
import android.util.Log
import java.io.File
import java.util.*
class Database()
class Database(trackbook: Trackbook)
{
val trackbook = trackbook
var ready: Boolean = false
lateinit var file: File
lateinit var connection: SQLiteDatabase
@ -14,6 +17,7 @@ class Database()
{
this.connection.close()
this.ready = false
this.trackbook.call_database_changed_listeners()
}
fun connect(file: File)
@ -23,6 +27,7 @@ class Database()
this.connection = openOrCreateDatabase(file, null)
this.initialize_tables()
this.ready = true
Log.i("VOUSSOIR", "Database.open: Calling all listeners")
}
fun commit()
@ -40,11 +45,29 @@ class Database()
this.connection.endTransaction()
}
fun insert_trkpt(device_id: String, trkpt: Trkpt)
{
val values = ContentValues().apply {
put("device_id", device_id)
put("lat", trkpt.latitude)
put("lon", trkpt.longitude)
put("time", GregorianCalendar.getInstance().time.time)
put("accuracy", trkpt.accuracy)
put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude)
}
if (! connection.inTransaction())
{
connection.beginTransaction()
}
connection.insert("trkpt", null, values)
}
private fun initialize_tables()
{
this.connection.beginTransaction()
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, star 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(lat, lon, time, device_id))")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT, PRIMARY KEY(lat, lon))")
this.connection.setTransactionSuccessful()
this.connection.endTransaction()

View File

@ -17,7 +17,6 @@
package org.y20k.trackbook
import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Resources
@ -41,8 +40,8 @@ import org.osmdroid.api.IMapController
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.FolderOverlay
import org.osmdroid.views.overlay.ItemizedIconOverlay
import org.osmdroid.views.overlay.Overlay
import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.TilesOverlay
@ -69,30 +68,44 @@ class MapFragment : Fragment()
private lateinit var currentBestLocation: Location
private lateinit var trackerService: TrackerService
private lateinit var trackbook: Trackbook
lateinit var rootView: View
var userInteraction: Boolean = false
lateinit var currentLocationButton: FloatingActionButton
lateinit var mainButton: ExtendedFloatingActionButton
private lateinit var mapView: MapView
private lateinit var current_position_folder: FolderOverlay
private var current_position_overlays = ArrayList<Overlay>()
private var currentTrackOverlay: SimpleFastPointOverlay? = null
private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay<OverlayItem>? = null
private lateinit var locationErrorBar: Snackbar
private lateinit var controller: IMapController
private var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL
private lateinit var homepoints_overlay_folder: FolderOverlay
private var homepoints_overlays = ArrayList<Overlay>()
private lateinit var database_changed_listener: DatabaseChangedListener
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?)
{
Log.i("VOUSSOIR", "MapFragment.onCreate")
super.onCreate(savedInstanceState)
// TODO make only MapFragment's status bar transparent - see:
// https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and
// https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63
// get current best location
currentBestLocation = getLastKnownLocation(activity as Context)
// get saved tracking state
this.trackbook = (requireContext().applicationContext as Trackbook)
database_changed_listener = object: DatabaseChangedListener
{
override fun database_changed()
{
Log.i("VOUSSOIR", "MapFragment database_ready_changed to ${trackbook.database.ready}")
if (trackbook.database.ready)
{
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
}
else
{
clear_homepoint_overlays()
}
update_main_button()
}
}
currentBestLocation = getLastKnownLocation(requireContext())
trackingState = PreferencesHelper.loadTrackingState()
}
@ -100,7 +113,6 @@ class MapFragment : Fragment()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
{
Log.i("VOUSSOIR", "MapFragment.onCreateView")
val context = activity as Context
// find views
rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map)
@ -124,26 +136,23 @@ class MapFragment : Fragment()
}
// store Density Scaling Factor
val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(context)
val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(requireContext())
// add compass to map
val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
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)
val app: Trackbook = (context.applicationContext as Trackbook)
val app: Trackbook = (requireContext().applicationContext as Trackbook)
app.load_homepoints()
homepoints_overlay_folder = FolderOverlay()
mapView.overlays.add(homepoints_overlay_folder)
createHomepointOverlays(context, mapView, app.homepoints)
// add my location overlay
current_position_folder = FolderOverlay()
mapView.overlays.add(current_position_folder)
create_homepoint_overlays(requireContext(), mapView, app.homepoints)
if (database_changed_listener !in trackbook.database_changed_listeners)
{
trackbook.database_changed_listeners.add(database_changed_listener)
}
centerMap(currentBestLocation)
@ -152,7 +161,7 @@ class MapFragment : Fragment()
currentTrackSpecialMarkerOverlay = null
// initialize main button state
updateMainButton(trackingState)
update_main_button()
// listen for user interaction
addInteractionListener()
@ -224,9 +233,13 @@ class MapFragment : Fragment()
Log.i("VOUSSOIR", "MapFragment.onDestroy")
super.onDestroy()
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (database_changed_listener in trackbook.database_changed_listeners)
{
trackbook.database_changed_listeners.remove(database_changed_listener)
}
}
/* Register the permission launcher for requesting location */
private val requestLocationPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
// permission was granted - re-bind service
@ -301,9 +314,6 @@ class MapFragment : Fragment()
}
}
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key)
{
@ -312,12 +322,14 @@ class MapFragment : Fragment()
if (activity != null)
{
trackingState = PreferencesHelper.loadTrackingState()
updateMainButton(trackingState)
update_main_button()
}
}
}
}
private fun addInteractionListener() {
private fun addInteractionListener()
{
mapView.setOnTouchListener { v, event ->
userInteraction = true
false
@ -340,10 +352,25 @@ class MapFragment : Fragment()
userInteraction = false
}
fun clear_current_position_overlays()
{
for (ov in current_position_overlays)
{
if (ov in mapView.overlays)
{
mapView.overlays.remove(ov)
}
}
current_position_overlays.clear();
}
/* Mark current position on map */
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED)
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
@ -374,47 +401,68 @@ class MapFragment : Fragment()
}
}
current_position_folder.items.clear()
val current_location_radius = Polygon()
current_location_radius.points = Polygon.pointsAsCircle(GeoPoint(location.latitude, location.longitude), location.accuracy.toDouble())
current_location_radius.fillPaint.color = fillcolor
current_location_radius.outlinePaint.color = Color.argb(0, 0, 0, 0)
current_position_folder.add(current_location_radius)
current_position_overlays.add(current_location_radius)
val overlayItems: java.util.ArrayList<OverlayItem> = java.util.ArrayList<OverlayItem>()
val overlayItem: OverlayItem = createOverlayItem(requireContext(), location.latitude, location.longitude, location.accuracy, location.provider.toString(), location.time)
overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem)
current_position_folder.add(createOverlay(requireContext(), overlayItems))
current_position_overlays.add(createOverlay(requireContext(), overlayItems))
for (ov in current_position_overlays)
{
mapView.overlays.add(ov)
}
}
fun createHomepointOverlays(context: Context, map_view: MapView, homepoints: List<Homepoint>)
fun clear_homepoint_overlays()
{
for (ov in homepoints_overlays)
{
if (ov in mapView.overlays)
{
mapView.overlays.remove(ov)
}
}
homepoints_overlays.clear();
}
fun create_homepoint_overlays(context: Context, map_view: MapView, homepoints: List<Homepoint>)
{
Log.i("VOUSSOIR", "MapFragmentLayoutHolder.createHomepointOverlays")
val overlayItems: java.util.ArrayList<OverlayItem> = java.util.ArrayList<OverlayItem>()
val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!!
homepoints_overlay_folder.items.clear()
clear_homepoint_overlays()
for (homepoint in homepoints)
{
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)
homepoints_overlay_folder.add(createOverlay(context, overlayItems))
val p = Polygon()
p.points = Polygon.pointsAsCircle(GeoPoint(homepoint.location.latitude, homepoint.location.longitude), homepoint.location.accuracy.toDouble())
p.fillPaint.color = Color.argb(64, 255, 193, 7)
p.outlinePaint.color = Color.argb(0, 0, 0, 0)
homepoints_overlay_folder.add(p)
homepoints_overlays.add(p)
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)
homepoints_overlays.add(createOverlay(context, overlayItems))
}
for (ov in homepoints_overlays)
{
mapView.overlays.add(ov)
}
}
/* Overlay current track on map */
fun overlayCurrentTrack(track: Track, trackingState: Int) {
fun create_current_track_overlay(track: Track, trackingState: Int)
{
if (currentTrackOverlay != null) {
mapView.overlays.remove(currentTrackOverlay)
}
@ -427,22 +475,26 @@ class MapFragment : Fragment()
}
}
/* Toggles state of main button and additional buttons (save & resume) */
fun updateMainButton(trackingState: Int)
fun update_main_button()
{
when (trackingState) {
Keys.STATE_TRACKING_STOPPED -> {
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = requireContext().getString(R.string.button_start)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_start)
currentLocationButton.isVisible = true
}
Keys.STATE_TRACKING_ACTIVE -> {
mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
mainButton.text = requireContext().getString(R.string.button_pause)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_pause)
currentLocationButton.isVisible = true
}
mainButton.isEnabled = trackbook.database.ready
currentLocationButton.isVisible = true
if (! trackbook.database.ready)
{
mainButton.text = "Database not ready"
mainButton.icon = null
}
else if (trackingState == Keys.STATE_TRACKING_STOPPED)
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = requireContext().getString(R.string.button_start)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_start)
}
else if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
mainButton.text = requireContext().getString(R.string.button_pause)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_pause)
}
}
@ -459,13 +511,7 @@ class MapFragment : Fragment()
if (locationErrorBar.isShown) locationErrorBar.dismiss()
}
}
/*
* End of declaration
*/
/*
* Defines callbacks for service binding, passed to bindService()
*/
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
bound = true
@ -474,7 +520,7 @@ class MapFragment : Fragment()
trackerService = binder.service
// get state of tracking and update button if necessary
trackingState = trackerService.trackingState
updateMainButton(trackingState)
update_main_button()
// register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
// start listening for location updates
@ -486,13 +532,7 @@ class MapFragment : Fragment()
handleServiceUnbind()
}
}
/*
* End of declaration
*/
/*
* Runnable: Periodically requests location
*/
private val periodicLocationRequestRunnable: Runnable = object : Runnable {
override fun run() {
// pull current state from service
@ -502,20 +542,17 @@ class MapFragment : Fragment()
networkProviderActive = trackerService.networkProviderActive
trackingState = trackerService.trackingState
// update location and track
markCurrentPosition(currentBestLocation, trackingState)
overlayCurrentTrack(track, trackingState)
create_current_position_overlays(currentBestLocation, trackingState)
create_current_track_overlay(track, trackingState)
// center map, if it had not been dragged/zoomed before
if (!userInteraction)
{
centerMap(currentBestLocation, true)
}
// show error snackbar if necessary
toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
// toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
// use the handler to start runnable again after specified delay
handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL)
}
}
/*
* End of declaration
*/
}

View File

@ -31,12 +31,24 @@ import org.y20k.trackbook.helpers.PreferencesHelper
import org.y20k.trackbook.helpers.PreferencesHelper.initPreferences
import java.io.File
/*
* Trackbook.class
*/
interface DatabaseChangedListener
{
fun database_changed()
}
class Trackbook(): Application() {
val database: Database = Database()
val database: Database = Database(this)
val homepoints: ArrayList<Homepoint> = ArrayList()
val database_changed_listeners = ArrayList<DatabaseChangedListener>()
fun call_database_changed_listeners()
{
for (listener in this.database_changed_listeners)
{
listener.database_changed()
}
}
override fun onCreate()
{
@ -63,6 +75,7 @@ class Trackbook(): Application() {
{
this.database.ready = false
}
this.call_database_changed_listeners()
}
fun load_homepoints()
{

View File

@ -230,7 +230,8 @@ class TrackerService: Service(), SensorEventListener
}
/* Overrides onDestroy from Service */
override fun onDestroy() {
override fun onDestroy()
{
LogHelper.i("VOUSSOIR", "TrackerService.onDestroy.")
super.onDestroy()
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
@ -255,8 +256,10 @@ class TrackerService: Service(), SensorEventListener
/* Overrides onSensorChanged from SensorEventListener */
override fun onSensorChanged(sensorEvent: SensorEvent?) {
var steps = 0f
if (sensorEvent != null) {
if (stepCountOffset == 0f) {
if (sensorEvent != null)
{
if (stepCountOffset == 0f)
{
// store steps previously recorded by the system
stepCountOffset = (sensorEvent.values[0] - 1) - 0 // subtract any steps recorded during this session in case the app was killed
}
@ -307,22 +310,28 @@ class TrackerService: Service(), SensorEventListener
/* Adds location listeners to location manager */
fun removeGpsLocationListener()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
locationManager.removeUpdates(gpsLocationListener)
gpsLocationListenerRegistered = false
LogHelper.v(TAG, "Removed GPS location listener.")
} else {
}
else
{
LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.")
}
}
fun removeNetworkLocationListener()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
locationManager.removeUpdates(networkLocationListener)
networkLocationListenerRegistered = false
LogHelper.v(TAG, "Removed Network location listener.")
} else {
}
else
{
LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.")
}
}
@ -330,7 +339,8 @@ class TrackerService: Service(), SensorEventListener
private fun startStepCounter()
{
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
if (!stepCounterAvailable) {
if (!stepCounterAvailable)
{
LogHelper.w(TAG, "Pedometer sensor not available.")
}
}
@ -445,33 +455,18 @@ class TrackerService: Service(), SensorEventListener
}
return true
}
/*
* Runnable: Periodically track updates (if recording active)
*/
private val periodicTrackUpdate: Runnable = object : Runnable
{
override fun run() {
val now: Date = GregorianCalendar.getInstance().time
val trkpt: Trkpt = Trkpt(location=currentBestLocation)
val trkpt = Trkpt(location=currentBestLocation)
Log.i("VOUSSOIR", "Processing point ${currentBestLocation.latitude}, ${currentBestLocation.longitude} ${now.time}.")
if (should_keep_point((currentBestLocation)))
{
val values = ContentValues().apply {
put("device_id", device_id)
put("lat", trkpt.latitude)
put("lon", trkpt.longitude)
put("time", now.time)
put("accuracy", trkpt.accuracy)
put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude)
put("star", 0)
}
if (! trackbook.database.connection.inTransaction())
{
trackbook.database.connection.beginTransaction()
}
trackbook.database.connection.insert("trkpt", null, values)
trackbook.database.insert_trkpt(device_id, trkpt)
track.trkpts.add(trkpt)
while (track.trkpts.size > 7200)
{
track.trkpts.removeFirst()
@ -479,20 +474,12 @@ class TrackerService: Service(), SensorEventListener
if (now.time - lastCommit.time > Keys.SAVE_TEMP_TRACK_INTERVAL)
{
if (trackbook.database.connection.inTransaction())
{
trackbook.database.commit()
}
trackbook.database.commit()
lastCommit = now
}
}
// update notification
displayNotification()
// re-run this in set interval
handler.postDelayed(this, Keys.TRACKING_INTERVAL)
}
}
/*
* End of declaration
*/
}

View File

@ -30,12 +30,10 @@ 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.Polygon
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.Homepoint
import org.y20k.trackbook.Keys
import org.y20k.trackbook.R
import org.y20k.trackbook.Track
@ -44,15 +42,6 @@ import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.*
//private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem>
/* Creates icon overlay for current position (used in MapFragment) */
fun createMyLocationOverlay(context: Context, map_view: MapView, currentPositionOverlay: ItemizedIconOverlay<OverlayItem>, location: Location, trackingState: Int)
{
}
/* Creates icon overlay for track */
fun createTrackOverlay(context: Context, map_view: MapView, track: Track, trackingState: Int)
@ -149,12 +138,12 @@ fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>): Itemi
{
return ItemizedIconOverlay<OverlayItem>(context, overlayItems,
object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean
{
return false
}
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
v.vibrate(50)
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean
{
Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show()
return true
}