checkpoint
This commit is contained in:
		
							parent
							
								
									ffd5fb6af3
								
							
						
					
					
						commit
						172ca703a9
					
				
					 14 changed files with 325 additions and 13 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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, | ||||
|         ) | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
							
								
								
									
										144
									
								
								app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								app/src/main/java/org/y20k/trackbook/get_path_from_uri.kt
									
									
									
									
									
										Normal 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() | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										16
									
								
								app/src/main/res/drawable/ic_zoom_in_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/res/drawable/ic_zoom_in_24dp.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										12
									
								
								app/src/main/res/drawable/ic_zoom_out_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/res/drawable/ic_zoom_out_24dp.xml
									
									
									
									
									
										Normal 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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue