Merge pull request #76 from TacoTheDank/master

Code cleanup, updates, and typo fixes
master
y20k 2020-08-03 09:49:50 +02:00 committed by GitHub
commit e200aec770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 1667 additions and 955 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

View File

@ -2,7 +2,7 @@ AUTHORS
=======
### Development
Trackbook is designed, developed and maintained by: [y20k](https://github.com/y20k)
Trackbook is designed, developed, and maintained by: [y20k](https://github.com/y20k)
### Translations
Chinese version: [yzqzss](https://github.com/yzqzss) | [weblate version history](https://hosted.weblate.org/changes/?lang=zh_HANS-CN&project=trackbook)

View File

@ -2,18 +2,20 @@ How to contribute to Trackbook
==============================
### Report a bug or suggest a new feature
Bugs and new features are being discussed on the GitHub [Issue Tracker](https://github.com/y20k/trackbook/issues). The issue "[Previous discussions and feature requests](https://github.com/y20k/trackbook/issues/57)" lists some of the features that were rejected - either because they did not fit conceptually or because I could not figure out how to implement them.
Bugs and new features are being discussed on the GitHub [Issue Tracker](https://github.com/y20k/trackbook/issues).
The issue "[Previous discussions and feature requests](https://github.com/y20k/trackbook/issues/57)" lists some of the features that were rejected - either because they did not fit conceptually or because I could not figure out how to implement them.
### Help with translations
The translations are managed on [Weblate](https://hosted.weblate.org/projects/trackbook/strings/). Help is much appreciated.
The translations are managed on [Weblate](https://hosted.weblate.org/projects/trackbook/strings/).
Help is much appreciated.
### Submit your own solutions
Help is very welcome. Be it in the form of code, or artwork, or enhancements to the website, or tutorial videos, or whatever.
Help is very welcome, be it in the form of code, artwork, enhancements to the website, tutorial videos, or whatever.
**But please** suggest new features or enhancements in advance on the [Issue Tracker](https://github.com/y20k/trackbook/issues) before implementing them.
### Suggested issues to tackle
[#19](https://github.com/y20k/trackbook/issues/19)
### Credit for your contributions
Contributors - like the main translators for a certain language - are listed as co-authors of this project in [AUTHORS.md](https://github.com/y20k/trackbook/blob/master/AUTHORS.md). Bonus: If you are on this list, you are automatically eligible for a free German beverage.
To be redeemed in Stuttgart.
Contributors - like the main translators for a certain language - are listed as co-authors of this project in [AUTHORS.md](https://github.com/y20k/trackbook/blob/master/AUTHORS.md).
Bonus: If you are on this list, you are automatically eligible for a free German beverage (to be redeemed in Stuttgart).

View File

@ -1,57 +1,96 @@
README
======
# Trackbook - Android Movement Recorder
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" width="192" />
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png"
width="192" />
**Version 2.0.x ("Echoes")**
Trackbook is a bare bones app for recording your movements. Trackbook is great for hiking, vacation or workout. Once started it traces your movements on a map. The map data is provided by [OpenStreetMap (OSM)](https://www.openstreetmap.org/).
Trackbook is a bare-bones app for recording your movements.
Trackbook is great for hiking, vacationing, or working out.
Once started, it traces your movements on a map.
The map data is provided by [OpenStreetMap (OSM)](https://www.openstreetmap.org/).
Trackbook is free software.
It is published under the [MIT open-source license](https://opensource.org/licenses/MIT).
Trackbook uses [osmdroid](https://github.com/osmdroid/osmdroid) to display the map, which is also free software published under the [Apache License](https://github.com/osmdroid/osmdroid/blob/master/LICENSE).
Want to help? Please check out the notes in [CONTRIBUTE.md](https://github.com/y20k/trackbook/blob/master/CONTRIBUTE.md) first.
Trackbook is free software. It is published under the [MIT open source license](https://opensource.org/licenses/MIT). Trackbook uses [osmdroid](https://github.com/osmdroid/osmdroid) to display the map, which is also free software published under the [Apache License](https://github.com/osmdroid/osmdroid/blob/master/LICENSE). Want to help? Please check out the notes in [CONTRIBUTE.md](https://github.com/y20k/trackbook/blob/master/CONTRIBUTE.md) first.
## Install Trackbook
You can install it via Google Play and F-Droid - or you can go and grab the latest APK on [GitHub](https://github.com/y20k/trackbook/releases).
[<img src="https://play.google.com/intl/de_de/badges/images/generic/en_badge_web_generic.png" width="192">](https://play.google.com/store/apps/details?id=org.y20k.trackbook)
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
width="192">](https://play.google.com/store/apps/details?id=org.y20k.trackbook)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
width="192">](https://f-droid.org/packages/org.y20k.trackbook/)
[<img src="https://cloud.githubusercontent.com/assets/9103935/14702535/45f6326a-07ab-11e6-9256-469c1dd51c22.png" width="192">](https://f-droid.org/repository/browse/?fdid=org.y20k.trackbook)
## Good To Know
### Start Recording via Quick Settings Tile
[<img src="https://user-images.githubusercontent.com/9103935/74753187-09a75f00-5270-11ea-82de-18c5b8737e2b.png" width="320">](https://user-images.githubusercontent.com/9103935/74753187-09a75f00-5270-11ea-82de-18c5b8737e2b.png)<br />
You can start a recording without opening Trackbook. Just pull down the System's Quick Settings and tap on the Start Recording tile. You need to manually add Trackbook's Recording tile to Quick Settings first. You can find information on customizing Quick Settings [here](https://support.google.com/android/answer/9083864?hl=en) and [here](https://www.xda-developers.com/get-custom-quick-settings-tiles/)
<img src="https://user-images.githubusercontent.com/9103935/74753187-09a75f00-5270-11ea-82de-18c5b8737e2b.png"
width="320" />
<br/>
You can start a recording without opening Trackbook.
Just pull down the System's Quick Settings and tap on the "Start Recording" tile.
You'll need to manually add Trackbook's Recording tile to Quick Settings first.
You can find information on customizing Quick Settings [here](https://support.google.com/android/answer/9083864) and [here](https://www.xda-developers.com/get-custom-quick-settings-tiles/).
### Save Recordings as GPX
Recordings can be exported as GPX ([GPS Exchange Format](https://en.wikipedia.org/wiki/GPS_Exchange_Format)). Tap on the save button in the lower right corner of a previously recorded track.
Recordings can be exported as GPX ([GPS Exchange Format](https://en.wikipedia.org/wiki/GPS_Exchange_Format)).
Tap on the save button in the lower-right corner of a previously recorded track.
### Copy GPX Files Manually
Trackbook automatically generates GPX files for every recording. You can find them in the folder `/Android/data/org.y20k.trackbook/files/gpx/` on your device's storage.
Trackbook automatically generates GPX files for every recording.
You can find them in the folder `/Android/data/org.y20k.trackbook/files/gpx/` on your device's storage.
### How Does Trackbook Measure Distance?
### How does Trackbook measure distance?
Trackbook calculates the distance between markers and adds them up.
### How Does Trackbook Measure Altitude?
Many devices have altitude sensors (of varying accuracy). Trackbook compares the altitude of each new marker with the previously stored altitude. The difference is added to either the uphill or downhill elevation value.
### How does Trackbook measure altitude?
Many devices have altitude sensors (of varying accuracy).
Trackbook compares the altitude of each new marker with the previously stored altitude.
The difference is added to either the uphill or downhill elevation value.
### What Does Accuracy Threshold Mean?
Every location fix, that Trackbook receives, is associated with an accuracy estimate. You can look up, how Android defines accuracy, in the [developer documentation](https://developer.android.com/reference/kotlin/android/location/Location.html#getaccuracy). `Accuracy Threshold` is the value, from which on location fixes are rejected. It can be adjusted in Trackbook's settings. You can increase the value, if your recordings tend to be incomplete. Trackbook will then also record less accurate location fixes.
### What does "accuracy threshold" mean?
Every location fix that Trackbook receives is associated with an accuracy estimate.
You can look up how Android defines accuracy in the [developer documentation](https://developer.android.com/reference/kotlin/android/location/Location.html#getaccuracy).
`Accuracy Threshold` is the value from which location fixes are rejected.
It can be adjusted in Trackbook's settings.
You can increase the value if your recordings tend to be incomplete.
Trackbook will then also record less accurate location fixes.
## Where are my old recordings?
The F-Droid version of Trackbook features an auto-importer for old recordings. Sadly I was not able to implement the auto-importer for the Play Store version of Trackbook due to SDK requirements / restrictions. That is partly my fault and I am very sorry. There is a (quite complicated) solution to get back your old recordings. Please head over to the [Wiki](https://github.com/y20k/trackbook/wiki) to find out how.
The F-Droid version of Trackbook features an auto-importer for old recordings.
Sadly I was not able to implement the auto-importer for the Play Store version of Trackbook due to SDK requirements / restrictions.
That is partly my fault and I am very sorry.
There is a (quite complicated) solution to get back your old recordings.
Please head over to the [Wiki](https://github.com/y20k/trackbook/wiki) to find out how.
## A Word on Privacy
Trackbook begins to store location data on device as soon a user presses the record button. Those recordings are stored in the directory `/Android/data/org.y20k.trackbook/files/`. They never leave the device. There is no web-service backing Trackbook.
## A word on privacy
Trackbook begins to store location data on a device as soon as a user presses the record button.
Those recordings are stored in the directory `/Android/data/org.y20k.trackbook/files/`.
They never leave the device.
There is no web-service backing Trackbook.
Trackbook does not use Google Play Services to get its location data. It will however try to use data from the [NETWORK_PROVIDER](https://developer.android.com/reference/android/location/LocationManager#NETWORK_PROVIDER) on your device to augment the location data it received via GPS. The NETWORK_PROVIDER is a system-wide service, that Trackbook has no control over. This service will usually query an online database for the location of cell towers or Wi-Fi access points a device can see. You can prevent those kinds of requests on your device, if you set the location preferences system-wide to `Device Only`. Additionally Trackbook offers a `Restrict to GPS` setting, that deactivates the NETWORK_PROVIDER just within the app.
Trackbook does not use Google Play Services to get its location data.
It will, however, try to use data from the [NETWORK_PROVIDER](https://developer.android.com/reference/android/location/LocationManager#NETWORK_PROVIDER) on your device to augment the location data it received via GPS.
The NETWORK_PROVIDER is a system-wide service that Trackbook has no control over.
This service will usually query an online database for the location of cell towers or Wi-Fi access points a device can see.
You can prevent those kinds of requests on your device if you set the location preferences system-wide to `Device Only`.
Additionally, Trackbook offers a `Restrict to GPS` setting that deactivates the NETWORK_PROVIDER just within the app.
## Screenshots (v2.0)
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/01-map-recording-active.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/01-map-recording-active.png)
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/02-map-context-menu.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/02-map-context-menu.png)
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/01-map-recording-active.png"
width="240" />
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/02-map-context-menu.png"
width="240" />
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/03-track-list.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/03-track-list.png)
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/04-track.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/04-track.png)
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/03-track-list.png"
width="240" />
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/04-track.png"
width="240" />
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/05-settings.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/05-settings.png)
[<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/06-quick-settings-tile.png" width="240">](https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/06-quick-settings-tile.png)
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/05-settings.png"
width="240" />
<img src="https://raw.githubusercontent.com/y20k/trackbook/master/metadata/en-US/phoneScreenshots/06-quick-settings-tile.png"
width="240" />

View File

@ -5,7 +5,7 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
compileSdkVersion 29
// buildToolsVersion is optional because the plugin uses a recommended version by default
buildToolsVersion "29.0.3"
defaultConfig {
applicationId 'org.y20k.trackbook'
@ -25,63 +25,46 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions{
disable 'MissingTranslation'
lintOptions {
disable 'MissingTranslation', 'GoogleAppIndexingWarning'
}
packagingOptions {
exclude 'META-INF/*'
}
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
// Comment out the below lines if you do not need to test resource shrinking
// minifyEnabled true
// shrinkResources true
// proguardFiles getDefaultProguardFile(
// 'proguard-android-optimize.txt'),
// 'proguard-rules.pro'
applicationIdSuffix ".debug"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
// AndroidX
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation "androidx.preference:preference-ktx:1.1.1"
implementation 'androidx.core:core-ktx:1.3.1'
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.0-rc01'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
// Gson
implementation 'com.google.code.gson:gson:2.8.6'
implementation "com.google.android.material:material:1.2.0-beta01"
implementation "com.google.code.gson:gson:2.8.6"
implementation "org.osmdroid:osmdroid-android:6.1.8"
// OpenStreetMap
implementation 'org.osmdroid:osmdroid-android:6.1.8'
}
androidExtensions {

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.y20k.trackbook">
package="org.y20k.trackbook">
<!-- USE GPS AND NETWORK - EXCLUDE NON-GPS DEVICES -->
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature android:name="android.hardware.location.network" />
<!-- NORMAL PERMISSIONS, automatically granted -->
@ -19,27 +20,26 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".Trackbook"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
android:name=".Trackbook"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- MAIN ACTIVITY -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- TRACKER SERVICE -->
<service
android:name=".TrackerService"
android:foregroundServiceType="location"
android:exported="false">
android:exported="false"
android:foregroundServiceType="location">
<intent-filter>
<action android:name="org.y20k.trackbook.action.START" />
<action android:name="org.y20k.trackbook.action.STOP" />
@ -50,8 +50,8 @@
<!-- TRACKING TOGGLE SERVICE SYSTEM QUICK SETTINGS -->
<service
android:name=".TrackingToggleTileService"
android:label="@string/quick_settings_tile_title_default"
android:icon="@drawable/ic_notification_icon_small_24dp"
android:label="@string/quick_settings_tile_title_default"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
@ -60,13 +60,13 @@
<!-- FILE PROVIDER GPX -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

View File

@ -44,8 +44,9 @@ object Keys {
const val ARG_TRACK_ID: String = "ArgTrackId"
// preferences
const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_38" // increment to current app version code to trigger housekeeping that runs only once
const val PREF_THEME_SELECTION: String= "prefThemeSelection"
const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY =
"ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_38" // increment to current app version code to trigger housekeeping that runs only once
const val PREF_THEME_SELECTION: String = "prefThemeSelection"
const val PREF_CURRENT_BEST_LOCATION_PROVIDER: String = "prefCurrentBestLocationProvider"
const val PREF_CURRENT_BEST_LOCATION_LATITUDE: String = "prefCurrentBestLocationLatitude"
const val PREF_CURRENT_BEST_LOCATION_LONGITUDE: String = "prefCurrentBestLocationLongitude"
@ -77,7 +78,7 @@ object Keys {
const val DIALOG_EMPTY_PAYLOAD_INT: Int = -1
// folder names
const val FOLDER_TEMP: String = "temp"
const val FOLDER_TEMP: String = "temp"
const val FOLDER_TRACKS: String = "tracks"
const val FOLDER_GPX: String = "gpx"
@ -95,21 +96,29 @@ object Keys {
const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100" // --> Date(0)
const val ONE_HOUR_IN_MILLISECONDS: Int = 3600000
const val EMPTY_STRING_RESOURCE: Int = 0
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1000L // 1 second in milliseconds
const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 15000L // 15 seconds in milliseconds
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 120000L // 2 minutes in milliseconds
const val STOP_OVER_THRESHOLD: Long = 300000L // 5 minutes in milliseconds
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long =
1000L // 1 second in milliseconds
const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long =
15000L // 15 seconds in milliseconds
const val SIGNIFICANT_TIME_DIFFERENCE: Long =
120000L // 2 minutes in milliseconds
const val STOP_OVER_THRESHOLD: Long =
300000L // 5 minutes in milliseconds
const val IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0 // 250 km/h
const val DEFAULT_LATITUDE: Double = 71.172500 // latitude Nordkapp, Norway
const val DEFAULT_LONGITUDE: Double = 25.784444 // longitude Nordkapp, Norway
const val DEFAULT_LATITUDE: Double =
71.172500 // latitude Nordkapp, Norway
const val DEFAULT_LONGITUDE: Double =
25.784444 // longitude Nordkapp, Norway
const val DEFAULT_ACCURACY: Float = 300f // in meters
const val DEFAULT_ALTITUDE: Double = 0.0
const val DEFAULT_TIME: Long = 0L
const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60000000000L // one minute in nanoseconds
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long =
60000000000L // one minute in nanoseconds
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
const val DEFAULT_ZOOM_LEVEL: Double = 16.0
const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded
const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD =
10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded
const val REQUEST_CODE_FOREGROUND = 42
// requests

View File

@ -69,7 +69,8 @@ class MainActivity : AppCompatActivity() {
// set up views
setContentView(R.layout.activity_main)
navHostFragment = supportFragmentManager.findFragmentById(R.id.main_container) as NavHostFragment
navHostFragment =
supportFragmentManager.findFragmentById(R.id.main_container) as NavHostFragment
bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation_view)
bottomNavigationView.setupWithNavController(navController = navHostFragment.navController)
@ -77,12 +78,13 @@ class MainActivity : AppCompatActivity() {
navHostFragment.navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.fragment_track -> {
runOnUiThread( Runnable() {
run(){
runOnUiThread {
run {
// mark menu item "Tracks" as checked
bottomNavigationView.menu.findItem(R.id.tracklist_fragment).setChecked(true)
bottomNavigationView.menu.findItem(R.id.tracklist_fragment)
.setChecked(true)
}
})
}
}
else -> {
// do nothing
@ -91,7 +93,8 @@ class MainActivity : AppCompatActivity() {
}
// register listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this as Context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(this as Context)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
@ -99,20 +102,22 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
// unregister listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this as Context).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(this as Context)
.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
Keys.PREF_THEME_SELECTION -> {
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection(this@MainActivity))
private val sharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
Keys.PREF_THEME_SELECTION -> {
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection(this@MainActivity))
}
}
}
}
/*
* End of declaration
*/

View File

@ -73,9 +73,20 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/* Overrides onStop from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// initialize layout
layout = MapFragmentLayoutHolder(activity as Context, this as MapOverlay.MarkerListener, inflater, container, currentBestLocation, trackingState)
layout = MapFragmentLayoutHolder(
activity as Context,
this as MapOverlay.MarkerListener,
inflater,
container,
currentBestLocation,
trackingState
)
// set up buttons
layout.currentLocationButton.setOnClickListener {
@ -104,11 +115,22 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
override fun onStart() {
super.onStart()
// request location permission if denied
if (ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) {
this.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), Keys.REQUEST_CODE_FOREGROUND)
if (ContextCompat.checkSelfPermission(
activity as Context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_DENIED
) {
this.requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
Keys.REQUEST_CODE_FOREGROUND
)
}
// bind to TrackerService
activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE)
activity?.bindService(
Intent(activity, TrackerService::class.java),
connection,
Context.BIND_AUTO_CREATE
)
}
@ -142,14 +164,22 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/* Overrides onRequestPermissionsResult from Fragment */
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
Keys.REQUEST_CODE_FOREGROUND -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
// permission was granted - re-bind service
activity?.unbindService(connection)
activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE)
activity?.bindService(
Intent(activity, TrackerService::class.java),
connection,
Context.BIND_AUTO_CREATE
)
LogHelper.i(TAG, "Request result: Location permission has been granted.")
} else {
// permission denied - unbind service
@ -163,7 +193,12 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
super.onYesNoDialog(type, dialogResult, payload, payloadString)
when (type) {
Keys.DIALOG_EMPTY_RECORDING -> {
@ -218,11 +253,18 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/* Saves track - shows dialog, if recording is still empty */
private fun saveTrack() {
if (track.wayPoints.isEmpty()) {
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(activity as Context, type = Keys.DIALOG_EMPTY_RECORDING, title = R.string.dialog_error_empty_recording_title, message = R.string.dialog_error_empty_recording_message, yesButton = R.string.dialog_error_empty_recording_action_resume)
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
activity as Context,
type = Keys.DIALOG_EMPTY_RECORDING,
title = R.string.dialog_error_empty_recording_title,
message = R.string.dialog_error_empty_recording_message,
yesButton = R.string.dialog_error_empty_recording_action_resume
)
} else {
GlobalScope.launch {
// step 1: create and store filenames for json and gpx files
track.trackUriString = FileHelper.getTrackFileUri(activity as Context, track).toString()
track.trackUriString =
FileHelper.getTrackFileUri(activity as Context, track).toString()
track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString()
// step 2: save track
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
@ -239,7 +281,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/* Opens a track in TrackFragment */
private fun openTrack(tracklistElement: TracklistElement) {
val bundle: Bundle = Bundle()
val bundle = Bundle()
bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name)
bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString)
bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString)
@ -251,16 +293,17 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
if (activity != null) {
trackingState = PreferencesHelper.loadTrackingState(activity as Context)
layout.updateRecordingButton(trackingState)
private val sharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
if (activity != null) {
trackingState = PreferencesHelper.loadTrackingState(activity as Context)
layout.updateRecordingButton(trackingState)
}
}
}
}
}
/*
* End of declaration
*/
@ -279,15 +322,18 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
trackingState = trackerService.trackingState
layout.updateRecordingButton(trackingState)
// register listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(activity as Context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(activity as Context)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
// start listening for location updates
handler.removeCallbacks(periodicLocationRequestRunnable)
handler.postDelayed(periodicLocationRequestRunnable, 0)
}
override fun onServiceDisconnected(arg0: ComponentName) {
bound = false
// unregister listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(activity as Context).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(activity as Context)
.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
// stop receiving location updates
handler.removeCallbacks(periodicLocationRequestRunnable)
}
@ -312,7 +358,9 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.Mark
layout.markCurrentPosition(currentBestLocation, trackingState)
layout.overlayCurrentTrack(track, trackingState)
// center map, if it had not been dragged/zoomed before
if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)}
if (!layout.userInteraction) {
layout.centerMap(currentBestLocation, true)
}
// show error snackbar if necessary
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
// use the handler to start runnable again after specified delay

View File

@ -63,7 +63,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val screen = preferenceManager.createPreferenceScreen(context)
// set up "Restrict to GPS" preference
val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
val preferenceGpsOnly = SwitchPreferenceCompat(activity as Context)
preferenceGpsOnly.title = getString(R.string.pref_gps_only_title)
preferenceGpsOnly.setIcon(R.drawable.ic_gps_24dp)
preferenceGpsOnly.key = Keys.PREF_GPS_ONLY
@ -72,26 +72,41 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceGpsOnly.setDefaultValue(false)
// set up "Use Imperial Measurements" preference
val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
preferenceImperialMeasurementUnits.title = getString(R.string.pref_imperial_measurement_units_title)
val preferenceImperialMeasurementUnits = SwitchPreferenceCompat(activity as Context)
preferenceImperialMeasurementUnits.title =
getString(R.string.pref_imperial_measurement_units_title)
preferenceImperialMeasurementUnits.setIcon(R.drawable.ic_square_foot_24px)
preferenceImperialMeasurementUnits.key = Keys.PREF_USE_IMPERIAL_UNITS
preferenceImperialMeasurementUnits.summaryOn = getString(R.string.pref_imperial_measurement_units_summary_imperial)
preferenceImperialMeasurementUnits.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric)
preferenceImperialMeasurementUnits.summaryOn =
getString(R.string.pref_imperial_measurement_units_summary_imperial)
preferenceImperialMeasurementUnits.summaryOff =
getString(R.string.pref_imperial_measurement_units_summary_metric)
preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits())
// set up "App Theme" preference
val preferenceThemeSelection: ListPreference = ListPreference(activity as Context)
val preferenceThemeSelection = ListPreference(activity as Context)
preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title)
preferenceThemeSelection.setIcon(R.drawable.ic_smartphone_24dp)
preferenceThemeSelection.key = Keys.PREF_THEME_SELECTION
preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${AppThemeHelper.getCurrentTheme(activity as Context)}"
preferenceThemeSelection.entries = arrayOf(getString(R.string.pref_theme_selection_mode_device_default), getString(R.string.pref_theme_selection_mode_light), getString(R.string.pref_theme_selection_mode_dark))
preferenceThemeSelection.entryValues = arrayOf(Keys.STATE_THEME_FOLLOW_SYSTEM, Keys.STATE_THEME_LIGHT_MODE, Keys.STATE_THEME_DARK_MODE)
preferenceThemeSelection.summary =
"${getString(R.string.pref_theme_selection_summary)} ${AppThemeHelper.getCurrentTheme(
activity as Context
)}"
preferenceThemeSelection.entries = arrayOf(
getString(R.string.pref_theme_selection_mode_device_default),
getString(R.string.pref_theme_selection_mode_light),
getString(R.string.pref_theme_selection_mode_dark)
)
preferenceThemeSelection.entryValues = arrayOf(
Keys.STATE_THEME_FOLLOW_SYSTEM,
Keys.STATE_THEME_LIGHT_MODE,
Keys.STATE_THEME_DARK_MODE
)
preferenceThemeSelection.setOnPreferenceChangeListener { preference, newValue ->
if (preference is ListPreference) {
val index: Int = preference.entryValues.indexOf(newValue)
preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${preference.entries.get(index)}"
preferenceThemeSelection.summary =
"${getString(R.string.pref_theme_selection_summary)} ${preference.entries[index]}"
return@setOnPreferenceChangeListener true
} else {
return@setOnPreferenceChangeListener false
@ -99,17 +114,22 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
}
// set up "Delete Non-Starred" preference
val preferenceDeleteNonStarred: Preference = Preference(activity as Context)
val preferenceDeleteNonStarred = Preference(activity as Context)
preferenceDeleteNonStarred.title = getString(R.string.pref_delete_non_starred_title)
preferenceDeleteNonStarred.setIcon(R.drawable.ic_delete_24dp)
preferenceDeleteNonStarred.summary = getString(R.string.pref_delete_non_starred_summary)
preferenceDeleteNonStarred.setOnPreferenceClickListener{
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_NON_STARRED, message = R.string.dialog_yes_no_message_delete_non_starred, yesButton = R.string.dialog_yes_no_positive_button_delete_non_starred)
preferenceDeleteNonStarred.setOnPreferenceClickListener {
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
context = activity as Context,
type = Keys.DIALOG_DELETE_NON_STARRED,
message = R.string.dialog_yes_no_message_delete_non_starred,
yesButton = R.string.dialog_yes_no_positive_button_delete_non_starred
)
return@setOnPreferenceClickListener true
}
// set up "Accuracy Threshold" preference
val preferenceAccuracyThreshold: SeekBarPreference = SeekBarPreference(activity as Context)
val preferenceAccuracyThreshold = SeekBarPreference(activity as Context)
preferenceAccuracyThreshold.title = getString(R.string.pref_accuracy_threshold_title)
preferenceAccuracyThreshold.setIcon(R.drawable.ic_timeline_24dp)
preferenceAccuracyThreshold.key = Keys.PREF_LOCATION_ACCURACY_THRESHOLD
@ -119,32 +139,39 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceAccuracyThreshold.setDefaultValue(Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY)
// set up "Reset" preference
val preferenceResetAdvanced: Preference = Preference(activity as Context)
val preferenceResetAdvanced = Preference(activity as Context)
preferenceResetAdvanced.title = getString(R.string.pref_reset_advanced_title)
preferenceResetAdvanced.setIcon(R.drawable.ic_undo_24dp)
preferenceResetAdvanced.summary = getString(R.string.pref_reset_advanced_summary)
preferenceResetAdvanced.setOnPreferenceClickListener{
preferenceResetAdvanced.setOnPreferenceClickListener {
preferenceAccuracyThreshold.value = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
return@setOnPreferenceClickListener true
}
// set up "App Version" preference
val preferenceAppVersion: Preference = Preference(context)
val preferenceAppVersion = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
preferenceAppVersion.setIcon(R.drawable.ic_info_24dp)
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString(
R.string.app_version_name)})"
preferenceAppVersion.summary =
"${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString(
R.string.app_version_name
)})"
preferenceAppVersion.setOnPreferenceClickListener {
// copy to clipboard
val clip: ClipData = ClipData.newPlainText("simple text", preferenceAppVersion.summary)
val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val cm: ClipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(clip)
Toast.makeText(activity as Context, R.string.toast_message_copied_to_clipboard, Toast.LENGTH_LONG).show()
Toast.makeText(
activity as Context,
R.string.toast_message_copied_to_clipboard,
Toast.LENGTH_LONG
).show()
return@setOnPreferenceClickListener true
}
// set up "Report Issue" preference
val preferenceReportIssue: Preference = Preference(context)
val preferenceReportIssue = Preference(context)
preferenceReportIssue.title = getString(R.string.pref_report_issue_title)
preferenceReportIssue.setIcon(R.drawable.ic_bug_report_24dp)
preferenceReportIssue.summary = getString(R.string.pref_report_issue_summary)
@ -159,20 +186,21 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
}
// set preference categories
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
preferenceCategoryGeneral.contains(preferenceGpsOnly)
val preferenceCategoryMaintenance: PreferenceCategory = PreferenceCategory(activity as Context)
val preferenceCategoryMaintenance =
PreferenceCategory(activity as Context)
preferenceCategoryMaintenance.title = getString(R.string.pref_maintenance_title)
preferenceCategoryMaintenance.contains(preferenceDeleteNonStarred)
val preferenceCategoryAdvanced: PreferenceCategory = PreferenceCategory(activity as Context)
val preferenceCategoryAdvanced = PreferenceCategory(activity as Context)
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
preferenceCategoryAdvanced.contains(preferenceAccuracyThreshold)
preferenceCategoryAdvanced.contains(preferenceResetAdvanced)
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
val preferenceCategoryAbout = PreferenceCategory(context)
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
preferenceCategoryAbout.contains(preferenceAppVersion)
preferenceCategoryAbout.contains(preferenceReportIssue)
@ -195,7 +223,12 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
when (type) {
Keys.DIALOG_DELETE_NON_STARRED -> {
when (dialogResult) {
@ -213,12 +246,13 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
/* Removes track and track files for given position - used by TracklistFragment */
fun deleteNonStarred(context: Context) {
private fun deleteNonStarred(context: Context) {
val backgroundJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + backgroundJob)
uiScope.launch {
var tracklist: Tracklist = FileHelper.readTracklist(context)
val deferred: Deferred<Tracklist> = async { FileHelper.deleteNonStarredSuspended(context, tracklist) }
val deferred: Deferred<Tracklist> =
async { FileHelper.deleteNonStarredSuspended(context, tracklist) }
// wait for result and store in tracklist
tracklist = deferred.await()
backgroundJob.cancel()

View File

@ -24,7 +24,9 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.LayoutInflater
import android.view.View
@ -46,7 +48,8 @@ import org.y20k.trackbook.helpers.TrackHelper
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlay.MarkerListener {
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener,
YesNoDialog.YesNoDialogListener, MapOverlay.MarkerListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackFragment::class.java)
@ -61,19 +64,30 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get track
val fileUriString: String = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
if (fileUriString.isNotBlank()) {
track = FileHelper.readTrack(activity as Context, Uri.parse(fileUriString))
val fileUriString: String =
arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
track = if (fileUriString.isNotBlank()) {
FileHelper.readTrack(Uri.parse(fileUriString))
} else {
track = Track()
Track()
}
}
/* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// initialize layout
layout = TrackFragmentLayoutHolder(activity as Context, this as MapOverlay.MarkerListener, inflater, container, track)
layout = TrackFragmentLayoutHolder(
activity as Context,
this as MapOverlay.MarkerListener,
inflater,
container,
track
)
// set up share button
layout.shareButton.setOnClickListener {
@ -81,18 +95,31 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
}
layout.shareButton.setOnLongClickListener {
val v = (activity as Context).getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
v.vibrate(50)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
v.vibrate(50)
}
shareGpxTrack()
return@setOnLongClickListener true
}
// set up delete button
layout.deleteButton.setOnClickListener {
val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${layout.trackNameView.text}"
YesNoDialog(this@TrackFragment 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)
val dialogMessage =
"${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${layout.trackNameView.text}"
YesNoDialog(this@TrackFragment 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
)
}
// set up rename button
layout.editButton.setOnClickListener {
RenameTrackDialog(this as RenameTrackDialog.RenameTrackListener).show(activity as Context, layout.trackNameView.text.toString())
RenameTrackDialog(this as RenameTrackDialog.RenameTrackListener).show(
activity as Context,
layout.trackNameView.text.toString()
)
}
return layout.rootView
@ -125,8 +152,18 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
val targetUri: Uri? = data.data
if (targetUri != null) {
// copy file async (= fire & forget - no return value needed)
GlobalScope.launch { FileHelper.saveCopyOfFileSuspended( activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri) }
Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
GlobalScope.launch {
FileHelper.saveCopyOfFileSuspended(
activity as Context,
originalFileUri = sourceUri,
targetFileUri = targetUri
)
}
Toast.makeText(
activity as Context,
R.string.toast_message_save_gpx,
Toast.LENGTH_LONG
).show()
}
}
}
@ -139,7 +176,13 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Overrides onRenameTrackDialog from RenameTrackDialog */
override fun onRenameTrackDialog(textInput: String) {
// rename track async (= fire & forget - no return value needed)
GlobalScope.launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) }
GlobalScope.launch {
FileHelper.renameTrackSuspended(
activity as Context,
layout.track,
textInput
)
}
// update name in layout
layout.track.name = textInput
layout.trackNameView.text = textInput
@ -147,7 +190,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
when (type) {
Keys.DIALOG_DELETE_TRACK -> {
when (dialogResult) {
@ -189,7 +237,11 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
if (packageManager != null && intent.resolveActivity(packageManager) != null) {
startActivityForResult(intent, Keys.REQUEST_SAVE_GPX)
} else {
Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
Toast.makeText(
activity as Context,
R.string.toast_message_install_file_helper,
Toast.LENGTH_LONG
).show()
}
}
@ -197,7 +249,11 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Share track as GPX via share sheet */
private fun shareGpxTrack() {
val gpxFile = Uri.parse(layout.track.gpxUriString).toFile()
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${requireActivity().applicationContext.packageName}.provider", gpxFile)
val gpxShareUri = FileProvider.getUriForFile(
this.activity as Context,
"${requireActivity().applicationContext.packageName}.provider",
gpxFile
)
val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
data = gpxShareUri
@ -211,7 +267,8 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) {
startActivity(shareIntent)
} else {
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG)
.show()
}
}

View File

@ -27,7 +27,7 @@ import org.y20k.trackbook.helpers.PreferencesHelper
/*
* Trackbook.class
*/
class Trackbook: Application() {
class Trackbook : Application() {
/* Define log tag */
@ -49,4 +49,4 @@ class Trackbook: Application() {
LogHelper.v(TAG, "Trackbook application terminated.")
}
}
}

View File

@ -48,7 +48,7 @@ import kotlin.coroutines.CoroutineContext
/*
* TrackerService class
*/
class TrackerService(): Service(), CoroutineScope, SensorEventListener {
class TrackerService : Service(), CoroutineScope, SensorEventListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
@ -58,16 +58,16 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
var trackingState: Int = Keys.STATE_TRACKING_NOT
var gpsProviderActive: Boolean = false
var networkProviderActive: Boolean = false
var useImperial: Boolean = false
var gpsOnly: Boolean = false
private var useImperial: Boolean = false
private var gpsOnly: Boolean = false
var locationAccuracyThreshold: Int = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
var stepCountOffset: Float = 0f
private var stepCountOffset: Float = 0f
var resumed: Boolean = false
var track: Track = Track()
var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false
var bound: Boolean = false
private var gpsLocationListenerRegistered: Boolean = false
private var networkLocationListenerRegistered: Boolean = false
private var bound: Boolean = false
private val binder = LocalBinder()
private val handler: Handler = Handler()
private lateinit var locationManager: LocationManager
@ -99,9 +99,10 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
networkLocationListener = createLocationListener()
trackingState = PreferencesHelper.loadTrackingState(this)
currentBestLocation = LocationHelper.getLastKnownLocation(this)
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
track = FileHelper.readTrack(FileHelper.getTempFileUri(this))
backgroundJob = Job()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
@ -111,16 +112,19 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
// SERVICE RESTART (via START_STICKY)
if (intent == null) {
if (trackingState == Keys.STATE_TRACKING_ACTIVE) {
LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.")
LogHelper.w(
TAG,
"Trackbook has been killed by the operating system. Trying to resume recording."
)
resumeTracking()
}
// ACTION STOP
// ACTION STOP
} else if (Keys.ACTION_STOP == intent.action) {
stopTracking()
// ACTION START
// ACTION START
} else if (Keys.ACTION_START == intent.action) {
startTracking()
// ACTION RESUME
// ACTION RESUME
} else if (Keys.ACTION_RESUME == intent.action) {
resumeTracking()
}
@ -172,7 +176,8 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
// remove notification
stopForeground(true)
// stop listening for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
// stop receiving location updates
removeGpsLocationListener()
removeNetworkLocationListener()
@ -189,11 +194,12 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
/* Overrides onSensorChanged from SensorEventListener */
override fun onSensorChanged(sensorEvent: SensorEvent?) {
var steps: Float = 0f
var steps = 0f
if (sensorEvent != null) {
if (stepCountOffset == 0f) {
// store steps previously recorded by the system
stepCountOffset = (sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed
stepCountOffset =
(sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed
}
// calculate step count - subtract steps previously recorded
steps = sensorEvent.values[0] - stepCountOffset
@ -206,16 +212,17 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
/* Resume tracking after stop/pause */
fun resumeTracking() {
// load temp track - returns an empty track if not available
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
track = FileHelper.readTrack(FileHelper.getTempFileUri(this))
// try to mark last waypoint as stopover
if (track.wayPoints.size > 0) {
val lastWayPointIndex = track.wayPoints.size - 1
track.wayPoints.get(lastWayPointIndex).isStopOver = true
track.wayPoints[lastWayPointIndex].isStopOver = true
}
// set resumed flag
resumed = true
// calculate length of recording break
track.recordingPaused = track.recordingPaused + TrackHelper.calculateDurationOfPause(track.recordingStop)
track.recordingPaused =
track.recordingPaused + TrackHelper.calculateDurationOfPause(track.recordingStop)
// start tracking
startTracking(newTrack = false)
}
@ -301,20 +308,27 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
currentBestLocation = location
}
}
override fun onProviderEnabled(provider: String) {
LogHelper.v(TAG, "onProviderEnabled $provider")
when (provider) {
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
LocationManager.GPS_PROVIDER -> gpsProviderActive =
LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive =
LocationHelper.isNetworkEnabled(locationManager)
}
}
override fun onProviderDisabled(provider: String) {
LogHelper.v(TAG, "onProviderDisabled $provider")
when (provider) {
LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
LocationManager.GPS_PROVIDER -> gpsProviderActive =
LocationHelper.isGpsEnabled(locationManager)
LocationManager.NETWORK_PROVIDER -> networkProviderActive =
LocationHelper.isNetworkEnabled(locationManager)
}
}
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
// deprecated method
}
@ -329,13 +343,25 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
// check if Network provider is available
if (gpsProviderActive) {
// check for location permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// adds GPS location listener
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f,gpsLocationListener)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0,
0f,
gpsLocationListener
)
gpsLocationListenerRegistered = true
LogHelper.v(TAG, "Added GPS location listener.")
} else {
LogHelper.w(TAG, "Unable to add GPS location listener. Location permission is not granted.")
LogHelper.w(
TAG,
"Unable to add GPS location listener. Location permission is not granted."
)
}
} else {
LogHelper.w(TAG, "Unable to add GPS location listener.")
@ -353,50 +379,83 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
// check if Network provider is available
if (networkProviderActive && !gpsOnly) {
// check for location permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// adds Network location listener
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f, networkLocationListener)
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
0,
0f,
networkLocationListener
)
networkLocationListenerRegistered = true
LogHelper.v(TAG, "Added Network location listener.")
} else {
LogHelper.w(TAG, "Unable to add Network location listener. Location permission is not granted.")
LogHelper.w(
TAG,
"Unable to add Network location listener. Location permission is not granted."
)
}
} else {
LogHelper.w(TAG, "Unable to add Network location listener.")
}
} else {
LogHelper.v(TAG, "Skipping registration. Network location listener has already been added.")
LogHelper.v(
TAG,
"Skipping registration. Network location listener has already been added."
)
}
}
/* 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 {
LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.")
LogHelper.w(
TAG,
"Unable to remove GPS location listener. Location permission is needed."
)
}
}
/* Adds location listeners to location manager */
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(gpsLocationListener)
networkLocationListenerRegistered = false
LogHelper.v(TAG, "Removed Network location listener.")
} else {
LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.")
LogHelper.w(
TAG,
"Unable to remove Network location listener. Location permission is needed."
)
}
}
/* Registers a step counter listener */
private fun startStepCounter() {
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
val stepCounterAvailable = sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER),
SensorManager.SENSOR_DELAY_UI
)
if (!stepCounterAvailable) {
LogHelper.w(TAG, "Pedometer sensor not available.")
track.stepCount = -1f
@ -406,7 +465,12 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
/* Displays / updates notification */
private fun displayNotification(): Notification {
val notification: Notification = notificationHelper.createNotification(trackingState, track.length, track.duration, useImperial)
val notification: Notification = notificationHelper.createNotification(
trackingState,
track.length,
track.duration,
useImperial
)
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
return notification
}
@ -415,7 +479,8 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
private val sharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
// preference "Restrict to GPS"
Keys.PREF_GPS_ONLY -> {
@ -431,7 +496,8 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
}
// preference "Accuracy Threshold"
Keys.PREF_LOCATION_ACCURACY_THRESHOLD -> {
locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this@TrackerService)
locationAccuracyThreshold =
PreferencesHelper.loadAccuracyThreshold(this@TrackerService)
}
}
}
@ -457,7 +523,12 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
private val periodicTrackUpdate: Runnable = object : Runnable {
override fun run() {
// add waypoint to track - step count is continuously updated in onSensorChanged
val result: Pair<Track, Boolean> = TrackHelper.addWayPointToTrack(this@TrackerService, track, currentBestLocation, locationAccuracyThreshold, resumed)
val result: Pair<Track, Boolean> = TrackHelper.addWayPointToTrack(
track,
currentBestLocation,
locationAccuracyThreshold,
resumed
)
// get track from result
track = result.first
// check if waypoint was successfully added (= result.second)
@ -478,4 +549,4 @@ class TrackerService(): Service(), CoroutineScope, SensorEventListener {
*/
}
}

View File

@ -31,7 +31,7 @@ import org.y20k.trackbook.helpers.PreferencesHelper
/*
* TrackingToggleTileService class
*/
class TrackingToggleTileService(): TileService() {
class TrackingToggleTileService : TileService() {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackingToggleTileService::class.java)
@ -52,11 +52,6 @@ class TrackingToggleTileService(): TileService() {
updateTile()
}
/* Overrides onTileRemoved from TileService */
override fun onTileRemoved() {
super.onTileRemoved()
}
/* Overrides onStartListening from TileService (tile becomes visible) */
override fun onStartListening() {
@ -66,7 +61,8 @@ class TrackingToggleTileService(): TileService() {
// set up tile
updateTile()
// register listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this@TrackingToggleTileService).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
PreferenceManager.getDefaultSharedPreferences(this@TrackingToggleTileService)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
@ -84,13 +80,8 @@ class TrackingToggleTileService(): TileService() {
override fun onStopListening() {
super.onStopListening()
// unregister listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this@TrackingToggleTileService).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
/* Overrides onDestroy from Service */
override fun onDestroy() {
super.onDestroy()
PreferenceManager.getDefaultSharedPreferences(this@TrackingToggleTileService)
.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}
@ -138,19 +129,18 @@ class TrackingToggleTileService(): TileService() {
/*
* Defines the listener for changes in shared preferences
*/
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
trackingState = PreferencesHelper.loadTrackingState(this)
updateTile()
private val sharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
Keys.PREF_TRACKING_STATE -> {
trackingState = PreferencesHelper.loadTrackingState(this)
updateTile()
}
}
}
}
/*
* End of declaration
*/
}
}

View File

@ -40,7 +40,8 @@ import org.y20k.trackbook.tracklist.TracklistAdapter
/*
* TracklistFragment class
*/
class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, YesNoDialog.YesNoDialogListener {
class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
YesNoDialog.YesNoDialogListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java)
@ -61,7 +62,11 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
/* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// find views
val rootView = inflater.inflate(R.layout.fragment_tracklist, container, false)
trackElementList = rootView.findViewById(R.id.track_element_list)
@ -77,8 +82,17 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// ask user
val adapterPosition: Int = viewHolder.adapterPosition
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 dialogMessage =
"${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)
@ -93,7 +107,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
/* Overrides onTrackElementTapped from TracklistElementAdapterListener */
override fun onTrackElementTapped(tracklistElement: TracklistElement) {
val bundle: Bundle = Bundle()
val bundle = Bundle()
bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name)
bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString)
bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString)
@ -103,13 +117,18 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
when (type) {
Keys.DIALOG_DELETE_TRACK -> {
when (dialogResult) {
// user tapped remove track
true -> {
toggleOnboardingLayout(tracklistAdapter.itemCount -1)
toggleOnboardingLayout(tracklistAdapter.itemCount - 1)
tracklistAdapter.removeTrack(activity as Context, payload)
}
// user tapped cancel
@ -139,11 +158,11 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
}
/*
* Inner class: custom LinearLayoutManager that overrides onLayoutCompleted
*/
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) {
inner class CustomLinearLayoutManager(context: Context) :
LinearLayoutManager(context, VERTICAL, false) {
override fun supportsPredictiveItemAnimations(): Boolean {
return true
@ -157,7 +176,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
if (deleteTrackId != -1L) {
val position: Int = tracklistAdapter.findPosition(deleteTrackId)
tracklistAdapter.removeTrack(this@TracklistFragment.activity as Context, position)
toggleOnboardingLayout(tracklistAdapter.itemCount -1)
toggleOnboardingLayout(tracklistAdapter.itemCount - 1)
}
}

View File

@ -32,24 +32,26 @@ import java.util.*
*/
@Keep
@Parcelize
data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
@Expose var length: Float = 0f,
@Expose var duration: Long = 0L,
@Expose var recordingPaused: Long = 0L,
@Expose var stepCount: Float = 0f,
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
@Expose var recordingStop: Date = recordingStart,
@Expose var maxAltitude: Double = 0.0,
@Expose var minAltitude: Double = 0.0,
@Expose var positiveElevation: Double = 0.0,
@Expose var negativeElevation: Double = 0.0,
@Expose var trackUriString: String = String(),
@Expose var gpxUriString: String = String(),
@Expose var latitude: Double = Keys.DEFAULT_LATITUDE,
@Expose var longitude: Double = Keys.DEFAULT_LONGITUDE,
@Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
@Expose var name: String = String()): Parcelable {
data class Track(
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
@Expose var length: Float = 0f,
@Expose var duration: Long = 0L,
@Expose var recordingPaused: Long = 0L,
@Expose var stepCount: Float = 0f,
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
@Expose var recordingStop: Date = recordingStart,
@Expose var maxAltitude: Double = 0.0,
@Expose var minAltitude: Double = 0.0,
@Expose var positiveElevation: Double = 0.0,
@Expose var negativeElevation: Double = 0.0,
@Expose var trackUriString: String = String(),
@Expose var gpxUriString: String = String(),
@Expose var latitude: Double = Keys.DEFAULT_LATITUDE,
@Expose var longitude: Double = Keys.DEFAULT_LONGITUDE,
@Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
@Expose var name: String = String()
) : Parcelable {
/* Creates a TracklistElement */
@ -75,4 +77,4 @@ data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMA
}
}
}

View File

@ -31,9 +31,11 @@ import java.util.*
*/
@Keep
@Parcelize
data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>(),
@Expose var modificationDate: Date = Date()): Parcelable {
data class Tracklist(
@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
@Expose val tracklistElements: MutableList<TracklistElement> = mutableListOf<TracklistElement>(),
@Expose var modificationDate: Date = Date()
) : Parcelable {
/* Return trackelement for given track id */
fun getTrackElement(trackId: Long): TracklistElement? {
@ -47,7 +49,11 @@ data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRA
/* Create a deep copy */
fun deepCopy(): Tracklist {
return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) }, modificationDate)
return Tracklist(
tracklistFormatVersion,
mutableListOf<TracklistElement>().apply { addAll(tracklistElements) },
modificationDate
)
}
}
}

View File

@ -29,18 +29,20 @@ import java.util.*
*/
@Keep
@Parcelize
data class TracklistElement(@Expose var name: String,
@Expose val date: Date,
@Expose val dateString: String,
@Expose val durationString: String,
@Expose val length: Float,
@Expose val trackUriString: String,
@Expose val gpxUriString: String,
@Expose var starred: Boolean = false): Parcelable {
data class TracklistElement(
@Expose var name: String,
@Expose val date: Date,
@Expose val dateString: String,
@Expose val durationString: String,
@Expose val length: Float,
@Expose val trackUriString: String,
@Expose val gpxUriString: String,
@Expose var starred: Boolean = false
) : Parcelable {
/* Returns unique ID for TracklistElement - currently the start date */
fun getTrackId(): Long {
return date.time
}
}
}

View File

@ -29,21 +29,23 @@ import kotlinx.android.parcel.Parcelize
*/
@Keep
@Parcelize
data class WayPoint(@Expose val provider: String,
@Expose val latitude: Double,
@Expose val longitude: Double,
@Expose val altitude: Double,
@Expose val accuracy: Float,
@Expose val time: Long,
@Expose val distanceToStartingPoint: Float = 0f,
@Expose val numberSatellites: Int = 0,
@Expose var isStopOver: Boolean = false,
@Expose var starred: Boolean = false): Parcelable {
data class WayPoint(
@Expose val provider: String,
@Expose val latitude: Double,
@Expose val longitude: Double,
@Expose val altitude: Double,
@Expose val accuracy: Float,
@Expose val time: Long,
@Expose val distanceToStartingPoint: Float = 0f,
@Expose val numberSatellites: Int = 0,
@Expose var isStopOver: Boolean = false,
@Expose var starred: Boolean = false
) : Parcelable {
/* Converts WayPoint into Location */
fun toLocation(): Location {
val location: Location = Location(provider)
val location = Location(provider)
location.latitude = latitude
location.longitude = longitude
location.altitude = altitude
@ -52,4 +54,4 @@ data class WayPoint(@Expose val provider: String,
return location
}
}
}

View File

@ -18,7 +18,6 @@
package org.y20k.trackbook.dialogs
import android.content.Context
import android.content.DialogInterface
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
@ -38,9 +37,14 @@ object ErrorDialog {
/* Construct and show dialog */
fun show(context: Context, errorTitle: Int, errorMessage: Int, errorDetails: String = String()) {
fun show(
context: Context,
errorTitle: Int,
errorMessage: Int,
errorDetails: String = String()
) {
// prepare dialog builder
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
val builder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
// set title
builder.setTitle(context.getString(errorTitle))
@ -81,12 +85,14 @@ object ErrorDialog {
errorMessageView.text = context.getString(errorMessage)
// add okay button
builder.setPositiveButton(R.string.dialog_generic_button_okay, DialogInterface.OnClickListener { _, _ ->
builder.setPositiveButton(
R.string.dialog_generic_button_okay
) { _, _ ->
// listen for click on okay button
// do nothing
})
}
// display error dialog
builder.show()
}
}
}

View File

@ -30,7 +30,7 @@ import org.y20k.trackbook.helpers.LogHelper
/*
* RenameTrackDialog class
*/
class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) {
class RenameTrackDialog(private var renameTrackListener: RenameTrackListener) {
/* Interface used to communicate back to activity */
interface RenameTrackListener {
@ -45,7 +45,7 @@ class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) {
/* Construct and show dialog */
fun show(context: Context, trackName: String) {
// prepare dialog builder
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context)
val builder = MaterialAlertDialogBuilder(context)
// get input field
val inflater = LayoutInflater.from(context)
@ -78,4 +78,4 @@ class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) {
builder.show()
}
}
}

View File

@ -24,7 +24,7 @@ import org.y20k.trackbook.helpers.LogHelper
/*
* YesNoDialog class
*/
class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) {
class YesNoDialog(private var yesNoDialogListener: YesNoDialogListener) {
/* Interface used to communicate back to activity */
interface YesNoDialogListener {
@ -37,35 +37,45 @@ class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) {
private val TAG = LogHelper.makeLogTag(YesNoDialog::class.java.simpleName)
/* Construct and show dialog - variant: message from string */
fun show(context: Context,
type: Int,
title: Int = Keys.EMPTY_STRING_RESOURCE,
message: Int,
yesButton: Int = R.string.dialog_yes_no_positive_button_default,
noButton: Int = R.string.dialog_generic_button_cancel,
payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT,
payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) {
fun show(
context: Context,
type: Int,
title: Int = Keys.EMPTY_STRING_RESOURCE,
message: Int,
yesButton: Int = R.string.dialog_yes_no_positive_button_default,
noButton: Int = R.string.dialog_generic_button_cancel,
payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT,
payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING
) {
// extract string from message resource and feed into main show method
show(context, type, title, context.getString(message), yesButton, noButton, payload, payloadString)
show(
context,
type,
title,
context.getString(message),
yesButton,
noButton,
payload,
payloadString
)
}
/* Construct and show dialog */
fun show(context: Context,
type: Int,
title: Int = Keys.EMPTY_STRING_RESOURCE,
messageString: String,
yesButton: Int = R.string.dialog_yes_no_positive_button_default,
noButton: Int = R.string.dialog_generic_button_cancel,
payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT,
payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) {
fun show(
context: Context,
type: Int,
title: Int = Keys.EMPTY_STRING_RESOURCE,
messageString: String,
yesButton: Int = R.string.dialog_yes_no_positive_button_default,
noButton: Int = R.string.dialog_generic_button_cancel,
payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT,
payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING
) {
// prepare dialog builder
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
val builder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
// set title and message
builder.setMessage(messageString)
@ -87,11 +97,11 @@ class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) {
}
// handle outside-click as "no"
builder.setOnCancelListener(){
builder.setOnCancelListener {
yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString)
}
// display dialog
builder.show()
}
}
}

View File

@ -19,11 +19,15 @@ package org.y20k.trackbook.extensions
import android.content.SharedPreferences
import java.lang.Double.doubleToRawLongBits
import java.lang.Double.longBitsToDouble
/* Puts a Double value in SharedPreferences */
fun SharedPreferences.Editor.putDouble(key: String, double: Double) = putLong(key, java.lang.Double.doubleToRawLongBits(double))
fun SharedPreferences.Editor.putDouble(key: String, double: Double) =
this.putLong(key, doubleToRawLongBits(double))
/* gets a Double value from SharedPreferences */
fun SharedPreferences.getDouble(key: String, default: Double) = java.lang.Double.longBitsToDouble(getLong(key, java.lang.Double.doubleToRawLongBits(default)))
fun SharedPreferences.getDouble(key: String, default: Double) =
longBitsToDouble(getLong(key, doubleToRawLongBits(default)))

View File

@ -99,6 +99,4 @@ object AppThemeHelper {
}
}
}

View File

@ -34,22 +34,24 @@ object DateTimeHelper {
/* Converts milliseconds to mm:ss or hh:mm:ss */
fun convertToReadableTime(context: Context, milliseconds: Long): String {
var timeString: String = String()
val timeString: String
val hours: Long = TimeUnit.MILLISECONDS.toHours(milliseconds)
val minutes: Long = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1)
val seconds: Long = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)
val minutes: Long =
TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1)
val seconds: Long =
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)
val h: String = context.getString(R.string.abbreviation_hours)
val m: String = context.getString(R.string.abbreviation_minutes)
val s: String = context.getString(R.string.abbreviation_seconds)
when (milliseconds >= Keys.ONE_HOUR_IN_MILLISECONDS) {
timeString = when (milliseconds >= Keys.ONE_HOUR_IN_MILLISECONDS) {
// CASE: format hh:mm:ss
true -> {
timeString = "$hours $h $minutes $m $seconds $s"
"$hours $h $minutes $m $seconds $s"
}
// CASE: format mm:ss
false -> {
timeString = "$minutes $m $seconds $s"
"$minutes $m $seconds $s"
}
}
return timeString
@ -58,7 +60,7 @@ object DateTimeHelper {
/* Create sortable string for date - used for filenames */
fun convertToSortableDateString(date: Date): String {
val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US)
val dateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US)
return dateFormat.format(date)
}
@ -70,14 +72,20 @@ object DateTimeHelper {
/* Creates a readable string date and time - used in the UI */
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)}"
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
fun calculateTimeDistance(previousLocation: Location?, location: Location): Long {
var timeDifference = 0L
// two data points needed to calculate time difference
if (previousLocation != null) {
// get time difference
@ -87,4 +95,4 @@ object DateTimeHelper {
}
}
}

View File

@ -35,6 +35,8 @@ import java.text.NumberFormat
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.ln
import kotlin.math.pow
/*
@ -48,10 +50,10 @@ object FileHelper {
/* Return an InputStream for given Uri */
fun getTextFileStream(context: Context, uri: Uri): InputStream? {
var stream : InputStream? = null
var stream: InputStream? = null
try {
stream = context.contentResolver.openInputStream(uri)
} catch (e : Exception) {
} catch (e: Exception) {
e.printStackTrace()
}
return stream
@ -61,14 +63,14 @@ object FileHelper {
/* Get file size for given Uri */
fun getFileSize(context: Context, uri: Uri): Long {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
return if (cursor != null) {
val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
val size: Long = cursor.getLong(sizeIndex)
cursor.close()
return size
size
} else {
return 0L
0L
}
}
@ -76,14 +78,14 @@ object FileHelper {
/* Get file name for given Uri */
fun getFileName(context: Context, uri: Uri): String {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
return if (cursor != null) {
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val name: String = cursor.getString(nameIndex)
cursor.close()
return name
name
} else {
return String()
String()
}
}
@ -110,8 +112,8 @@ object FileHelper {
fun readTracklist(context: Context): Tracklist {
LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}")
// get JSON from text file
val json: String = readTextFile(context, getTracklistFileUri(context))
var tracklist: Tracklist = Tracklist()
val json: String = readTextFile(getTracklistFileUri(context))
var tracklist = Tracklist()
when (json.isNotBlank()) {
// convert JSON and return as tracklist
true -> try {
@ -125,10 +127,10 @@ object FileHelper {
/* Reads track from storage using GSON */
fun readTrack(context: Context, fileUri: Uri): Track {
fun readTrack(fileUri: Uri): Track {
// get JSON from text file
val json: String = readTextFile(context, fileUri)
var track: Track = Track()
val json: String = readTextFile(fileUri)
var track = Track()
when (json.isNotEmpty()) {
// convert JSON and return as track
true -> try {
@ -154,16 +156,19 @@ object FileHelper {
/* Creates Uri for Gpx file of a track */
fun getGpxFileUri(context: Context, track: Track): Uri = File(context.getExternalFilesDir(Keys.FOLDER_GPX), getGpxFileName(track)).toUri()
fun getGpxFileUri(context: Context, track: Track): Uri =
File(context.getExternalFilesDir(Keys.FOLDER_GPX), getGpxFileName(track)).toUri()
/* Creates file name for Gpx file of a track */
fun getGpxFileName(track: Track): String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION
fun getGpxFileName(track: Track): String =
DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION
/* Creates Uri for json track file */
fun getTrackFileUri(context: Context, track: Track): Uri {
val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION
val fileName: String =
DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri()
}
@ -175,7 +180,11 @@ object FileHelper {
/* Suspend function: Wrapper for saveTracklist */
suspend fun addTrackAndSaveTracklistSuspended(context: Context, track: Track, modificationDate: Date = track.recordingStop) {
suspend fun addTrackAndSaveTracklistSuspended(
context: Context,
track: Track,
modificationDate: Date = track.recordingStop
) {
return suspendCoroutine { cont ->
val tracklist: Tracklist = readTracklist(context)
tracklist.tracklistElements.add(track.toTracklistElement(context))
@ -193,7 +202,11 @@ object FileHelper {
/* Suspend function: Wrapper for saveTracklist */
suspend fun saveTracklistSuspended(context: Context, tracklist: Tracklist, modificationDate: Date) {
suspend fun saveTracklistSuspended(
context: Context,
tracklist: Tracklist,
modificationDate: Date
) {
return suspendCoroutine { cont ->
cont.resume(saveTracklist(context, tracklist, modificationDate))
}
@ -217,7 +230,11 @@ object FileHelper {
/* Suspend function: Wrapper for deleteTrack */
suspend fun deleteTrackSuspended(context: Context, position: Int, tracklist: Tracklist): Tracklist {
suspend fun deleteTrackSuspended(
context: Context,
position: Int,
tracklist: Tracklist
): Tracklist {
return suspendCoroutine { cont ->
cont.resume(deleteTrack(context, position, tracklist))
}
@ -240,14 +257,19 @@ object FileHelper {
/* Suspend function: Wrapper for readTracklist */
suspend fun readTracklistSuspended(context: Context): Tracklist {
return suspendCoroutine {cont ->
return suspendCoroutine { cont ->
cont.resume(readTracklist(context))
}
}
/* Suspend function: Wrapper for copyFile */
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
suspend fun saveCopyOfFileSuspended(
context: Context,
originalFileUri: Uri,
targetFileUri: Uri,
deleteOriginal: Boolean = false
) {
return suspendCoroutine { cont ->
cont.resume(copyFile(context, originalFileUri, targetFileUri, deleteOriginal))
}
@ -285,7 +307,7 @@ object FileHelper {
tracklist.modificationDate = modificationDate
// convert to JSON
val gson: Gson = getCustomGson()
var json: String = String()
var json = String()
try {
json = gson.toJson(tracklist)
} catch (e: Exception) {
@ -304,12 +326,11 @@ object FileHelper {
}
/* Renames track */
private fun renameTrack(context: Context, track: Track, newName: String) {
// search track in tracklist
val tracklist: Tracklist = readTracklist(context)
var trackUriString: String = String()
var trackUriString = String()
tracklist.tracklistElements.forEach { tracklistElement ->
if (tracklistElement.getTrackId() == track.getTrackId()) {
// rename tracklist element
@ -329,13 +350,17 @@ object FileHelper {
/* Deletes multiple tracks */
private fun deleteTracks(context: Context, tracklistElements: MutableList<TracklistElement>, tracklist: Tracklist): Tracklist {
private fun deleteTracks(
context: Context,
tracklistElements: MutableList<TracklistElement>,
tracklist: Tracklist
): Tracklist {
tracklistElements.forEach { tracklistElement ->
// delete track files
tracklistElement.trackUriString.toUri().toFile().delete()
tracklistElement.gpxUriString.toUri().toFile().delete()
}
tracklist.tracklistElements.removeAll{ tracklistElements.contains(it) }
tracklist.tracklistElements.removeAll { tracklistElements.contains(it) }
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
return tracklist
}
@ -348,14 +373,23 @@ object FileHelper {
tracklistElement.trackUriString.toUri().toFile().delete()
tracklistElement.gpxUriString.toUri().toFile().delete()
// remove track element from list
tracklist.tracklistElements.removeIf { TrackHelper.getTrackId(it) == TrackHelper.getTrackId(tracklistElement) }
tracklist.tracklistElements.removeIf {
TrackHelper.getTrackId(it) == TrackHelper.getTrackId(
tracklistElement
)
}
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
return tracklist
}
/* Copies file to specified target */
private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
private fun copyFile(
context: Context,
originalFileUri: Uri,
targetFileUri: Uri,
deleteOriginal: Boolean = false
) {
val inputStream = context.contentResolver.openInputStream(originalFileUri)
val outputStream = context.contentResolver.openOutputStream(targetFileUri)
if (outputStream != null) {
@ -370,7 +404,7 @@ object FileHelper {
/* Converts track to JSON */
private fun getTrackJsonString(track: Track): String {
val gson: Gson = getCustomGson()
var json: String = String()
var json = String()
try {
json = gson.toJson(track)
} catch (e: Exception) {
@ -389,7 +423,6 @@ object FileHelper {
}
/* Converts byte value into a human readable format */
// Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html
fun getReadableByteCount(bytes: Long, si: Boolean = true): String {
@ -401,13 +434,13 @@ object FileHelper {
if (bytes < unit) return "$bytes B"
// calculate exp
val exp: Int = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
val exp: Int = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
// determine prefix symbol
val prefix: String = ((if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i")
// calculate result and set number format
val result: Double = bytes / Math.pow(unit.toDouble(), exp.toDouble())
val result: Double = bytes / unit.toDouble().pow(exp.toDouble())
val numberFormat = NumberFormat.getNumberInstance()
numberFormat.maximumFractionDigits = 1
@ -416,7 +449,7 @@ object FileHelper {
/* Reads InputStream from file uri and returns it as String */
private fun readTextFile(context: Context, fileUri: Uri): String {
private fun readTextFile(fileUri: Uri): String {
// todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
// https://developer.android.com/training/secure-file-sharing/retrieve-info
val file: File = fileUri.toFile()
@ -426,11 +459,12 @@ object FileHelper {
}
// read until last line reached
val stream: InputStream = file.inputStream()
val reader: BufferedReader = BufferedReader(InputStreamReader(stream))
val reader = BufferedReader(InputStreamReader(stream))
val builder: StringBuilder = StringBuilder()
reader.forEachLine {
builder.append(it)
builder.append("\n") }
builder.append("\n")
}
stream.close()
return builder.toString()
}
@ -444,8 +478,13 @@ object FileHelper {
/* Writes given bitmap as image file to storage */
private fun writeImageFile(context: Context, bitmap: Bitmap, file: File, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 75) {
if (file.exists()) file.delete ()
private fun writeImageFile(
bitmap: Bitmap,
file: File,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 75
) {
if (file.exists()) file.delete()
try {
val out = FileOutputStream(file)
bitmap.compress(format, quality, out)
@ -456,4 +495,4 @@ object FileHelper {
}
}
}
}

View File

@ -89,45 +89,51 @@ object LengthUnitHelper {
/* Determines which unit system the device is using (metric or imperial) */
fun useImperialUnits(): Boolean {
// America (US), Liberia (LR), Myanmar(MM) use the imperial system
val imperialSystemCountries = Arrays.asList("US", "LR", "MM")
val imperialSystemCountries = listOf("US", "LR", "MM")
val countryCode = Locale.getDefault().country
return imperialSystemCountries.contains(countryCode)
}
/* Converts for the given unit System distance and duration values to a readable velocity string */
fun convertToVelocityString(trackDuration: Long, trackRecordingPause: Long, trackLength: Float, useImperialUnits: Boolean = false) : String {
var speed: String = "0"
fun convertToVelocityString(
trackDuration: Long,
trackRecordingPause: Long,
trackLength: Float,
useImperialUnits: Boolean = false
): String {
var speed = "0"
// duration minus pause in seconds
val duration: Long = (trackDuration - trackRecordingPause) / 1000L
if (duration > 0L) {
// speed in km/h / mph
val velocity: Double = convertMetersPerSecond((trackLength / duration), useImperialUnits)
val velocity: Double =
convertMetersPerSecond((trackLength / duration), useImperialUnits)
// create readable speed string
var bd: BigDecimal = BigDecimal.valueOf(velocity)
bd = bd.setScale(1, RoundingMode.HALF_UP)
speed = bd.toPlainString()
}
when (useImperialUnits) {
true -> return "$speed mph"
false -> return "$speed km/h"
return when (useImperialUnits) {
true -> "$speed mph"
false -> "$speed km/h"
}
}
/* Coverts meters per second to either km/h or mph */
fun convertMetersPerSecond(metersPerSecond: Float, useImperial: Boolean = false): Double {
if (useImperial) {
return if (useImperial) {
// mph
return metersPerSecond * 2.2369362920544
metersPerSecond * 2.2369362920544
} else {
// km/h
return metersPerSecond * 3.6
metersPerSecond * 3.6
}
}
}
}

View File

@ -40,7 +40,7 @@ object LocationHelper {
/* Get default location */
fun getDefaultLocation(): Location {
val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
val defaultLocation = Location(LocationManager.NETWORK_PROVIDER)
defaultLocation.latitude = Keys.DEFAULT_LATITUDE
defaultLocation.longitude = Keys.DEFAULT_LONGITUDE
defaultLocation.accuracy = Keys.DEFAULT_ACCURACY
@ -62,14 +62,24 @@ object LocationHelper {
// get last location that Trackbook has stored
var lastKnownLocation: Location = PreferencesHelper.loadCurrentBestLocation(context)
// try to get the last location the system has stored - it is probably more recent
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val lastKnownLocationGps: Location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) ?: lastKnownLocation
val lastKnownLocationNetwork: Location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) ?: lastKnownLocation
when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) {
true -> lastKnownLocation = lastKnownLocationGps
false -> lastKnownLocation = lastKnownLocationNetwork
}
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val lastKnownLocationGps: Location =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
?: lastKnownLocation
val lastKnownLocationNetwork: Location =
locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
?: lastKnownLocation
lastKnownLocation =
when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) {
true -> lastKnownLocationGps
false -> lastKnownLocationNetwork
}
}
return lastKnownLocation
}
@ -87,7 +97,7 @@ object LocationHelper {
// check whether the new location fix is newer or older
val timeDelta: Long = location.time - currentBestLocation.time
val isSignificantlyNewer: Boolean = timeDelta > Keys.SIGNIFICANT_TIME_DIFFERENCE
val isSignificantlyOlder:Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE
val isSignificantlyOlder: Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE
when {
// if it's been more than two minutes since the current location, use the new location because the user has likely moved
@ -118,25 +128,24 @@ object LocationHelper {
/* Checks if GPS location provider is available and enabled */
fun isGpsEnabled(locationManager: LocationManager): Boolean {
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
return if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
} else {
return false
false
}
}
/* Checks if Network location provider is available and enabled */
fun isNetworkEnabled(locationManager: LocationManager): Boolean {
if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER)) {
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
return if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER)) {
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} else {
return false
false
}
}
/* Checks if given location is new */
fun isRecentEnough(location: Location): Boolean {
val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos
@ -146,26 +155,35 @@ object LocationHelper {
/* Checks if given location is accurate */
fun isAccurateEnough(location: Location, locationAccuracyThreshold: Int): Boolean {
val isAccurate: Boolean
when (location.provider) {
LocationManager.GPS_PROVIDER -> isAccurate = location.accuracy < locationAccuracyThreshold
else -> isAccurate = location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider
return when (location.provider) {
LocationManager.GPS_PROVIDER -> location.accuracy < locationAccuracyThreshold
else -> location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider
}
return isAccurate
}
/* Checks if the first location of track is plausible */
fun isFirstLocationPlausible(secondLocation: Location, track: Track): Boolean {
// speed in km/h
val speed: Double = calculateSpeed(firstLocation = track.wayPoints[0].toLocation(), secondLocation = secondLocation, firstTimestamp = track.recordingStart.time, secondTimestamp = GregorianCalendar.getInstance().time.time)
val speed: Double = calculateSpeed(
firstLocation = track.wayPoints[0].toLocation(),
secondLocation = secondLocation,
firstTimestamp = track.recordingStart.time,
secondTimestamp = GregorianCalendar.getInstance().time.time
)
// plausible = speed under 250 km/h
return speed < Keys.IMPLAUSIBLE_TRACK_START_SPEED
}
/* Calculates speed */
private fun calculateSpeed(firstLocation: Location, secondLocation: Location, firstTimestamp: Long, secondTimestamp: Long, useImperial: Boolean = false): Double {
private fun calculateSpeed(
firstLocation: Location,
secondLocation: Location,
firstTimestamp: Long,
secondTimestamp: Long,
useImperial: Boolean = false
): Double {
// time difference in seconds
val timeDifference: Long = (secondTimestamp - firstTimestamp) / 1000L
// distance in meters
@ -183,10 +201,10 @@ object LocationHelper {
val distanceThreshold: Float
val averageAccuracy: Float = (previousLocation.accuracy + location.accuracy) / 2
// increase the distance threshold if one or both locations are
if (averageAccuracy > Keys.DEFAULT_THRESHOLD_DISTANCE) {
distanceThreshold = averageAccuracy
distanceThreshold = if (averageAccuracy > Keys.DEFAULT_THRESHOLD_DISTANCE) {
averageAccuracy
} else {
distanceThreshold = Keys.DEFAULT_THRESHOLD_DISTANCE
Keys.DEFAULT_THRESHOLD_DISTANCE
}
// location is different when far enough away from previous location
return calculateDistance(previousLocation, location) > distanceThreshold
@ -194,8 +212,8 @@ object LocationHelper {
/* Calculates distance in meters between two locations */
fun calculateDistance(previousLocation: Location?, location: Location): Float {
var distance: Float = 0f
fun calculateDistance(previousLocation: Location?, location: Location): Float {
var distance = 0f
// two data points needed to calculate distance
if (previousLocation != null) {
// add up distance
@ -206,20 +224,26 @@ object LocationHelper {
/* Calculate elevation differences */
fun calculateElevationDifferences(previousLocation: Location?, location: Location, track: Track): Pair<Double, Double> {
fun calculateElevationDifferences(
previousLocation: Location?,
location: Location,
track: Track
): Pair<Double, Double> {
// store current values
var positiveElevation: Double = track.positiveElevation
var negativeElevation: Double = track.negativeElevation
if (previousLocation != null) {
// factor is bigger than 1 if the time stamp difference is larger than the movement recording interval
val timeDifferenceFactor: Long = (location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL
val timeDifferenceFactor: Long =
(location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL
// get elevation difference and sum it up
val altitudeDifference: Double = location.altitude - previousLocation.altitude
if (altitudeDifference > 0 && altitudeDifference < Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
positiveElevation = track.positiveElevation + altitudeDifference // upwards movement
}
if (altitudeDifference < 0 && altitudeDifference > -Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
negativeElevation = track.negativeElevation + altitudeDifference // downwards movement
negativeElevation =
track.negativeElevation + altitudeDifference // downwards movement
}
}
return Pair(positiveElevation, negativeElevation)
@ -234,4 +258,4 @@ object LocationHelper {
}
}
}

View File

@ -78,9 +78,9 @@ object LogHelper {
private fun log(tag: String, level: Int, t: Throwable?, vararg messages: Any) {
val message: String
if (t == null && messages.size == 1) {
message = if (t == null && messages.size == 1) {
// handle this common case without the extra cost of creating a stringbuffer:
message = messages[0].toString()
messages[0].toString()
} else {
val sb = StringBuilder()
for (m in messages) {
@ -89,7 +89,7 @@ object LogHelper {
if (t != null) {
sb.append("\n").append(Log.getStackTraceString(t))
}
message = sb.toString()
sb.toString()
}
Log.println(level, tag, message)
@ -112,4 +112,4 @@ object LogHelper {
// Log.println(level, tag, message)
// }
}
}
}

View File

@ -21,6 +21,8 @@ package org.y20k.trackbook.helpers
import android.content.Context
import android.graphics.drawable.Drawable
import android.location.Location
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.widget.Toast
import androidx.core.content.ContextCompat
@ -38,7 +40,7 @@ import java.util.*
/*
* MapHelper class
*/
class MapOverlay (private var markerListener: MarkerListener) {
class MapOverlay(private var markerListener: MarkerListener) {
/* Interface used to communicate back to activity/fragment */
interface MarkerListener {
@ -51,7 +53,11 @@ class MapOverlay (private var markerListener: MarkerListener) {
/* Creates icon overlay for current position (used in MapFragment) */
fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem> {
fun createMyLocationOverlay(
context: Context,
location: Location,
trackingState: Int
): ItemizedIconOverlay<OverlayItem> {
val overlayItems = ArrayList<OverlayItem>()
val locationIsOld = LocationHelper.isOldLocation(location)
@ -61,22 +67,41 @@ class MapOverlay (private var markerListener: MarkerListener) {
when (trackingState) {
// CASE: Tracking active
Keys.STATE_TRACKING_ACTIVE -> {
when (locationIsOld) {
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_grey_24dp)!!
false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_24dp)!!
newMarker = when (locationIsOld) {
true -> ContextCompat.getDrawable(
context,
R.drawable.ic_marker_location_red_grey_24dp
)!!
false -> ContextCompat.getDrawable(
context,
R.drawable.ic_marker_location_red_24dp
)!!
}
}
// CASE. Tracking is NOT active
else -> {
when (locationIsOld) {
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_grey_24dp)!!
false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_24dp)!!
newMarker = when (locationIsOld) {
true -> ContextCompat.getDrawable(
context,
R.drawable.ic_marker_location_blue_grey_24dp
)!!
false -> ContextCompat.getDrawable(
context,
R.drawable.ic_marker_location_blue_24dp
)!!
}
}
}
// add marker to list of overlay items
val overlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time)
val overlayItem = createOverlayItem(
context,
location.latitude,
location.longitude,
location.accuracy,
location.provider,
location.time
)
overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem)
@ -86,7 +111,11 @@ class MapOverlay (private var markerListener: MarkerListener) {
/* Creates icon overlay for track */
fun createTrackOverlay(context: Context, track: Track, trackingState: Int): ItemizedIconOverlay<OverlayItem> {
fun createTrackOverlay(
context: Context,
track: Track,
trackingState: Int
): ItemizedIconOverlay<OverlayItem> {
val overlayItems = ArrayList<OverlayItem>()
val wayPoints = track.wayPoints
@ -99,28 +128,55 @@ class MapOverlay (private var markerListener: MarkerListener) {
when (trackingState) {
// CASE: Recording is active
Keys.STATE_TRACKING_ACTIVE -> {
if (wayPoint.starred) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!
} else if (wayPoint.isStopOver) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
} else {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!!
newMarker = when {
wayPoint.starred -> {
ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!
}
wayPoint.isStopOver -> {
ContextCompat.getDrawable(
context,
R.drawable.ic_marker_track_location_grey_24dp
)!!
}
else -> {
ContextCompat.getDrawable(
context,
R.drawable.ic_marker_track_location_red_24dp
)!!
}
}
}
// CASE: Recording is paused/stopped
else -> {
if (wayPoint.starred) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!
} else if (wayPoint.isStopOver) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
} else {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_blue_24dp)!!
newMarker = when {
wayPoint.starred -> {
ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!
}
wayPoint.isStopOver -> {
ContextCompat.getDrawable(
context,
R.drawable.ic_marker_track_location_grey_24dp
)!!
}
else -> {
ContextCompat.getDrawable(
context,
R.drawable.ic_marker_track_location_blue_24dp
)!!
}
}
}
}
// create overlay item and add to list of overlay items
val overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
val overlayItem = createOverlayItem(
context,
wayPoint.latitude,
wayPoint.longitude,
wayPoint.accuracy,
wayPoint.provider,
wayPoint.time
)
overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem)
}
@ -131,30 +187,60 @@ class MapOverlay (private var markerListener: MarkerListener) {
/* Creates a marker overlay item */
private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem {
val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}"
private fun createOverlayItem(
context: Context,
latitude: Double,
longitude: Double,
accuracy: Float,
provider: String,
time: Long
): OverlayItem {
val title =
"${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(
SimpleDateFormat.MEDIUM,
Locale.getDefault()
).format(time)}"
//val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
val description: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
val position: GeoPoint = GeoPoint(latitude, longitude)
val description =
"${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(
SimpleDateFormat.MEDIUM,
Locale.getDefault()
)
.format(time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat(
"#0.00"
).format(accuracy)} (${provider})"
val position = GeoPoint(latitude, longitude)
return OverlayItem(title, description, position)
}
/* Creates an overlay */
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>): ItemizedIconOverlay<OverlayItem> {
private fun createOverlay(
context: Context,
overlayItems: ArrayList<OverlayItem>
): ItemizedIconOverlay<OverlayItem> {
return ItemizedIconOverlay<OverlayItem>(context, overlayItems,
object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
markerListener.onMarkerTapped(item.point.latitude, item.point.longitude)
return true
}
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
v.vibrate(50)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(
VibrationEffect.createOneShot(
50, VibrationEffect.DEFAULT_AMPLITUDE
)
)
} else {
v.vibrate(50)
}
Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show()
return true
}
})
}
}
}

View File

@ -41,11 +41,17 @@ class NotificationHelper(private val trackerService: TrackerService) {
/* Main class variables */
private val notificationManager: NotificationManager = trackerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val notificationManager: NotificationManager =
trackerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
/* Creates notification */
fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification {
fun createNotification(
trackingState: Int,
trackLength: Float,
duration: Long,
useImperial: Boolean
): Notification {
// create notification channel if necessary
if (shouldCreateNotificationChannel()) {
@ -53,7 +59,8 @@ class NotificationHelper(private val trackerService: TrackerService) {
}
// Build notification
val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING)
val builder =
NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING)
builder.setContentIntent(showActionPendingIntent)
builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp)
builder.setContentText(getContentString(trackerService, duration, trackLength, useImperial))
@ -63,13 +70,23 @@ class NotificationHelper(private val trackerService: TrackerService) {
Keys.STATE_TRACKING_ACTIVE -> {
builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_running))
builder.addAction(stopAction)
builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_active_48dp)!!.toBitmap())
builder.setLargeIcon(
AppCompatResources.getDrawable(
trackerService,
R.drawable.ic_notification_icon_large_tracking_active_48dp
)!!.toBitmap()
)
}
else -> {
builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_not_running))
builder.addAction(resumeAction)
builder.addAction(showAction)
builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_stopped_48dp)!!.toBitmap())
builder.setLargeIcon(
AppCompatResources.getDrawable(
trackerService,
R.drawable.ic_notification_icon_large_tracking_stopped_48dp
)!!.toBitmap()
)
}
}
@ -79,56 +96,77 @@ class NotificationHelper(private val trackerService: TrackerService) {
/* Build context text for notification builder */
private fun getContentString(context: Context, duration: Long, trackLength: Float, useImperial: Boolean): String {
return "${LengthUnitHelper.convertDistanceToString(trackLength, useImperial)}${DateTimeHelper.convertToReadableTime(context, duration)}"
private fun getContentString(
context: Context,
duration: Long,
trackLength: Float,
useImperial: Boolean
): String {
return "${LengthUnitHelper.convertDistanceToString(
trackLength,
useImperial
)} ${DateTimeHelper.convertToReadableTime(context, duration)}"
}
/* Checks if notification channel should be created */
private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists()
private fun shouldCreateNotificationChannel() =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists()
/* Checks if notification channel exists */
@RequiresApi(Build.VERSION_CODES.O)
private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
private fun nowPlayingChannelExists() =
notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
/* Create a notification channel */
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val notificationChannel = NotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING,
val notificationChannel = NotificationChannel(
Keys.NOTIFICATION_CHANNEL_RECORDING,
trackerService.getString(R.string.notification_channel_recording_name),
NotificationManager.IMPORTANCE_LOW)
.apply { description = trackerService.getString(R.string.notification_channel_recording_description) }
NotificationManager.IMPORTANCE_LOW
)
.apply {
description =
trackerService.getString(R.string.notification_channel_recording_description)
}
notificationManager.createNotificationChannel(notificationChannel)
}
/* Notification pending intents */
private val stopActionPendingIntent = PendingIntent.getService(
trackerService,14,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),0)
trackerService, 14,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP), 0
)
private val resumeActionPendingIntent = PendingIntent.getService(
trackerService, 16,
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),0)
private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run {
addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java))
getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT)
}
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME), 0
)
private val showActionPendingIntent: PendingIntent? =
TaskStackBuilder.create(trackerService).run {
addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java))
getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT)
}
/* Notification actions */
private val stopAction = NotificationCompat.Action(
R.drawable.ic_notification_action_stop_24dp,
trackerService.getString(R.string.notification_stop),
stopActionPendingIntent)
stopActionPendingIntent
)
private val resumeAction = NotificationCompat.Action(
R.drawable.ic_notification_action_resume_36dp,
trackerService.getString(R.string.notification_resume),
resumeActionPendingIntent)
resumeActionPendingIntent
)
private val showAction = NotificationCompat.Action(
R.drawable.ic_notification_action_show_36dp,
trackerService.getString(R.string.notification_show),
showActionPendingIntent)
showActionPendingIntent
)
}
}

View File

@ -80,7 +80,10 @@ object PreferencesHelper {
// get preferences
val settings = PreferenceManager.getDefaultSharedPreferences(context)
// load length unit system
return settings.getBoolean(Keys.PREF_USE_IMPERIAL_UNITS, LengthUnitHelper.useImperialUnits())
return settings.getBoolean(
Keys.PREF_USE_IMPERIAL_UNITS,
LengthUnitHelper.useImperialUnits()
)
}
@ -97,7 +100,10 @@ object PreferencesHelper {
// get preferences
val settings = PreferenceManager.getDefaultSharedPreferences(context)
// load tracking state
return settings.getInt(Keys.PREF_LOCATION_ACCURACY_THRESHOLD, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY)
return settings.getInt(
Keys.PREF_LOCATION_ACCURACY_THRESHOLD,
Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
)
}
@ -105,15 +111,23 @@ object PreferencesHelper {
fun loadCurrentBestLocation(context: Context): Location {
// get preferences
val settings = PreferenceManager.getDefaultSharedPreferences(context)
val provider: String = settings.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER
val provider: String = settings.getString(
Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER,
LocationManager.NETWORK_PROVIDER
) ?: LocationManager.NETWORK_PROVIDER
// create location
val currentBestLocation: Location = Location(provider)
val currentBestLocation = Location(provider)
// load location attributes
currentBestLocation.latitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, Keys.DEFAULT_LATITUDE)
currentBestLocation.longitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, Keys.DEFAULT_LONGITUDE)
currentBestLocation.accuracy = settings.getFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, Keys.DEFAULT_ACCURACY)
currentBestLocation.altitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, Keys.DEFAULT_ALTITUDE)
currentBestLocation.time = settings.getLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, Keys.DEFAULT_TIME)
currentBestLocation.latitude =
settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, Keys.DEFAULT_LATITUDE)
currentBestLocation.longitude =
settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, Keys.DEFAULT_LONGITUDE)
currentBestLocation.accuracy =
settings.getFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, Keys.DEFAULT_ACCURACY)
currentBestLocation.altitude =
settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, Keys.DEFAULT_ALTITUDE)
currentBestLocation.time =
settings.getLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, Keys.DEFAULT_TIME)
return currentBestLocation
}
@ -135,7 +149,9 @@ object PreferencesHelper {
/* Load currently selected app theme */
fun loadThemeSelection(context: Context): String {
return PreferenceManager.getDefaultSharedPreferences(context).getString(Keys.PREF_THEME_SELECTION, Keys.STATE_THEME_FOLLOW_SYSTEM) ?: Keys.STATE_THEME_FOLLOW_SYSTEM
return PreferenceManager.getDefaultSharedPreferences(context)
.getString(Keys.PREF_THEME_SELECTION, Keys.STATE_THEME_FOLLOW_SYSTEM)
?: Keys.STATE_THEME_FOLLOW_SYSTEM
}
@ -154,4 +170,4 @@ object PreferencesHelper {
editor.apply()
}
}
}

View File

@ -47,8 +47,13 @@ object TrackHelper {
tracklistElement.date.time
/* Adds given locatiom as waypoint to track */
fun addWayPointToTrack(context: Context, track: Track, location: Location, locationAccuracyThreshold: Int, resumed: Boolean): Pair<Track, Boolean> {
/* Adds given location as waypoint to track */
fun addWayPointToTrack(
track: Track,
location: Location,
locationAccuracyThreshold: Int,
resumed: Boolean
): Pair<Track, Boolean> {
// get previous location
val previousLocation: Location?
var numberOfWayPoints: Int = track.wayPoints.size
@ -58,7 +63,11 @@ object TrackHelper {
previousLocation = null
}
// CASE: Second location - check if first location was plausible & remove implausible location
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, track)) {
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(
location,
track
)
) {
previousLocation = null
numberOfWayPoints = 0
track.wayPoints.removeAt(0)
@ -76,8 +85,8 @@ object TrackHelper {
// add only if recent and accurate and different
val shouldBeAdded: Boolean = (LocationHelper.isRecentEnough(location) &&
LocationHelper.isAccurateEnough(location, locationAccuracyThreshold) &&
LocationHelper.isDifferentEnough(previousLocation, location))
LocationHelper.isAccurateEnough(location, locationAccuracyThreshold) &&
LocationHelper.isDifferentEnough(previousLocation, location))
// // Debugging for shouldBeAdded - remove for production
// val recentEnough: Boolean = LocationHelper.isRecentEnough(location)
@ -95,7 +104,8 @@ object TrackHelper {
if (shouldBeAdded) {
// update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
if (!resumed) {
track.length = track.length + LocationHelper.calculateDistance(previousLocation, location)
track.length =
track.length + LocationHelper.calculateDistance(previousLocation, location)
}
if (location.altitude != 0.0) {
@ -105,12 +115,23 @@ object TrackHelper {
track.minAltitude = location.altitude
} else {
// calculate elevation values (upwards / downwards movements)
val elevationDifferences: Pair<Double, Double> = LocationHelper.calculateElevationDifferences(previousLocation, location, track)
val elevationDifferences: Pair<Double, Double> =
LocationHelper.calculateElevationDifferences(
previousLocation,
location,
track
)
// check if any differences were calculated
if (elevationDifferences != Pair(track.positiveElevation, track.negativeElevation)) {
if (elevationDifferences != Pair(
track.positiveElevation,
track.negativeElevation
)
) {
// update altitude values
if (location.altitude > track.maxAltitude) track.maxAltitude = location.altitude
if (location.altitude < track.minAltitude) track.minAltitude = location.altitude
if (location.altitude > track.maxAltitude) track.maxAltitude =
location.altitude
if (location.altitude < track.minAltitude) track.minAltitude =
location.altitude
// update elevation values (do not update if resumed -> we do not want to add values calculated during a recording pause)
if (!resumed) {
track.positiveElevation = elevationDifferences.first
@ -122,16 +143,17 @@ object TrackHelper {
// toggle stop over status, if necessary
if (track.wayPoints.size < 0) {
track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
track.wayPoints[track.wayPoints.size - 1].isStopOver =
LocationHelper.isStopOver(previousLocation, location)
}
// save number of satellites
val numberOfSatellites: Int
val extras = location.extras
if (extras != null && extras.containsKey("satellites")) {
numberOfSatellites = extras.getInt("satellites", 0)
numberOfSatellites = if (extras != null && extras.containsKey("satellites")) {
extras.getInt("satellites", 0)
} else {
numberOfSatellites = 0
0
}
// add current location as point to center on for later display
@ -139,7 +161,18 @@ object TrackHelper {
track.longitude = location.longitude
// add location as new waypoint
track.wayPoints.add(WayPoint(provider = location.provider, latitude = location.latitude, longitude = location.longitude, altitude = location.altitude, accuracy = location.accuracy, time = location.time, distanceToStartingPoint = track.length, numberSatellites = numberOfSatellites))
track.wayPoints.add(
WayPoint(
provider = location.provider,
latitude = location.latitude,
longitude = location.longitude,
altitude = location.altitude,
accuracy = location.accuracy,
time = location.time,
distanceToStartingPoint = track.length,
numberSatellites = numberOfSatellites
)
)
}
return Pair(track, shouldBeAdded)
@ -153,13 +186,12 @@ object TrackHelper {
/* Creates GPX string for given track */
fun createGpxString(track: Track): String {
var gpxString: String
// add header
gpxString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n" +
"<gpx version=\"1.1\" creator=\"Trackbook App (Android)\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\n"
var gpxString: String = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n" +
"<gpx version=\"1.1\" creator=\"Trackbook App (Android)\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\n"
// add track
gpxString += createGpxTrk(track)
@ -227,12 +259,20 @@ object TrackHelper {
if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
waypoint.starred = !waypoint.starred
when (waypoint.starred) {
true -> Toast.makeText(context, R.string.toast_message_poi_added, Toast.LENGTH_LONG).show()
false -> Toast.makeText(context, R.string.toast_message_poi_removed, Toast.LENGTH_LONG).show()
true -> Toast.makeText(
context,
R.string.toast_message_poi_added,
Toast.LENGTH_LONG
).show()
false -> Toast.makeText(
context,
R.string.toast_message_poi_removed,
Toast.LENGTH_LONG
).show()
}
}
}
return track
}
}
}

View File

@ -41,7 +41,14 @@ object UiHelper {
/* Sets layout margins for given view in DP */
fun setViewMargins(context: Context, view: View, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) {
private fun setViewMargins(
context: Context,
view: View,
left: Int = 0,
right: Int = 0,
top: Int = 0,
bottom: Int = 0
) {
val scalingFactor: Float = context.resources.displayMetrics.density
val l: Int = (left * scalingFactor).toInt()
val r: Int = (right * scalingFactor).toInt()
@ -56,7 +63,16 @@ object UiHelper {
/* Sets layout margins for given view in percent */
fun setViewMarginsPercentage(context: Context, view: View, height: Int, width: Int, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) {
fun setViewMarginsPercentage(
context: Context,
view: View,
height: Int,
width: Int,
left: Int = 0,
right: Int = 0,
top: Int = 0,
bottom: Int = 0
) {
val l: Int = ((width / 100.0f) * left).toInt()
val r: Int = ((width / 100.0f) * right).toInt()
val t: Int = ((height / 100.0f) * top).toInt()
@ -69,28 +85,58 @@ object UiHelper {
* Inner class: Callback that detects a left swipe
* Credit: https://github.com/kitek/android-rv-swipe-delete/blob/master/app/src/main/java/pl/kitek/rvswipetodelete/SwipeToDeleteCallback.kt
*/
abstract class SwipeToDeleteCallback(context: Context): ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
abstract class SwipeToDeleteCallback(context: Context) :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
private val deleteIcon = ContextCompat.getDrawable(context, R.drawable.ic_remove_circle_24dp)
private val deleteIcon =
ContextCompat.getDrawable(context, R.drawable.ic_remove_circle_24dp)
private val intrinsicWidth: Int = deleteIcon?.intrinsicWidth ?: 0
private val intrinsicHeight: Int = deleteIcon?.intrinsicHeight ?: 0
private val background: ColorDrawable = ColorDrawable()
private val backgroundColor = context.resources.getColor(R.color.list_card_delete_background, null)
private val clearPaint: Paint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
private val backgroundColor =
context.resources.getColor(R.color.list_card_delete_background, null)
private val clearPaint: Paint =
Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
// do nothing
return false
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val itemHeight = itemView.bottom - itemView.top
val isCanceled = dX == 0f && !isCurrentlyActive
if (isCanceled) {
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
clearCanvas(
c,
itemView.right + dX,
itemView.top.toFloat(),
itemView.right.toFloat(),
itemView.bottom.toFloat()
)
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
return
}
@ -126,4 +172,4 @@ object UiHelper {
* End of inner class
*/
}
}

View File

@ -39,7 +39,8 @@ import java.util.*
/*
* TracklistAdapter class
*/
class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class TracklistAdapter(private val fragment: Fragment) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java)
@ -54,7 +55,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Listener Interface */
interface TracklistAdapterListener {
fun onTrackElementTapped(tracklistElement: TracklistElement) { }
fun onTrackElementTapped(tracklistElement: TracklistElement) {}
// fun onTrackElementStarred(trackId: Long, starred: Boolean)
}
@ -65,7 +66,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
tracklistListener = fragment as TracklistAdapterListener
// load tracklist
tracklist = FileHelper.readTracklist(context)
tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date }
tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date }
}
@ -111,7 +112,8 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
val backgroundJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + backgroundJob)
uiScope.launch {
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, position, tracklist) }
val deferred: Deferred<Tracklist> =
async { FileHelper.deleteTrackSuspended(context, position, tracklist) }
// wait for result and store in tracklist
tracklist = deferred.await()
notifyItemRemoved(position)
@ -122,7 +124,7 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/* Finds current position of track element in adapter list */
fun findPosition(trackId: Long): Int {
tracklist.tracklistElements.forEachIndexed {index, tracklistElement ->
tracklist.tracklistElements.forEachIndexed { index, tracklistElement ->
if (tracklistElement.getTrackId() == trackId) return index
}
return -1
@ -143,7 +145,11 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
}
}
GlobalScope.launch {
FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time)
FileHelper.saveTracklistSuspended(
context,
tracklist,
GregorianCalendar.getInstance().time
)
}
}
@ -152,11 +158,17 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
private fun createTrackDataString(position: Int): String {
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
val trackDataString: String
when (tracklistElement.name == tracklistElement.dateString) {
trackDataString = when (tracklistElement.name == tracklistElement.dateString) {
// CASE: no individual name set - exclude date
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)}${tracklistElement.durationString}"
true -> "${LengthUnitHelper.convertDistanceToString(
tracklistElement.length,
useImperial
)} ${tracklistElement.durationString}"
// CASE: no individual name set - include date
false -> trackDataString = "${tracklistElement.dateString}${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)}${tracklistElement.durationString}"
false -> "${tracklistElement.dateString}${LengthUnitHelper.convertDistanceToString(
tracklistElement.length,
useImperial
)} ${tracklistElement.durationString}"
}
return trackDataString
}
@ -165,7 +177,8 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/*
* Inner class: DiffUtil.Callback that determines changes in data - improves list performance
*/
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() {
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.tracklistElements[oldItemPosition]
@ -195,7 +208,8 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
/*
* Inner class: ViewHolder for a track element
*/
private inner class TrackElementViewHolder (trackElementLayout: View): RecyclerView.ViewHolder(trackElementLayout) {
private inner class TrackElementViewHolder(trackElementLayout: View) :
RecyclerView.ViewHolder(trackElementLayout) {
val trackElement: ConstraintLayout = trackElementLayout.findViewById(R.id.track_element)
val trackNameView: TextView = trackElementLayout.findViewById(R.id.track_name)
val trackDataView: TextView = trackElementLayout.findViewById(R.id.track_data)

View File

@ -51,18 +51,25 @@ import org.y20k.trackbook.helpers.PreferencesHelper
/*
* MapFragmentLayoutHolder class
*/
data class MapFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlay.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, private val startLocation: Location, private val trackingState: Int) {
data class MapFragmentLayoutHolder(
private var context: Context,
private var markerListener: MapOverlay.MarkerListener,
private var inflater: LayoutInflater,
private var container: ViewGroup?,
private val startLocation: Location,
private val trackingState: Int
) {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(MapFragmentLayoutHolder::class.java)
/* Main class variables */
val rootView: View
val mapView: MapView
val rootView: View = inflater.inflate(R.layout.fragment_map, container, false)
private val mapView: MapView
val currentLocationButton: FloatingActionButton
val recordingButton: FloatingActionButton
val recordingButtonSubMenu: Group
private val recordingButtonSubMenu: Group
val saveButton: FloatingActionButton
val clearButton: FloatingActionButton
val resumeButton: FloatingActionButton
@ -77,7 +84,6 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Init block */
init {
// find views
rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.fab_location_button)
recordingButton = rootView.findViewById(R.id.fab_main_button)
@ -102,14 +108,19 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
}
// add compass to map
val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
val compassOverlay =
CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
compassOverlay.enableCompass()
compassOverlay.setCompassCenter(36f, 60f)
mapView.overlays.add(compassOverlay)
// add my location overlay
currentPositionOverlay = MapOverlay(markerListener).createMyLocationOverlay(context, startLocation, trackingState)
currentPositionOverlay = MapOverlay(markerListener).createMyLocationOverlay(
context,
startLocation,
trackingState
)
mapView.overlays.add(currentPositionOverlay)
centerMap(startLocation)
@ -127,7 +138,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Listen for user interaction */
@SuppressLint("ClickableViewAccessibility")
private fun addInteractionListener() {
mapView.setOnTouchListener { v, event ->
mapView.setOnTouchListener { _, _ ->
userInteraction = true
false
}
@ -148,7 +159,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Save current best location and state of map to shared preferences */
fun saveState(currentBestLocation: Location) {
PreferencesHelper.saveCurrentBestLocation(context, currentBestLocation)
PreferencesHelper.saveZoomLevel(context, mapView.getZoomLevelDouble())
PreferencesHelper.saveZoomLevel(context, mapView.zoomLevelDouble)
// reset user interaction state
userInteraction = false
}
@ -157,7 +168,8 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
/* Mark current position on map */
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_NOT) {
mapView.overlays.remove(currentPositionOverlay)
currentPositionOverlay = MapOverlay(markerListener).createMyLocationOverlay(context, location, trackingState)
currentPositionOverlay =
MapOverlay(markerListener).createMyLocationOverlay(context, location, trackingState)
mapView.overlays.add(currentPositionOverlay)
}
@ -168,7 +180,8 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
mapView.overlays.remove(currentTrackOverlay)
}
if (track.wayPoints.isNotEmpty()) {
currentTrackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, trackingState)
currentTrackOverlay =
MapOverlay(markerListener).createTrackOverlay(context, track, trackingState)
mapView.overlays.add(currentTrackOverlay)
}
}
@ -201,10 +214,13 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
}
/* Toggles content and visibility of the location error snackbar */
fun toggleLocationErrorBar(gpsProviderActive: Boolean, networkProviderActive: Boolean) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_DENIED
) {
// CASE: Location permission not granted
locationErrorBar.setText(R.string.snackbar_message_location_permission_denied)
locationErrorBar.show()
@ -218,4 +234,4 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
}
}
}
}

View File

@ -50,14 +50,20 @@ import kotlin.math.roundToInt
/*
* TrackFragmentLayoutHolder class
*/
data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlay.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, var track: Track) {
data class TrackFragmentLayoutHolder(
private var context: Context,
private var markerListener: MapOverlay.MarkerListener,
private var inflater: LayoutInflater,
private var container: ViewGroup?,
var track: Track
) {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java)
/* Main class variables */
val rootView: View
val rootView: View = inflater.inflate(R.layout.fragment_track, container, false)
val shareButton: ImageButton
val deleteButton: ImageButton
val editButton: ImageButton
@ -90,7 +96,6 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
/* Init block */
init {
// find views
rootView = inflater.inflate(R.layout.fragment_track, container, false)
mapView = rootView.findViewById(R.id.map)
shareButton = rootView.findViewById(R.id.save_button)
deleteButton = rootView.findViewById(R.id.delete_button)
@ -134,13 +139,15 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
}
// add compass to map
val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
val compassOverlay =
CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
compassOverlay.enableCompass()
compassOverlay.setCompassCenter(36f, 60f)
mapView.overlays.add(compassOverlay)
// create map overlay
trackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
trackOverlay =
MapOverlay(markerListener).createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
if (track.wayPoints.isNotEmpty()) {
mapView.overlays.add(trackOverlay)
}
@ -168,7 +175,11 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
mapView.overlays.remove(trackOverlay)
}
if (track.wayPoints.isNotEmpty()) {
trackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
trackOverlay = MapOverlay(markerListener).createTrackOverlay(
context,
track,
Keys.STATE_TRACKING_NOT
)
mapView.overlays.add(trackOverlay)
}
}
@ -190,9 +201,9 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
private fun setupStatisticsViews() {
// get step count string
val steps: String
if (track.stepCount == -1f) steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
else steps = track.stepCount.roundToInt().toString()
val steps: String =
if (track.stepCount == -1f) context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
else track.stepCount.roundToInt().toString()
// populate views
trackNameView.text = track.name
@ -200,19 +211,29 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
stepsView.text = steps
waypointsView.text = track.wayPoints.size.toString()
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration)
velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.length, useImperialUnits)
velocityView.text = LengthUnitHelper.convertToVelocityString(
track.duration,
track.recordingPaused,
track.length,
useImperialUnits
)
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart)
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart)
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits)
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits)
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits)
maxAltitudeView.text =
LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)
minAltitudeView.text =
LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits)
positiveElevationView.text =
LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits)
negativeElevationView.text =
LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits)
// show / hide recording pause
if (track.recordingPaused != 0L) {
recordingPausedLabelView.visibility = View.VISIBLE
recordingPausedView.visibility = View.VISIBLE
recordingPausedView.text = DateTimeHelper.convertToReadableTime(context, track.recordingPaused)
recordingPausedView.text =
DateTimeHelper.convertToReadableTime(context, track.recordingPaused)
} else {
recordingPausedLabelView.visibility = View.GONE
recordingPausedView.visibility = View.GONE
@ -220,8 +241,9 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
// inform user about possible accuracy issues with altitude measurements
elevationDataViews.referencedIds.forEach { id ->
(rootView.findViewById(id) as View).setOnClickListener{
Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show()
(rootView.findViewById(id) as View).setOnClickListener {
Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG)
.show()
}
}
// make track name on statistics sheet clickable
@ -234,7 +256,8 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
/* Shows/hides the statistics sheet */
private fun toggleStatisticsSheetVisibility() {
when (statisticsSheetBehavior.state) {
BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state =
BottomSheetBehavior.STATE_COLLAPSED
else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
@ -246,26 +269,31 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
override fun onStateChanged(bottomSheet: View, newState: Int) {
when (newState) {
BottomSheetBehavior.STATE_EXPANDED -> {
statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded)
statisticsSheet.background =
context.getDrawable(R.drawable.shape_statistics_background_expanded)
trackManagementViews.visibility = View.VISIBLE
shareButton.visibility = View.GONE
// bottomSheet.setPadding(0,24,0,0)
}
else -> {
statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed)
statisticsSheet.background =
context.getDrawable(R.drawable.shape_statistics_background_collapsed)
trackManagementViews.visibility = View.GONE
shareButton.visibility = View.VISIBLE
// bottomSheet.setPadding(0,0,0,0)
}
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (slideOffset < 0.125f) {
statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed)
statisticsSheet.background =
context.getDrawable(R.drawable.shape_statistics_background_collapsed)
trackManagementViews.visibility = View.GONE
shareButton.visibility = View.VISIBLE
} else {
statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded)
statisticsSheet.background =
context.getDrawable(R.drawable.shape_statistics_background_expanded)
trackManagementViews.visibility = View.VISIBLE
shareButton.visibility = View.GONE
}
@ -274,4 +302,4 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
}
}
}

View File

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM16,12v3c0,0.22 -0.03,0.47 -0.07,0.7l-0.1,0.65 -0.37,0.65c-0.72,1.24 -2.04,2 -3.46,2s-2.74,-0.77 -3.46,-2l-0.37,-0.64 -0.1,-0.65C8.03,15.48 8,15.23 8,15v-4c0,-0.23 0.03,-0.48 0.07,-0.7l0.1,-0.65 0.37,-0.65c0.3,-0.52 0.72,-0.97 1.21,-1.31l0.57,-0.39 0.74,-0.18c0.31,-0.08 0.63,-0.12 0.94,-0.12 0.32,0 0.63,0.04 0.95,0.12l0.68,0.16 0.61,0.42c0.5,0.34 0.91,0.78 1.21,1.31l0.38,0.65 0.1,0.65c0.04,0.22 0.07,0.47 0.07,0.69v1zM10,14h4v2h-4zM10,10h4v2h-4z"/>
<path
android:fillColor="@color/icon_default"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM16,12v3c0,0.22 -0.03,0.47 -0.07,0.7l-0.1,0.65 -0.37,0.65c-0.72,1.24 -2.04,2 -3.46,2s-2.74,-0.77 -3.46,-2l-0.37,-0.64 -0.1,-0.65C8.03,15.48 8,15.23 8,15v-4c0,-0.23 0.03,-0.48 0.07,-0.7l0.1,-0.65 0.37,-0.65c0.3,-0.52 0.72,-0.97 1.21,-1.31l0.57,-0.39 0.74,-0.18c0.31,-0.08 0.63,-0.12 0.94,-0.12 0.32,0 0.63,0.04 0.95,0.12l0.68,0.16 0.61,0.42c0.5,0.34 0.91,0.78 1.21,1.31l0.38,0.65 0.1,0.65c0.04,0.22 0.07,0.47 0.07,0.69v1zM10,14h4v2h-4zM10,10h4v2h-4z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"
android:fillColor="@color/trackbook_white" />
android:fillColor="@color/trackbook_white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M21,3L3,10.53v0.98l6.84,2.65L12.48,21h0.98L21,3z"/>
android:pathData="M21,3L3,10.53v0.98l6.84,2.65L12.48,21h0.98L21,3z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/location_button_background"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
</vector>

View File

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M16,9v10H8V9h8m-1.5,-6h-5l-1,1H5v2h14V4h-3.5l-1,-1zM18,7H6v12c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7z"/>
<path
android:fillColor="@color/icon_default"
android:pathData="M16,9v10H8V9h8m-1.5,-6h-5l-1,1H5v2h14V4h-3.5l-1,-1zM18,7H6v12c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_default"
android:pathData="M14.06,9.02l0.92,0.92L5.92,19L5,19v-0.92l9.06,-9.06M17.66,3c-0.25,0 -0.51,0.1 -0.7,0.29l-1.83,1.83 3.75,3.75 1.83,-1.83c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.2,-0.2 -0.45,-0.29 -0.71,-0.29zM14.06,6.19L3,17.25L3,21h3.75L17.81,9.94l-3.75,-3.75z"/>
android:pathData="M14.06,9.02l0.92,0.92L5.92,19L5,19v-0.92l9.06,-9.06M17.66,3c-0.25,0 -0.51,0.1 -0.7,0.29l-1.83,1.83 3.75,3.75 1.83,-1.83c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.2,-0.2 -0.45,-0.29 -0.71,-0.29zM14.06,6.19L3,17.25L3,21h3.75L17.81,9.94l-3.75,-3.75z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="#DC2B00"/>
android:fillColor="#DC2B00"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="@color/trackbook_white" />
android:fillColor="@color/trackbook_white"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_default"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
</vector>

View File

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
<path
android:fillColor="@color/icon_default"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>

View File

@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillAlpha="0.33"
android:fillColor="@color/trackbook_blue"
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0" />
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillAlpha="0.33"
android:fillColor="@color/trackbook_blue"
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0" />
<path
android:fillColor="@color/trackbook_grey_light"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillAlpha="0.33"
android:fillColor="@color/trackbook_red"
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0" />
<path
android:fillColor="@color/trackbook_red"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillAlpha="0.33"
android:fillColor="@color/trackbook_red"
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
android:pathData="M48,48m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0" />
<path
android:fillColor="@color/trackbook_grey_light"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@color/trackbook_grey_light"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@color/trackbook_red"
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
android:pathData="M48,48m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0" />
</vector>

View File

@ -1,12 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp"
android:height="24dp"
android:width="24dp">
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M44,12.99L20.69,8.74L5.12,5.89C2.29,5.38 0,7.44 0,10.49v67.38c0,3.06 2.29,5.96 5.12,6.47l15.57,2.85l22.19,4.05L44,91.46V12.99z"/>
android:pathData="M44,12.99L20.69,8.74L5.12,5.89C2.29,5.38 0,7.44 0,10.49v67.38c0,3.06 2.29,5.96 5.12,6.47l15.57,2.85l22.19,4.05L44,91.46V12.99z" />
<path
android:fillColor="@color/trackbook_black"
android:pathData="M90.88,5.89L75.31,8.74L53.12,12.79L52,12.99v78.46l23.31,-4.26l15.57,-2.85c2.83,-0.52 5.12,-3.41 5.12,-6.47V10.49C96,7.44 93.71,5.38 90.88,5.89z"/>
android:pathData="M90.88,5.89L75.31,8.74L53.12,12.79L52,12.99v78.46l23.31,-4.26l15.57,-2.85c2.83,-0.52 5.12,-3.41 5.12,-6.47V10.49C96,7.44 93.71,5.38 90.88,5.89z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="36dp">
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
</vector>
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="36dp">
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="36dp">
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M6,6h12v12H6z"/>
android:pathData="M6,6h12v12H6z" />
</vector>

View File

@ -1,12 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportHeight="192.0"
android:viewportWidth="192.0"
android:width="48dp" >
android:viewportHeight="192.0">
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M96,96m-96,0a96,96 0,1 1,192 0a96,96 0,1 1,-192 0"/>
android:pathData="M96,96m-96,0a96,96 0,1 1,192 0a96,96 0,1 1,-192 0" />
<path
android:fillColor="@color/trackbook_red_dark"
android:pathData="M48,96a48,46.5 0,1 0,96 0a48,46.5 0,1 0,-96 0z"/>
android:pathData="M48,96a48,46.5 0,1 0,96 0a48,46.5 0,1 0,-96 0z" />
</vector>

View File

@ -1,12 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportHeight="192.0"
android:viewportWidth="192.0"
android:width="48dp" >
android:viewportHeight="192.0">
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M96,96m-96,0a96,96 0,1 1,192 0a96,96 0,1 1,-192 0"/>
android:pathData="M96,96m-96,0a96,96 0,1 1,192 0a96,96 0,1 1,-192 0" />
<path
android:fillColor="@color/trackbook_white"
android:pathData="M48,96a48,46.5 0,1 0,96 0a48,46.5 0,1 0,-96 0z"/>
android:pathData="M48,96a48,46.5 0,1 0,96 0a48,46.5 0,1 0,-96 0z" />
</vector>

View File

@ -1,12 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="96.0"
android:viewportWidth="96.0"
android:width="24dp"
android:height="24dp"
android:width="24dp">
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@color/trackbook_white"
android:pathData="M44,12.99L20.69,8.74L5.12,5.89C2.29,5.38 0,7.44 0,10.49v67.38c0,3.06 2.29,5.96 5.12,6.47l15.57,2.85l22.19,4.05L44,91.46V12.99z"/>
android:pathData="M44,12.99L20.69,8.74L5.12,5.89C2.29,5.38 0,7.44 0,10.49v67.38c0,3.06 2.29,5.96 5.12,6.47l15.57,2.85l22.19,4.05L44,91.46V12.99z" />
<path
android:fillColor="@color/trackbook_white"
android:pathData="M90.88,5.89L75.31,8.74L53.12,12.79L52,12.99v78.46l23.31,-4.26l15.57,-2.85c2.83,-0.52 5.12,-3.41 5.12,-6.47V10.49C96,7.44 93.71,5.38 90.88,5.89z"/>
android:pathData="M90.88,5.89L75.31,8.74L53.12,12.79L52,12.99v78.46l23.31,-4.26l15.57,-2.85c2.83,-0.52 5.12,-3.41 5.12,-6.47V10.49C96,7.44 93.71,5.38 90.88,5.89z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z"/>
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z" />
</vector>

View File

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M19,12v7L5,19v-7L3,12v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5 -5,-5 1.41,-1.41L11,12.67L11,3h2z"/>
<path
android:fillColor="@color/icon_default"
android:pathData="M19,12v7L5,19v-7L3,12v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5 -5,-5 1.41,-1.41L11,12.67L11,3h2z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_white"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/trackbook_black"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_default"
android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z"/>
android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
</vector>

View File

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M17.66,17.66l-1.06,1.06l-0.71,-0.71l1.06,-1.06l-1.94,-1.94l-1.06,1.06l-0.71,-0.71l1.06,-1.06l-1.94,-1.94l-1.06,1.06l-0.71,-0.71l1.06,-1.06L9.7,9.7l-1.06,1.06l-0.71,-0.71l1.06,-1.06L7.05,7.05L5.99,8.11L5.28,7.4l1.06,-1.06L4,4v14c0,1.1 0.9,2 2,2h14L17.66,17.66zM7,17v-5.76L12.76,17H7z"/>
<path
android:fillColor="@color/icon_default"
android:pathData="M17.66,17.66l-1.06,1.06l-0.71,-0.71l1.06,-1.06l-1.94,-1.94l-1.06,1.06l-0.71,-0.71l1.06,-1.06l-1.94,-1.94l-1.06,1.06l-0.71,-0.71l1.06,-1.06L9.7,9.7l-1.06,1.06l-0.71,-0.71l1.06,-1.06L7.05,7.05L5.99,8.11L5.28,7.4l1.06,-1.06L4,4v14c0,1.1 0.9,2 2,2h14L17.66,17.66zM7,17v-5.76L12.76,17H7z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_star_selected"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_lightweight"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_red"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_default"
android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,8h2v4h2L7,8h2v4h2L11,8h2v4h2L15,8h2v4h2L19,8h2v8z"/>
android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,8h2v4h2L7,8h2v4h2L11,8h2v4h2L15,8h2v4h2L19,8h2v8z" />
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/icon_default"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z" />
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" android:color="@color/bottom_navigation_element" />
<item android:state_checked="true" android:color="@color/bottom_navigation_element_selected" />
<item android:color="@color/bottom_navigation_element" android:state_checked="false" />
<item android:color="@color/bottom_navigation_element_selected" android:state_checked="true" />
</selector>

View File

@ -2,17 +2,20 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="0dp"
android:bottom="-2dp"
android:left="-2dp"
android:right="-2dp"
android:bottom="-2dp">
android:top="0dp">
<shape
android:shape="rectangle">
<shape android:shape="rectangle">
<solid android:color="@color/statistic_sheet_background_collapsed" />
<corners android:topLeftRadius="20dp" android:topRightRadius="20dp" />
<stroke android:width="1dp" android:color="@color/statistic_sheet_background_border"/>
<corners
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
<stroke
android:width="1dp"
android:color="@color/statistic_sheet_background_border" />
</shape>

View File

@ -2,17 +2,20 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="0dp"
android:bottom="-2dp"
android:left="-2dp"
android:right="-2dp"
android:bottom="-2dp">
android:top="0dp">
<shape
android:shape="rectangle">
<shape android:shape="rectangle">
<solid android:color="@color/statistic_sheet_background_expanded" />
<corners android:topLeftRadius="20dp" android:topRightRadius="20dp" />
<stroke android:width="1dp" android:color="@color/statistic_sheet_background_border"/>
<corners
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
<stroke
android:width="1dp"
android:color="@color/statistic_sheet_background_border" />
</shape>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -54,4 +53,4 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_details_link" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -29,4 +28,4 @@
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map_fragment"
@ -14,8 +13,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/descr_map_current_track"
android:visibility="visible">
</org.osmdroid.views.MapView>
android:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -43,4 +42,4 @@
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -8,26 +7,26 @@
android:background="@color/app_window_background"
tools:context=".TracklistFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_element_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="28dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_element_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="28dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- ONBOARDING -->
<include
layout="@layout/track_onboarding"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/track_element_list"
tools:visibility="gone" />
<!-- ONBOARDING -->
<include
layout="@layout/track_onboarding"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/track_element_list"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/track_list_onboarding"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/track_list_onboarding">
android:layout_height="match_parent">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -27,7 +27,7 @@
<fragment
android:id="@+id/settings_fragment"
android:name="org.y20k.trackbook.SettingsFragment"
android:label="Settings"/>
android:label="Settings" />
<!-- TRACKS (LIST OF TRACKS) -->
<fragment
@ -46,11 +46,11 @@
android:id="@+id/fragment_track"
android:name="org.y20k.trackbook.TrackFragment"
android:label="Track"
tools:layout="@layout/fragment_track" >
tools:layout="@layout/fragment_track">
<argument
android:name="delete_track_id"
app:argType="long"
android:defaultValue="-1L" />
android:defaultValue="-1L"
app:argType="long" />
</fragment>
</navigation>

View File

@ -1,95 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- placeholder text - todo remove -->
<!-- App Name -->
<string name="app_name">Trackbook</string>
<!-- please do not translate app_name - transcription into different alphabet types is fine though -->
<!-- Tabs -->
<string name="tab_map">地图</string>
<string name="tab_tracks">记录</string>
<string name="tab_settings">设置</string>
<!-- Notification -->
<string name="notification_title_trackbook_running">Trackbook 正在运行</string>
<string name="notification_title_trackbook_not_running">Trackbook 已暂停</string>
<string name="notification_stop">暂停</string>
<string name="notification_resume">恢复</string>
<string name="notification_show">显示</string>
<string name="notification_channel_recording_name">运动记录状态</string>
<string name="notification_channel_recording_description">显示持续时间和距离.停止移动录制的选项.</string>
<!-- Snackbar Messages -->
<string name="snackbar_message_location_offline">位置服务已关闭Trackbook 将不会运行.</string>
<string name="snackbar_message_location_permission_denied">位置权限未被授予 Trackbook 无法工作.</string>
<!-- FAB Sub Menu -->
<string name="fab_sub_menu_clear">删除</string>
<string name="fab_sub_menu_save">保存</string>
<string name="fab_sub_menu_resume">恢复</string>
<!-- Dialogs -->
<string name="dialog_generic_button_cancel">取消</string>
<string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_details_button">显示详情</string>
<string name="dialog_error_empty_recording_title">无法保存</string>
<string name="dialog_error_empty_recording_message">Trackbook目前没有记录到任何航点.</string>
<string name="dialog_error_empty_recording_action_resume">恢复记录</string>
<string name="dialog_rename_track_button">重命名</string>
<string name="dialog_rename_track_input_hint">输入一个新名称</string>
<string name="dialog_share_gpx">分享GPX文件到</string>
<string name="dialog_yes_no_positive_button_default"></string>
<string name="dialog_yes_no_positive_button_delete_recording">删除</string>
<string name="dialog_yes_no_message_delete_recording">删除此记录?</string>
<!-- Toast Messages -->
<string name="toast_message_elevation_info">提示:海拔数据的准确性取决于您的设备.测量整个路线的上坡和下坡海拔.</string>
<string name="toast_message_install_file_helper">请先安装一个文件管理器或GPX轨迹查看器.</string>
<!-- placeholder text - todo remove -->
<!-- App Name -->
<string name="app_name">Trackbook</string>
<!-- please do not translate app_name - transcription into different alphabet types is fine though -->
<!-- Tabs -->
<string name="tab_map">地图</string>
<string name="tab_tracks">记录</string>
<string name="tab_settings">设置</string>
<!-- Notification -->
<string name="notification_title_trackbook_running">Trackbook 正在运行</string>
<string name="notification_title_trackbook_not_running">Trackbook 已暂停</string>
<string name="notification_stop">暂停</string>
<string name="notification_resume">恢复</string>
<string name="notification_show">显示</string>
<string name="notification_channel_recording_name">运动记录状态</string>
<string name="notification_channel_recording_description">显示持续时间和距离.停止移动录制的选项.</string>
<!-- Snackbar Messages -->
<string name="snackbar_message_location_offline">位置服务已关闭Trackbook 将不会运行.</string>
<string name="snackbar_message_location_permission_denied">位置权限未被授予 Trackbook 无法工作.</string>
<!-- FAB Sub Menu -->
<string name="fab_sub_menu_clear">删除</string>
<string name="fab_sub_menu_save">保存</string>
<string name="fab_sub_menu_resume">恢复</string>
<!-- Dialogs -->
<string name="dialog_generic_button_cancel">取消</string>
<string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_details_button">显示详情</string>
<string name="dialog_error_empty_recording_title">无法保存</string>
<string name="dialog_error_empty_recording_message">Trackbook目前没有记录到任何航点.</string>
<string name="dialog_error_empty_recording_action_resume">恢复记录</string>
<string name="dialog_rename_track_button">重命名</string>
<string name="dialog_rename_track_input_hint">输入一个新名称</string>
<string name="dialog_share_gpx">分享GPX文件到</string>
<string name="dialog_yes_no_positive_button_default"></string>
<string name="dialog_yes_no_positive_button_delete_recording">删除</string>
<string name="dialog_yes_no_message_delete_recording">删除此记录?</string>
<!-- Toast Messages -->
<string name="toast_message_elevation_info">提示:海拔数据的准确性取决于您的设备.测量整个路线的上坡和下坡海拔.</string>
<string name="toast_message_install_file_helper">请先安装一个文件管理器或GPX轨迹查看器.</string>
<!-- Map Markers -->
<string name="marker_description_time">时间</string>
<string name="marker_description_accuracy">误差</string>
<!-- Statistics Sheet -->
<string name="statistics_sheet_p_distance">总里程:</string>
<string name="statistics_sheet_p_steps">步数:</string>
<string name="statistics_sheet_p_steps_no_pedometer">计步器不可用</string>
<string name="statistics_sheet_p_waypoints">航点数量:</string>
<string name="statistics_sheet_p_duration">用时:</string>
<string name="statistics_sheet_p_recording_start">追踪开始:</string>
<string name="statistics_sheet_p_recording_stop">追踪结束:</string>
<string name="statistics_sheet_p_max_altitude">最高航点:</string>
<string name="statistics_sheet_p_min_altitude">最低航点:</string>
<string name="statistics_sheet_p_positive_elevation">海拔(上坡):</string>
<string name="statistics_sheet_p_negative_elevation">海拔(下坡):</string>
<!-- Menu Actions -->
<!-- Onboarding Layout -->
<string name="layout_onboarding_description_app_icon">Trackbook 应用图标</string>
<!-- Track Tab Onboarding -->
<string name="track_list_onboarding_h1_part_1">你的运动记录</string>
<string name="track_list_onboarding_h1_part_2">... 会显示在这里.</string>
<!-- Settings -->
<string name="pref_accuracy_threshold_summary">丢弃误差大于</string>
<string name="pref_accuracy_threshold_title">误差的阈值</string>
<string name="pref_advanced_title">高级</string>
<string name="pref_general_title">一般</string>
<string name="pref_gps_only_title">仅限于GPS</string>
<string name="pref_gps_only_summary_gps_and_network">当前使用GPS和网络进行定位.</string>
<string name="pref_gps_only_summary_gps_only">当前使用GPS进行定位.</string>
<string name="pref_imperial_measurement_units_summary_metric">当前使用公制单位(千米,米).</string>
<string name="pref_imperial_measurement_units_summary_imperial">当前使用英制单位(英里,英尺).</string>
<string name="pref_imperial_measurement_units_title">使用英制</string>
<string name="pref_reset_advanced_summary">高级设置重置为默认值.</string>
<string name="pref_reset_advanced_title">重置</string>
<!-- Abbreviations -->
<string name="abbreviation_hours"></string>
<string name="abbreviation_minutes"></string>
<string name="abbreviation_seconds"></string>
<!-- Descriptions -->
<string name="descr_map_current_track">航迹图</string>
<string name="descr_map_last_track">最后一个轨迹的航迹图</string>
<string name="descr_fab_main_start">开始录制按钮</string>
<string name="descr_fab_sub_menu_button_save">保存按钮</string>
<string name="descr_fab_sub_menu_button_clear">删除按钮</string>
<string name="descr_fab_sub_menu_button_resume">恢复按钮</string>
<string name="descr_mark_starred_button">加星按钮</string>
<string name="descr_statistics_sheet_delete_button">删除记录按钮</string>
<string name="descr_statistics_sheet_edit_button">记录编辑按钮</string>
<string name="descr_statistics_sheet_save_button">以GPX文件分享按钮</string>
<!-- Sample Text -->
<string name="sample_text_track_data" translatable="false">23.0 km • 5 时 23 分 42 秒</string>
<string name="sample_text_track_name" translatable="false">1969年7月20日</string>
<string name="sample_text_default_data" translatable="false">轨迹数据丢失</string>
<string name="marker_description_time">时间</string>
<string name="marker_description_accuracy">误差</string>
<!-- Statistics Sheet -->
<string name="statistics_sheet_p_distance">总里程:</string>
<string name="statistics_sheet_p_steps">步数:</string>
<string name="statistics_sheet_p_steps_no_pedometer">计步器不可用</string>
<string name="statistics_sheet_p_waypoints">航点数量:</string>
<string name="statistics_sheet_p_duration">用时:</string>
<string name="statistics_sheet_p_recording_start">追踪开始:</string>
<string name="statistics_sheet_p_recording_stop">追踪结束:</string>
<string name="statistics_sheet_p_max_altitude">最高航点:</string>
<string name="statistics_sheet_p_min_altitude">最低航点:</string>
<string name="statistics_sheet_p_positive_elevation">海拔(上坡):</string>
<string name="statistics_sheet_p_negative_elevation">海拔(下坡):</string>
<!-- Menu Actions -->
<!-- Onboarding Layout -->
<string name="layout_onboarding_description_app_icon">Trackbook 应用图标</string>
<!-- Track Tab Onboarding -->
<string name="track_list_onboarding_h1_part_1">你的运动记录</string>
<string name="track_list_onboarding_h1_part_2">... 会显示在这里.</string>
<!-- Settings -->
<string name="pref_accuracy_threshold_summary">丢弃误差大于</string>
<string name="pref_accuracy_threshold_title">误差的阈值</string>
<string name="pref_advanced_title">高级</string>
<string name="pref_general_title">一般</string>
<string name="pref_gps_only_title">仅限于GPS</string>
<string name="pref_gps_only_summary_gps_and_network">当前使用GPS和网络进行定位.</string>
<string name="pref_gps_only_summary_gps_only">当前使用GPS进行定位.</string>
<string name="pref_imperial_measurement_units_summary_metric">当前使用公制单位(千米,米).</string>
<string name="pref_imperial_measurement_units_summary_imperial">当前使用英制单位(英里,英尺).</string>
<string name="pref_imperial_measurement_units_title">使用英制</string>
<string name="pref_reset_advanced_summary">高级设置重置为默认值.</string>
<string name="pref_reset_advanced_title">重置</string>
<!-- Abbreviations -->
<string name="abbreviation_hours"></string>
<string name="abbreviation_minutes"></string>
<string name="abbreviation_seconds"></string>
<!-- Descriptions -->
<string name="descr_map_current_track">航迹图</string>
<string name="descr_map_last_track">最后一个轨迹的航迹图</string>
<string name="descr_fab_main_start">开始录制按钮</string>
<string name="descr_fab_sub_menu_button_save">保存按钮</string>
<string name="descr_fab_sub_menu_button_clear">删除按钮</string>
<string name="descr_fab_sub_menu_button_resume">恢复按钮</string>
<string name="descr_mark_starred_button">加星按钮</string>
<string name="descr_statistics_sheet_delete_button">删除记录按钮</string>
<string name="descr_statistics_sheet_edit_button">记录编辑按钮</string>
<string name="descr_statistics_sheet_save_button">以GPX文件分享按钮</string>
<!-- Sample Text -->
<string name="sample_text_track_data" translatable="false">23.0 km • 5 时 23 分 42 秒</string>
<string name="sample_text_track_name" translatable="false">1969年7月20日</string>
<string name="sample_text_default_data" translatable="false">轨迹数据丢失</string>
</resources>

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources />

View File

@ -31,17 +31,21 @@
<!-- COLOR NAMES -->
<color name="trackbook_grey">#FF595959</color> <!-- derived from recommended dark theme surface color -->
<!-- derived from recommended dark theme surface color -->
<color name="trackbook_grey">#FF595959</color>
<color name="trackbook_grey_light">#FF7D7D7D</color>
<color name="trackbook_grey_lighter">#FFDADADA</color>
<color name="trackbook_grey_very_light">#FFF2F2F2</color>
<color name="trackbook_grey_dark">#FF414141</color>
<color name="trackbook_grey_darker">#FF2D2D2D</color>
<color name="trackbook_red">#DC3D33</color> <!-- Slightly muted variant of -> Material Design 2: Red 600 -->
<!-- Slightly muted variant of -> Material Design 2: Red 600 -->
<color name="trackbook_red">#DC3D33</color>
<color name="trackbook_red_dark">#FFCA2D23</color>
<color name="trackbook_black">#FF121212</color> <!-- Material Design recommended dark theme surface color -->
<!-- Material Design recommended dark theme surface color -->
<color name="trackbook_black">#FF121212</color>
<color name="trackbook_white">#FFFFFFFF</color>
<color name="trackbook_transparent">#00ffffff</color>

View File

@ -1,6 +1,9 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<item name="custom_match_parent" type="dimen">-1</item> <!-- CUSTOM MATCH_PARENT - see https://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html#MATCH_PARENT -->
<!-- CUSTOM MATCH_PARENT - see https://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html#MATCH_PARENT -->
<item name="custom_match_parent" type="dimen">-1</item>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="bottom_sheet_width">@dimen/custom_match_parent</dimen>
</resources>

View File

@ -20,9 +20,11 @@
<item name="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
</style>
<style name="NegativeButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
<item name="android:textColor">@color/text_default</item>
</style>
<style name="PositiveButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
<item name="android:textColor">@color/text_default</item>
</style>

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
</paths>
<external-path
name="external_files"
path="." />
</paths>

View File

@ -1,16 +1,19 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.72'
ext {
kotlin_version = '1.3.72'
navigation_version = '2.3.0'
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@ -20,7 +23,6 @@ allprojects {
repositories {
google()
jcenter()
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Sat May 30 22:16:24 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

53
gradlew vendored
View File

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

22
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@ -65,6 +84,7 @@ set CMD_LINE_ARGS=%*
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

View File

@ -1,2 +1,3 @@
# F-Droid Assets
This folder contains the assets used on [Trackbook's F-Droid store page](https://f-droid.org/repository/browse/?fdid=org.y20k.trackbook). More about adding assets -> [All About Descriptions, Graphics, and Screenshots](https://f-droid.org/en/docs/All_About_Descriptions_Graphics_and_Screenshots/).
This folder contains the assets used on [Trackbook's F-Droid store page](https://f-droid.org/packages/org.y20k.trackbook/).
More about adding assets -> [All About Descriptions, Graphics, and Screenshots](https://f-droid.org/docs/All_About_Descriptions_Graphics_and_Screenshots/).

View File

@ -1,17 +1,17 @@
Trackbook is a bare bones app for recording your movements. Trackbook is great for hiking, vacation or workout. Once started it traces your movements on a map. The map data is provided by OpenStreetMap (OSM).
Trackbook is a bare-bones app for recording your movements. Trackbook is great for hiking, vacationing, or working out. Once started, it traces your movements on a map. The map data is provided by OpenStreetMap (OSM).
Trackbook is free software. It is published under the MIT open source license. Trackbook uses osmdroid to display the map, which is also free software published under the Apache License.
Trackbook is free software. It is published under the MIT open-source license. Trackbook uses osmdroid to display the map, which is also free software published under the Apache License.
GOOD TO KNOW
------------
START RECORDING VIA QUICK SETTINGS TILE
You can start a recording without opening Trackbook. Just pull down the System's Quick Settings and tap on the Start Recording tile. You need to manually add Trackbook's Recording tile to Quick Settings first. Information on customizing Quick Settings:
https://support.google.com/android/answer/9083864?hl=en
You can start a recording without opening Trackbook. Just pull down the System's Quick Settings and tap on the Start Recording tile. You'll need to manually add Trackbook's Recording tile to Quick Settings first. Information on customizing Quick Settings:
https://support.google.com/android/answer/9083864
https://www.xda-developers.com/get-custom-quick-settings-tiles/
SAVE RECORDINGS AS GPX
Recordings can be exported as GPX (GPS Exchange Format). Tap on the save button in the lower right corner of a previously recorded track.
Recordings can be exported as GPX (GPS Exchange Format). Tap on the save button in the lower-right corner of a previously recorded track.
COPY GPX FILES MANUALLY
Trackbook automatically generates GPX files for every recording. You can find them in the folder /Android/data/org.y20k.trackbook/files/gpx/ on your device's storage.
@ -23,7 +23,7 @@ HOW DOES TRACKBOOK MEASURE ALTITUDE?
Many devices have altitude sensors (of varying accuracy). Trackbook compares the altitude of each new marker with the previously stored altitude. The difference is added to either the uphill or downhill elevation value.
WHAT DOES ACCURACY THRESHOLD MEAN?
Every location fix, that Trackbook receives, is associated with an accuracy estimate. You can look up, how Android defines accuracy, in the developer documentation. "Accuracy Threshold" is the value, from which on location fixes are rejected. It can be adjusted in Trackbook's settings. You can increase the value, if your recordings tend to be incomplete. Trackbook will then also record less accurate location fixes.
Every location fix that Trackbook receives is associated with an accuracy estimate. You can look up how Android defines accuracy in the developer documentation. "Accuracy Threshold" is the value from which location fixes are rejected. It can be adjusted in Trackbook's settings. You can increase the value if your recordings tend to be incomplete. Trackbook will then also record less accurate location fixes.
WHERE ARE MY OLD RECORDINGS?
----------------------------
@ -31,6 +31,6 @@ The F-Droid version of Trackbook features an auto-importer for old recordings. S
A WORD ON PRIVACY
-----------------
Trackbook begins to store location data on device as soon a user presses the record button. Those recordings are stored in the directory /Android/data/org.y20k.trackbook/files/. They never leave the device. There is no web-service backing Trackbook.
Trackbook begins to store location data on a device as soon as a user presses the record button. Those recordings are stored in the directory /Android/data/org.y20k.trackbook/files/. They never leave the device. There is no web-service backing Trackbook.
Trackbook does not use Google Play Services to get its location data. It will however try to use data from the NETWORK_PROVIDER on your device to augment the location data it received via GPS. The NETWORK_PROVIDER is a system-wide service, that Trackbook has no control over. This service will usually query an online database for the location of cell towers or Wi-Fi access points a device can see. You can prevent those kinds of requests on your device, if you set the location preferences system-wide to "Device Only". Additionally Trackbook offers a "Restrict to GPS" setting, that deactivates the NETWORK_PROVIDER just within the app.
Trackbook does not use Google Play Services to get its location data. It will, however, try to use data from the NETWORK_PROVIDER on your device to augment the location data it received via GPS. The NETWORK_PROVIDER is a system-wide service that Trackbook has no control over. This service will usually query an online database for the location of cell towers or Wi-Fi access points a device can see. You can prevent those kinds of requests on your device if you set the location preferences system-wide to "Device Only". Additionally, Trackbook offers a "Restrict to GPS" setting that deactivates the NETWORK_PROVIDER just within the app.