checkpoint

master
voussoir 2023-03-11 12:30:24 -08:00
parent ffd5fb6af3
commit 172ca703a9
14 changed files with 325 additions and 13 deletions

View File

@ -58,6 +58,7 @@ object Keys {
const val PREF_OMIT_RESTS: String = "prefOmitRests"
const val PREF_COMMIT_INTERVAL: String = "prefCommitInterval"
const val PREF_DEVICE_ID: String = "prefDeviceID"
const val PREF_DATABASE_DIRECTORY: String = "prefDatabaseDirectory"
// states
const val STATE_TRACKING_STOPPED: Int = 0
@ -117,6 +118,7 @@ object Keys {
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 5_000_000_000L // 5s in nanoseconds
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
const val DEFAULT_ZOOM_LEVEL: Double = 16.0
const val DEFAULT_OMIT_RESTS: Boolean = true
const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded
// notification

View File

@ -19,6 +19,7 @@ package org.y20k.trackbook
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
@ -145,6 +146,11 @@ class MainActivity: AppCompatActivity()
Log.i("VOUSSOIR", "MainActivity: device_id has changed.")
trackbook.load_database()
}
Keys.PREF_DATABASE_DIRECTORY ->
{
trackbook.load_database()
}
}
}

View File

@ -72,6 +72,8 @@ class MapFragment : Fragment()
lateinit var rootView: View
var userInteraction: Boolean = false
lateinit var currentLocationButton: FloatingActionButton
lateinit var zoom_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton
lateinit var mainButton: ExtendedFloatingActionButton
private lateinit var mapView: MapView
private var current_position_overlays = ArrayList<Overlay>()
@ -117,6 +119,8 @@ class MapFragment : Fragment()
rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.location_button)
zoom_in_button = rootView.findViewById(R.id.zoom_in_button)
zoom_out_button = rootView.findViewById(R.id.zoom_out_button)
mainButton = rootView.findViewById(R.id.main_button)
locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE)
@ -167,11 +171,17 @@ class MapFragment : Fragment()
addInteractionListener()
// set up buttons
mainButton.setOnClickListener {
handleTrackingManagementMenu()
}
currentLocationButton.setOnClickListener {
centerMap(currentBestLocation, animated = true)
}
mainButton.setOnClickListener {
handleTrackingManagementMenu()
zoom_in_button.setOnClickListener {
controller.zoomTo(mapView.zoomLevelDouble + 0.5, 250)
}
zoom_out_button.setOnClickListener {
controller.zoomTo(mapView.zoomLevelDouble - 0.5, 250)
}
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -481,7 +491,7 @@ class MapFragment : Fragment()
currentLocationButton.isVisible = true
if (! trackbook.database.ready)
{
mainButton.text = "Database not ready"
mainButton.text = requireContext().getString(R.string.button_not_ready)
mainButton.icon = null
}
else if (trackingState == Keys.STATE_TRACKING_STOPPED)

View File

@ -17,21 +17,35 @@
package org.y20k.trackbook
import YesNoDialog
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.preference.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.preference.contains
import get_path_from_uri
import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.LengthUnitHelper
import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.PreferencesHelper
import org.y20k.trackbook.helpers.random_device_id
const val INTENT_DATABASE_DIRECTORY_PICKER = 12121
/*
* SettingsFragment class
*/
@ -101,7 +115,6 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
screen.addPreference(preferenceThemeSelection)
// set up "Recording Accuracy" preference
val DEFAULT_OMIT_RESTS = true
val preferenceOmitRests: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
preferenceOmitRests.isSingleLineTitle = false
preferenceOmitRests.title = getString(R.string.pref_omit_rests_title)
@ -109,7 +122,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceOmitRests.key = Keys.PREF_OMIT_RESTS
preferenceOmitRests.summaryOn = getString(R.string.pref_omit_rests_on)
preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off)
preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS)
preferenceOmitRests.setDefaultValue(Keys.DEFAULT_OMIT_RESTS)
preferenceCategoryGeneral.contains(preferenceOmitRests)
screen.addPreference(preferenceOmitRests)
@ -119,13 +132,56 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceDeviceID.key = Keys.PREF_DEVICE_ID
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + PreferencesHelper.load_device_id()
preferenceDeviceID.setDefaultValue(random_device_id())
preferenceCategoryGeneral.contains(preferenceDeviceID)
preferenceDeviceID.setOnPreferenceChangeListener { preference, newValue ->
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + newValue
return@setOnPreferenceChangeListener true
}
preferenceCategoryGeneral.contains(preferenceDeviceID)
screen.addPreference(preferenceDeviceID)
val preferenceDatabaseFolder: Preference = Preference(context)
var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
Log.i("VOUSSOIR", "I'm not dead yet.")
if (result.resultCode != Activity.RESULT_OK)
{
return@registerForActivityResult
}
if (result.data == null)
{
return@registerForActivityResult
}
if (result.data!!.data == null)
{
return@registerForActivityResult
}
val uri: Uri = result.data!!.data!!
val docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
val path: String = get_path_from_uri(context, docUri) ?: ""
Log.i("VOUSSOIR", "We got " + path)
PreferencesHelper.save_database_folder(path)
preferenceDatabaseFolder.summary = (getString(R.string.pref_database_folder_summary) + "\n" + path).trim()
}
preferenceDatabaseFolder.title = "Database Directory"
preferenceDatabaseFolder.setIcon(R.drawable.ic_save_to_storage_24dp)
preferenceDatabaseFolder.key = Keys.PREF_DATABASE_DIRECTORY
preferenceDatabaseFolder.summary = (getString(R.string.pref_database_folder_summary) + "\n" + PreferencesHelper.load_database_folder()).trim()
preferenceDatabaseFolder.setOnPreferenceClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
resultLauncher.launch(intent)
}
return@setOnPreferenceClickListener true
}
preferenceDatabaseFolder.setOnPreferenceChangeListener { preference, newValue ->
preferenceDatabaseFolder.summary = "Directory to contain your database file." + "\n" + newValue
return@setOnPreferenceChangeListener true
}
preferenceCategoryGeneral.contains(preferenceDatabaseFolder)
screen.addPreference(preferenceDatabaseFolder)
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
screen.addPreference(preferenceCategoryAbout)

View File

@ -66,9 +66,20 @@ class Trackbook(): Application() {
fun load_database()
{
Log.i("VOUSSOIR", "Trackbook.load_database")
val folder = PreferencesHelper.load_database_folder()
this.database.commit()
if (this.database.ready)
{
this.database.close()
}
if (folder == "")
{
this.database.ready = false
return
}
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
{
this.database.connect(File("/storage/emulated/0/Syncthing/GPX/trkpt_${PreferencesHelper.load_device_id()}.db"))
this.database.connect(File(folder + "/trkpt_${PreferencesHelper.load_device_id()}.db"))
this.load_homepoints()
}
else
@ -77,6 +88,7 @@ class Trackbook(): Application() {
}
this.call_database_changed_listeners()
}
fun load_homepoints()
{
Log.i("VOUSSOIR", "Trackbook.load_homepoints")

View File

@ -59,6 +59,7 @@ class TrackerService: Service(), SensorEventListener
var commitInterval: Int = Keys.COMMIT_INTERVAL
var currentBestLocation: Location = getDefaultLocation()
var lastCommit: Date = Keys.DEFAULT_DATE
var location_min_time_ms: Long = 0
var stepCountOffset: Float = 0f
lateinit var track: Track
var gpsLocationListenerRegistered: Boolean = false
@ -98,7 +99,7 @@ class TrackerService: Service(), SensorEventListener
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0,
location_min_time_ms,
0f,
gpsLocationListener,
)

View File

@ -1,9 +1,18 @@
package org.y20k.trackbook.helpers
import android.annotation.TargetApi
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import java.lang.Math.abs
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.*
import kotlin.random.Random
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
private val RNG = SecureRandom()

View File

@ -0,0 +1,144 @@
// Thank you @asifmujteba!
// https://gist.github.com/asifmujteba/d89ba9074bc941de1eaa
import android.annotation.TargetApi
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
@TargetApi(Build.VERSION_CODES.KITKAT)
fun get_path_from_uri(context: Context, uri: Uri): String?
{
val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri))
{
// ExternalStorageProvider
if (isExternalStorageDocument(uri))
{
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true))
{
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
// TODO handle non-primary volumes
}
else if (isDownloadsDocument(uri))
{
val id = DocumentsContract.getDocumentId(uri)
val contentUri: Uri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
else if (isMediaDocument(uri))
{
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
if ("image" == type)
{
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
else if ("video" == type)
{
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
else if ("audio" == type)
{
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(
split[1]
)
return getDataColumn(context, contentUri, selection, selectionArgs)
}
}
else if ("content".equals(uri.getScheme(), ignoreCase = true))
{
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.getLastPathSegment()
else getDataColumn(context,
uri,
null,
null)
}
else if ("file".equals(uri.getScheme(), ignoreCase = true))
{
return uri.getPath()
}
return null
}
fun getDataColumn(
context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?,
): String?
{
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try
{
cursor = context.getContentResolver().query(uri!!, projection, selection, selectionArgs,
null)
if (cursor != null && cursor.moveToFirst())
{
val index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally
{
if (cursor != null) cursor.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
fun isExternalStorageDocument(uri: Uri): Boolean
{
return "com.android.externalstorage.documents" == uri.getAuthority()
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
fun isDownloadsDocument(uri: Uri): Boolean
{
return "com.android.providers.downloads.documents" == uri.getAuthority()
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
fun isMediaDocument(uri: Uri): Boolean
{
return "com.android.providers.media.documents" == uri.getAuthority()
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
fun isGooglePhotosUri(uri: Uri): Boolean
{
return "com.google.android.apps.photos.content" == uri.getAuthority()
}

View File

@ -71,7 +71,7 @@ fun createTrackOverlay(context: Context, map_view: MapView, track: Track, tracki
.setPointStyle(style)
.setRadius(6F * scalingFactor) // radius is set in px - scaling factor makes that display density independent (= dp)
.setIsClickable(true)
.setCellSize(15) // 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.
.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.
val overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
map_view.overlays.add(overlay)
}

View File

@ -56,6 +56,16 @@ object PreferencesHelper {
return v
}
fun load_database_folder(): String
{
return sharedPreferences.getString(Keys.PREF_DATABASE_DIRECTORY, "") ?: ""
}
fun save_database_folder(path: String)
{
sharedPreferences.edit { putString(Keys.PREF_DATABASE_DIRECTORY, path) }
}
fun loadZoomLevel(): Double
{
return sharedPreferences.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL)

View File

@ -0,0 +1,16 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="@color/location_button_background"
android:pathData="M 1.98 11 L 22.02 11 L 22.02 13 L 1.98 13 Z"/>
<path
android:name="path_1"
android:fillColor="@color/location_button_background"
android:pathData="M 11 22.02 L 11 1.98 L 13 1.98 L 13 22.02 Z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="@color/location_button_background"
android:pathData="M 1.98 11 L 22.02 11 L 22.02 13 L 1.98 13 Z"/>
</vector>

View File

@ -58,6 +58,36 @@
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/location_button_icon" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/zoom_out_button"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/descr_button_zoom_out"
android:src="@drawable/ic_zoom_out_24dp"
app:backgroundTint="@color/location_button_background"
app:fabSize="mini"
app:layout_constraintBottom_toTopOf="@+id/location_button"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/location_button_icon" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/zoom_in_button"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/descr_button_zoom_in"
android:src="@drawable/ic_zoom_in_24dp"
app:backgroundTint="@color/location_button_background"
app:fabSize="mini"
app:layout_constraintBottom_toTopOf="@+id/zoom_out_button"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/location_button_icon" />
<!-- GROUPS -->
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -23,6 +23,7 @@
<string name="button_pause">Stop</string>
<string name="button_save">Save</string>
<string name="button_start">Record</string>
<string name="button_not_ready">Database not set up.</string>
<!-- Dialogs -->
<string name="dialog_delete_current_recording_message">Discard current recording?</string>
<string name="dialog_delete_current_recording_button_discard">Discard</string>
@ -84,7 +85,8 @@
<string name="pref_altitude_smoothing_value_summary" translatable="false">Number of waypoints used to smooth the elevation curve.</string>
<string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string>
<string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string>
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices.</string>
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices:</string>
<string name="pref_database_folder_summary">Directory to contain your database file:</string>
<string name="pref_auto_export_interval_title">Auto Export Interval</string>
<string name="pref_device_id">Device ID</string>
<string name="pref_advanced_title">Advanced</string>
@ -117,6 +119,8 @@
<!-- Descriptions -->
<string name="descr_button_delete">Discard recording</string>
<string name="descr_button_location">Center on current location</string>
<string name="descr_button_zoom_out">Zoom out</string>
<string name="descr_button_zoom_in">Zoom in</string>
<string name="descr_button_pause">Stop recording</string>
<string name="descr_button_resume">Resume recording</string>
<string name="descr_button_save">Save recording</string>