From 172ca703a985641f7bf14b81c0d367b209dedba1 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Sat, 11 Mar 2023 12:30:24 -0800 Subject: [PATCH] checkpoint --- app/src/main/java/org/y20k/trackbook/Keys.kt | 2 + .../java/org/y20k/trackbook/MainActivity.kt | 6 + .../java/org/y20k/trackbook/MapFragment.kt | 16 +- .../org/y20k/trackbook/SettingsFragment.kt | 66 +++++++- .../main/java/org/y20k/trackbook/Trackbook.kt | 14 +- .../java/org/y20k/trackbook/TrackerService.kt | 3 +- .../main/java/org/y20k/trackbook/functions.kt | 11 +- .../org/y20k/trackbook/get_path_from_uri.kt | 144 ++++++++++++++++++ .../trackbook/helpers/MapOverlayHelper.kt | 2 +- .../trackbook/helpers/PreferencesHelper.kt | 10 ++ app/src/main/res/drawable/ic_zoom_in_24dp.xml | 16 ++ .../main/res/drawable/ic_zoom_out_24dp.xml | 12 ++ app/src/main/res/layout/fragment_map.xml | 30 ++++ app/src/main/res/values/strings.xml | 6 +- 14 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt create mode 100644 app/src/main/res/drawable/ic_zoom_in_24dp.xml create mode 100644 app/src/main/res/drawable/ic_zoom_out_24dp.xml diff --git a/app/src/main/java/org/y20k/trackbook/Keys.kt b/app/src/main/java/org/y20k/trackbook/Keys.kt index e9b59e4..d202ff5 100644 --- a/app/src/main/java/org/y20k/trackbook/Keys.kt +++ b/app/src/main/java/org/y20k/trackbook/Keys.kt @@ -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 diff --git a/app/src/main/java/org/y20k/trackbook/MainActivity.kt b/app/src/main/java/org/y20k/trackbook/MainActivity.kt index 6802a6c..cc53687 100644 --- a/app/src/main/java/org/y20k/trackbook/MainActivity.kt +++ b/app/src/main/java/org/y20k/trackbook/MainActivity.kt @@ -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() + } } } diff --git a/app/src/main/java/org/y20k/trackbook/MapFragment.kt b/app/src/main/java/org/y20k/trackbook/MapFragment.kt index b16840c..206cc8a 100644 --- a/app/src/main/java/org/y20k/trackbook/MapFragment.kt +++ b/app/src/main/java/org/y20k/trackbook/MapFragment.kt @@ -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() @@ -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) diff --git a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt index 10b742b..fc48de1 100644 --- a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt +++ b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt @@ -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) diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook.kt b/app/src/main/java/org/y20k/trackbook/Trackbook.kt index 0a241e5..08b5952 100644 --- a/app/src/main/java/org/y20k/trackbook/Trackbook.kt +++ b/app/src/main/java/org/y20k/trackbook/Trackbook.kt @@ -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") diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt index b2f2446..9e03629 100644 --- a/app/src/main/java/org/y20k/trackbook/TrackerService.kt +++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt @@ -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, ) diff --git a/app/src/main/java/org/y20k/trackbook/functions.kt b/app/src/main/java/org/y20k/trackbook/functions.kt index 85fcb37..1a80b5b 100644 --- a/app/src/main/java/org/y20k/trackbook/functions.kt +++ b/app/src/main/java/org/y20k/trackbook/functions.kt @@ -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() diff --git a/app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt b/app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt new file mode 100644 index 0000000..5acf16b --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt @@ -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? +{ + 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() +} \ No newline at end of file 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 22a6a68..916f930 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/MapOverlayHelper.kt @@ -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) } diff --git a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt index 5f05132..6a0a808 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/res/drawable/ic_zoom_in_24dp.xml b/app/src/main/res/drawable/ic_zoom_in_24dp.xml new file mode 100644 index 0000000..7aa8d2c --- /dev/null +++ b/app/src/main/res/drawable/ic_zoom_in_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_zoom_out_24dp.xml b/app/src/main/res/drawable/ic_zoom_out_24dp.xml new file mode 100644 index 0000000..ee5a796 --- /dev/null +++ b/app/src/main/res/drawable/ic_zoom_out_24dp.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index df94f88..98ffeae 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -58,6 +58,36 @@ app:layout_constraintEnd_toEndOf="parent" app:tint="@color/location_button_icon" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fce762..25a1e45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Stop Save Record + Database not set up. Discard current recording? Discard @@ -84,7 +85,8 @@ Number of waypoints used to smooth the elevation curve. Altitude Smoothing Automatically export GPX file after this many hours. - A unique ID to distinguish tracks recorded across multiple devices. + A unique ID to distinguish tracks recorded across multiple devices: + Directory to contain your database file: Auto Export Interval Device ID Advanced @@ -117,6 +119,8 @@ Discard recording Center on current location + Zoom out + Zoom in Stop recording Resume recording Save recording