Kotlin Rewrite - everything is new
This commit is contained in:
parent
b09259594f
commit
99265afe58
155 changed files with 6115 additions and 6802 deletions
74
.gitignore
vendored
74
.gitignore
vendored
|
@ -1,8 +1,68 @@
|
|||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/projectFilesBackup
|
||||
.idea/
|
||||
#.idea/workspace.xml
|
||||
#.idea/tasks.xml
|
||||
#.idea/gradle.xml
|
||||
#.idea/assetWizardSettings.xml
|
||||
#.idea/dictionaries
|
||||
#.idea/libraries
|
||||
#.idea/caches
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
|
@ -1,7 +1,7 @@
|
|||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright (c) 2016-19 - Y20K.org
|
||||
Copyright (c) 2016-20 - Y20K.org
|
||||
--------------------------------
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -4,7 +4,9 @@ README
|
|||
Trackbook - Movement Recorder for Android
|
||||
-----------------------------------------
|
||||
|
||||
**Version 1.2.x ("San Tropez")**
|
||||
**Version 2.0.x ("Echoes")**
|
||||
|
||||
**Please note: Trackbook is currently being completely re-written in Kotlin. No line of code is left unchanged. The process is not finished yet.**
|
||||
|
||||
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/).
|
||||
|
||||
|
|
|
@ -1,48 +1,89 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
compileSdkVersion 29
|
||||
// buildToolsVersion is optional because the plugin uses a recommended version by default
|
||||
|
||||
defaultConfig {
|
||||
applicationId project.ext.applicationId
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
versionCode project.ext.versionCode
|
||||
versionName project.ext.versionName
|
||||
applicationId 'org.y20k.trackbook'
|
||||
minSdkVersion 25
|
||||
targetSdkVersion 27
|
||||
versionCode 27
|
||||
versionName '2.0.0'
|
||||
resConfigs "en"
|
||||
}
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
resConfigs "en", "da", "de", "fr", "id", "it", "ja", "nb-rNO", "nl", "sv", "zh-rCN"
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions{
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
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
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
||||
}
|
||||
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'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion"
|
||||
implementation "androidx.cardview:cardview:$cardviewVersion"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation "androidx.preference:preference-ktx:1.1.0"
|
||||
|
||||
implementation "com.google.android.material:material:$materialVersion"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
|
||||
|
||||
implementation "org.osmdroid:osmdroid-android:$osmdroidVersion"
|
||||
implementation "com.google.code.gson:gson:$gsonVersion"
|
||||
|
||||
implementation "com.google.android.material:material:1.1.0-beta01"
|
||||
implementation "com.google.code.gson:gson:2.8.6"
|
||||
|
||||
implementation "org.osmdroid:osmdroid-android:6.1.5"
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
|
16
app/proguard-rules.pro
vendored
16
app/proguard-rules.pro
vendored
|
@ -1,14 +1,10 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/solaris/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
|
@ -16,6 +12,10 @@
|
|||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# stop re-ordering of gson elements
|
||||
-dontshrink
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
|
@ -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">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.y20k.trackbook">
|
||||
|
||||
<!-- EXCLUDE NON-GPS DEVICES -->
|
||||
<!-- 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.network" />
|
||||
|
||||
<!-- NORMAL PERMISSIONS, automatically granted -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -18,27 +19,19 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:name=".Trackbook"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backupscheme"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/TrackbookAppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<!-- MAIN ACTIVITY -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".Trackbook"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/TrackbookAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTop">
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<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>
|
||||
<!-- TODO App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW intent filter. See issue explanation for more details. -->
|
||||
</activity>
|
||||
|
||||
<!-- TRACKER SERVICE -->
|
||||
|
@ -53,17 +46,20 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- EXPORT HELPER (FILE PROVIDER) -->
|
||||
|
||||
<provider
|
||||
android:name=".helpers.ExportHelper"
|
||||
android:authorities="org.y20k.trackbook.exporthelper.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>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
|
||||
</manifest>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
134
app/src/main/java/org/y20k/trackbook/Keys.kt
Normal file
134
app/src/main/java/org/y20k/trackbook/Keys.kt
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Keys.kt
|
||||
* Implements the keys used throughout the app
|
||||
* This object hosts all keys used to control Trackbook's state
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* Keys object
|
||||
*/
|
||||
object Keys {
|
||||
|
||||
// application name
|
||||
const val APPLICATION_NAME: String = "Trackbook"
|
||||
|
||||
// version numbers
|
||||
const val CURRENT_TRACK_FORMAT_VERSION: Int = 4
|
||||
const val CURRENT_TRACKLIST_FORMAT_VERSION: Int = 0
|
||||
|
||||
// other values
|
||||
const val MAXIMUM_TRACK_FILES: Int = 25
|
||||
const val FIFTY_METER_RADIUS: Int = 50
|
||||
const val UNIT_METRIC: Int = 1
|
||||
const val UNIT_IMPERIAL: Int = -1
|
||||
|
||||
// intent actions
|
||||
const val ACTION_START: String = "org.y20k.trackbooks.action.START"
|
||||
const val ACTION_STOP: String = "org.y20k.trackbooks.action.STOP"
|
||||
const val ACTION_RESUME: String = "org.y20k.transistors.action.RESUME"
|
||||
|
||||
// args
|
||||
const val ARG_TRACK_TITLE: String = "ArgTrackTitle"
|
||||
const val ARG_TRACK_FILE_URI: String = "ArgTrackFileUri"
|
||||
const val ARG_GPX_FILE_URI: String = "ArgGpxFileUri"
|
||||
const val ARG_TRACK_ID: String = "ArgTrackId"
|
||||
|
||||
// preferences
|
||||
const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_37" // increment to current app version code to trigger housekeeping that runs only once
|
||||
const val PREF_NIGHT_MODE_STATE: String= "prefNightModeState"
|
||||
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"
|
||||
const val PREF_CURRENT_BEST_LOCATION_ACCURACY: String = "prefCurrentBestLocationAccuracy"
|
||||
const val PREF_CURRENT_BEST_LOCATION_ALTITUDE: String = "prefCurrentBestLocationAltitude"
|
||||
const val PREF_CURRENT_BEST_LOCATION_TIME: String = "prefCurrentBestLocationTime"
|
||||
const val PREF_MAP_ZOOM_LEVEL: String = "prefMapZoomLevel"
|
||||
const val PREF_TRACKING_STATE: String = "prefTrackingState"
|
||||
const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits"
|
||||
const val PREF_GPS_ONLY: String = "prefGpsOnly"
|
||||
const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold"
|
||||
const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold"
|
||||
|
||||
// states
|
||||
const val STATE_NOT_TRACKING: Int = 0
|
||||
const val STATE_TRACKING_ACTIVE: Int = 1
|
||||
const val STATE_TRACKING_STOPPED: Int = 2
|
||||
|
||||
// dialog types
|
||||
const val DIALOG_EMPTY_RECORDING: Int = 0
|
||||
const val DIALOG_REMOVE_TRACK: Int = 1
|
||||
|
||||
// dialog results
|
||||
const val DIALOG_RESULT_DEFAULT: Int = -1
|
||||
const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""
|
||||
const val DIALOG_EMPTY_PAYLOAD_INT: Int = -1
|
||||
const val DIALOG_RESULT_SAVE_DIALOG: Int = 1
|
||||
const val DIALOG_RESULT_CLEAR_DIALOG: Int = 2
|
||||
const val DIALOG_RESULT_DELETE_DIALOG: Int = 3
|
||||
const val DIALOG_RESULT_EXPORT_DIALOG: Int = 4
|
||||
const val DIALOG_RESULT_EMPTY_RECORDING_DIALOG: Int = 5
|
||||
|
||||
// folder names
|
||||
const val FOLDER_TEMP: String = "temp"
|
||||
const val FOLDER_TRACKS: String = "tracks"
|
||||
const val FOLDER_GPX: String = "gpx"
|
||||
|
||||
// file names and extensions
|
||||
const val GPX_FILE_EXTENSION: String = ".gpx"
|
||||
const val TRACKBOOK_LEGACY_FILE_EXTENSION: String = ".trackbook"
|
||||
const val TRACKBOOK_FILE_EXTENSION: String = ".json"
|
||||
const val TEMP_FILE: String = "temp.json"
|
||||
const val TRACKLIST_FILE: String = "tracklist.json"
|
||||
const val PODCAST_COVER_FILE: String = "cover.jpg"
|
||||
const val PODCAST_SMALL_COVER_FILE: String = "cover-small.jpg"
|
||||
const val DEBUG_LOG_FILE: String = "log-can-be-deleted.txt"
|
||||
const val FILE_TYPE_TEMP: Int = 0
|
||||
const val FILE_TYPE_TRACK: Int = 1
|
||||
|
||||
|
||||
// default values
|
||||
val DEFAULT_DATE: Date = Date(0L)
|
||||
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 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_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 REQUEST_CODE_FOREGROUND = 42
|
||||
|
||||
// requests
|
||||
|
||||
// results
|
||||
|
||||
// notification
|
||||
const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1
|
||||
const val NOTIFICATION_CHANNEL_RECORDING: String = "notificationChannelIdRecordingChannel"
|
||||
|
||||
}
|
|
@ -1,925 +0,0 @@
|
|||
/**
|
||||
* MainActivity.java
|
||||
* Implements the app's main activity
|
||||
* The main activity sets up the main view
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.os.Vibrator;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.osmdroid.config.Configuration;
|
||||
import org.y20k.trackbook.helpers.DialogHelper;
|
||||
import org.y20k.trackbook.helpers.ExportHelper;
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.NightModeHelper;
|
||||
import org.y20k.trackbook.helpers.TrackbookKeys;
|
||||
import org.y20k.trackbook.layout.NonSwipeableViewPager;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* MainActivity class
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = MainActivity.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private TrackerService mTrackerService;
|
||||
private BottomNavigationView mBottomNavigationView;
|
||||
private NonSwipeableViewPager mViewPager;
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
private boolean mTrackerServiceRunning;
|
||||
private boolean mPermissionsGranted;
|
||||
private boolean mFloatingActionButtonSubMenuVisible;
|
||||
private List<String> mMissingPermissions;
|
||||
private FloatingActionButton mFloatingActionButtonMain;
|
||||
private FloatingActionButton mFloatingActionButtonSubSave;
|
||||
private FloatingActionButton mFloatingActionButtonSubClear;
|
||||
private FloatingActionButton mFloatingActionButtonSubResume;
|
||||
private FloatingActionButton mFloatingActionButtonLocation;
|
||||
private CardView mFloatingActionButtonSubSaveLabel;
|
||||
private CardView mFloatingActionButtonSubClearLabel;
|
||||
private CardView mFloatingActionButtonSubResumeLabel;
|
||||
private BroadcastReceiver mTrackingChangedReceiver;
|
||||
private int mFloatingActionButtonState;
|
||||
private int mSelectedTab;
|
||||
|
||||
private boolean mBound = false;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// check state of External Storage
|
||||
checkExternalStorageState();
|
||||
|
||||
// empty cache
|
||||
ExportHelper.emptyCacheDirectory(this);
|
||||
|
||||
// load saved state of app
|
||||
loadFloatingActionButtonState(this);
|
||||
|
||||
// check permissions on Android 6 and higher
|
||||
mPermissionsGranted = false;
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
// check permissions
|
||||
mMissingPermissions = checkPermissions();
|
||||
mPermissionsGranted = mMissingPermissions.size() == 0;
|
||||
} else {
|
||||
mPermissionsGranted = true;
|
||||
}
|
||||
|
||||
// initialize state
|
||||
if (savedInstanceState != null) {
|
||||
// restore if saved instance is available
|
||||
mTrackerServiceRunning = savedInstanceState.getBoolean(INSTANCE_TRACKING_STATE, false);
|
||||
mSelectedTab = savedInstanceState.getInt(INSTANCE_SELECTED_TAB, FRAGMENT_ID_MAP);
|
||||
mFloatingActionButtonSubMenuVisible = savedInstanceState.getBoolean(INSTANCE_FAB_SUB_MENU_VISIBLE, false);
|
||||
} else {
|
||||
// use default values
|
||||
mTrackerServiceRunning = false;
|
||||
mSelectedTab = FRAGMENT_ID_MAP;
|
||||
mFloatingActionButtonSubMenuVisible = false;
|
||||
}
|
||||
|
||||
// set user agent to prevent getting banned from the osm servers
|
||||
Configuration.getInstance().setUserAgentValue(BuildConfig.APPLICATION_ID);
|
||||
// set the path for osmdroid's files (e.g. tile cache)
|
||||
Configuration.getInstance().setOsmdroidBasePath(this.getExternalFilesDir(null));
|
||||
|
||||
// set up main layout
|
||||
setupLayout();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
// bind to TrackerService
|
||||
Intent intent = new Intent(this, TrackerService.class);
|
||||
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
// register broadcast receiver for changed tracking state
|
||||
mTrackingChangedReceiver = createTrackingChangedReceiver();
|
||||
IntentFilter trackingStoppedIntentFilter = new IntentFilter(ACTION_TRACKING_STATE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mTrackingChangedReceiver, trackingStoppedIntentFilter);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// load state of Floating Action Button
|
||||
loadFloatingActionButtonState(this);
|
||||
|
||||
// handle incoming intent (from notification)
|
||||
handleIncomingIntent();
|
||||
|
||||
// if not in onboarding mode: set state of FloatingActionButton
|
||||
if (mFloatingActionButtonMain != null) {
|
||||
setFloatingActionButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
// unbind from TrackerService
|
||||
unbindService(mConnection);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogHelper.v(LOG_TAG, "onDestroy called.");
|
||||
|
||||
// reset selected tab
|
||||
mSelectedTab = FRAGMENT_ID_MAP;
|
||||
|
||||
// disable broadcast receiver
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mTrackingChangedReceiver);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
|
||||
Map<String, Integer> perms = new HashMap<>();
|
||||
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
|
||||
perms.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED);
|
||||
for (int i = 0; i < permissions.length; i++)
|
||||
perms.put(permissions[i], grantResults[i]);
|
||||
|
||||
// check for ACCESS_FINE_LOCATION and WRITE_EXTERNAL_STORAGE
|
||||
Boolean location = perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
// Boolean storage = perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (location) {
|
||||
// permissions granted - notify user
|
||||
Toast.makeText(this, R.string.toast_message_permissions_granted, Toast.LENGTH_SHORT).show();
|
||||
mPermissionsGranted = true;
|
||||
// switch to main map layout
|
||||
setupLayout();
|
||||
} else {
|
||||
// permissions denied - notify user
|
||||
Toast.makeText(this, R.string.toast_message_unable_to_start_app, Toast.LENGTH_SHORT).show();
|
||||
mPermissionsGranted = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putBoolean(INSTANCE_TRACKING_STATE, mTrackerServiceRunning);
|
||||
outState.putInt(INSTANCE_SELECTED_TAB, mSelectedTab);
|
||||
outState.putBoolean(INSTANCE_FAB_SUB_MENU_VISIBLE, mFloatingActionButtonSubMenuVisible);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
||||
/* Handles FloatingActionButton dialog results - called by MainActivityMapFragment after Saving and/or clearing the map */
|
||||
public void onFloatingActionButtonResult(int requestCode, int resultCode) {
|
||||
switch(requestCode) {
|
||||
case RESULT_SAVE_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// user chose SAVE
|
||||
handleStateAfterSave();
|
||||
LogHelper.v(LOG_TAG, "Save dialog result: SAVE");
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
LogHelper.v(LOG_TAG, "Save dialog result: CANCEL");
|
||||
}
|
||||
break;
|
||||
case RESULT_CLEAR_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// user chose CLEAR
|
||||
handleStateAfterClear();
|
||||
LogHelper.v(LOG_TAG, "Clear map dialog result: CLEAR");
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
LogHelper.v(LOG_TAG, "Clear map dialog result: User chose CANCEL.");
|
||||
}
|
||||
break;
|
||||
case RESULT_EMPTY_RECORDING_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// User chose RESUME RECORDING
|
||||
handleResumeButtonClick((View)mFloatingActionButtonMain);
|
||||
LogHelper.v(LOG_TAG, "Empty recording dialog result: RESUME");
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
// User chose CANCEL - do nothing just hide the sub menu
|
||||
showFloatingActionButtonMenu(false);
|
||||
LogHelper.v(LOG_TAG, "Empty recording dialog result: CANCEL");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Handles the visual state after a save action */
|
||||
private void handleStateAfterSave() {
|
||||
// display and update tracks tab
|
||||
mBottomNavigationView.setSelectedItemId(R.id.navigation_last_tracks);
|
||||
|
||||
// dismiss notification
|
||||
dismissNotification();
|
||||
|
||||
// hide Floating Action Button sub menu
|
||||
showFloatingActionButtonMenu(false);
|
||||
|
||||
// update Floating Action Button icon
|
||||
mFloatingActionButtonState = FAB_STATE_DEFAULT;
|
||||
setFloatingActionButtonState();
|
||||
}
|
||||
|
||||
|
||||
/* Start tracker service */
|
||||
private void startTrackerService() {
|
||||
// start service so that it keeps running after unbind
|
||||
Intent intent = new Intent(this, TrackerService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// ... start service in foreground to prevent it being killed on Oreo
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Start recording movements */
|
||||
private void startRecording(Location lastLocation) {
|
||||
startTrackerService();
|
||||
if (mBound) {
|
||||
mTrackerService.startTracking(lastLocation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Resume recording movements */
|
||||
private void resumeRecording(Location lastLocation) {
|
||||
startTrackerService();
|
||||
if (mBound) {
|
||||
mTrackerService.resumeTracking(lastLocation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Stop recording movements */
|
||||
private void stopRecording() {
|
||||
if (mBound) {
|
||||
mTrackerService.stopTracking();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Dismiss notification */
|
||||
private void dismissNotification() {
|
||||
if (mBound) {
|
||||
mTrackerService.dismissNotification();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Handles the visual state after a save action */
|
||||
private void handleStateAfterClear() {
|
||||
// dismiss notification
|
||||
dismissNotification();
|
||||
|
||||
// hide Floating Action Button sub menu
|
||||
showFloatingActionButtonMenu(false);
|
||||
|
||||
// update Floating Action Button icon
|
||||
mFloatingActionButtonState = FAB_STATE_DEFAULT;
|
||||
setFloatingActionButtonState();
|
||||
}
|
||||
|
||||
|
||||
/* Handles tap on the button "save" */
|
||||
private void handleSaveButtonClick() {
|
||||
// save button click is handled by onActivityResult in MainActivityMapFragment
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
mainActivityMapFragment.onActivityResult(RESULT_SAVE_DIALOG, Activity.RESULT_OK, getIntent());
|
||||
}
|
||||
|
||||
|
||||
/* Handles tap on the button "clear" */
|
||||
private void handleClearButtonClick() {
|
||||
// prepare delete dialog
|
||||
int dialogTitle = -1;
|
||||
String dialogMessage = getString(R.string.dialog_clear_content);
|
||||
int dialogPositiveButton = R.string.dialog_clear_action_clear;
|
||||
int dialogNegativeButton = R.string.dialog_default_action_cancel;
|
||||
// show delete dialog
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton);
|
||||
dialogFragment.setTargetFragment(mainActivityMapFragment, RESULT_CLEAR_DIALOG);
|
||||
dialogFragment.show(getSupportFragmentManager(), "ClearDialog");
|
||||
// results of dialog are handled by onActivityResult in MainActivityMapFragment
|
||||
}
|
||||
|
||||
|
||||
/* Handles tap on the button "resume" */
|
||||
private void handleResumeButtonClick(View view) {
|
||||
|
||||
// get last location from MainActivity Fragment // todo check -> may produce NullPointerException
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
Location lastLocation = mainActivityMapFragment.getCurrentBestLocation();
|
||||
|
||||
if (lastLocation != null) {
|
||||
// show snackbar
|
||||
Snackbar.make(view, R.string.snackbar_message_tracking_resumed, Snackbar.LENGTH_SHORT).setAction("Action", null).show();
|
||||
// resume tracking
|
||||
resumeRecording(lastLocation);
|
||||
// hide sub menu
|
||||
showFloatingActionButtonMenu(false);
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Loads state of Floating Action Button from preferences */
|
||||
private void loadFloatingActionButtonState(Context context) {
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
mFloatingActionButtonState = settings.getInt(PREFS_FAB_STATE, FAB_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
|
||||
/* Set up main layout */
|
||||
private void setupLayout() {
|
||||
if (mPermissionsGranted) {
|
||||
// point to the main map layout
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// create adapter that returns fragments for the maim map and the last track display
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// set up the ViewPager with the sections adapter.
|
||||
mViewPager = (NonSwipeableViewPager) findViewById(R.id.fragmentContainer);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
// setup bottom navigation
|
||||
mBottomNavigationView = findViewById(R.id.navigation);
|
||||
mBottomNavigationView.setOnNavigationItemSelectedListener(getOnNavigationItemSelectedListener());
|
||||
|
||||
// get references to the record button and show/hide its sub menu
|
||||
mFloatingActionButtonMain = findViewById(R.id.fabMainButton);
|
||||
mFloatingActionButtonLocation = findViewById(R.id.fabLocationButton);
|
||||
mFloatingActionButtonSubSave = findViewById(R.id.fabSubMenuButtonSave);
|
||||
mFloatingActionButtonSubSaveLabel = findViewById(R.id.fabSubMenuLabelSave);
|
||||
mFloatingActionButtonSubClear = findViewById(R.id.fabSubMenuButtonClear);
|
||||
mFloatingActionButtonSubClearLabel = findViewById(R.id.fabSubMenuLabelClear);
|
||||
mFloatingActionButtonSubResume = findViewById(R.id.fabSubMenuButtonResume);
|
||||
mFloatingActionButtonSubResumeLabel = findViewById(R.id.fabSubMenuLabelResume);
|
||||
if (mFloatingActionButtonSubMenuVisible) {
|
||||
showFloatingActionButtonMenu(true);
|
||||
} else {
|
||||
showFloatingActionButtonMenu(false);
|
||||
}
|
||||
|
||||
// restore selected tab
|
||||
if (mSelectedTab == FRAGMENT_ID_TRACKS) {
|
||||
mBottomNavigationView.setSelectedItemId(R.id.navigation_last_tracks);
|
||||
} else {
|
||||
mBottomNavigationView.setSelectedItemId(R.id.navigation_map);
|
||||
}
|
||||
|
||||
// add listeners to buttons
|
||||
addListenersToViews();
|
||||
|
||||
} else {
|
||||
// point to the on main onboarding layout
|
||||
setContentView(R.layout.main_onboarding);
|
||||
|
||||
// show the okay button and attach listener
|
||||
Button okayButton = (Button) findViewById(R.id.button_okay);
|
||||
okayButton.setOnClickListener(new View.OnClickListener() {
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (mMissingPermissions != null && !mMissingPermissions.isEmpty()) {
|
||||
// request permissions
|
||||
String[] params = mMissingPermissions.toArray(new String[mMissingPermissions.size()]);
|
||||
requestPermissions(params, REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Add listeners to ui buttons */
|
||||
private void addListenersToViews() {
|
||||
|
||||
mFloatingActionButtonMain.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleFloatingActionButtonClick(view);
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleSaveButtonClick();
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubSaveLabel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleSaveButtonClick();
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleClearButtonClick();
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubClearLabel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleClearButtonClick();
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubResume.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleResumeButtonClick(view);
|
||||
}
|
||||
});
|
||||
mFloatingActionButtonSubResumeLabel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleResumeButtonClick(view);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
mFloatingActionButtonLocation.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
mainActivityMapFragment.handleShowMyLocation();
|
||||
}
|
||||
});
|
||||
|
||||
// secret night mode switch
|
||||
mFloatingActionButtonLocation.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
NightModeHelper.switchMode(MainActivity.this);
|
||||
// vibrate 50 milliseconds
|
||||
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
vibrator.vibrate(50);
|
||||
// recreate activity
|
||||
recreate();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Handles tap on the record button */
|
||||
private void handleFloatingActionButtonClick(View view) {
|
||||
|
||||
switch (mFloatingActionButtonState) {
|
||||
case FAB_STATE_DEFAULT:
|
||||
|
||||
// get last location from MainActivity Fragment // todo check -> may produce NullPointerException
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
Location lastLocation = mainActivityMapFragment.getCurrentBestLocation();
|
||||
|
||||
if (lastLocation != null) {
|
||||
// show snackbar
|
||||
Snackbar.make(view, R.string.snackbar_message_tracking_started, Snackbar.LENGTH_SHORT).setAction("Action", null).show();
|
||||
// start recording
|
||||
startRecording(lastLocation);
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case FAB_STATE_RECORDING:
|
||||
// show snackbar
|
||||
Snackbar.make(view, R.string.snackbar_message_tracking_stopped, Snackbar.LENGTH_SHORT).setAction("Action", null).show();
|
||||
// stop tracker service
|
||||
stopRecording();
|
||||
|
||||
break;
|
||||
|
||||
case FAB_STATE_SAVE:
|
||||
// toggle visibility floating action button sub menu
|
||||
showFloatingActionButtonMenu(!mFloatingActionButtonSubMenuVisible);
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// update tracking state in MainActivityMapFragment // todo check -> may produce NullPointerException
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
mainActivityMapFragment.setTrackingState(mTrackerServiceRunning);
|
||||
}
|
||||
|
||||
|
||||
/* Set state of FloatingActionButton */
|
||||
private void setFloatingActionButtonState() {
|
||||
|
||||
switch (mFloatingActionButtonState) {
|
||||
case FAB_STATE_DEFAULT:
|
||||
mFloatingActionButtonMain.hide(); // workaround todo remove asap
|
||||
mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp);
|
||||
mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_start));
|
||||
if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap
|
||||
break;
|
||||
case FAB_STATE_RECORDING:
|
||||
mFloatingActionButtonMain.hide(); // workaround todo remove asap
|
||||
mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_red_24dp);
|
||||
mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_stop));
|
||||
if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap
|
||||
break;
|
||||
case FAB_STATE_SAVE:
|
||||
mFloatingActionButtonMain.hide(); // workaround todo remove asap
|
||||
mFloatingActionButtonMain.setImageResource(R.drawable.ic_save_white_24dp);
|
||||
mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_options));
|
||||
if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap
|
||||
break;
|
||||
default:
|
||||
mFloatingActionButtonMain.hide(); // workaround todo remove asap
|
||||
mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp);
|
||||
mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_start));
|
||||
if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Shows (and hides) the sub menu of the floating action button */
|
||||
private void showFloatingActionButtonMenu(boolean visible) {
|
||||
if (visible) {
|
||||
mFloatingActionButtonSubResume.show();
|
||||
mFloatingActionButtonSubResumeLabel.setVisibility(View.VISIBLE);
|
||||
mFloatingActionButtonSubClear.show();
|
||||
mFloatingActionButtonSubClearLabel.setVisibility(View.VISIBLE);
|
||||
mFloatingActionButtonSubSave.show();
|
||||
mFloatingActionButtonSubSaveLabel.setVisibility(View.VISIBLE);
|
||||
mFloatingActionButtonSubMenuVisible = true;
|
||||
} else {
|
||||
mFloatingActionButtonSubResume.hide();
|
||||
mFloatingActionButtonSubResumeLabel.setVisibility(View.INVISIBLE);
|
||||
mFloatingActionButtonSubClear.hide();
|
||||
mFloatingActionButtonSubClearLabel.setVisibility(View.INVISIBLE);
|
||||
mFloatingActionButtonSubSave.hide();
|
||||
mFloatingActionButtonSubSaveLabel.setVisibility(View.INVISIBLE);
|
||||
mFloatingActionButtonSubMenuVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Handles taps on the bottom navigation */
|
||||
private BottomNavigationView.OnNavigationItemSelectedListener getOnNavigationItemSelectedListener() {
|
||||
return new BottomNavigationView.OnNavigationItemSelectedListener() {
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.navigation_map:
|
||||
// show the Floating Action Button
|
||||
mFloatingActionButtonMain.show();
|
||||
|
||||
// show the my location button
|
||||
mFloatingActionButtonLocation.show();
|
||||
|
||||
// show map fragment
|
||||
mSelectedTab = FRAGMENT_ID_MAP;
|
||||
mViewPager.setCurrentItem(mSelectedTab);
|
||||
|
||||
return true;
|
||||
|
||||
case R.id.navigation_last_tracks:
|
||||
// hide the Floating Action Button - and its sub menu
|
||||
mFloatingActionButtonMain.hide();
|
||||
showFloatingActionButtonMenu(false);
|
||||
|
||||
// hide the my location button
|
||||
mFloatingActionButtonLocation.hide();
|
||||
|
||||
// show tracks fragment
|
||||
mSelectedTab = FRAGMENT_ID_TRACKS;
|
||||
mViewPager.setCurrentItem(mSelectedTab);
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
// show the Floating Action Button
|
||||
mFloatingActionButtonMain.show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Handles new incoming intents */
|
||||
private void handleIncomingIntent() {
|
||||
Intent intent = getIntent();
|
||||
LogHelper.v(LOG_TAG, "Main Activity received intent. Content: " + intent.toString());
|
||||
String intentAction = intent.getAction();
|
||||
switch (intentAction) {
|
||||
case ACTION_SHOW_MAP:
|
||||
// show map fragment
|
||||
mBottomNavigationView.setSelectedItemId(R.id.navigation_map);
|
||||
|
||||
// clear intent
|
||||
intent.setAction(ACTION_DEFAULT);
|
||||
|
||||
break;
|
||||
|
||||
case ACTION_CLEAR:
|
||||
// show map fragment
|
||||
mBottomNavigationView.setSelectedItemId(R.id.navigation_map);
|
||||
|
||||
// show clear dialog
|
||||
handleClearButtonClick();
|
||||
|
||||
// clear intent
|
||||
intent.setAction(ACTION_DEFAULT);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Inform user and give haptic feedback (vibration) */
|
||||
private void longPressFeedback(int stringResource) {
|
||||
// inform user
|
||||
Toast.makeText(this, stringResource, Toast.LENGTH_LONG).show();
|
||||
// vibrate 50 milliseconds
|
||||
Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (v != null) {
|
||||
v.vibrate(50);
|
||||
// v.vibrate(VibrationEffect.createOneShot(50, DEFAULT_AMPLITUDE)); // todo check if there is a support library vibrator
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Check which permissions have been granted */
|
||||
private List<String> checkPermissions() {
|
||||
List<String> permissions = new ArrayList<>();
|
||||
|
||||
// check for location permission
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
// add missing permission
|
||||
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
// // check for storage permission
|
||||
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
// // add missing permission
|
||||
// permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
// }
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
|
||||
/* Creates receiver for stopped tracking */
|
||||
private BroadcastReceiver createTrackingChangedReceiver() {
|
||||
return new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
// change state
|
||||
mTrackerServiceRunning = intent.getBooleanExtra(EXTRA_TRACKING_STATE, false);
|
||||
if (mTrackerServiceRunning) {
|
||||
mFloatingActionButtonState = FAB_STATE_RECORDING;
|
||||
} else {
|
||||
mFloatingActionButtonState = FAB_STATE_SAVE;
|
||||
}
|
||||
setFloatingActionButtonState();
|
||||
|
||||
// pass tracking state to MainActivityMapFragment // todo check -> may produce NullPointerException
|
||||
MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP);
|
||||
mainActivityMapFragment.setTrackingState(mTrackerServiceRunning);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Checks the state of External Storage */
|
||||
private void checkExternalStorageState() {
|
||||
|
||||
String state = Environment.getExternalStorageState();
|
||||
if (!state.equals(Environment.MEDIA_MOUNTED)) {
|
||||
LogHelper.e(LOG_TAG, "Error: Unable to mount External Storage. Current state: " + state);
|
||||
|
||||
// move MainActivity to back
|
||||
moveTaskToBack(true);
|
||||
|
||||
// shutting down app
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
||||
//
|
||||
// public SectionsPagerAdapter(FragmentManager fm) {
|
||||
// super(fm);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Fragment getItem(int position) {
|
||||
// // getItem is called to instantiate the fragment for the given page.
|
||||
// switch (position) {
|
||||
// case FRAGMENT_ID_MAP:
|
||||
// return new MainActivityMapFragment();
|
||||
// case FRAGMENT_ID_TRACKS:
|
||||
// return new MainActivityTrackFragment();
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getCount() {
|
||||
// return 2;
|
||||
// }
|
||||
//
|
||||
// public Fragment getFragment(int pos) {
|
||||
// return getItem(pos);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Defines callbacks for service binding, passed to bindService()
|
||||
*/
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
TrackerService.LocalBinder binder = (TrackerService.LocalBinder) service;
|
||||
mTrackerService = binder.getService();
|
||||
mBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0) {
|
||||
mBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: SectionsPagerAdapter that returns a fragment corresponding to one of the tabs.
|
||||
* see also: https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html
|
||||
* and: http://www.truiton.com/2015/12/android-activity-fragment-communication/
|
||||
*/
|
||||
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final SparseArray<WeakReference<Fragment>> instantiatedFragments = new SparseArray<>();
|
||||
|
||||
public SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
// getItem is called to instantiate the fragment for the given page.
|
||||
switch (position) {
|
||||
case FRAGMENT_ID_MAP:
|
||||
return new MainActivityMapFragment();
|
||||
case FRAGMENT_ID_TRACKS:
|
||||
return new MainActivityTrackFragment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
// show 2 total pages.
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case FRAGMENT_ID_MAP:
|
||||
return getString(R.string.tab_map);
|
||||
case FRAGMENT_ID_TRACKS:
|
||||
return getString(R.string.tab_last_tracks);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(final ViewGroup container, final int position) {
|
||||
final Fragment fragment = (Fragment) super.instantiateItem(container, position);
|
||||
instantiatedFragments.put(position, new WeakReference<>(fragment));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(final ViewGroup container, final int position, final Object object) {
|
||||
instantiatedFragments.remove(position);
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Fragment getFragment(final int position) {
|
||||
final WeakReference<Fragment> wr = instantiatedFragments.get(position);
|
||||
if (wr != null) {
|
||||
return wr.get();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
}
|
85
app/src/main/java/org/y20k/trackbook/MainActivity.kt
Normal file
85
app/src/main/java/org/y20k/trackbook/MainActivity.kt
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* MainActivity.kt
|
||||
* Implements the main activity of the app
|
||||
* The MainActivity hosts fragments for: current map, track list, settings
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.y20k.trackbook.helpers.ImportHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.PreferencesHelper
|
||||
|
||||
|
||||
/*
|
||||
* MainActivity class
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(MainActivity::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private lateinit var navHostFragment: NavHostFragment
|
||||
private lateinit var bottomNavigationView: BottomNavigationView
|
||||
|
||||
|
||||
/* Overrides onCreate from AppCompatActivity */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// set user agent to prevent getting banned from the osm servers
|
||||
Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
|
||||
// set the path for osmdroid's files (e.g. tile cache)
|
||||
Configuration.getInstance().osmdroidBasePath = this.getExternalFilesDir(null)
|
||||
|
||||
// set up views
|
||||
setContentView(R.layout.activity_main)
|
||||
navHostFragment = supportFragmentManager.findFragmentById(R.id.main_container) as NavHostFragment
|
||||
bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation_view)
|
||||
bottomNavigationView.setupWithNavController(navController = navHostFragment.navController)
|
||||
|
||||
// listen for navigation changes
|
||||
navHostFragment.navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
when (destination.id) {
|
||||
R.id.fragment_track -> {
|
||||
runOnUiThread( Runnable() {
|
||||
run(){
|
||||
// mark menu item "Tracks" as checked
|
||||
bottomNavigationView.menu.findItem(R.id.tracklist_fragment).setChecked(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
else -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert old tracks (one-time import)
|
||||
if (PreferencesHelper.isHouseKeepingNecessary(this)) {
|
||||
ImportHelper.convertOldTracks(this)
|
||||
PreferencesHelper.saveHouseKeepingNecessaryState(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,741 +0,0 @@
|
|||
/**
|
||||
* MainActivityMapFragment.java
|
||||
* Implements the map fragment used in the map tab of the main activity
|
||||
* This fragment displays a map using osmdroid
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.osmdroid.api.IMapController;
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay;
|
||||
import org.osmdroid.views.overlay.TilesOverlay;
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay;
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.helpers.DialogHelper;
|
||||
import org.y20k.trackbook.helpers.LocationHelper;
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.MapHelper;
|
||||
import org.y20k.trackbook.helpers.NightModeHelper;
|
||||
import org.y20k.trackbook.helpers.StorageHelper;
|
||||
import org.y20k.trackbook.helpers.TrackbookKeys;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* MainActivityMapFragment class
|
||||
*/
|
||||
public class MainActivityMapFragment extends Fragment implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = MainActivityMapFragment.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private Activity mActivity;
|
||||
private Track mTrack;
|
||||
private boolean mFirstStart;
|
||||
private Snackbar mLocationOffBar;
|
||||
private BroadcastReceiver mTrackUpdatedReceiver;
|
||||
private SettingsContentObserver mSettingsContentObserver;
|
||||
private MapView mMapView;
|
||||
private IMapController mController;
|
||||
private StorageHelper mStorageHelper;
|
||||
private LocationManager mLocationManager;
|
||||
private LocationListener mGPSListener;
|
||||
private LocationListener mNetworkListener;
|
||||
private ItemizedIconOverlay mMyLocationOverlay;
|
||||
private ItemizedIconOverlay mTrackOverlay;
|
||||
private Location mCurrentBestLocation;
|
||||
private boolean mTrackerServiceRunning;
|
||||
private boolean mLocalTrackerRunning;
|
||||
private boolean mLocationSystemSetting;
|
||||
private boolean mFragmentVisible;
|
||||
|
||||
|
||||
/* Constructor (default) */
|
||||
public MainActivityMapFragment() {
|
||||
}
|
||||
|
||||
|
||||
/* Return a new Instance of MainActivityMapFragment */
|
||||
public static MainActivityMapFragment newInstance() {
|
||||
return new MainActivityMapFragment();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// get activity
|
||||
mActivity = getActivity();
|
||||
|
||||
// restore first start state and tracking state
|
||||
mFirstStart = true;
|
||||
mTrackerServiceRunning = false;
|
||||
loadTrackerServiceState(mActivity);
|
||||
if (savedInstanceState != null) {
|
||||
mFirstStart = savedInstanceState.getBoolean(INSTANCE_FIRST_START, true);
|
||||
}
|
||||
|
||||
// create storage helper
|
||||
mStorageHelper = new StorageHelper(mActivity);
|
||||
|
||||
// acquire reference to Location Manager
|
||||
mLocationManager = (LocationManager) mActivity.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
// CASE 1: get saved location if possible
|
||||
if (savedInstanceState != null) {
|
||||
Location savedLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION);
|
||||
// check if saved location is still current
|
||||
if (LocationHelper.isCurrent(savedLocation)) {
|
||||
mCurrentBestLocation = savedLocation;
|
||||
} else {
|
||||
mCurrentBestLocation = null;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE 2: get last known location if no saved location or saved location is too old
|
||||
if (mCurrentBestLocation == null && mLocationManager.getProviders(true).size() > 0) {
|
||||
mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager);
|
||||
}
|
||||
|
||||
// CASE 3: location services are available but unable to get location - this should not happen
|
||||
if (mCurrentBestLocation == null) {
|
||||
mCurrentBestLocation = new Location(LocationManager.NETWORK_PROVIDER);
|
||||
mCurrentBestLocation.setLatitude(DEFAULT_LATITUDE);
|
||||
mCurrentBestLocation.setLongitude(DEFAULT_LONGITUDE);
|
||||
}
|
||||
|
||||
// get state of location system setting
|
||||
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity);
|
||||
|
||||
// create content observer for changes in System Settings
|
||||
mSettingsContentObserver = new SettingsContentObserver( new Handler());
|
||||
|
||||
// register broadcast receiver for new WayPoints
|
||||
mTrackUpdatedReceiver = createTrackUpdatedReceiver();
|
||||
IntentFilter trackUpdatedIntentFilter = new IntentFilter(ACTION_TRACK_UPDATED);
|
||||
LocalBroadcastManager.getInstance(mActivity).registerReceiver(mTrackUpdatedReceiver, trackUpdatedIntentFilter);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
// create basic map
|
||||
mMapView = new MapView(inflater.getContext());
|
||||
|
||||
// get map controller
|
||||
mController = mMapView.getController();
|
||||
|
||||
// basic map setup
|
||||
mMapView.setTileSource(TileSourceFactory.MAPNIK);
|
||||
mMapView.setTilesScaledToDpi(true);
|
||||
|
||||
// set dark map tiles, if necessary
|
||||
if (NightModeHelper.getNightMode(mActivity)) {
|
||||
mMapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS);
|
||||
}
|
||||
|
||||
// add multi-touch capability
|
||||
mMapView.setMultiTouchControls(true);
|
||||
|
||||
// disable default zoom controls
|
||||
mMapView.getZoomController().setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER);
|
||||
|
||||
// add compass to map
|
||||
CompassOverlay compassOverlay = new CompassOverlay(mActivity, new InternalCompassOrientationProvider(mActivity), mMapView);
|
||||
compassOverlay.enableCompass();
|
||||
mMapView.getOverlays().add(compassOverlay);
|
||||
|
||||
// initiate map state
|
||||
if (savedInstanceState != null) {
|
||||
// restore saved instance of map
|
||||
GeoPoint position = new GeoPoint(savedInstanceState.getDouble(INSTANCE_LATITUDE_MAIN_MAP, DEFAULT_LATITUDE), savedInstanceState.getDouble(INSTANCE_LONGITUDE_MAIN_MAP, DEFAULT_LONGITUDE));
|
||||
mController.setCenter(position);
|
||||
mController.setZoom(savedInstanceState.getDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, 16f));
|
||||
// restore current location
|
||||
mCurrentBestLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION);
|
||||
} else if (mCurrentBestLocation != null) {
|
||||
// fallback or first run: set map to current position
|
||||
GeoPoint position = convertToGeoPoint(mCurrentBestLocation);
|
||||
mController.setCenter(position);
|
||||
mController.setZoom(16f);
|
||||
}
|
||||
|
||||
// inform user that new/better location is on its way
|
||||
if (mFirstStart && !mTrackerServiceRunning) {
|
||||
Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_acquiring_location), Toast.LENGTH_LONG).show();
|
||||
mFirstStart = false;
|
||||
}
|
||||
|
||||
// // load track from saved instance
|
||||
// if (savedInstanceState != null) {
|
||||
// mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_MAIN_MAP);
|
||||
// }
|
||||
|
||||
// mark user's location on map
|
||||
if (mCurrentBestLocation != null && !mTrackerServiceRunning) {
|
||||
mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isCurrent(mCurrentBestLocation), false);
|
||||
mMapView.getOverlays().add(mMyLocationOverlay);
|
||||
}
|
||||
|
||||
return mMapView;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// set visibility
|
||||
mFragmentVisible = true;
|
||||
|
||||
// load state of tracker service - see if anything changed
|
||||
loadTrackerServiceState(mActivity);
|
||||
|
||||
// load track from temp file if it exists
|
||||
if (mStorageHelper.tempFileExists()) {
|
||||
LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper();
|
||||
loadTempTrackAsyncHelper.execute();
|
||||
}
|
||||
|
||||
// // CASE 1: recording active
|
||||
// if (mTrackerServiceRunning) {
|
||||
// // request an updated track recording from service
|
||||
// ((MainActivity)mActivity).requestTrack();
|
||||
// }
|
||||
//
|
||||
// // CASE 2: recording stopped - temp file exists
|
||||
// else if (mStorageHelper.tempFileExists()) {
|
||||
// // load track from temp file if it exists
|
||||
// LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper();
|
||||
// loadTempTrackAsyncHelper.execute();
|
||||
// }
|
||||
|
||||
// // CASE 3: not recording and no temp file
|
||||
// else if (mTrack != null) {
|
||||
// // just draw existing track data (from saved instance)
|
||||
// drawTrackOverlay(mTrack);
|
||||
// }
|
||||
|
||||
// show/hide the location off notification bar
|
||||
toggleLocationOffBar();
|
||||
|
||||
// start preliminary tracking - if no TrackerService is running
|
||||
if (!mTrackerServiceRunning && mFragmentVisible) {
|
||||
startPreliminaryTracking();
|
||||
}
|
||||
|
||||
// register content observer for changes in System Settings
|
||||
mActivity.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver );
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// set visibility
|
||||
mFragmentVisible = false;
|
||||
|
||||
// disable preliminary location listeners
|
||||
stopPreliminaryTracking();
|
||||
|
||||
// disable content observer for changes in System Settings
|
||||
mActivity.getContentResolver().unregisterContentObserver(mSettingsContentObserver);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
|
||||
// deactivate map
|
||||
mMapView.onDetach();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LogHelper.v(LOG_TAG, "onDestroy called.");
|
||||
|
||||
// reset first start state
|
||||
mFirstStart = true;
|
||||
|
||||
// disable broadcast receivers
|
||||
LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mTrackUpdatedReceiver);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch(requestCode) {
|
||||
case RESULT_SAVE_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// user chose SAVE
|
||||
if (mTrack.getSize() > 0) {
|
||||
// Track is not empty - clear map AND save track
|
||||
clearMap(true);
|
||||
// FloatingActionButton state is already being handled in MainActivity
|
||||
((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode);
|
||||
LogHelper.v(LOG_TAG, "Save dialog result: SAVE");
|
||||
} else {
|
||||
// track is empty
|
||||
handleEmptyRecordingSaveRequest();
|
||||
}
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
LogHelper.v(LOG_TAG, "Save dialog result: CANCEL");
|
||||
}
|
||||
break;
|
||||
case RESULT_CLEAR_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// User chose CLEAR
|
||||
if (mTrack.getSize() > 0) {
|
||||
// Track is not empty - notify user
|
||||
Toast.makeText(mActivity, getString(R.string.toast_message_track_clear), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
// clear map, DO NOT save track
|
||||
clearMap(false);
|
||||
// handle FloatingActionButton state in MainActivity
|
||||
((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode);
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
LogHelper.v(LOG_TAG, "Clear dialog result: CANCEL");
|
||||
}
|
||||
break;
|
||||
case RESULT_EMPTY_RECORDING_DIALOG:
|
||||
// handle FloatingActionButton state and possible Resume-Action in MainActivity
|
||||
((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putBoolean(INSTANCE_FIRST_START, mFirstStart);
|
||||
outState.putBoolean(INSTANCE_TRACKING_STATE, mTrackerServiceRunning);
|
||||
outState.putParcelable(INSTANCE_CURRENT_LOCATION, mCurrentBestLocation);
|
||||
outState.putDouble(INSTANCE_LATITUDE_MAIN_MAP, mMapView.getMapCenter().getLatitude());
|
||||
outState.putDouble(INSTANCE_LONGITUDE_MAIN_MAP, mMapView.getMapCenter().getLongitude());
|
||||
outState.putDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, mMapView.getZoomLevelDouble());
|
||||
// outState.putParcelable(INSTANCE_TRACK_MAIN_MAP, mTrack);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
||||
/* Setter for tracking state */
|
||||
public void setTrackingState(boolean trackingState) {
|
||||
mTrackerServiceRunning = trackingState;
|
||||
|
||||
// turn on/off tracking for MainActivity Fragment - prevent double tracking
|
||||
if (mTrackerServiceRunning) {
|
||||
stopPreliminaryTracking();
|
||||
} else if (!mLocalTrackerRunning && mFragmentVisible) {
|
||||
startPreliminaryTracking();
|
||||
}
|
||||
|
||||
if (mTrack != null) {
|
||||
drawTrackOverlay(mTrack); // TODO check if redundant
|
||||
}
|
||||
|
||||
// update marker
|
||||
updateMyLocationMarker();
|
||||
LogHelper.v(LOG_TAG, "TrackingState: " + trackingState);
|
||||
}
|
||||
|
||||
|
||||
/* Getter for current best location */
|
||||
public Location getCurrentBestLocation() {
|
||||
if (mLocationSystemSetting) {
|
||||
return mCurrentBestLocation;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Handles tap on the my location button */
|
||||
public boolean handleShowMyLocation() {
|
||||
|
||||
// do nothing if location setting is off
|
||||
if (toggleLocationOffBar()) {
|
||||
stopPreliminaryTracking();
|
||||
return false;
|
||||
}
|
||||
|
||||
GeoPoint position;
|
||||
|
||||
// get current position
|
||||
if (mTrackerServiceRunning && mTrack != null && mTrack.getSize() > 0) {
|
||||
// get current Location from tracker service
|
||||
mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() - 1);
|
||||
} else if (mCurrentBestLocation == null) {
|
||||
// app does not have any location fix
|
||||
mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager);
|
||||
}
|
||||
|
||||
// check if really got a position
|
||||
if (mCurrentBestLocation != null) {
|
||||
position = convertToGeoPoint(mCurrentBestLocation);
|
||||
|
||||
// center map on current position
|
||||
mController.setCenter(position);
|
||||
|
||||
// mark user's new location on map and remove last marker
|
||||
updateMyLocationMarker();
|
||||
|
||||
// inform user about location quality
|
||||
String locationInfo;
|
||||
long locationAge = (SystemClock.elapsedRealtimeNanos() - mCurrentBestLocation.getElapsedRealtimeNanos()) / 1000000;
|
||||
String locationAgeString = LocationHelper.convertToReadableTime(locationAge, false);
|
||||
if (locationAgeString == null) {
|
||||
locationAgeString = mActivity.getString(R.string.toast_message_last_location_age_one_hour);
|
||||
}
|
||||
locationInfo = " " + locationAgeString + " | " + mCurrentBestLocation.getProvider();
|
||||
Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_last_location) + locationInfo, Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
} else {
|
||||
Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Removes track crumbs from map */
|
||||
private void clearMap(boolean saveTrack) {
|
||||
|
||||
// clear map
|
||||
if (mTrackOverlay != null) {
|
||||
mMapView.getOverlays().remove(mTrackOverlay);
|
||||
mTrackOverlay = null;
|
||||
}
|
||||
|
||||
if (saveTrack) {
|
||||
// save track object if requested
|
||||
SaveTrackAsyncHelper saveTrackAsyncHelper = new SaveTrackAsyncHelper();
|
||||
saveTrackAsyncHelper.execute();
|
||||
Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_save_track), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
// clear track object and delete temp file
|
||||
mTrack = null;
|
||||
mStorageHelper.deleteTempFile();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Handles case when user chose to save recording with zero waypoints */ // todo implement
|
||||
private void handleEmptyRecordingSaveRequest() {
|
||||
// prepare empty recording dialog ("Unable to save")
|
||||
int dialogTitle = R.string.dialog_error_empty_recording_title;
|
||||
String dialogMessage = getString(R.string.dialog_error_empty_recording_content);
|
||||
int dialogPositiveButton = R.string.dialog_error_empty_recording_action_resume;
|
||||
int dialogNegativeButton = R.string.dialog_default_action_cancel;
|
||||
// show empty recording dialog
|
||||
DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton);
|
||||
dialogFragment.setTargetFragment(this, RESULT_EMPTY_RECORDING_DIALOG);
|
||||
dialogFragment.show(((AppCompatActivity)mActivity).getSupportFragmentManager(), "EmptyRecordingDialog");
|
||||
// results of dialog are handled by onActivityResult
|
||||
}
|
||||
|
||||
|
||||
/* Start preliminary tracking for map */
|
||||
private void startPreliminaryTracking() {
|
||||
if (mLocationSystemSetting && !mLocalTrackerRunning) {
|
||||
// create location listeners
|
||||
List locationProviders = mLocationManager.getAllProviders();
|
||||
if (locationProviders.contains(LocationManager.GPS_PROVIDER)) {
|
||||
mGPSListener = createLocationListener();
|
||||
mLocalTrackerRunning = true;
|
||||
}
|
||||
if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) {
|
||||
mNetworkListener = createLocationListener();
|
||||
mLocalTrackerRunning = true;
|
||||
}
|
||||
// register listeners
|
||||
LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
||||
LogHelper.v(LOG_TAG, "Starting preliminary tracking.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Removes gps and network location listeners */
|
||||
private void stopPreliminaryTracking() {
|
||||
if (mLocalTrackerRunning) {
|
||||
mLocalTrackerRunning = false;
|
||||
// remove listeners
|
||||
LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
||||
LogHelper.v(LOG_TAG, "Stopping preliminary tracking.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates listener for changes in location status */
|
||||
private LocationListener createLocationListener() {
|
||||
return new LocationListener() {
|
||||
public void onLocationChanged(Location location) {
|
||||
// check if the new location is better
|
||||
if (mCurrentBestLocation == null || LocationHelper.isBetterLocation(location, mCurrentBestLocation)) {
|
||||
// save location
|
||||
mCurrentBestLocation = location;
|
||||
// mark user's new location on map and remove last marker
|
||||
updateMyLocationMarker();
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status);
|
||||
}
|
||||
|
||||
public void onProviderEnabled(String provider) {
|
||||
LogHelper.v(LOG_TAG, "Location provider enabled: " + provider);
|
||||
}
|
||||
|
||||
public void onProviderDisabled(String provider) {
|
||||
LogHelper.v(LOG_TAG, "Location provider disabled: " + provider);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Updates marker for current user location */
|
||||
private void updateMyLocationMarker() {
|
||||
mMapView.getOverlays().remove(mMyLocationOverlay);
|
||||
// only update while not tracking
|
||||
if (!mTrackerServiceRunning) {
|
||||
mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isCurrent(mCurrentBestLocation), false);
|
||||
mMapView.getOverlays().add(mMyLocationOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Draws track onto overlay */
|
||||
private void drawTrackOverlay(Track track) {
|
||||
mMapView.getOverlays().remove(mTrackOverlay);
|
||||
mTrackOverlay = null;
|
||||
if (track == null || track.getSize() == 0) {
|
||||
LogHelper.i(LOG_TAG, "Waiting for a track. Showing preliminary location.");
|
||||
mTrackOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, false, mTrackerServiceRunning);
|
||||
Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_acquiring_location), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
LogHelper.v(LOG_TAG, "Drawing track overlay.");
|
||||
mTrackOverlay = MapHelper.createTrackOverlay(mActivity, track, mTrackerServiceRunning);
|
||||
}
|
||||
mMapView.getOverlays().add(mTrackOverlay);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Toggles snackbar indicating that location setting is off */
|
||||
private boolean toggleLocationOffBar() {
|
||||
// create snackbar indicator for location setting off
|
||||
if (mLocationOffBar == null) {
|
||||
mLocationOffBar = Snackbar.make(mMapView, R.string.snackbar_message_location_offline, Snackbar.LENGTH_INDEFINITE).setAction("Action", null);
|
||||
}
|
||||
|
||||
// get state of location system setting
|
||||
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity);
|
||||
|
||||
// show snackbar if necessary
|
||||
if (!mLocationSystemSetting) {
|
||||
// show snackbar
|
||||
mLocationOffBar.show();
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// hide snackbar
|
||||
mLocationOffBar.dismiss();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Creates receiver for new WayPoints */
|
||||
private BroadcastReceiver createTrackUpdatedReceiver() {
|
||||
return new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.hasExtra(EXTRA_TRACK) && intent.hasExtra(EXTRA_LAST_LOCATION)) {
|
||||
// draw track on map
|
||||
mTrack = intent.getParcelableExtra(EXTRA_TRACK);
|
||||
drawTrackOverlay(mTrack);
|
||||
// center map over last location
|
||||
mCurrentBestLocation = intent.getParcelableExtra(EXTRA_LAST_LOCATION);
|
||||
mController.setCenter(convertToGeoPoint(mCurrentBestLocation));
|
||||
// clear intent
|
||||
intent.setAction(ACTION_DEFAULT);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Converts Location to GeoPoint */
|
||||
private GeoPoint convertToGeoPoint (Location location) {
|
||||
if (location != null) {
|
||||
return new GeoPoint(location.getLatitude(), location.getLongitude());
|
||||
} else {
|
||||
return new GeoPoint(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Loads state tracker service from preferences */
|
||||
private void loadTrackerServiceState(Context context) {
|
||||
// TODO: get state directly from service, create a ServiceConnection.
|
||||
// see: https://github.com/ena1106/FragmentBoundServiceExample/blob/master/app/src/main/java/it/ena1106/fragmentboundservice/BoundFragment.java
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
mTrackerServiceRunning = settings.getBoolean(PREFS_TRACKER_SERVICE_RUNNING, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings
|
||||
*/
|
||||
private class SettingsContentObserver extends ContentObserver {
|
||||
|
||||
SettingsContentObserver(Handler handler) {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deliverSelfNotifications() {
|
||||
return super.deliverSelfNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
super.onChange(selfChange);
|
||||
LogHelper.v(LOG_TAG, "System Setting change detected.");
|
||||
|
||||
// check if location setting was changed
|
||||
boolean previousLocationSystemSetting = mLocationSystemSetting;
|
||||
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity);
|
||||
if (previousLocationSystemSetting != mLocationSystemSetting) {
|
||||
LogHelper.v(LOG_TAG, "Location Setting change detected.");
|
||||
toggleLocationOffBar();
|
||||
}
|
||||
|
||||
// start / stop preliminary tracking
|
||||
if (!mLocationSystemSetting) {
|
||||
stopPreliminaryTracking();
|
||||
} else if (!mTrackerServiceRunning && mFragmentVisible) {
|
||||
startPreliminaryTracking();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: Saves track to external storage using AsyncTask
|
||||
*/
|
||||
private class SaveTrackAsyncHelper extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
LogHelper.v(LOG_TAG, "Saving track object in background.");
|
||||
// save track object
|
||||
mStorageHelper.saveTrack(mTrack, FILE_MOST_CURRENT_TRACK);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
// clear track object
|
||||
LogHelper.v(LOG_TAG, "Saving finished.");
|
||||
mTrack = null;
|
||||
|
||||
// notify track fragment that save is finished
|
||||
Intent i = new Intent();
|
||||
i.setAction(ACTION_TRACK_SAVE);
|
||||
i.putExtra(EXTRA_SAVE_FINISHED, true);
|
||||
LocalBroadcastManager.getInstance(mActivity).sendBroadcast(i);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: Loads track from external storage using AsyncTask
|
||||
*/
|
||||
private class LoadTempTrackAsyncHelper extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
LogHelper.v(LOG_TAG, "Loading temporary track object in background.");
|
||||
// load track object
|
||||
mTrack = mStorageHelper.loadTrack(FILE_TEMP_TRACK);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
LogHelper.v(LOG_TAG, "Loading finished.");
|
||||
|
||||
// draw track on map
|
||||
if (mTrack != null) {
|
||||
drawTrackOverlay(mTrack);
|
||||
}
|
||||
|
||||
// delete temp file
|
||||
// mStorageHelper.deleteTempFile(); // todo check if necessary
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
|
@ -1,729 +0,0 @@
|
|||
/**
|
||||
* MainActivityTrackFragment.java
|
||||
* Implements the track fragment used in the track tab of the main activity
|
||||
* This fragment displays a saved track
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.location.Location;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import org.osmdroid.api.IMapController;
|
||||
import org.osmdroid.events.MapEventsReceiver;
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay;
|
||||
import org.osmdroid.views.overlay.MapEventsOverlay;
|
||||
import org.osmdroid.views.overlay.TilesOverlay;
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay;
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.helpers.DialogHelper;
|
||||
import org.y20k.trackbook.helpers.DropdownAdapter;
|
||||
import org.y20k.trackbook.helpers.ExportHelper;
|
||||
import org.y20k.trackbook.helpers.LengthUnitHelper;
|
||||
import org.y20k.trackbook.helpers.LocationHelper;
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.MapHelper;
|
||||
import org.y20k.trackbook.helpers.NightModeHelper;
|
||||
import org.y20k.trackbook.helpers.StorageHelper;
|
||||
import org.y20k.trackbook.helpers.TrackbookKeys;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* MainActivityTrackFragment class
|
||||
*/
|
||||
public class MainActivityTrackFragment extends Fragment implements AdapterView.OnItemSelectedListener, MapEventsReceiver, TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = MainActivityTrackFragment.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private FragmentActivity mActivity;
|
||||
private View mRootView;
|
||||
private MapView mMapView;
|
||||
private LinearLayout mOnboardingView;
|
||||
private IMapController mController;
|
||||
private ItemizedIconOverlay mTrackOverlay;
|
||||
private DropdownAdapter mDropdownAdapter;
|
||||
private ConstraintLayout mTrackManagementLayout;
|
||||
private Spinner mDropdown;
|
||||
private View mStatisticsSheet;
|
||||
private View mStatisticsView;
|
||||
private TextView mDistanceView;
|
||||
private TextView mStepsView;
|
||||
private TextView mWaypointsView;
|
||||
private TextView mDurationView;
|
||||
private TextView mRecordingStartView;
|
||||
private TextView mRecordingStopView;
|
||||
private TextView mMaxAltitudeView;
|
||||
private TextView mMinAltitudeView;
|
||||
private TextView mPositiveElevationView;
|
||||
private TextView mNegativeElevationView;
|
||||
private Group mElevationDataViews;
|
||||
private Group mStatisticsHeaderViews;
|
||||
private BottomSheetBehavior mStatisticsSheetBehavior;
|
||||
private int mCurrentTrack;
|
||||
private Track mTrack;
|
||||
private BroadcastReceiver mTrackSavedReceiver;
|
||||
|
||||
|
||||
/* Return a new Instance of MainActivityTrackFragment */
|
||||
public static MainActivityTrackFragment newInstance() {
|
||||
return new MainActivityTrackFragment();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// action bar has options menu
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// store activity
|
||||
mActivity = getActivity();
|
||||
|
||||
// get current track
|
||||
if (savedInstanceState != null) {
|
||||
mCurrentTrack = savedInstanceState.getInt(INSTANCE_CURRENT_TRACK, 0);
|
||||
} else {
|
||||
mCurrentTrack = 0;
|
||||
}
|
||||
|
||||
// create drop-down adapter
|
||||
mDropdownAdapter = new DropdownAdapter(mActivity);
|
||||
|
||||
// listen for finished save operation
|
||||
mTrackSavedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.hasExtra(EXTRA_SAVE_FINISHED) && intent.getBooleanExtra(EXTRA_SAVE_FINISHED, false)) {
|
||||
LogHelper.v(LOG_TAG, "Save operation detected. Start loading the new track.");
|
||||
|
||||
// update dropdown menu (and load track in onItemSelected)
|
||||
mDropdownAdapter.refresh();
|
||||
mDropdownAdapter.notifyDataSetChanged();
|
||||
mDropdown.setAdapter(mDropdownAdapter);
|
||||
mDropdown.setSelection(0, true);
|
||||
|
||||
// remove onboarding if necessary
|
||||
switchOnboardingLayout();
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter trackSavedReceiverIntentFilter = new IntentFilter(ACTION_TRACK_SAVE);
|
||||
LocalBroadcastManager.getInstance(mActivity).registerReceiver(mTrackSavedReceiver, trackSavedReceiverIntentFilter);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
// inflate root view from xml
|
||||
mRootView = inflater.inflate(R.layout.fragment_main_track, container, false);
|
||||
|
||||
// get reference to onboarding layout
|
||||
mOnboardingView = (LinearLayout) mRootView.findViewById(R.id.track_tab_onboarding);
|
||||
|
||||
// get reference to basic map
|
||||
mMapView = (MapView) mRootView.findViewById(R.id.track_map);
|
||||
|
||||
// get map controller
|
||||
mController = mMapView.getController();
|
||||
|
||||
// basic map setup
|
||||
mMapView.setTileSource(TileSourceFactory.MAPNIK);
|
||||
mMapView.setTilesScaledToDpi(true);
|
||||
|
||||
// set dark map tiles, if necessary
|
||||
if (NightModeHelper.getNightMode(mActivity)) {
|
||||
mMapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS);
|
||||
}
|
||||
|
||||
// add multi-touch capability
|
||||
mMapView.setMultiTouchControls(true);
|
||||
|
||||
// disable default zoom controls
|
||||
mMapView.getZoomController().setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER);
|
||||
|
||||
// add compass to map
|
||||
CompassOverlay compassOverlay = new CompassOverlay(mActivity, new InternalCompassOrientationProvider(mActivity), mMapView);
|
||||
compassOverlay.enableCompass();
|
||||
// move the compass overlay down a bit
|
||||
compassOverlay.setCompassCenter(35.0f, 96.0f);
|
||||
mMapView.getOverlays().add(compassOverlay);
|
||||
|
||||
// initiate map state
|
||||
if (savedInstanceState != null) {
|
||||
// restore saved instance of map
|
||||
GeoPoint position = new GeoPoint(savedInstanceState.getDouble(INSTANCE_LATITUDE_TRACK_MAP, DEFAULT_LATITUDE), savedInstanceState.getDouble(INSTANCE_LONGITUDE_TRACK_MAP, DEFAULT_LONGITUDE));
|
||||
mController.setCenter(position);
|
||||
mController.setZoom(savedInstanceState.getDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, 16f));
|
||||
} else {
|
||||
mController.setZoom(16f);
|
||||
}
|
||||
|
||||
// get views for track selector
|
||||
mTrackManagementLayout = (ConstraintLayout) mRootView.findViewById(R.id.track_management_layout);
|
||||
mDropdown = (Spinner) mRootView.findViewById(R.id.track_selector);
|
||||
|
||||
// attach listeners to export and delete buttons
|
||||
ImageButton shareButton = (ImageButton) mRootView.findViewById(R.id.share_button);
|
||||
ImageButton exportButton = (ImageButton) mRootView.findViewById(R.id.export_button);
|
||||
ImageButton deleteButton = (ImageButton) mRootView.findViewById(R.id.delete_button);
|
||||
shareButton.setOnClickListener(getShareButtonListener());
|
||||
exportButton.setOnClickListener(getExportButtonListener());
|
||||
deleteButton.setOnClickListener(getDeleteButtonListener());
|
||||
|
||||
// get views for statistics sheet
|
||||
mStatisticsView = mRootView.findViewById(R.id.statistics_view);
|
||||
mStatisticsSheet = mRootView.findViewById(R.id.statistics_sheet);
|
||||
mDistanceView = (TextView) mRootView.findViewById(R.id.statistics_data_distance);
|
||||
mStepsView = (TextView) mRootView.findViewById(R.id.statistics_data_steps);
|
||||
mWaypointsView = (TextView) mRootView.findViewById(R.id.statistics_data_waypoints);
|
||||
mDurationView = (TextView) mRootView.findViewById(R.id.statistics_data_duration);
|
||||
mRecordingStartView = (TextView) mRootView.findViewById(R.id.statistics_data_recording_start);
|
||||
mRecordingStopView = (TextView) mRootView.findViewById(R.id.statistics_data_recording_stop);
|
||||
mMaxAltitudeView = (TextView) mRootView.findViewById(R.id.statistics_data_max_altitude);
|
||||
mMinAltitudeView = (TextView) mRootView.findViewById(R.id.statistics_data_min_altitude);
|
||||
mPositiveElevationView = (TextView) mRootView.findViewById(R.id.statistics_data_positive_elevation);
|
||||
mNegativeElevationView = (TextView) mRootView.findViewById(R.id.statistics_data_negative_elevation);
|
||||
mElevationDataViews = (Group) mRootView.findViewById(R.id.elevation_data);
|
||||
mStatisticsHeaderViews = (Group) mRootView.findViewById(R.id.statistics_header);
|
||||
|
||||
|
||||
// display map and statistics
|
||||
if (savedInstanceState != null) {
|
||||
// get track from saved instance and display map and statistics
|
||||
mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_TRACK_MAP);
|
||||
displayTrack();
|
||||
} else if (mTrack == null) {
|
||||
// load track and display map and statistics
|
||||
LoadTrackAsyncHelper loadTrackAsyncHelper = new LoadTrackAsyncHelper();
|
||||
loadTrackAsyncHelper.execute();
|
||||
} else {
|
||||
// just display map and statistics
|
||||
displayTrack();
|
||||
}
|
||||
|
||||
// set up and show statistics sheet
|
||||
mStatisticsSheetBehavior = BottomSheetBehavior.from(mStatisticsSheet);
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
mStatisticsSheetBehavior.setBottomSheetCallback(getStatisticsSheetCallback());
|
||||
|
||||
// attach listener for taps on elevation views
|
||||
attachTapListenerToElevationViews();
|
||||
|
||||
// attach listener for taps on statistics sheet header
|
||||
attachTapListenerToStatisticHeaderViews();
|
||||
|
||||
// attach listener for taps on statistics - for US and other states plagued by Imperial units
|
||||
if (LengthUnitHelper.getUnitSystem() == IMPERIAL || Locale.getDefault().getCountry().equals("GB")) {
|
||||
attachTapListenerToStatisticsSheet();
|
||||
}
|
||||
|
||||
// enable additional gestures
|
||||
MapEventsOverlay OverlayEventos = new MapEventsOverlay(this);
|
||||
mMapView.getOverlays().add(OverlayEventos);
|
||||
|
||||
return mRootView;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mDropdown.setAdapter(mDropdownAdapter);
|
||||
mDropdown.setOnItemSelectedListener(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// show / hide the onboarding layout
|
||||
switchOnboardingLayout();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
|
||||
// deactivate map
|
||||
mMapView.onDetach();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LogHelper.v(LOG_TAG, "onDestroy called.");
|
||||
|
||||
// remove listener
|
||||
LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mTrackSavedReceiver);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
// update current track
|
||||
mCurrentTrack = i;
|
||||
|
||||
// load track and display map and statistics
|
||||
LoadTrackAsyncHelper loadTrackAsyncHelper = new LoadTrackAsyncHelper();
|
||||
loadTrackAsyncHelper.execute(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putDouble(INSTANCE_LATITUDE_TRACK_MAP, mMapView.getMapCenter().getLatitude());
|
||||
outState.putDouble(INSTANCE_LONGITUDE_TRACK_MAP, mMapView.getMapCenter().getLongitude());
|
||||
outState.putDouble(INSTANCE_ZOOM_LEVEL_TRACK_MAP, mMapView.getZoomLevelDouble());
|
||||
outState.putParcelable(INSTANCE_TRACK_TRACK_MAP, mTrack);
|
||||
outState.putInt(INSTANCE_CURRENT_TRACK, mCurrentTrack);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch(requestCode) {
|
||||
case RESULT_DELETE_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
deleteCurrentTrack();
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
LogHelper.v(LOG_TAG, "Delete dialog result: CANCEL");
|
||||
}
|
||||
break;
|
||||
case RESULT_EXPORT_DIALOG:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// user chose EXPORT
|
||||
ExportHelper.exportToGpx(mActivity, mTrack);
|
||||
} else if (resultCode == Activity.RESULT_CANCELED){
|
||||
// User chose CANCEL
|
||||
LogHelper.v(LOG_TAG, "Export to GPX: User chose CANCEL.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean longPressHelper(GeoPoint p) {
|
||||
if (mTrack != null) {
|
||||
// vibrate 50 milliseconds
|
||||
Vibrator vibrator = (Vibrator) mActivity.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
vibrator.vibrate(50);
|
||||
// zoom to bounding box (= edge coordinates of map)
|
||||
mMapView.zoomToBoundingBox(mTrack.getBoundingBox(), true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* Displays map and statistics for track */
|
||||
private void displayTrack() {
|
||||
GeoPoint position;
|
||||
|
||||
if (mTrack != null && mTrack.getSize() > 0) {
|
||||
// set end of track as position
|
||||
Location lastLocation = mTrack.getWayPointLocation(mTrack.getSize() -1);
|
||||
position = new GeoPoint(lastLocation.getLatitude(), lastLocation.getLongitude());
|
||||
|
||||
String recordingStart = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStart()) + " " +
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStart());
|
||||
String recordingStop = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStop()) + " " +
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStop());
|
||||
String stepsTaken;
|
||||
if (mTrack.getStepCount() == -1) {
|
||||
stepsTaken = getString(R.string.statistics_sheet_p_steps_no_pedometer);
|
||||
} else {
|
||||
stepsTaken = String.valueOf(Math.round(mTrack.getStepCount()));
|
||||
}
|
||||
|
||||
// populate length views
|
||||
displayCurrentLengthUnits();
|
||||
// populate other views
|
||||
mStepsView.setText(stepsTaken);
|
||||
mWaypointsView.setText(String.valueOf(mTrack.getWayPoints().size()));
|
||||
mDurationView.setText(LocationHelper.convertToReadableTime(mTrack.getTrackDuration(), true));
|
||||
mRecordingStartView.setText(recordingStart);
|
||||
mRecordingStopView.setText(recordingStop);
|
||||
|
||||
// show/hide elevation views depending on file format version
|
||||
if (mTrack.getTrackFormatVersion() > 1 && mTrack.getMinAltitude() > 0) {
|
||||
// show elevation views
|
||||
mElevationDataViews.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// hide elevation views
|
||||
mElevationDataViews.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// draw track on map
|
||||
drawTrackOverlay(mTrack);
|
||||
} else {
|
||||
position = new GeoPoint(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
|
||||
}
|
||||
|
||||
// center map over position
|
||||
mController.setCenter(position);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Draws track onto overlay */
|
||||
private void drawTrackOverlay(Track track) {
|
||||
mMapView.getOverlays().remove(mTrackOverlay);
|
||||
mTrackOverlay = MapHelper.createTrackOverlay(mActivity, track, false);
|
||||
mMapView.getOverlays().add(mTrackOverlay);
|
||||
}
|
||||
|
||||
|
||||
/* show the onboarding layout, if no track has been recorded yet */
|
||||
private void switchOnboardingLayout() {
|
||||
if (mDropdownAdapter.isEmpty()){
|
||||
// show onboarding layout
|
||||
mMapView.setVisibility(View.GONE);
|
||||
mOnboardingView.setVisibility(View.VISIBLE);
|
||||
mTrackManagementLayout.setVisibility(View.GONE);
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
mStatisticsSheet.setVisibility(View.GONE);
|
||||
} else {
|
||||
// show normal layout
|
||||
mOnboardingView.setVisibility(View.GONE);
|
||||
mMapView.setVisibility(View.VISIBLE);
|
||||
mTrackManagementLayout.setVisibility(View.VISIBLE);
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
mStatisticsSheet.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Displays views in statistic sheet according to current locale */
|
||||
private void displayCurrentLengthUnits() {
|
||||
mDistanceView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()));
|
||||
mPositiveElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getPositiveElevation()));
|
||||
mNegativeElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getNegativeElevation()));
|
||||
mMaxAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMaxAltitude()));
|
||||
mMinAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMinAltitude()));
|
||||
}
|
||||
|
||||
|
||||
/* Switches views in statistic sheet between Metric and Imperial */
|
||||
private void displayOppositeLengthUnits() {
|
||||
int oppositeLengthUnit = LengthUnitHelper.getUnitSystem() * -1;
|
||||
mDistanceView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance(), oppositeLengthUnit));
|
||||
mPositiveElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getPositiveElevation(), oppositeLengthUnit));
|
||||
mNegativeElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getNegativeElevation(), oppositeLengthUnit));
|
||||
mMaxAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMaxAltitude(), oppositeLengthUnit));
|
||||
mMinAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMinAltitude(), oppositeLengthUnit));
|
||||
}
|
||||
|
||||
|
||||
/* Deletes currently visible track */
|
||||
private void deleteCurrentTrack() {
|
||||
|
||||
// delete track file and refresh dropdown adapter
|
||||
if (mDropdownAdapter.getItem(mCurrentTrack).getTrackFile().delete()) {
|
||||
mDropdownAdapter.refresh();
|
||||
mDropdownAdapter.notifyDataSetChanged();
|
||||
mDropdown.setAdapter(mDropdownAdapter);
|
||||
} else {
|
||||
LogHelper.e(LOG_TAG, "Unable to delete recording.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDropdownAdapter.isEmpty()) {
|
||||
// show onboarding
|
||||
switchOnboardingLayout();
|
||||
} else {
|
||||
// show next track
|
||||
mDropdown.setSelection(0, true);
|
||||
mCurrentTrack = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Creates BottomSheetCallback for the statistics sheet - needed in onCreateView */
|
||||
private BottomSheetBehavior.BottomSheetCallback getStatisticsSheetCallback() {
|
||||
return new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||
// react to state change
|
||||
switch (newState) {
|
||||
case BottomSheetBehavior.STATE_EXPANDED:
|
||||
// statistics sheet expanded
|
||||
mTrackManagementLayout.setVisibility(View.INVISIBLE);
|
||||
mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_expanded));
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_COLLAPSED:
|
||||
// statistics sheet collapsed
|
||||
mTrackManagementLayout.setVisibility(View.VISIBLE);
|
||||
mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_collapsed));
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_HIDDEN:
|
||||
// statistics sheet hidden
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
||||
// reset length unit displays
|
||||
displayCurrentLengthUnits();
|
||||
// react to dragging events
|
||||
if (slideOffset < 0.5f) {
|
||||
mTrackManagementLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTrackManagementLayout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (slideOffset < 0.125f) {
|
||||
mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_collapsed));
|
||||
} else {
|
||||
mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_expanded));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Creates OnClickListener for the export button - needed in onCreateView */
|
||||
private View.OnClickListener getShareButtonListener() {
|
||||
return new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = ExportHelper.getGpxFileIntent(mActivity, mTrack);
|
||||
// create intent to show chooser
|
||||
String title = getString(R.string.dialog_share_gpx);
|
||||
// String title = getResources().getString(R.string.chooser_title);
|
||||
Intent chooser = Intent.createChooser(intent, title);
|
||||
if (intent.resolveActivity(mActivity.getPackageManager()) != null) {
|
||||
startActivity(chooser);
|
||||
} else {
|
||||
Toast.makeText(mActivity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Creates OnClickListener for the export button - needed in onCreateView */
|
||||
private View.OnClickListener getExportButtonListener() {
|
||||
return new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// dialog text components
|
||||
int dialogTitle;
|
||||
int dialogPositiveButton;
|
||||
int dialogNegativeButton;
|
||||
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault());
|
||||
String recordingStartDate = df.format(mTrack.getRecordingStart());
|
||||
String dialogMessage;
|
||||
|
||||
// get text elements for delete dialog
|
||||
if (ExportHelper.gpxFileExists(mTrack)) {
|
||||
// CASE: OVERWRITE - GPX file exists
|
||||
dialogTitle = R.string.dialog_export_title_overwrite;
|
||||
dialogMessage = getString(R.string.dialog_export_content_overwrite) + " (" + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()) + ")";
|
||||
dialogPositiveButton = R.string.dialog_export_action_overwrite;
|
||||
dialogNegativeButton = R.string.dialog_default_action_cancel;
|
||||
} else {
|
||||
// CASE: EXPORT - GPX file does NOT yet exits
|
||||
dialogTitle = R.string.dialog_export_title_export;
|
||||
dialogMessage = getString(R.string.dialog_export_content_export) + " (" + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()) + ")";
|
||||
dialogPositiveButton = R.string.dialog_export_action_export;
|
||||
dialogNegativeButton = R.string.dialog_default_action_cancel;
|
||||
}
|
||||
|
||||
// show delete dialog - results are handles by onActivityResult
|
||||
DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton);
|
||||
dialogFragment.setTargetFragment(MainActivityTrackFragment.this, RESULT_EXPORT_DIALOG);
|
||||
dialogFragment.show(mActivity.getSupportFragmentManager(), "ExportDialog");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Creates OnClickListener for the delete button - needed in onCreateView */
|
||||
private View.OnClickListener getDeleteButtonListener() {
|
||||
return new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// get text elements for delete dialog
|
||||
int dialogTitle = R.string.dialog_delete_title;
|
||||
int dialogPositiveButton = R.string.dialog_delete_action_delete;
|
||||
int dialogNegativeButton = R.string.dialog_default_action_cancel;
|
||||
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault());
|
||||
String recordingStartDate = df.format(mTrack.getRecordingStart());
|
||||
String dialogMessage = getString(R.string.dialog_delete_content) + " " + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance());
|
||||
|
||||
// show delete dialog - results are handles by onActivityResult
|
||||
DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton);
|
||||
dialogFragment.setTargetFragment(MainActivityTrackFragment.this, RESULT_DELETE_DIALOG);
|
||||
dialogFragment.show(mActivity.getSupportFragmentManager(), "DeleteDialog");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Add tap listener to elevation data views */
|
||||
private void attachTapListenerToElevationViews() {
|
||||
int referencedIds[] = mElevationDataViews.getReferencedIds();
|
||||
for (int id : referencedIds) {
|
||||
mRootView.findViewById(id).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// inform user about possible issues with altitude measurements
|
||||
Toast.makeText(mActivity, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Add tap listener to statistic header views */
|
||||
private void attachTapListenerToStatisticHeaderViews() {
|
||||
int referencedIds[] = mStatisticsHeaderViews.getReferencedIds();
|
||||
for (int id : referencedIds) {
|
||||
mRootView.findViewById(id).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (mStatisticsSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
} else {
|
||||
mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Add tap listener to statistics sheet */
|
||||
private void attachTapListenerToStatisticsSheet() {
|
||||
mStatisticsView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if(event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
displayOppositeLengthUnits();
|
||||
} else if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
displayCurrentLengthUnits();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: Loads track from external storage using AsyncTask
|
||||
*/
|
||||
private class LoadTrackAsyncHelper extends AsyncTask<Integer, Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Integer... ints) {
|
||||
LogHelper.v(LOG_TAG, "Loading track object in background.");
|
||||
|
||||
StorageHelper storageHelper = new StorageHelper(mActivity);
|
||||
if (ints.length > 0) {
|
||||
// get track file from dropdown adapter
|
||||
int item = ints[0];
|
||||
File trackFile = mDropdownAdapter.getItem(item).getTrackFile();
|
||||
LogHelper.v(LOG_TAG, "Loading track number " + item);
|
||||
mTrack = storageHelper.loadTrack(trackFile);
|
||||
} else {
|
||||
// load track object from most current file
|
||||
LogHelper.v(LOG_TAG, "No specific track specified. Loading most current one.");
|
||||
mTrack = storageHelper.loadTrack(FILE_MOST_CURRENT_TRACK);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
|
||||
// display track on map
|
||||
displayTrack();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
289
app/src/main/java/org/y20k/trackbook/MapFragment.kt
Normal file
289
app/src/main/java/org/y20k/trackbook/MapFragment.kt
Normal file
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* MapFragment.kt
|
||||
* Implements the MapFragment fragment
|
||||
* A MapFragment displays a map using osmdroid as well as the controls to start / stop a recording
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import YesNoDialog
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.TracklistElement
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
||||
|
||||
|
||||
/*
|
||||
* MapFragment class
|
||||
*/
|
||||
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private var bound: Boolean = false
|
||||
private val handler: Handler = Handler()
|
||||
private var trackingState: Int = Keys.STATE_NOT_TRACKING
|
||||
private var gpsProviderActive: Boolean = false
|
||||
private var networkProviderActive: Boolean = false
|
||||
private var track: Track = Track()
|
||||
private lateinit var currentBestLocation: Location
|
||||
private lateinit var layout: MapFragmentLayoutHolder
|
||||
private lateinit var trackerService: TrackerService
|
||||
|
||||
|
||||
/* Overrides onCreate from Fragment */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// get current best location
|
||||
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
|
||||
// get saved tracking state
|
||||
trackingState = PreferencesHelper.loadTrackingState(activity as Context)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStop from Fragment */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// initialize layout
|
||||
layout = MapFragmentLayoutHolder(activity as Context, inflater, container, currentBestLocation, trackingState)
|
||||
|
||||
// set up buttons
|
||||
layout.currentLocationButton.setOnClickListener {
|
||||
layout.centerMap(currentBestLocation, animated = true)
|
||||
}
|
||||
layout.recordingButton.setOnClickListener {
|
||||
handleTrackingManagementMenu()
|
||||
}
|
||||
layout.saveButton.setOnClickListener {
|
||||
saveTrack()
|
||||
}
|
||||
layout.clearButton.setOnClickListener {
|
||||
trackerService.clearTrack()
|
||||
}
|
||||
layout.resumeButton.setOnClickListener {
|
||||
// start service via intent so that it keeps running after unbind
|
||||
startTrackerService()
|
||||
trackerService.resumeTracking()
|
||||
}
|
||||
|
||||
return layout.rootView
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStart from Fragment */
|
||||
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)
|
||||
}
|
||||
// bind to TrackerService
|
||||
activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onResume from Fragment */
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// show hide the location error snackbar
|
||||
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
|
||||
// set map center
|
||||
layout.centerMap(currentBestLocation)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onPause from Fragment */
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
layout.saveState(currentBestLocation)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStop from Fragment */
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// unbind from TrackerService
|
||||
activity?.unbindService(connection)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onRequestPermissionsResult from Fragment */
|
||||
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)
|
||||
LogHelper.i(TAG, "Request result: Location permission has been granted.")
|
||||
} else {
|
||||
// permission denied - unbind service
|
||||
activity?.unbindService(connection)
|
||||
}
|
||||
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
super.onYesNoDialog(type, dialogResult, payload, payloadString)
|
||||
when (type) {
|
||||
Keys.DIALOG_EMPTY_RECORDING -> {
|
||||
when (dialogResult) {
|
||||
// user tapped resume
|
||||
true -> {
|
||||
trackerService.resumeTracking()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Start tracker service */
|
||||
private fun startTrackerService() {
|
||||
val intent = Intent(activity, TrackerService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// ... start service in foreground to prevent it being killed on Oreo
|
||||
activity?.startForegroundService(intent)
|
||||
} else {
|
||||
activity?.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
|
||||
private fun handleTrackingManagementMenu() {
|
||||
when (trackingState) {
|
||||
Keys.STATE_TRACKING_STOPPED -> layout.toggleRecordingButtonSubMenu()
|
||||
Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking()
|
||||
Keys.STATE_NOT_TRACKING -> {
|
||||
// start service via intent so that it keeps running after unbind
|
||||
startTrackerService()
|
||||
trackerService.startTracking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
// step 1: create and store filenames for json and gpx files
|
||||
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)
|
||||
// step 3: save tracklist - suspended
|
||||
FileHelper.addTrackAndSaveTracklistSuspended(activity as Context, track)
|
||||
// step 3: clear track
|
||||
trackerService.clearTrack()
|
||||
// step 4: open track in TrackFragement
|
||||
openTrack(track.toTracklistElement(activity as Context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Opens a track in TrackFragment */
|
||||
private fun openTrack(tracklistElement: TracklistElement) {
|
||||
val bundle: 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)
|
||||
bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement))
|
||||
findNavController().navigate(R.id.fragment_track, bundle)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Defines callbacks for service binding, passed to bindService()
|
||||
*/
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as TrackerService.LocalBinder
|
||||
trackerService = binder.service
|
||||
bound = true
|
||||
// start listening for location updates
|
||||
handler.removeCallbacks(periodicLocationRequestRunnable)
|
||||
handler.postDelayed(periodicLocationRequestRunnable, 0)
|
||||
}
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
bound = false
|
||||
// stop receiving location updates
|
||||
handler.removeCallbacks(periodicLocationRequestRunnable)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Runnable: Periodically requests location
|
||||
*/
|
||||
private val periodicLocationRequestRunnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
// pull values from service
|
||||
currentBestLocation = trackerService.currentBestLocation
|
||||
track = trackerService.track
|
||||
gpsProviderActive = trackerService.gpsProviderActive
|
||||
networkProviderActive = trackerService.networkProviderActive
|
||||
trackingState = trackerService.trackingState
|
||||
// update location and track
|
||||
layout.markCurrentPosition(currentBestLocation, trackingState)
|
||||
layout.overlayCurrentTrack(track, trackingState)
|
||||
layout.updateRecordingButton(trackingState)
|
||||
// center map, if it had not been dragged/zoomed before
|
||||
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
|
||||
handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
}
|
109
app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
Normal file
109
app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* SettingsFragment.kt
|
||||
* Implements the SettingsFragment fragment
|
||||
* A SettingsFragment displays the user accessible settings of the app
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.*
|
||||
import org.y20k.trackbook.helpers.LengthUnitHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
|
||||
/*
|
||||
* SettingsFragment class
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(SettingsFragment::class.java)
|
||||
|
||||
|
||||
/* Overrides onViewCreated from PreferenceFragmentCompat */
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// set the background color
|
||||
view.setBackgroundColor(resources.getColor(R.color.app_window_background, null))
|
||||
// add padding - necessary because translucent status bar is used
|
||||
val topPadding = this.resources.displayMetrics.density * 24 // 24 dp * display density
|
||||
view.setPadding(0, topPadding.toInt(), 0, 0)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onCreatePreferences from PreferenceFragmentCompat */
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
||||
val context = preferenceManager.context
|
||||
val screen = preferenceManager.createPreferenceScreen(context)
|
||||
|
||||
// set up "Enable Imperial Measurements" preference
|
||||
val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
|
||||
preferenceImperialMeasurementUnits.title = getString(R.string.pref_imperial_measurement_units_title)
|
||||
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.setDefaultValue(LengthUnitHelper.useImperialUnits())
|
||||
|
||||
// set up "Restrict to GPS" preference
|
||||
val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
|
||||
preferenceGpsOnly.title = getString(R.string.pref_gps_only_title)
|
||||
preferenceGpsOnly.key = Keys.PREF_GPS_ONLY
|
||||
preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only)
|
||||
preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network)
|
||||
preferenceGpsOnly.setDefaultValue(false)
|
||||
|
||||
// set up "Accuracy Threshold" preference
|
||||
val preferenceAccuracyThreshold: SeekBarPreference = SeekBarPreference(activity as Context)
|
||||
preferenceAccuracyThreshold.title = getString(R.string.pref_accuracy_threshold_title)
|
||||
preferenceAccuracyThreshold.key = Keys.PREF_LOCATION_ACCURACY_THRESHOLD
|
||||
preferenceAccuracyThreshold.summary = getString(R.string.pref_accuracy_threshold_summary)
|
||||
preferenceAccuracyThreshold.showSeekBarValue = true
|
||||
preferenceAccuracyThreshold.max = 50
|
||||
preferenceAccuracyThreshold.setDefaultValue(Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY)
|
||||
|
||||
// set up "Reset" preference
|
||||
val preferenceResetAdvanced: Preference = Preference(activity as Context)
|
||||
preferenceResetAdvanced.title = getString(R.string.pref_reset_advanced_title)
|
||||
preferenceResetAdvanced.summary = getString(R.string.pref_reset_advanced_summary)
|
||||
preferenceResetAdvanced.setOnPreferenceClickListener{
|
||||
preferenceAccuracyThreshold.value = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
// set preference categories
|
||||
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
|
||||
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
|
||||
preferenceCategoryGeneral.contains(preferenceGpsOnly)
|
||||
val preferenceCategoryAdvanced: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
|
||||
preferenceCategoryAdvanced.contains(preferenceAccuracyThreshold)
|
||||
preferenceCategoryAdvanced.contains(preferenceResetAdvanced)
|
||||
|
||||
// setup preference screen
|
||||
screen.addPreference(preferenceCategoryGeneral)
|
||||
screen.addPreference(preferenceImperialMeasurementUnits)
|
||||
screen.addPreference(preferenceGpsOnly)
|
||||
screen.addPreference(preferenceCategoryAdvanced)
|
||||
screen.addPreference(preferenceAccuracyThreshold)
|
||||
screen.addPreference(preferenceResetAdvanced)
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
}
|
143
app/src/main/java/org/y20k/trackbook/TrackFragment.kt
Normal file
143
app/src/main/java/org/y20k/trackbook/TrackFragment.kt
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* TrackFragment.kt
|
||||
* Implements the TrackFragment fragment
|
||||
* A TrackFragment displays a previously recorded track
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
|
||||
import YesNoDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.y20k.trackbook.Keys.ARG_TRACK_ID
|
||||
import org.y20k.trackbook.dialogs.RenameTrackDialog
|
||||
import org.y20k.trackbook.helpers.FileHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
|
||||
|
||||
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TrackFragment::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private lateinit var layout:TrackFragmentLayoutHolder
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// initialize layout
|
||||
layout = TrackFragmentLayoutHolder(activity as Context, inflater, container, arguments)
|
||||
|
||||
// set up share button
|
||||
layout.shareButton.setOnClickListener {
|
||||
shareGpXTrack()
|
||||
}
|
||||
// set up delete button
|
||||
layout.deleteButton.setOnClickListener {
|
||||
val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_remove_recording)}\n\n- ${layout.trackNameView.text}"
|
||||
YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_REMOVE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_remove_recording)
|
||||
}
|
||||
// set up rename button
|
||||
layout.editButton.setOnClickListener {
|
||||
RenameTrackDialog(this as RenameTrackDialog.RenameTrackListener).show(activity as Context, layout.trackNameView.text.toString())
|
||||
}
|
||||
|
||||
return layout.rootView
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onResume from Fragment */
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// update zoom level and map center
|
||||
layout.updateMapView()
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onPause from Fragment */
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// save zoom level and map center
|
||||
layout.saveViewStateToTrack()
|
||||
}
|
||||
|
||||
|
||||
/* 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) }
|
||||
// update name in layout
|
||||
layout.track.name = textInput
|
||||
layout.trackNameView.text = textInput
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
when (type) {
|
||||
Keys.DIALOG_REMOVE_TRACK -> {
|
||||
when (dialogResult) {
|
||||
// user tapped remove track
|
||||
true -> {
|
||||
// switch to TracklistFragment and remove track there
|
||||
val trackId: Long = arguments?.getLong(ARG_TRACK_ID, -1L) ?: -1L
|
||||
val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to trackId)
|
||||
findNavController().navigate(R.id.tracklist_fragment, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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, "${activity!!.applicationContext.packageName}.provider", gpxFile)
|
||||
val shareIntent: Intent = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
data = gpxShareUri
|
||||
type = "application/gpx+xml"
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
putExtra(Intent.EXTRA_STREAM, gpxShareUri)
|
||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.dialog_share_gpx))
|
||||
}, null)
|
||||
|
||||
// show share sheet - if file helper is available
|
||||
val packageManager: PackageManager? = activity?.packageManager
|
||||
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) {
|
||||
startActivity(shareIntent)
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* Trackbook.java
|
||||
* Implements the Trackbook class
|
||||
* Trackbook starts up the app and sets up the basic theme (Day / Night)
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.NightModeHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Trackbook.class
|
||||
*/
|
||||
public class Trackbook extends Application {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = Trackbook.class.getSimpleName();
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// set Day / Night theme state
|
||||
NightModeHelper.restoreSavedState(this);
|
||||
|
||||
// todo remove
|
||||
// if (Build.VERSION.SDK_INT >= 28) {
|
||||
// // Android P might introduce a system wide theme option - in that case: follow system (28 = Build.VERSION_CODES.P)
|
||||
// AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
// } else {
|
||||
// // try to get last state the user chose
|
||||
// NightModeHelper.restoreSavedState(this);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogHelper.v(LOG_TAG, "Trackbook application terminated.");
|
||||
}
|
||||
|
||||
}
|
51
app/src/main/java/org/y20k/trackbook/Trackbook2.kt
Normal file
51
app/src/main/java/org/y20k/trackbook/Trackbook2.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Trackbook.kt
|
||||
* Implements the Trackbook class
|
||||
* Trackbook is the base Application class that sets up day and night theme
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import android.app.Application
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.NightModeHelper
|
||||
|
||||
|
||||
/*
|
||||
* Trackbook.class
|
||||
*/
|
||||
class Trackbook: Application() {
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(Trackbook::class.java)
|
||||
|
||||
|
||||
/* Implements onCreate */
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
LogHelper.v(TAG, "Trackbook application started.")
|
||||
// set Day / Night theme state
|
||||
NightModeHelper.restoreSavedState(this)
|
||||
}
|
||||
|
||||
|
||||
/* Implements onTerminate */
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
LogHelper.v(TAG, "Trackbook application terminated.")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,618 +0,0 @@
|
|||
/**
|
||||
* TrackerService.java
|
||||
* Implements the app's movement tracker service
|
||||
* The TrackerService creates a Track object and displays a notification
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.CountDownTimer;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.helpers.LocationHelper;
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.NotificationHelper;
|
||||
import org.y20k.trackbook.helpers.StorageHelper;
|
||||
import org.y20k.trackbook.helpers.TrackbookKeys;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.hardware.Sensor.TYPE_STEP_COUNTER;
|
||||
|
||||
|
||||
/**
|
||||
* TrackerService class
|
||||
*/
|
||||
public class TrackerService extends Service implements TrackbookKeys, SensorEventListener {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = TrackerService.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private Track mTrack;
|
||||
private CountDownTimer mTimer;
|
||||
private LocationManager mLocationManager;
|
||||
private SensorManager mSensorManager;
|
||||
private float mStepCountOffset;
|
||||
private LocationListener mGPSListener = null;
|
||||
private LocationListener mNetworkListener = null;
|
||||
private SettingsContentObserver mSettingsContentObserver;
|
||||
private Location mCurrentBestLocation;
|
||||
private Notification mNotification;
|
||||
private NotificationCompat.Builder mNotificationBuilder;
|
||||
private NotificationManager mNotificationManager;
|
||||
private boolean mTrackerServiceRunning;
|
||||
private boolean mLocationSystemSetting;
|
||||
private boolean mResumedFlag;
|
||||
|
||||
private final IBinder mBinder = new LocalBinder(); // todo move to onCreate
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// prepare notification channel and get NotificationManager
|
||||
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationHelper.createNotificationChannel(this);
|
||||
|
||||
// acquire reference to Location Manager
|
||||
mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
// acquire reference to Sensor Manager
|
||||
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
// get state of location system setting
|
||||
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext());
|
||||
|
||||
// create content observer for changes in System Settings
|
||||
mSettingsContentObserver = new SettingsContentObserver(new Handler());
|
||||
|
||||
// initialize the resume flag
|
||||
mResumedFlag = false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// a client is binding to the service with bindService()
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// return true if you would like to have the service's onRebind(Intent) method later called when new clients bind to it.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRebind(Intent intent) {
|
||||
// a client is binding to the service with bindService(), after onUnbind() has already been called
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
// SERVICE RESTART (via START_STICKY)
|
||||
if (intent == null) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREFS_TRACKER_SERVICE_RUNNING, false)) {
|
||||
LogHelper.w(LOG_TAG, "Trackbook has been killed by the operating system. Trying to resume recording.");
|
||||
resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager));
|
||||
}
|
||||
}
|
||||
// ACTION STOP
|
||||
else if (ACTION_STOP.equals(intent.getAction())) {
|
||||
stopTracking();
|
||||
}
|
||||
// ACTION RESUME
|
||||
else if (ACTION_RESUME.equals(intent.getAction())) {
|
||||
resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager));
|
||||
}
|
||||
|
||||
// START_STICKY is used for services that are explicitly started and stopped as needed
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
LogHelper.v(LOG_TAG, "onTaskRemoved called.");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LogHelper.v(LOG_TAG, "onDestroy called.");
|
||||
|
||||
if (mTrackerServiceRunning) {
|
||||
stopTracking();
|
||||
}
|
||||
|
||||
// remove TrackerService from foreground state
|
||||
stopForeground(true);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent sensorEvent) {
|
||||
// save the step count offset (steps previously recorded by the system) and subtract any steps recorded during this session in case the app was killed
|
||||
if (mStepCountOffset == 0) {
|
||||
mStepCountOffset = (sensorEvent.values[0] - 1) - mTrack.getStepCount();
|
||||
}
|
||||
|
||||
// calculate step count
|
||||
float stepCount = sensorEvent.values[0] - mStepCountOffset;
|
||||
|
||||
// set step count in track
|
||||
mTrack.setStepCount(stepCount);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int i) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Start tracking location */
|
||||
public void startTracking(Location lastLocation) {
|
||||
if (mLocationSystemSetting) {
|
||||
LogHelper.v(LOG_TAG, "Start tracking");
|
||||
|
||||
// create a new track - if requested
|
||||
mTrack = new Track();
|
||||
|
||||
// get last location
|
||||
if (lastLocation != null) {
|
||||
mCurrentBestLocation = lastLocation;
|
||||
} else {
|
||||
mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager);
|
||||
}
|
||||
|
||||
// begin recording
|
||||
startMovementRecording();
|
||||
|
||||
} else {
|
||||
LogHelper.i(LOG_TAG, "Location Setting is turned off.");
|
||||
Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Resume tracking after stop/pause */
|
||||
public void resumeTracking(Location lastLocation) {
|
||||
if (mLocationSystemSetting) {
|
||||
LogHelper.v(LOG_TAG, "Recording resumed");
|
||||
|
||||
// switch the resume flag
|
||||
mResumedFlag = true;
|
||||
|
||||
// create a new track - if requested
|
||||
StorageHelper storageHelper = new StorageHelper(this);
|
||||
if (storageHelper.tempFileExists()) {
|
||||
// load temp track file
|
||||
mTrack = storageHelper.loadTrack(FILE_TEMP_TRACK);
|
||||
// try to mark last waypoint as stopover
|
||||
int lastWayPoint = mTrack.getSize() - 1;
|
||||
if (lastWayPoint >= 0) {
|
||||
mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true);
|
||||
}
|
||||
} else {
|
||||
// fallback, if tempfile did not exist
|
||||
LogHelper.e(LOG_TAG, "Unable to find previously saved track temp file.");
|
||||
mTrack = new Track();
|
||||
}
|
||||
|
||||
// get last location
|
||||
mCurrentBestLocation = lastLocation;
|
||||
// FALLBACK: use last recorded location
|
||||
if (mCurrentBestLocation == null && mTrack.getSize() > 0) {
|
||||
mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() -1);
|
||||
}
|
||||
|
||||
// begin recording
|
||||
startMovementRecording();
|
||||
|
||||
} else {
|
||||
LogHelper.i(LOG_TAG, "Location Setting is turned off.");
|
||||
Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Stop tracking location */
|
||||
public void stopTracking() {
|
||||
LogHelper.v(LOG_TAG, "Recording stopped");
|
||||
|
||||
// catches a bug that leaves the ui in a incorrect state after a crash
|
||||
if (!mTrackerServiceRunning) {
|
||||
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE);
|
||||
broadcastTrackingStateChange();
|
||||
return;
|
||||
}
|
||||
|
||||
// store current date and time
|
||||
mTrack.setRecordingEnd();
|
||||
|
||||
// stop timer
|
||||
mTimer.cancel();
|
||||
|
||||
// broadcast an updated track
|
||||
broadcastTrackUpdate();
|
||||
|
||||
// save a temp file in case the activity has been killed
|
||||
SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper();
|
||||
saveTempTrackAsyncHelper.execute();
|
||||
|
||||
// change notification
|
||||
displayNotification(false);
|
||||
|
||||
// reset resume flag
|
||||
mResumedFlag = false;
|
||||
|
||||
// remove listeners
|
||||
stopFindingLocation();
|
||||
mSensorManager.unregisterListener(this);
|
||||
|
||||
// disable content observer for changes in System Settings
|
||||
this.getContentResolver().unregisterContentObserver(mSettingsContentObserver);
|
||||
|
||||
// remove TrackerService from foreground state
|
||||
stopForeground(false);
|
||||
}
|
||||
|
||||
|
||||
/* Dismiss notification */
|
||||
public void dismissNotification() {
|
||||
// save state
|
||||
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_DEFAULT);
|
||||
// cancel notification
|
||||
mNotificationManager.cancel(TRACKER_SERVICE_NOTIFICATION_ID); // todo check if necessary?
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
|
||||
/* Starts to record movements */
|
||||
private void startMovementRecording() {
|
||||
// initialize step counter
|
||||
mStepCountOffset = 0;
|
||||
|
||||
// add last location as WayPoint to track
|
||||
addWayPointToTrack();
|
||||
|
||||
// put up notification
|
||||
displayNotification(true);
|
||||
|
||||
// create gps and network location listeners
|
||||
startFindingLocation();
|
||||
|
||||
// start timer that periodically request a location update
|
||||
startRequestingLocationChanges();
|
||||
|
||||
// start counting steps
|
||||
startStepCounter();
|
||||
|
||||
// register content observer for changes in System Settings
|
||||
this.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver);
|
||||
|
||||
// start service in foreground
|
||||
startForeground(TRACKER_SERVICE_NOTIFICATION_ID, mNotification);
|
||||
}
|
||||
|
||||
|
||||
/* Registers a step counter listener */
|
||||
private void startStepCounter() {
|
||||
boolean stepCounterAvailable = mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI);
|
||||
if (stepCounterAvailable) {
|
||||
LogHelper.v(LOG_TAG, "Pedometer sensor available: Registering listener.");
|
||||
} else {
|
||||
LogHelper.i(LOG_TAG, "Pedometer sensor not available.");
|
||||
mTrack.setStepCount(-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Set timer to periodically retrieve new locations and to prevent endless tracking */
|
||||
private void startRequestingLocationChanges() {
|
||||
final long previouslyRecordedDuration = mTrack.getTrackDuration();
|
||||
mTimer = new CountDownTimer(EIGHT_HOURS_IN_MILLISECONDS, FIFTEEN_SECONDS_IN_MILLISECONDS) {
|
||||
@Override
|
||||
public void onTick(long millisUntilFinished) {
|
||||
// update track duration - and add duration from previously interrupted / paused session
|
||||
long duration = EIGHT_HOURS_IN_MILLISECONDS - millisUntilFinished + previouslyRecordedDuration;
|
||||
mTrack.setDuration(duration);
|
||||
// try to add WayPoint to Track
|
||||
addWayPointToTrack();
|
||||
// update notification
|
||||
mNotification = NotificationHelper.getUpdatedNotification(TrackerService.this, mNotificationBuilder, mTrack);
|
||||
mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification);
|
||||
// save a temp file in case the service has been killed by the system
|
||||
SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper();
|
||||
saveTempTrackAsyncHelper.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
// stop tracking after eight hours
|
||||
stopTracking();
|
||||
}
|
||||
};
|
||||
mTimer.start();
|
||||
}
|
||||
|
||||
|
||||
/* Display notification */
|
||||
private void displayNotification(boolean trackingState) {
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL);
|
||||
mNotification = NotificationHelper.getNotification(this, mNotificationBuilder, mTrack, trackingState);
|
||||
mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification); // todo check if necessary in pre Android O
|
||||
}
|
||||
|
||||
|
||||
/* Adds a new WayPoint to current track */
|
||||
private void addWayPointToTrack() {
|
||||
|
||||
boolean success = false;
|
||||
Location previousLocation = null;
|
||||
int trackSize = mTrack.getSize();
|
||||
|
||||
if (trackSize == 0) {
|
||||
// if accurate AND current
|
||||
if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isCurrent(mCurrentBestLocation)) {
|
||||
// add first location to track
|
||||
success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation);
|
||||
} else {
|
||||
// just send a broadcast indicating that current location fix not not suited
|
||||
broadcastTrackUpdate();
|
||||
}
|
||||
} else {
|
||||
// get location of previous WayPoint
|
||||
previousLocation = mTrack.getWayPointLocation(trackSize - 1);
|
||||
|
||||
// default value for average speed
|
||||
float averageSpeed = 0f;
|
||||
|
||||
// compute average speed if new location came from network provider
|
||||
if (trackSize > 1 && LocationManager.NETWORK_PROVIDER.equals(mCurrentBestLocation.getProvider())) {
|
||||
Location firstWayPoint = mTrack.getWayPointLocation(0);
|
||||
float distance = firstWayPoint.distanceTo(previousLocation);
|
||||
long timeDifference = previousLocation.getElapsedRealtimeNanos() - firstWayPoint.getElapsedRealtimeNanos();
|
||||
averageSpeed = distance / ((float) timeDifference / ONE_SECOND_IN_NANOSECOND);
|
||||
}
|
||||
|
||||
// if accurate AND new
|
||||
if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isNewWayPoint(previousLocation, mCurrentBestLocation, averageSpeed)) {
|
||||
// add current best location to track
|
||||
success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (mResumedFlag) {
|
||||
int lastWayPoint = mTrack.getSize() - 2;
|
||||
if (lastWayPoint >= 0) {
|
||||
// mark last location as stop over
|
||||
mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true);
|
||||
}
|
||||
mResumedFlag = false;
|
||||
} else {
|
||||
// update distance, if not resumed
|
||||
mTrack.updateDistance(previousLocation, mCurrentBestLocation);
|
||||
}
|
||||
|
||||
// send local broadcast if new WayPoint was added
|
||||
broadcastTrackUpdate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Broadcasts a track update */
|
||||
private void broadcastTrackUpdate() {
|
||||
if (mTrack != null) {
|
||||
Intent i = new Intent();
|
||||
i.setAction(ACTION_TRACK_UPDATED);
|
||||
i.putExtra(EXTRA_TRACK, mTrack);
|
||||
i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates a location listener */
|
||||
private LocationListener createLocationListener() {
|
||||
return new LocationListener() {
|
||||
public void onLocationChanged(Location location) {
|
||||
// check if the new location is better
|
||||
if (LocationHelper.isBetterLocation(location, mCurrentBestLocation)) {
|
||||
// save location
|
||||
mCurrentBestLocation = location;
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status);
|
||||
}
|
||||
|
||||
public void onProviderEnabled(String provider) {
|
||||
LogHelper.v(LOG_TAG, "Location provider enabled: " + provider);
|
||||
}
|
||||
|
||||
public void onProviderDisabled(String provider) {
|
||||
LogHelper.v(LOG_TAG, "Location provider disabled: " + provider);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* Creates gps and network location listeners */
|
||||
private void startFindingLocation() {
|
||||
|
||||
// register location listeners and request updates
|
||||
List locationProviders = mLocationManager.getAllProviders();
|
||||
if (locationProviders.contains(LocationManager.GPS_PROVIDER)) {
|
||||
mGPSListener = createLocationListener();
|
||||
mTrackerServiceRunning = true;
|
||||
}
|
||||
if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) {
|
||||
mNetworkListener = createLocationListener();
|
||||
mTrackerServiceRunning = true;
|
||||
}
|
||||
LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
||||
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_RECORDING);
|
||||
|
||||
// notify MainActivity
|
||||
broadcastTrackingStateChange();
|
||||
}
|
||||
|
||||
|
||||
/* Removes gps and network location listeners */
|
||||
private void stopFindingLocation() {
|
||||
// remove listeners
|
||||
LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
||||
mTrackerServiceRunning = false;
|
||||
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE);
|
||||
|
||||
// notify MainActivity
|
||||
broadcastTrackingStateChange();
|
||||
}
|
||||
|
||||
|
||||
/* Sends a broadcast with tracking changed */
|
||||
private void broadcastTrackingStateChange() {
|
||||
Intent i = new Intent();
|
||||
i.setAction(ACTION_TRACKING_STATE_CHANGED);
|
||||
i.putExtra(EXTRA_TRACK, mTrack);
|
||||
i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation);
|
||||
i.putExtra(EXTRA_TRACKING_STATE, mTrackerServiceRunning);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i);
|
||||
}
|
||||
|
||||
|
||||
/* Saves state of Tracker Service and floating Action Button */
|
||||
private void saveTrackerServiceState(boolean trackerServiceRunning, int fabState) {
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putBoolean(PREFS_TRACKER_SERVICE_RUNNING, trackerServiceRunning);
|
||||
editor.putInt(PREFS_FAB_STATE, fabState);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: Local Binder that returns this service
|
||||
*/
|
||||
public class LocalBinder extends Binder {
|
||||
TrackerService getService() {
|
||||
// return this instance of TrackerService so clients can call public methods
|
||||
return TrackerService.this;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings
|
||||
*/
|
||||
public class SettingsContentObserver extends ContentObserver {
|
||||
|
||||
public SettingsContentObserver(Handler handler) {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deliverSelfNotifications() {
|
||||
return super.deliverSelfNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
super.onChange(selfChange);
|
||||
LogHelper.v(LOG_TAG, "System Setting change detected.");
|
||||
|
||||
// check if location setting was changed
|
||||
boolean previousLocationSystemSetting = mLocationSystemSetting;
|
||||
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext());
|
||||
if (previousLocationSystemSetting != mLocationSystemSetting && !mLocationSystemSetting && mTrackerServiceRunning) {
|
||||
LogHelper.v(LOG_TAG, "Location Setting turned off while tracking service running.");
|
||||
if (mTrack != null) {
|
||||
stopTracking();
|
||||
}
|
||||
stopForeground(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: Saves track to external storage using AsyncTask
|
||||
*/
|
||||
private class SaveTempTrackAsyncHelper extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
LogHelper.v(LOG_TAG, "Saving temporary track object in background.");
|
||||
// save track object
|
||||
StorageHelper storageHelper = new StorageHelper(TrackerService.this);
|
||||
storageHelper.saveTrack(mTrack, FILE_TEMP_TRACK);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
LogHelper.v(LOG_TAG, "Saving finished.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
356
app/src/main/java/org/y20k/trackbook/TrackerService.kt
Normal file
356
app/src/main/java/org/y20k/trackbook/TrackerService.kt
Normal file
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
* TrackerService.kt
|
||||
* Implements the app's movement tracker service
|
||||
* The TrackerService keeps track of the current location
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
/*
|
||||
* TrackerService class
|
||||
*/
|
||||
class TrackerService(): Service(), CoroutineScope, SensorEventListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
var trackingState: Int = Keys.STATE_NOT_TRACKING
|
||||
var gpsProviderActive: Boolean = false
|
||||
var networkProviderActive: Boolean = false
|
||||
var useImperial: Boolean = false
|
||||
var gpsOnly: Boolean = false
|
||||
var locationAccuracyThreshold: Int = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY
|
||||
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
|
||||
var stepCountOffset: Float = 0f
|
||||
var track: Track = Track()
|
||||
private val binder = LocalBinder()
|
||||
private val handler: Handler = Handler()
|
||||
private lateinit var locationManager: LocationManager
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
private lateinit var notificationHelper: NotificationHelper
|
||||
private lateinit var gpsLocationListener: LocationListener
|
||||
private lateinit var networkLocationListener: LocationListener
|
||||
private lateinit var backgroundJob: Job
|
||||
|
||||
|
||||
/* Overrides coroutineContext variable */
|
||||
override val coroutineContext: CoroutineContext get() = backgroundJob + Dispatchers.Main
|
||||
|
||||
|
||||
/* Overrides onCreate from Service */
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationHelper = NotificationHelper(this)
|
||||
gpsProviderActive = LocationHelper.isGpsEnabled(locationManager)
|
||||
networkProviderActive = LocationHelper.isNetworkEnabled(locationManager)
|
||||
gpsLocationListener = createLocationListener()
|
||||
networkLocationListener = createLocationListener()
|
||||
useImperial = PreferencesHelper.loadUseImperialUnits(this)
|
||||
locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this)
|
||||
trackingState = PreferencesHelper.loadTrackingState(this)
|
||||
currentBestLocation = LocationHelper.getLastKnownLocation(this)
|
||||
track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this))
|
||||
backgroundJob = Job()
|
||||
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStartCommand from Service */
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
// 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.")
|
||||
resumeTracking()
|
||||
}
|
||||
// ACTION STOP
|
||||
} else if (Keys.ACTION_STOP == intent.action) {
|
||||
stopTracking()
|
||||
// ACTION START
|
||||
} else if (Keys.ACTION_START == intent.action) {
|
||||
startTracking()
|
||||
// ACTION RESUME
|
||||
} else if (Keys.ACTION_RESUME == intent.action) {
|
||||
resumeTracking()
|
||||
}
|
||||
|
||||
// START_STICKY is used for services that are explicitly started and stopped as needed
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onBind from Service */
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
addLocationListeners()
|
||||
return binder
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onDestroy from Service */
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
LogHelper.i(TAG, "onDestroy called.")
|
||||
if (trackingState == Keys.STATE_TRACKING_ACTIVE) stopTracking()
|
||||
stopForeground(true)
|
||||
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
|
||||
removeLocationListeners()
|
||||
backgroundJob.cancel()
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onAccuracyChanged from SensorEventListener */
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||
LogHelper.v(TAG, "Accuracy changed: $accuracy")
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onSensorChanged from SensorEventListener */
|
||||
override fun onSensorChanged(sensorEvent: SensorEvent?) {
|
||||
var steps: Float = 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
|
||||
}
|
||||
// calculate step count - subtract steps previously recorded
|
||||
steps = sensorEvent.values[0] - stepCountOffset
|
||||
}
|
||||
// update step count in track
|
||||
track.stepCount = steps
|
||||
}
|
||||
|
||||
|
||||
/* Resume tracking after stop/pause */
|
||||
fun resumeTracking() {
|
||||
// load temp track - returns an empty track if not available
|
||||
track = FileHelper.readTrack(this, 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
|
||||
}
|
||||
// start tracking
|
||||
startTracking(newTrack = false)
|
||||
}
|
||||
|
||||
|
||||
/* Start tracking location */
|
||||
fun startTracking(newTrack: Boolean = true) {
|
||||
if (newTrack) {
|
||||
track.recordingStart = GregorianCalendar.getInstance().time
|
||||
track.recordingStop = track.recordingStart
|
||||
track.name = DateTimeHelper.convertToReadableDate(track.recordingStart)
|
||||
stepCountOffset = 0f
|
||||
}
|
||||
trackingState = Keys.STATE_TRACKING_ACTIVE
|
||||
PreferencesHelper.saveTrackingState(this, trackingState)
|
||||
startStepCounter()
|
||||
handler.postDelayed(periodicTrackUpdate, 0)
|
||||
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
|
||||
}
|
||||
|
||||
|
||||
/* Stop tracking location */
|
||||
fun stopTracking() {
|
||||
track.recordingStop = GregorianCalendar.getInstance().time
|
||||
trackingState = Keys.STATE_TRACKING_STOPPED
|
||||
PreferencesHelper.saveTrackingState(this, trackingState)
|
||||
sensorManager.unregisterListener(this)
|
||||
handler.removeCallbacks(periodicTrackUpdate)
|
||||
displayNotification()
|
||||
stopForeground(false)
|
||||
}
|
||||
|
||||
|
||||
/* Clear track recording */
|
||||
fun clearTrack() {
|
||||
track = Track()
|
||||
FileHelper.deleteTempFile(this)
|
||||
trackingState = Keys.STATE_NOT_TRACKING
|
||||
PreferencesHelper.saveTrackingState(this, trackingState)
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
|
||||
/* Saves track recording to storage */
|
||||
fun saveTrack() {
|
||||
// save track using "deferred await"
|
||||
launch {
|
||||
// step 1: create and store filenames for json and gpx files
|
||||
track.trackUriString = FileHelper.getTrackFileUri(this@TrackerService, track).toString()
|
||||
track.gpxUriString = FileHelper.getGpxFileUri(this@TrackerService, track).toString()
|
||||
// step 2: save track
|
||||
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
|
||||
// step 3: save tracklist
|
||||
FileHelper.addTrackAndSaveTracklistSuspended(this@TrackerService, track)
|
||||
// step 3: clear track
|
||||
clearTrack()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates location listener */
|
||||
private fun createLocationListener(): LocationListener {
|
||||
return object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
// update currentBestLocation if a better location is available
|
||||
if (LocationHelper.isBetterLocation(location, currentBestLocation)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
|
||||
// deprecated method
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Adds location listeners to location manager */
|
||||
private fun addLocationListeners() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (gpsProviderActive) {
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f,gpsLocationListener)
|
||||
}
|
||||
if (networkProviderActive) {
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f,networkLocationListener)
|
||||
}
|
||||
} else {
|
||||
LogHelper.w(TAG, "Unable to request device location. Permission is not granted.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Removes location listeners from location manager */
|
||||
private fun removeLocationListeners() {
|
||||
locationManager.removeUpdates(gpsLocationListener)
|
||||
locationManager.removeUpdates(networkLocationListener)
|
||||
}
|
||||
|
||||
|
||||
/* Registers a step counter listener */
|
||||
private fun startStepCounter() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Displays / updates notification */
|
||||
private fun displayNotification(): Notification {
|
||||
val notification: Notification = notificationHelper.createNotification(trackingState, track.length, track.duration, useImperial)
|
||||
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
|
||||
return notification
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Defines the listener for changes in shared preferences
|
||||
*/
|
||||
val sharedPreferenceChangeListener = object : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
Keys.PREF_GPS_ONLY -> gpsOnly = PreferencesHelper.loadGpsOnly(this@TrackerService)
|
||||
Keys.PREF_USE_IMPERIAL_UNITS -> useImperial = PreferencesHelper.loadUseImperialUnits(this@TrackerService)
|
||||
Keys.PREF_LOCATION_ACCURACY_THRESHOLD -> locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this@TrackerService)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: Local Binder that returns this service
|
||||
*/
|
||||
inner class LocalBinder : Binder() {
|
||||
val service: TrackerService = this@TrackerService
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Runnable: Periodically track updates (if recording active)
|
||||
*/
|
||||
private val periodicTrackUpdate: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
// add waypoint to track - step count is continuously updated in onSensorChanged
|
||||
track = TrackHelper.addWayPointToTrack(track, currentBestLocation, locationAccuracyThreshold)
|
||||
// update notification
|
||||
displayNotification()
|
||||
// save temp track using GlobalScope.launch = fire & forget (no return value from save)
|
||||
GlobalScope.launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) }
|
||||
// re-run this in 10 seconds
|
||||
handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
|
||||
}
|
161
app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
Normal file
161
app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
Normal file
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* TracklistFragment.kt
|
||||
* Implements the TracklistFragment fragment
|
||||
* A TracklistFragment displays a list recorded tracks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import YesNoDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.y20k.escapepod.helpers.UiHelper
|
||||
import org.y20k.trackbook.core.TracklistElement
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.TrackHelper
|
||||
import org.y20k.trackbook.tracklist.TracklistAdapter
|
||||
|
||||
|
||||
/*
|
||||
* TracklistFragment class
|
||||
*/
|
||||
class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, YesNoDialog.YesNoDialogListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private lateinit var tracklistAdapter: TracklistAdapter
|
||||
private lateinit var trackElementList: RecyclerView
|
||||
private lateinit var tracklistOnboarding: ConstraintLayout
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// create tracklist adapter
|
||||
tracklistAdapter = TracklistAdapter(this)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
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)
|
||||
tracklistOnboarding = rootView.findViewById(R.id.track_list_onboarding)
|
||||
|
||||
// set up recycler view
|
||||
trackElementList.layoutManager = CustomLinearLayoutManager(activity as Context)
|
||||
trackElementList.itemAnimator = DefaultItemAnimator()
|
||||
trackElementList.adapter = tracklistAdapter
|
||||
|
||||
// enable swipe to delete
|
||||
val swipeHandler = object : UiHelper.SwipeToDeleteCallback(activity as Context) {
|
||||
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_remove_recording)}\n\n- ${tracklistAdapter.getTrackName(adapterPosition)}"
|
||||
YesNoDialog(this@TracklistFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_REMOVE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_remove_recording, payload = adapterPosition)
|
||||
}
|
||||
}
|
||||
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||
itemTouchHelper.attachToRecyclerView(rootView.findViewById(R.id.track_element_list))
|
||||
|
||||
// toggle onboarding layout
|
||||
toggleOnboardingLayout(tracklistAdapter.itemCount)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onTrackElementTapped from TracklistElementAdapterListener */
|
||||
override fun onTrackElementTapped(tracklistElement: TracklistElement) {
|
||||
val bundle: 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)
|
||||
bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement))
|
||||
findNavController().navigate(R.id.fragment_track, bundle)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
when (type) {
|
||||
Keys.DIALOG_REMOVE_TRACK -> {
|
||||
when (dialogResult) {
|
||||
// user tapped remove track
|
||||
true -> {
|
||||
toggleOnboardingLayout(tracklistAdapter.itemCount -1)
|
||||
tracklistAdapter.removeTrack(activity as Context, payload)
|
||||
}
|
||||
// user tapped cancel
|
||||
false -> {
|
||||
tracklistAdapter.notifyItemChanged(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// toggle onboarding layout
|
||||
private fun toggleOnboardingLayout(trackCount: Int) {
|
||||
when (trackCount == 0) {
|
||||
true -> tracklistOnboarding.visibility = View.VISIBLE // show onboarding layout
|
||||
false -> tracklistOnboarding.visibility = View.GONE // hide onboarding layout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: custom LinearLayoutManager that overrides onLayoutCompleted
|
||||
*/
|
||||
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) {
|
||||
|
||||
override fun supportsPredictiveItemAnimations(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLayoutCompleted(state: RecyclerView.State?) {
|
||||
super.onLayoutCompleted(state)
|
||||
// handle delete request from TrackFragment - after layout calculations are complete
|
||||
val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L
|
||||
arguments?.putLong(Keys.ARG_TRACK_ID, -1L)
|
||||
if (deleteTrackId != -1L) {
|
||||
val position: Int = tracklistAdapter.findPosition(deleteTrackId)
|
||||
tracklistAdapter.removeTrack(this@TracklistFragment.activity as Context, position)
|
||||
toggleOnboardingLayout(tracklistAdapter.itemCount -1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
|
@ -1,337 +0,0 @@
|
|||
/**
|
||||
* Track.java
|
||||
* Implements the Track class
|
||||
* A Track stores a list of WayPoints
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.core;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.y20k.trackbook.helpers.LocationHelper;
|
||||
import org.y20k.trackbook.helpers.TrackbookKeys;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Track class
|
||||
*/
|
||||
public class Track implements TrackbookKeys, Parcelable {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = Track.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private final int mTrackFormatVersion;
|
||||
private final List<WayPoint> mWayPoints;
|
||||
private float mTrackLength;
|
||||
private long mDuration;
|
||||
private float mStepCount;
|
||||
private final Date mRecordingStart;
|
||||
private Date mRecordingStop;
|
||||
private double mMaxAltitude;
|
||||
private double mMinAltitude;
|
||||
private double mPositiveElevation;
|
||||
private double mNegativeElevation;
|
||||
private BoundingBox mBoundingBox;
|
||||
|
||||
|
||||
/* Generic Constructor */
|
||||
public Track(int trackFormatVersion, List<WayPoint> wayPoints, float trackLength, long duration, float stepCount, Date recordingStart, Date recordingStop, double maxAltitude, double minAltitude, double positiveElevation, double negativeElevation, BoundingBox boundingBox) {
|
||||
mTrackFormatVersion = trackFormatVersion;
|
||||
mWayPoints = wayPoints;
|
||||
mTrackLength = trackLength;
|
||||
mDuration = duration;
|
||||
mStepCount = stepCount;
|
||||
mRecordingStart = recordingStart;
|
||||
mRecordingStop = recordingStop;
|
||||
mMaxAltitude = maxAltitude;
|
||||
mMinAltitude = minAltitude;
|
||||
mPositiveElevation = positiveElevation;
|
||||
mNegativeElevation = negativeElevation;
|
||||
mBoundingBox = boundingBox;
|
||||
}
|
||||
|
||||
|
||||
/* Copy Constructor */
|
||||
public Track(Track track) {
|
||||
this(track.getTrackFormatVersion(), track.getWayPoints(), track.getTrackLength(), track.getTrackDuration(), track.getStepCount(), track.getRecordingStart(), track.getRecordingStop(), track.getMaxAltitude(), track.getMinAltitude(), track.getPositiveElevation(), track.getNegativeElevation(), track.getBoundingBox());
|
||||
}
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public Track() {
|
||||
mTrackFormatVersion = CURRENT_TRACK_FORMAT_VERSION;
|
||||
mWayPoints = new ArrayList<WayPoint>();
|
||||
mTrackLength = 0f;
|
||||
mDuration = 0;
|
||||
mStepCount = 0f;
|
||||
mRecordingStart = GregorianCalendar.getInstance().getTime();
|
||||
mRecordingStop = mRecordingStart;
|
||||
mMaxAltitude = 0f;
|
||||
mMinAltitude = 0f;
|
||||
mPositiveElevation = 0f;
|
||||
mNegativeElevation = 0f;
|
||||
mBoundingBox = new BoundingBox();
|
||||
}
|
||||
|
||||
|
||||
/* Constructor used by CREATOR */
|
||||
protected Track(Parcel in) {
|
||||
mTrackFormatVersion = in.readInt();
|
||||
mWayPoints = in.createTypedArrayList(WayPoint.CREATOR);
|
||||
mTrackLength = in.readFloat();
|
||||
mDuration = in.readLong();
|
||||
mStepCount = in.readFloat();
|
||||
mRecordingStart = new Date(in.readLong());
|
||||
mRecordingStop = new Date(in.readLong());
|
||||
mMaxAltitude = in.readDouble();
|
||||
mMinAltitude = in.readDouble();
|
||||
mPositiveElevation = in.readDouble();
|
||||
mNegativeElevation = in.readDouble();
|
||||
mBoundingBox = new BoundingBox(in.readDouble(), in.readDouble(),in.readDouble(),in.readDouble());
|
||||
// BoundingBox(double north, double east, double south, double west)
|
||||
}
|
||||
|
||||
|
||||
/* CREATOR for Track object used to do parcel related operations */
|
||||
public static final Creator<Track> CREATOR = new Creator<Track>() {
|
||||
@Override
|
||||
public Track createFromParcel(Parcel in) {
|
||||
return new Track(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Track[] newArray(int size) {
|
||||
return new Track[size];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* Adds new WayPoint */
|
||||
public boolean addWayPoint(@Nullable Location previousLocation, Location newLocation) {
|
||||
|
||||
// toggle stop over status, if necessary
|
||||
boolean isStopOver = LocationHelper.isStopOver(previousLocation, newLocation);
|
||||
if (isStopOver) {
|
||||
int wayPointCount = mWayPoints.size();
|
||||
mWayPoints.get(wayPointCount-1).setIsStopOver(isStopOver);
|
||||
}
|
||||
|
||||
// create new WayPoint
|
||||
WayPoint wayPoint = new WayPoint(newLocation, false, mTrackLength);
|
||||
|
||||
// add new WayPoint to track
|
||||
return mWayPoints.add(wayPoint);
|
||||
}
|
||||
|
||||
|
||||
/* Updates distance */
|
||||
public boolean updateDistance(@Nullable Location previousLocation, Location newLocation){
|
||||
// two data points needed to calculate distance
|
||||
if (previousLocation != null) {
|
||||
// add up distance
|
||||
mTrackLength = mTrackLength + previousLocation.distanceTo(newLocation);
|
||||
return true;
|
||||
} else {
|
||||
// this was the first waypoint
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Toggles stop over status of last waypoint */
|
||||
public void toggleLastWayPointStopOverStatus(boolean stopOver) {
|
||||
int wayPointCount = mWayPoints.size();
|
||||
mWayPoints.get(wayPointCount-1).setIsStopOver(stopOver);
|
||||
}
|
||||
|
||||
|
||||
/* Sets end time and date of recording */
|
||||
public void setRecordingEnd() {
|
||||
mRecordingStop = GregorianCalendar.getInstance().getTime();
|
||||
}
|
||||
|
||||
|
||||
/* Setter for duration of track */
|
||||
public void setDuration(long duration) {
|
||||
mDuration = duration;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for step count of track */
|
||||
public void setStepCount(float stepCount) {
|
||||
mStepCount = stepCount;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for maximum altitude of recording */
|
||||
public void setMaxAltitude(double maxAltitude) {
|
||||
mMaxAltitude = maxAltitude;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for lowest altitude of recording */
|
||||
public void setMinAltitude(double minAltitude) {
|
||||
mMinAltitude = minAltitude;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for positive elevation of recording (cumulative altitude difference) */
|
||||
public void setPositiveElevation(double positiveElevation) {
|
||||
mPositiveElevation = positiveElevation;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for negative elevation of recording (cumulative altitude difference) */
|
||||
public void setNegativeElevation(double negativeElevation) {
|
||||
mNegativeElevation = negativeElevation;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for this track's BoundingBox - a data structure describing the edge coordinates of a track */
|
||||
public void setBoundingBox(BoundingBox boundingBox) {
|
||||
mBoundingBox = boundingBox;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for file/track format version */
|
||||
public int getTrackFormatVersion() {
|
||||
return mTrackFormatVersion;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for mWayPoints */
|
||||
public List<WayPoint> getWayPoints() {
|
||||
return mWayPoints;
|
||||
}
|
||||
|
||||
|
||||
/* Getter size of Track / number of WayPoints */
|
||||
public int getSize() {
|
||||
return mWayPoints.size();
|
||||
}
|
||||
|
||||
|
||||
/* Getter for track length */
|
||||
public float getTrackLength() {
|
||||
return mTrackLength;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for duration of track */
|
||||
public long getTrackDuration() {
|
||||
return mDuration;
|
||||
}
|
||||
|
||||
/* Getter for start date of recording */
|
||||
public Date getRecordingStart() {
|
||||
return mRecordingStart;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for stop date of recording */
|
||||
public Date getRecordingStop() {
|
||||
return mRecordingStop;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for step count of recording */
|
||||
public float getStepCount() {
|
||||
return mStepCount;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for maximum altitude of recording */
|
||||
public double getMaxAltitude() {
|
||||
return mMaxAltitude;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for lowest altitude of recording */
|
||||
public double getMinAltitude() {
|
||||
return mMinAltitude;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for positive elevation of recording (cumulative altitude difference) */
|
||||
public double getPositiveElevation() {
|
||||
return mPositiveElevation;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for negative elevation of recording (cumulative altitude difference) */
|
||||
public double getNegativeElevation() {
|
||||
return mNegativeElevation;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for this track's BoundingBox - a data structure describing the edge coordinates of a track */
|
||||
public BoundingBox getBoundingBox() { return mBoundingBox; }
|
||||
|
||||
|
||||
/* Getter recorded distance */
|
||||
public Double getTrackDistance() {
|
||||
int size = mWayPoints.size();
|
||||
if (size > 0) {
|
||||
return (double)mWayPoints.get(size - 1).getDistanceToStartingPoint();
|
||||
} else {
|
||||
return (double)0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Getter for location of specific WayPoint */
|
||||
public Location getWayPointLocation(int index) {
|
||||
return mWayPoints.get(index).getLocation();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeInt(mTrackFormatVersion);
|
||||
parcel.writeTypedList(mWayPoints);
|
||||
parcel.writeFloat(mTrackLength);
|
||||
parcel.writeLong(mDuration);
|
||||
parcel.writeFloat(mStepCount);
|
||||
parcel.writeLong(mRecordingStart.getTime());
|
||||
parcel.writeLong(mRecordingStop.getTime());
|
||||
parcel.writeDouble(mMaxAltitude);
|
||||
parcel.writeDouble(mMinAltitude);
|
||||
parcel.writeDouble(mPositiveElevation);
|
||||
parcel.writeDouble(mNegativeElevation);
|
||||
parcel.writeDouble(mBoundingBox.getLatNorth());
|
||||
parcel.writeDouble(mBoundingBox.getLonEast());
|
||||
parcel.writeDouble(mBoundingBox.getLatSouth());
|
||||
parcel.writeDouble(mBoundingBox.getLonWest());
|
||||
// BoundingBox(double north, double east, double south, double west)
|
||||
}
|
||||
|
||||
|
||||
}
|
77
app/src/main/java/org/y20k/trackbook/core/Track.kt
Normal file
77
app/src/main/java/org/y20k/trackbook/core/Track.kt
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Track.kt
|
||||
* Implements the Track data class
|
||||
* A Track stores a list of WayPoints
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.helpers.DateTimeHelper
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* Track data class
|
||||
*/
|
||||
@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 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 */
|
||||
fun toTracklistElement(context: Context): TracklistElement {
|
||||
val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart)
|
||||
val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration)
|
||||
return TracklistElement(
|
||||
name = name,
|
||||
date = recordingStart,
|
||||
dateString = readableDateString,
|
||||
length = length,
|
||||
durationString = readableDurationString,
|
||||
trackUriString = trackUriString,
|
||||
gpxUriString = gpxUriString,
|
||||
starred = false
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* Returns unique ID for Track - currently the start date */
|
||||
fun getTrackId(): Long {
|
||||
return recordingStart.time
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/**
|
||||
* TrackBuilder.java
|
||||
* Implements a builder for the Track class
|
||||
* A TrackBuilder can build a track object depending on the version of its file format
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
import org.y20k.trackbook.helpers.MapHelper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* TrackBuilder class
|
||||
*/
|
||||
public class TrackBuilder {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = TrackBuilder.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private final int mTrackFormatVersion;
|
||||
private final List<WayPoint> mWayPoints;
|
||||
private final float mTrackLength;
|
||||
private final long mDuration;
|
||||
private final float mStepCount;
|
||||
private final Date mRecordingStart;
|
||||
private final Date mRecordingStop;
|
||||
private final double mMaxAltitude;
|
||||
private final double mMinAltitude;
|
||||
private final double mPositiveElevation;
|
||||
private final double mNegativeElevation;
|
||||
private BoundingBox mBoundingBox;
|
||||
|
||||
|
||||
/* Generic Constructor */
|
||||
public TrackBuilder(int trackFormatVersion, List<WayPoint> wayPoints, float trackLength, long duration, float stepCount, Date recordingStart, Date recordingStop, double maxAltitude, double minAltitude, double positiveElevation, double negativeElevation, BoundingBox boundingBox) {
|
||||
mTrackFormatVersion = trackFormatVersion;
|
||||
mWayPoints = wayPoints;
|
||||
mTrackLength = trackLength;
|
||||
mDuration = duration;
|
||||
mStepCount = stepCount;
|
||||
mRecordingStart = recordingStart;
|
||||
mRecordingStop = recordingStop;
|
||||
mMaxAltitude = maxAltitude;
|
||||
mMinAltitude = minAltitude;
|
||||
mPositiveElevation = positiveElevation;
|
||||
mNegativeElevation = negativeElevation;
|
||||
mBoundingBox = boundingBox;
|
||||
}
|
||||
|
||||
|
||||
/* Builds and return a Track object */
|
||||
public Track toTrack() {
|
||||
switch (mTrackFormatVersion) {
|
||||
case 1:
|
||||
// file format version 1 - does not have elevation data stored
|
||||
return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, 0f, 0f, 0f, 0f, new BoundingBox());
|
||||
case 2:
|
||||
// file format version 2 - does not have edge coordinates stored
|
||||
return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, MapHelper.calculateBoundingBox(mWayPoints));
|
||||
case 3:
|
||||
// file format version 3 (current version)
|
||||
return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, new BoundingBox());
|
||||
default:
|
||||
LogHelper.e(LOG_TAG, "Unknown file format version: " + mTrackFormatVersion);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/**
|
||||
* TrackBundle.java
|
||||
* Implements a TrackBundle
|
||||
* TrackBundle is a container for file and corresponding name of a track
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core;
|
||||
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* TrackBundle class
|
||||
*/
|
||||
public class TrackBundle {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = TrackBundle.class.getSimpleName();
|
||||
|
||||
/* Main class variables */
|
||||
private final File mTrackFile;
|
||||
private final String mTrackName;
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public TrackBundle(File file) {
|
||||
mTrackFile = file;
|
||||
mTrackName = buildTrackName(file);
|
||||
}
|
||||
|
||||
|
||||
/* Getter for track file */
|
||||
public File getTrackFile() {
|
||||
return mTrackFile;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for track name */
|
||||
public String getTrackName() {
|
||||
return mTrackName;
|
||||
}
|
||||
|
||||
|
||||
/* Builds a readable track name from the track's file name */
|
||||
private String buildTrackName(File file) {
|
||||
|
||||
// get file name without extension
|
||||
String readableTrackName = file.getName();
|
||||
readableTrackName = readableTrackName.substring(0, readableTrackName.indexOf(".trackbook"));
|
||||
|
||||
try {
|
||||
// convert file name to date
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
|
||||
Date trackDate = dateFormat.parse(readableTrackName);
|
||||
|
||||
// convert date to track name string according to current locale
|
||||
readableTrackName = DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault()).format(trackDate) + " - " +
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(trackDate);
|
||||
|
||||
} catch (ParseException e) {
|
||||
LogHelper.w(LOG_TAG, "Unable to parse file name into date object (yyyy-MM-dd-HH-mm-ss): " + e);
|
||||
}
|
||||
|
||||
return readableTrackName;
|
||||
}
|
||||
|
||||
}
|
53
app/src/main/java/org/y20k/trackbook/core/Tracklist.kt
Normal file
53
app/src/main/java/org/y20k/trackbook/core/Tracklist.kt
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Tracklist.kt
|
||||
* Implements the Tracklist data class
|
||||
* A Tracklist stores a list of Tracks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.helpers.TrackHelper
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* Tracklist data class
|
||||
*/
|
||||
@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 {
|
||||
|
||||
/* Return trackelement for given track id */
|
||||
fun getTrackElement(trackId: Long): TracklistElement? {
|
||||
tracklistElements.forEach { tracklistElement ->
|
||||
if (TrackHelper.getTrackId(tracklistElement) == trackId) {
|
||||
return tracklistElement
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/* Create a deep copy */
|
||||
fun deepCopy(): Tracklist {
|
||||
return Tracklist(tracklistFormatVersion, mutableListOf<TracklistElement>().apply { addAll(tracklistElements) }, modificationDate)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* TracklistElement.kt
|
||||
* Implements the TracklistElement data class
|
||||
* A TracklistElement data about a Track
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* TracklistElement data class
|
||||
*/
|
||||
@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 {
|
||||
|
||||
/* Returns unique ID for TracklistElement - currently the start date */
|
||||
fun getTrackId(): Long {
|
||||
return date.time
|
||||
}
|
||||
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
/**
|
||||
* WayPoint.java
|
||||
* Implements the WayPoint class
|
||||
* A WayPoint stores a location plus additional metadata
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.core;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
|
||||
/**
|
||||
* WayPoint class
|
||||
*/
|
||||
public class WayPoint implements Parcelable {
|
||||
|
||||
private Location mLocation;
|
||||
private boolean mIsStopOver;
|
||||
private float mDistanceToStartingPoint;
|
||||
private final int mNumberSatellites;
|
||||
|
||||
/* Constructor */
|
||||
public WayPoint(Location location, boolean isStopOver, float distanceToStartingPoint) {
|
||||
mLocation = location;
|
||||
mIsStopOver = isStopOver;
|
||||
mDistanceToStartingPoint = distanceToStartingPoint;
|
||||
|
||||
// save number of satellites
|
||||
Bundle extras = location.getExtras();
|
||||
if (extras != null && extras.containsKey("satellites")) {
|
||||
mNumberSatellites = extras.getInt("satellites", 0);
|
||||
mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON
|
||||
} else {
|
||||
mNumberSatellites = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Constructor used by CREATOR */
|
||||
protected WayPoint(Parcel in) {
|
||||
mLocation = Location.CREATOR.createFromParcel(in);
|
||||
mIsStopOver = in.readByte() != 0;
|
||||
mDistanceToStartingPoint = in.readFloat();
|
||||
mNumberSatellites = in.readInt();
|
||||
}
|
||||
|
||||
|
||||
/* CREATOR for WayPoint object used to do parcel related operations */
|
||||
public static final Creator<WayPoint> CREATOR = new Creator<WayPoint>() {
|
||||
@Override
|
||||
public WayPoint createFromParcel(Parcel in) {
|
||||
return new WayPoint(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WayPoint[] newArray(int size) {
|
||||
return new WayPoint[size];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* Getter for mLocation */
|
||||
public Location getLocation() {
|
||||
return mLocation;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for mIsStopOver */
|
||||
public boolean getIsStopOver() {
|
||||
return mIsStopOver;
|
||||
}
|
||||
|
||||
|
||||
/* Getter for mDistanceToStartingPoint */
|
||||
public float getDistanceToStartingPoint() {
|
||||
return mDistanceToStartingPoint;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for mLocation */
|
||||
public void setLocation(Location location) {
|
||||
mLocation = location;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for mIsStopOver */
|
||||
public void setIsStopOver(boolean isStopOver) {
|
||||
mIsStopOver = isStopOver;
|
||||
}
|
||||
|
||||
|
||||
/* Setter for mDistanceToStartingPoint */
|
||||
public void setDistanceToStartingPoint(float distanceToStartingPoint) {
|
||||
mDistanceToStartingPoint = distanceToStartingPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON
|
||||
mLocation.writeToParcel(parcel, flags);
|
||||
parcel.writeByte((byte) (mIsStopOver ? 1 : 0));
|
||||
parcel.writeFloat(mDistanceToStartingPoint);
|
||||
parcel.writeInt(mNumberSatellites);
|
||||
}
|
||||
}
|
54
app/src/main/java/org/y20k/trackbook/core/WayPoint.kt
Normal file
54
app/src/main/java/org/y20k/trackbook/core/WayPoint.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* WayPoint.kt
|
||||
* Implements the WayPoint data class
|
||||
* A WayPoint stores a location plus additional metadata
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.core
|
||||
|
||||
import android.location.Location
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
|
||||
/*
|
||||
* WayPoint data class
|
||||
*/
|
||||
@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): Parcelable {
|
||||
|
||||
|
||||
/* Converts WayPoint into Location */
|
||||
fun toLocation(): Location {
|
||||
val location: Location = Location(provider)
|
||||
location.latitude = latitude
|
||||
location.longitude = longitude
|
||||
location.altitude = altitude
|
||||
location.accuracy = accuracy
|
||||
location.time = time
|
||||
return location
|
||||
}
|
||||
|
||||
}
|
92
app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt
Normal file
92
app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* ErrorDialog.kt
|
||||
* Implements the ErrorDialog object
|
||||
* A ErrorDialog shows an error dialog with details
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
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
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
|
||||
/*
|
||||
* ErrorDialog object
|
||||
*/
|
||||
object ErrorDialog {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(ErrorDialog::class.java)
|
||||
|
||||
|
||||
/* Construct and show dialog */
|
||||
fun show(context: Context, errorTitle: Int, errorMessage: Int, errorDetails: String = String()) {
|
||||
// prepare dialog builder
|
||||
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
|
||||
|
||||
// set title
|
||||
builder.setTitle(context.getString(errorTitle))
|
||||
|
||||
// get views
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
val view: View = inflater.inflate(R.layout.dialog_generic_with_details, null)
|
||||
val errorMessageView: TextView = view.findViewById(R.id.dialog_message) as TextView
|
||||
val errorDetailsLinkView: TextView = view.findViewById(R.id.dialog_details_link) as TextView
|
||||
val errorDetailsView: TextView = view.findViewById(R.id.dialog_details) as TextView
|
||||
|
||||
// set dialog view
|
||||
builder.setView(view)
|
||||
|
||||
// set detail view
|
||||
if (errorDetails.isNotEmpty()) {
|
||||
// show details link
|
||||
errorDetailsLinkView.visibility = View.VISIBLE
|
||||
|
||||
// allow scrolling on details view
|
||||
errorDetailsView.movementMethod = ScrollingMovementMethod()
|
||||
|
||||
// show and hide details on click
|
||||
errorDetailsLinkView.setOnClickListener {
|
||||
when (errorDetailsView.visibility) {
|
||||
View.GONE -> errorDetailsView.visibility = View.VISIBLE
|
||||
View.VISIBLE -> errorDetailsView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
// set details text view
|
||||
errorDetailsView.text = errorDetails
|
||||
} else {
|
||||
// hide details link
|
||||
errorDetailsLinkView.visibility = View.GONE
|
||||
}
|
||||
|
||||
// set text views
|
||||
errorMessageView.text = context.getString(errorMessage)
|
||||
|
||||
// add okay button
|
||||
builder.setPositiveButton(R.string.dialog_generic_button_okay, DialogInterface.OnClickListener { _, _ ->
|
||||
// listen for click on okay button
|
||||
// do nothing
|
||||
})
|
||||
|
||||
// display error dialog
|
||||
builder.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* RenameTrackDialog.kt
|
||||
* Implements the RenameTrackDialog class
|
||||
* A RenameTrackDialog offers user to change name of track
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
|
||||
/*
|
||||
* RenameTrackDialog class
|
||||
*/
|
||||
class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) {
|
||||
|
||||
/* Interface used to communicate back to activity */
|
||||
interface RenameTrackListener {
|
||||
fun onRenameTrackDialog(textInput: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG = LogHelper.makeLogTag(RenameTrackDialog::class.java.simpleName)
|
||||
|
||||
|
||||
/* Construct and show dialog */
|
||||
fun show(context: Context, trackName: String) {
|
||||
// prepare dialog builder
|
||||
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context)
|
||||
|
||||
// get input field
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.dialog_rename_track, null)
|
||||
val inputField = view.findViewById<View>(R.id.dialog_rename_track_input_edit_text) as EditText
|
||||
|
||||
// pre-fill with current track name
|
||||
inputField.setText(trackName, TextView.BufferType.EDITABLE)
|
||||
inputField.setSelection(trackName.length)
|
||||
inputField.inputType = InputType.TYPE_CLASS_TEXT
|
||||
|
||||
// set dialog view
|
||||
builder.setView(view)
|
||||
|
||||
// add "add" button
|
||||
builder.setPositiveButton(R.string.dialog_rename_track_button) { _, _ ->
|
||||
// hand text over to initiating activity
|
||||
inputField.text?.let {
|
||||
renameTrackListener.onRenameTrackDialog(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// add cancel button
|
||||
builder.setNegativeButton(R.string.dialog_generic_button_cancel) { _, _ ->
|
||||
// listen for click on cancel button
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// display add dialog
|
||||
builder.show()
|
||||
}
|
||||
|
||||
}
|
97
app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt
Normal file
97
app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* YesNoDialog
|
||||
* Implements the YesNoDialog class
|
||||
* A YesNoDialog asks the user if he/she wants to do something or notpackage org.y20k.trackbook.dialogs
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
/*
|
||||
* YesNoDialog class
|
||||
*/
|
||||
class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) {
|
||||
|
||||
/* Interface used to communicate back to activity */
|
||||
interface YesNoDialogListener {
|
||||
fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
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) {
|
||||
// extract string from message resource and feed into main show method
|
||||
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) {
|
||||
|
||||
// prepare dialog builder
|
||||
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme)
|
||||
|
||||
// set title and message
|
||||
builder.setMessage(messageString)
|
||||
if (title != Keys.EMPTY_STRING_RESOURCE) {
|
||||
builder.setTitle(context.getString(title))
|
||||
}
|
||||
|
||||
|
||||
// add yes button
|
||||
builder.setPositiveButton(yesButton) { _, _ ->
|
||||
// listen for click on yes button
|
||||
yesNoDialogListener.onYesNoDialog(type, true, payload, payloadString)
|
||||
}
|
||||
|
||||
// add no button
|
||||
builder.setNegativeButton(noButton) { _, _ ->
|
||||
// listen for click on no button
|
||||
yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString)
|
||||
}
|
||||
|
||||
// handle outside-click as "no"
|
||||
builder.setOnCancelListener(){
|
||||
yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString)
|
||||
}
|
||||
|
||||
// display dialog
|
||||
builder.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* SharedPreferencesExt.kt
|
||||
* Implements the SharedPreferencesExt extension functions
|
||||
* SharedPreferencesExt displays provides additional functions for dealing with shared preferences
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.extensions
|
||||
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
|
||||
/* Puts a Double value in SharedPreferences */
|
||||
fun SharedPreferences.Editor.putDouble(key: String, double: Double) = putLong(key, java.lang.Double.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)))
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* DateTimeHelper.kt
|
||||
* Implements the DateTimeHelper object
|
||||
* A DateTimeHelper provides helper methods for converting Date and Time objects
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/*
|
||||
* DateTimeHelper object
|
||||
*/
|
||||
object DateTimeHelper {
|
||||
|
||||
/* Converts milliseconds to mm:ss or hh:mm:ss */
|
||||
fun convertToReadableTime(context: Context, milliseconds: Long): String {
|
||||
var timeString: String = 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 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) {
|
||||
// CASE: format hh:mm:ss
|
||||
true -> {
|
||||
timeString = "$hours $h $minutes $m $seconds $s"
|
||||
}
|
||||
// CASE: format mm:ss
|
||||
false -> {
|
||||
timeString = "$minutes $m $seconds $s"
|
||||
}
|
||||
}
|
||||
return timeString
|
||||
}
|
||||
|
||||
|
||||
/* Create sortable string from date - used for filenames */
|
||||
fun convertToSortableDateString(date: Date): String {
|
||||
val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US)
|
||||
return dateFormat.format(date)
|
||||
}
|
||||
|
||||
|
||||
/* Creates a readable string from date - used in the UI */
|
||||
fun convertToReadableDate(date: Date, dateStyle: Int = DateFormat.LONG): String {
|
||||
return DateFormat.getDateInstance(dateStyle, Locale.getDefault()).format(date)
|
||||
}
|
||||
|
||||
|
||||
/* Calculates time difference between two locations */
|
||||
fun calculateTimeDistance(previousLocation: Location?, location: Location): Long {
|
||||
var timeDifference: Long = 0L
|
||||
// two data points needed to calculate time difference
|
||||
if (previousLocation != null) {
|
||||
// get time difference
|
||||
timeDifference = location.time - previousLocation.time
|
||||
}
|
||||
return timeDifference
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/**
|
||||
* DialogHelper.java
|
||||
* Implements the DialogHelper class
|
||||
* A DialogHelper creates a customizable alert dialog
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
|
||||
/**
|
||||
* DialogHelper class
|
||||
*/
|
||||
public class DialogHelper extends DialogFragment implements TrackbookKeys {
|
||||
|
||||
/* Constructs a new instance */
|
||||
public static DialogHelper newInstance(int title, String message, int positiveButton, int negativeButton) {
|
||||
DialogHelper fragment = new DialogHelper();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_DIALOG_TITLE, title);
|
||||
args.putString(ARG_DIALOG_MESSAGE, message);
|
||||
args.putInt(ARG_DIALOG_BUTTON_POSITIVE, positiveButton);
|
||||
args.putInt(ARG_DIALOG_BUTTON_NEGATIVE, negativeButton);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Bundle args = getArguments();
|
||||
|
||||
// get text elements
|
||||
int title = args.getInt(ARG_DIALOG_TITLE);
|
||||
String message = args.getString(ARG_DIALOG_MESSAGE);
|
||||
int positiveButton = args.getInt(ARG_DIALOG_BUTTON_POSITIVE);
|
||||
int negativeButton = args.getInt(ARG_DIALOG_BUTTON_NEGATIVE);
|
||||
|
||||
// build dialog
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
if (title != -1) {
|
||||
dialogBuilder.setTitle(title);
|
||||
}
|
||||
dialogBuilder.setMessage(message);
|
||||
dialogBuilder.setPositiveButton(positiveButton,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
Fragment target = getTargetFragment();
|
||||
if (target != null) {
|
||||
target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getActivity().getIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
); dialogBuilder.setNegativeButton(negativeButton,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
Fragment target = getTargetFragment();
|
||||
if (target != null) {
|
||||
target.onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, getActivity().getIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
/**
|
||||
* DropdownHelper.java
|
||||
* Implements a dropdown menu
|
||||
* The dropdown menu used to select tracks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Resources;
|
||||
import android.database.DataSetObserver;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.y20k.trackbook.R;
|
||||
import org.y20k.trackbook.core.TrackBundle;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.ThemedSpinnerAdapter;
|
||||
|
||||
|
||||
/**
|
||||
* DropdownHelper class
|
||||
*/
|
||||
public class DropdownAdapter extends BaseAdapter implements ThemedSpinnerAdapter, TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = DropdownAdapter.class.getSimpleName();
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private final Activity mActivity;
|
||||
private final ThemedSpinnerAdapter.Helper mDropdownAdapterHelper;
|
||||
private List<TrackBundle> mTrackBundleList;
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public DropdownAdapter(Activity activity) {
|
||||
// store activity
|
||||
mActivity = activity;
|
||||
|
||||
// fill list with track bundles
|
||||
initializeTrackBundleList();
|
||||
|
||||
// create an adapter helper
|
||||
mDropdownAdapterHelper = new ThemedSpinnerAdapter.Helper(activity);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
|
||||
mDropdownAdapterHelper.setDropDownViewTheme(theme);
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resources.Theme getDropDownViewTheme() {
|
||||
return mDropdownAdapterHelper.getDropDownViewTheme();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
// getView -> collapsed view of dropdown
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater();
|
||||
view = inflater.inflate(R.layout.custom_dropdown_item_collapsed, parent, false);
|
||||
}
|
||||
((TextView) view).setText(getItem(position).getTrackName());
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
// getDropDownView -> expanded view of dropdown
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater();
|
||||
// view = inflater.inflate(R.layout.custom_dropdown_item_expanded, parent, false);
|
||||
view = inflater.inflate(R.layout.support_simple_spinner_dropdown_item, parent, false);
|
||||
}
|
||||
((TextView) view).setText(getItem(position).getTrackName());
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void registerDataSetObserver(DataSetObserver dataSetObserver) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void unregisterDataSetObserver(DataSetObserver dataSetObserver) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mTrackBundleList.size();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TrackBundle getItem(int i) {
|
||||
return mTrackBundleList.get(i);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getItemId(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return mTrackBundleList.size() == 0;
|
||||
}
|
||||
|
||||
|
||||
/* Refreshes the adapter data */
|
||||
public void refresh() {
|
||||
// re-initialize the adapter's array list
|
||||
initializeTrackBundleList();
|
||||
}
|
||||
|
||||
|
||||
/* Initializes list of track bundles */
|
||||
private void initializeTrackBundleList() {
|
||||
|
||||
// get list of files from storage
|
||||
StorageHelper storageHelper = new StorageHelper(mActivity);
|
||||
File files[] = storageHelper.getListOfTrackbookFiles();
|
||||
|
||||
// fill list with track bundles
|
||||
mTrackBundleList = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
String fileName = file.getName();
|
||||
if (fileName.endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !fileName.startsWith(FILE_NAME_TEMP)) {
|
||||
mTrackBundleList.add(new TrackBundle(file));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
/**
|
||||
* ExportHelper.java
|
||||
* Implements the ExportHelper class
|
||||
* A ExportHelper can convert Track object into a GPX string
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Environment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.y20k.trackbook.R;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.core.WayPoint;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
/**
|
||||
* ExportHelper class
|
||||
*/
|
||||
public final class ExportHelper extends FileProvider implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = ExportHelper.class.getSimpleName();
|
||||
|
||||
|
||||
/* Checks if a GPX file for given track is already present */
|
||||
public static boolean gpxFileExists(Track track) {
|
||||
File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
return createFile(track, folder).exists();
|
||||
}
|
||||
|
||||
|
||||
/* Exports given track to GPX */
|
||||
public static boolean exportToGpx(Context context, Track track) {
|
||||
// get file for given track
|
||||
File gpxFile = createFile(track, getDownloadFolder());
|
||||
|
||||
// get GPX string representation for given track
|
||||
String gpxString = createGpxString(track);
|
||||
|
||||
// write GPX file
|
||||
if (writeGpxToFile(gpxString, gpxFile)) {
|
||||
String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString();
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
} else {
|
||||
String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString();
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates Intent used to bring up an Android share sheet */
|
||||
public static Intent getGpxFileIntent(Context context, Track track) {
|
||||
|
||||
// create file in Cache directory for given track
|
||||
File gpxFile = createFile(track, context.getCacheDir());
|
||||
|
||||
// get GPX string representation for given track
|
||||
String gpxString = createGpxString(track);
|
||||
|
||||
// write GPX file
|
||||
if (writeGpxToFile(gpxString, gpxFile)) {
|
||||
String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString();
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString();
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
// create intent
|
||||
String authority = "org.y20k.trackbook.exporthelper.provider";
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.setDataAndType(FileProvider.getUriForFile(context, authority, gpxFile), "application/gpx+xml");
|
||||
intent.setType("application/gpx+xml");
|
||||
intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, authority, gpxFile));
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
/* Empties the internal chache directory */
|
||||
public static void emptyCacheDirectory(Context context) {
|
||||
// todo implement a date check - delete only stuff that is a week old
|
||||
File[] cacheFiles = context.getCacheDir().listFiles();
|
||||
for (File file: cacheFiles) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get "Download" folder */
|
||||
private static File getDownloadFolder() {
|
||||
File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
if (folder != null && !folder.exists()) {
|
||||
LogHelper.v(LOG_TAG, "Creating new folder: " + folder.toString());
|
||||
folder.mkdirs();
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
|
||||
/* Return a GPX filepath for a given track */
|
||||
private static File createFile(Track track, File folder) {
|
||||
Date recordingStart = track.getRecordingStart();
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
|
||||
|
||||
return new File(folder, dateFormat.format(recordingStart) + FILE_TYPE_GPX_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
/* Writes given GPX string to given file */
|
||||
private static boolean writeGpxToFile (String gpxString, File gpxFile) {
|
||||
// write track
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(gpxFile))) {
|
||||
LogHelper.v(LOG_TAG, "Saving track to external storage: " + gpxFile.toString());
|
||||
bw.write(gpxString);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + gpxFile.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates GPX formatted string */
|
||||
private static String createGpxString(Track track) {
|
||||
String gpxString;
|
||||
|
||||
// add header
|
||||
gpxString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n" +
|
||||
"<gpx version=\"1.1\" creator=\"Transistor 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 = gpxString + addTrack(track);
|
||||
|
||||
// add closing tag
|
||||
gpxString = gpxString + "</gpx>\n";
|
||||
|
||||
return gpxString;
|
||||
}
|
||||
|
||||
|
||||
/* Creates Track */
|
||||
private static String addTrack(Track track) {
|
||||
StringBuilder gpxTrack = new StringBuilder("");
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
// add opening track tag
|
||||
gpxTrack.append("\t<trk>\n");
|
||||
|
||||
// add name to track
|
||||
gpxTrack.append("\t\t<name>");
|
||||
gpxTrack.append("Trackbook Recording");
|
||||
gpxTrack.append("</name>\n");
|
||||
|
||||
// add opening track segment tag
|
||||
gpxTrack.append("\t\t<trkseg>\n");
|
||||
|
||||
// add route point
|
||||
for (WayPoint wayPoint:track.getWayPoints()) {
|
||||
// get location from waypoint
|
||||
Location location = wayPoint.getLocation();
|
||||
|
||||
// add longitude and latitude
|
||||
gpxTrack.append("\t\t\t<trkpt lat=\"");
|
||||
gpxTrack.append(location.getLatitude());
|
||||
gpxTrack.append("\" lon=\"");
|
||||
gpxTrack.append(location.getLongitude());
|
||||
gpxTrack.append("\">\n");
|
||||
|
||||
// add time
|
||||
gpxTrack.append("\t\t\t\t<time>");
|
||||
gpxTrack.append(dateFormat.format(new Date(location.getTime())));
|
||||
gpxTrack.append("</time>\n");
|
||||
|
||||
// add altitude
|
||||
gpxTrack.append("\t\t\t\t<ele>");
|
||||
gpxTrack.append(location.getAltitude());
|
||||
gpxTrack.append("</ele>\n");
|
||||
|
||||
// add closing tag
|
||||
gpxTrack.append("\t\t\t</trkpt>\n");
|
||||
}
|
||||
|
||||
// add closing track segment tag
|
||||
gpxTrack.append("\t\t</trkseg>\n");
|
||||
|
||||
// add closing track tag
|
||||
gpxTrack.append("\t</trk>\n");
|
||||
|
||||
return gpxTrack.toString();
|
||||
}
|
||||
|
||||
}
|
431
app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt
Normal file
431
app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt
Normal file
|
@ -0,0 +1,431 @@
|
|||
/*
|
||||
* FileHelper.kt
|
||||
* Implements the FileHelper object
|
||||
* A FileHelper provides helper methods for reading and writing files from and to device storage
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.Tracklist
|
||||
import org.y20k.trackbook.core.TracklistElement
|
||||
import java.io.*
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
/*
|
||||
* FileHelper object
|
||||
*/
|
||||
object FileHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java)
|
||||
|
||||
|
||||
/* Return an InputStream for given Uri */
|
||||
fun getTextFileStream(context: Context, uri: Uri): InputStream? {
|
||||
var stream : InputStream? = null
|
||||
try {
|
||||
stream = context.contentResolver.openInputStream(uri)
|
||||
} catch (e : Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
|
||||
/* 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) {
|
||||
val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.moveToFirst()
|
||||
val size: Long = cursor.getLong(sizeIndex)
|
||||
cursor.close()
|
||||
return size
|
||||
} else {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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) {
|
||||
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
val name: String = cursor.getString(nameIndex)
|
||||
cursor.close()
|
||||
return name
|
||||
} else {
|
||||
return String()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Clears given folder - keeps given number of files */
|
||||
fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) {
|
||||
if (folder != null && folder.exists()) {
|
||||
val files = folder.listFiles()
|
||||
val fileCount: Int = files.size
|
||||
files.sortBy { it.lastModified() }
|
||||
for (fileNumber in files.indices) {
|
||||
if (fileNumber < fileCount - keep) {
|
||||
files[fileNumber].delete()
|
||||
}
|
||||
}
|
||||
if (deleteFolder && keep == 0) {
|
||||
folder.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Reads tracklist from storage using GSON */
|
||||
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()
|
||||
when (json.isNotBlank()) {
|
||||
// convert JSON and return as tracklist
|
||||
true -> try {
|
||||
tracklist = getCustomGson().fromJson(json, Tracklist::class.java)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return tracklist
|
||||
}
|
||||
|
||||
|
||||
/* Reads track from storage using GSON */
|
||||
fun readTrack(context: Context, fileUri: Uri): Track {
|
||||
// get JSON from text file
|
||||
val json: String = readTextFile(context, fileUri)
|
||||
var track: Track = Track()
|
||||
when (json.isNotEmpty()) {
|
||||
// convert JSON and return as track
|
||||
true -> try {
|
||||
track = getCustomGson().fromJson(json, Track::class.java)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
|
||||
/* Deletes temp track file */
|
||||
fun deleteTempFile(context: Context) {
|
||||
getTempFileUri(context).toFile().delete()
|
||||
}
|
||||
|
||||
|
||||
/* Checks if temp track file exists */
|
||||
fun tempFileExists(context: Context): Boolean {
|
||||
return getTempFileUri(context).toFile().exists()
|
||||
}
|
||||
|
||||
|
||||
/* Creates Uri for Gpx file of a track */
|
||||
fun getGpxFileUri(context: Context, track: Track): Uri {
|
||||
val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_GPX), fileName).toUri()
|
||||
}
|
||||
|
||||
|
||||
/* Creates Uri for json track file */
|
||||
fun getTrackFileUri(context: Context, track: Track): Uri {
|
||||
val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri()
|
||||
}
|
||||
|
||||
|
||||
/* Creates Uri for json temp track file */
|
||||
fun getTempFileUri(context: Context): Uri {
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri()
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for saveTracklist */
|
||||
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))
|
||||
cont.resume(saveTracklist(context, tracklist, modificationDate))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for renameTrack */
|
||||
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(renameTrack(context, track, newName))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for saveTracklist */
|
||||
suspend fun saveTracklistSuspended(context: Context, tracklist: Tracklist, modificationDate: Date) {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(saveTracklist(context, tracklist, modificationDate))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for saveTrack */
|
||||
suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(saveTrack(track, saveGpxToo))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for saveTempTrack */
|
||||
suspend fun saveTempTrackSuspended(context: Context, track: Track) {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(saveTempTrack(context, track))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for deleteTrack */
|
||||
suspend fun deleteTrackSuspended(context: Context, position: Int, tracklist: Tracklist): Tracklist {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(deleteTrack(context, position, tracklist))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for readTracklist */
|
||||
suspend fun readTracklistSuspended(context: Context): Tracklist {
|
||||
return suspendCoroutine {cont ->
|
||||
cont.resume(readTracklist(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for copyFile */
|
||||
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri) {
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(copyFile(context, originalFileUri, targetFileUri, deleteOriginal = true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Save Track as JSON to storage */
|
||||
private fun saveTrack(track: Track, saveGpxToo: Boolean) {
|
||||
val jsonString: String = getTrackJsonString(track)
|
||||
if (jsonString.isNotBlank()) {
|
||||
// write track file
|
||||
writeTextFile(jsonString, Uri.parse(track.trackUriString))
|
||||
}
|
||||
if (saveGpxToo) {
|
||||
val gpxString: String = TrackHelper.createGpxString(track)
|
||||
if (gpxString.isNotBlank()) {
|
||||
// write GPX file
|
||||
writeTextFile(gpxString, Uri.parse(track.gpxUriString))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Save Temp Track as JSON to storage */
|
||||
private fun saveTempTrack(context: Context, track: Track) {
|
||||
val json: String = getTrackJsonString(track)
|
||||
if (json.isNotBlank()) {
|
||||
writeTextFile(json, getTempFileUri(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Saves track tracklist as JSON text file */
|
||||
private fun saveTracklist(context: Context, tracklist: Tracklist, modificationDate: Date) {
|
||||
tracklist.modificationDate = modificationDate
|
||||
// convert to JSON
|
||||
val gson: Gson = getCustomGson()
|
||||
var json: String = String()
|
||||
try {
|
||||
json = gson.toJson(tracklist)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
if (json.isNotBlank()) {
|
||||
// write text file
|
||||
writeTextFile(json, getTracklistFileUri(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates Uri for tracklist file */
|
||||
private fun getTracklistFileUri(context: Context): Uri {
|
||||
return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Renames track */
|
||||
private fun renameTrack(context: Context, track: Track, newName: String) {
|
||||
// search track in tracklist
|
||||
val tracklist: Tracklist = readTracklist(context)
|
||||
var trackUriString: String = String()
|
||||
tracklist.tracklistElements.forEach { tracklistElement ->
|
||||
if (tracklistElement.getTrackId() == track.getTrackId()) {
|
||||
// rename tracklist element
|
||||
tracklistElement.name = newName
|
||||
trackUriString = tracklistElement.trackUriString
|
||||
}
|
||||
}
|
||||
if (trackUriString.isNotEmpty()) {
|
||||
// save tracklist
|
||||
saveTracklist(context, tracklist, GregorianCalendar.getInstance().time)
|
||||
// rename track
|
||||
track.name = newName
|
||||
// save track
|
||||
saveTrack(track, saveGpxToo = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Deletes track */
|
||||
private fun deleteTrack(context: Context, position: Int, tracklist: Tracklist): Tracklist {
|
||||
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
|
||||
// delete track files
|
||||
tracklistElement.trackUriString.toUri().toFile().delete()
|
||||
tracklistElement.gpxUriString.toUri().toFile().delete()
|
||||
// remove track element from list
|
||||
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) {
|
||||
val inputStream = context.contentResolver.openInputStream(originalFileUri)
|
||||
val outputStream = context.contentResolver.openOutputStream(targetFileUri)
|
||||
if (outputStream != null) {
|
||||
inputStream?.copyTo(outputStream)
|
||||
}
|
||||
if (deleteOriginal) {
|
||||
context.contentResolver.delete(originalFileUri, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Converts track to JSON */
|
||||
private fun getTrackJsonString(track: Track): String {
|
||||
val gson: Gson = getCustomGson()
|
||||
var json: String = String()
|
||||
try {
|
||||
json = gson.toJson(track)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
/* Creates a Gson object */
|
||||
private fun getCustomGson(): Gson {
|
||||
val gsonBuilder = GsonBuilder()
|
||||
gsonBuilder.setDateFormat("M/d/yy hh:mm a")
|
||||
gsonBuilder.excludeFieldsWithoutExposeAnnotation()
|
||||
return gsonBuilder.create()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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 {
|
||||
|
||||
// check if Decimal prefix symbol (SI) or Binary prefix symbol (IEC) requested
|
||||
val unit: Long = if (si) 1000L else 1024L
|
||||
|
||||
// just return bytes if file size is smaller than requested unit
|
||||
if (bytes < unit) return "$bytes B"
|
||||
|
||||
// calculate exp
|
||||
val exp: Int = (Math.log(bytes.toDouble()) / Math.log(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 numberFormat = NumberFormat.getNumberInstance()
|
||||
numberFormat.maximumFractionDigits = 1
|
||||
|
||||
return numberFormat.format(result) + " " + prefix + "B"
|
||||
}
|
||||
|
||||
|
||||
/* Reads InputStream from file uri and returns it as String */
|
||||
private fun readTextFile(context: Context, 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()
|
||||
// check if file exists
|
||||
if (!file.exists()) {
|
||||
return String()
|
||||
}
|
||||
// read until last line reached
|
||||
val stream: InputStream = file.inputStream()
|
||||
val reader: BufferedReader = BufferedReader(InputStreamReader(stream))
|
||||
val builder: StringBuilder = StringBuilder()
|
||||
reader.forEachLine {
|
||||
builder.append(it)
|
||||
builder.append("\n") }
|
||||
stream.close()
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
|
||||
/* Writes given text to file on storage */
|
||||
private fun writeTextFile(text: String, fileUri: Uri) {
|
||||
val file: File = fileUri.toFile()
|
||||
file.writeText(text)
|
||||
}
|
||||
|
||||
|
||||
/* 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 ()
|
||||
try {
|
||||
val out = FileOutputStream(file)
|
||||
bitmap.compress(format, quality, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
165
app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt
Normal file
165
app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* ImportHelper.kt
|
||||
* Implements the ImportHelper object
|
||||
* A ImportHelper manages the one-time import of old .trackbook files
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.WayPoint
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* ImportHelper data class
|
||||
*/
|
||||
object ImportHelper {
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(ImportHelper::class.java)
|
||||
|
||||
|
||||
|
||||
/* Converts older tracks of type .trackbook into the new format */
|
||||
fun convertOldTracks(context: Context) {
|
||||
val oldTracks: ArrayList<Track> = arrayListOf()
|
||||
val trackFolder: File? = context.getExternalFilesDir(Keys.FOLDER_TRACKS)
|
||||
|
||||
if (trackFolder != null && trackFolder.exists() && trackFolder.isDirectory) {
|
||||
trackFolder.listFiles()?.forEach { file ->
|
||||
if (file.name.endsWith(".trackbook")) {
|
||||
// read until last line reached
|
||||
val stream: InputStream = file.inputStream()
|
||||
val reader: BufferedReader = BufferedReader(InputStreamReader(stream))
|
||||
val builder: StringBuilder = StringBuilder()
|
||||
reader.forEachLine {
|
||||
builder.append(it)
|
||||
builder.append("\n") }
|
||||
stream.close()
|
||||
// get content of file
|
||||
val fileContent: String = builder.toString()
|
||||
// get LegacyTrack from JSON
|
||||
val gsonBuilder = GsonBuilder()
|
||||
gsonBuilder.setDateFormat("M/d/yy hh:mm a")
|
||||
val oldTrack: LegacyTrack = gsonBuilder.create().fromJson(fileContent, LegacyTrack::class.java)
|
||||
oldTracks.add(oldTrack.toTrack())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save track using "deferred await"
|
||||
if (oldTracks.isNotEmpty()) {
|
||||
GlobalScope.launch {
|
||||
oldTracks.forEach { oldTrack ->
|
||||
// step 1: create and store filenames for json and gpx files
|
||||
oldTrack.trackUriString = FileHelper.getTrackFileUri(context, oldTrack).toString()
|
||||
oldTrack.gpxUriString = FileHelper.getGpxFileUri(context, oldTrack).toString()
|
||||
// step 2: save track
|
||||
FileHelper.saveTrackSuspended(oldTrack, saveGpxToo = true)
|
||||
// step 3: save tracklist
|
||||
FileHelper.addTrackAndSaveTracklistSuspended(context, oldTrack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: Legacy version of Track - used for one-time import only
|
||||
* Warning: Works only as long as targetSdkVersion < 28
|
||||
*/
|
||||
private data class LegacyTrack (
|
||||
@SerializedName("b") var mTrackFormatVersion: Int = 0,
|
||||
@SerializedName("c") var mWayPoints: List<LegacyWayPoint>,
|
||||
@SerializedName("d") var mTrackLength: Float = 0f,
|
||||
@SerializedName("e") var mDuration: Long = 0,
|
||||
@SerializedName("f") var mStepCount: Float = 0f,
|
||||
@SerializedName("g") var mRecordingStart: Date = GregorianCalendar.getInstance().time,
|
||||
@SerializedName("h") var mRecordingStop: Date = mRecordingStart,
|
||||
@SerializedName("i") var mMaxAltitude: Double = 0.0,
|
||||
@SerializedName("j") var mMinAltitude: Double = 0.0,
|
||||
@SerializedName("k") var mPositiveElevation: Double = 0.0,
|
||||
@SerializedName("l") var mNegativeElevation: Double = 0.0) {
|
||||
|
||||
|
||||
/* Converts */
|
||||
fun toTrack():Track {
|
||||
val track: Track = Track()
|
||||
track.trackFormatVersion = mTrackFormatVersion
|
||||
mWayPoints.forEach { legacyWayPoint ->
|
||||
val wayPoint: WayPoint= WayPoint(
|
||||
provider = legacyWayPoint.mLocation.provider,
|
||||
latitude = legacyWayPoint.mLocation.latitude,
|
||||
longitude = legacyWayPoint.mLocation.longitude,
|
||||
altitude = legacyWayPoint.mLocation.altitude,
|
||||
accuracy = legacyWayPoint.mLocation.accuracy,
|
||||
time = legacyWayPoint.mLocation.time,
|
||||
distanceToStartingPoint = legacyWayPoint.mDistanceToStartingPoint,
|
||||
numberSatellites = legacyWayPoint.mNumberSatellites,
|
||||
isStopOver = legacyWayPoint.mIsStopOver
|
||||
)
|
||||
track.wayPoints.add(wayPoint)
|
||||
}
|
||||
track.length = mTrackLength
|
||||
track.duration = mDuration
|
||||
track.stepCount = mStepCount
|
||||
track.recordingStart = mRecordingStart
|
||||
track.recordingStop = mRecordingStop
|
||||
track.maxAltitude = mMaxAltitude
|
||||
track.minAltitude = mMinAltitude
|
||||
track.positiveElevation = mPositiveElevation
|
||||
track.negativeElevation = mNegativeElevation
|
||||
track.latitude = track.wayPoints[0].latitude
|
||||
track.longitude = track.wayPoints[0].longitude
|
||||
track.name = DateTimeHelper.convertToReadableDate(mRecordingStart)
|
||||
return track
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: Legacy version of WayPoint - used for one-time import only
|
||||
* Warning: Works only as long as targetSdkVersion < 28
|
||||
*/
|
||||
private data class LegacyWayPoint (
|
||||
var mLocation: Location,
|
||||
var mIsStopOver: Boolean = false,
|
||||
var mDistanceToStartingPoint: Float = 0f,
|
||||
var mNumberSatellites: Int = 0) {
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/**
|
||||
* LengthUnitHelper.java
|
||||
* Implements the LengthUnitHelper class
|
||||
* A LengthUnitHelper offers helper methods for dealing with unit systems and locales
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* LengthUnitHelper class
|
||||
*/
|
||||
public final class LengthUnitHelper implements TrackbookKeys {
|
||||
|
||||
|
||||
/* Converts for the default locale a distance value to a readable string */
|
||||
public static String convertDistanceToString(double distance) {
|
||||
return convertDistanceToString(distance, getUnitSystem());
|
||||
}
|
||||
|
||||
|
||||
/* Converts for the given uni System a distance value to a readable string */
|
||||
public static String convertDistanceToString(double distance, int unitSystem) {
|
||||
String unit;
|
||||
NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||
|
||||
// check for locale and set unit system accordingly
|
||||
if (unitSystem == IMPERIAL) {
|
||||
// miles and feet
|
||||
if (distance > 1610) {
|
||||
// convert distance to miles
|
||||
distance = distance * 0.000621371192;
|
||||
// set measurement unit
|
||||
unit = "mi";
|
||||
// set number precision
|
||||
numberFormat.setMaximumFractionDigits(2);
|
||||
} else {
|
||||
// convert distance to feet
|
||||
distance = distance * 3.28084f;
|
||||
// set measurement unit
|
||||
unit = "ft";
|
||||
// set number precision
|
||||
numberFormat.setMaximumFractionDigits(0);
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
// kilometer and meter
|
||||
if (distance >= 1000) {
|
||||
// convert distance to kilometer
|
||||
distance = distance * 0.001f;
|
||||
// set measurement unit
|
||||
unit = "km";
|
||||
// set number precision
|
||||
numberFormat.setMaximumFractionDigits(2);
|
||||
} else {
|
||||
// set measurement unit
|
||||
unit = "m";
|
||||
// set number precision
|
||||
numberFormat.setMaximumFractionDigits(0);
|
||||
}
|
||||
|
||||
}
|
||||
// format distance according to current locale
|
||||
return numberFormat.format(distance) + " " + unit;
|
||||
}
|
||||
|
||||
|
||||
/* Determines which unit system the device is using (metric or imperial) */
|
||||
public static int getUnitSystem() {
|
||||
// America (US), Liberia (LR), Myanmar(MM) use the imperial system
|
||||
List<String> imperialSystemCountries = Arrays.asList("US", "LR", "MM");
|
||||
String countryCode = Locale.getDefault().getCountry();
|
||||
if (imperialSystemCountries.contains(countryCode)){
|
||||
return IMPERIAL;
|
||||
} else {
|
||||
return METRIC;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* LengthUnitHelper.kt
|
||||
* Implements the LengthUnitHelper object
|
||||
* A LengthUnitHelper offers helper methods for dealing with unit systems and locales
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* LengthUnitHelper object
|
||||
*/
|
||||
object LengthUnitHelper {
|
||||
|
||||
|
||||
/* Converts for the given uni System a distance value to a readable string */
|
||||
fun convertDistanceToString(distance: Float, useImperial: Boolean = false): String {
|
||||
return convertDistanceToString(distance.toDouble(), useImperial)
|
||||
}
|
||||
|
||||
|
||||
/* Converts for the given uni System a distance value to a readable string */
|
||||
fun convertDistanceToString(distance: Double, useImperial: Boolean = false): String {
|
||||
val readableDistance: Double
|
||||
val unit: String
|
||||
val numberFormat = NumberFormat.getNumberInstance()
|
||||
|
||||
// check for locale and set unit system accordingly
|
||||
when (useImperial) {
|
||||
// CASE: miles and feet
|
||||
true -> {
|
||||
if (distance > 1610) {
|
||||
// convert distance to miles
|
||||
readableDistance = distance * 0.000621371192f
|
||||
// set measurement unit
|
||||
unit = "mi"
|
||||
// set number precision
|
||||
numberFormat.maximumFractionDigits = 2
|
||||
} else {
|
||||
// convert distance to feet
|
||||
readableDistance = distance * 3.28084f
|
||||
// set measurement unit
|
||||
unit = "ft"
|
||||
// set number precision
|
||||
numberFormat.maximumFractionDigits = 0
|
||||
}
|
||||
}
|
||||
// CASE: kilometer and meter
|
||||
false -> {
|
||||
if (distance >= 1000) {
|
||||
// convert distance to kilometer
|
||||
readableDistance = distance * 0.001f
|
||||
// set measurement unit
|
||||
unit = "km"
|
||||
// set number precision
|
||||
numberFormat.maximumFractionDigits = 2
|
||||
} else {
|
||||
// no need to convert
|
||||
readableDistance = distance
|
||||
// set measurement unit
|
||||
unit = "m"
|
||||
// set number precision
|
||||
numberFormat.maximumFractionDigits = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// format distance according to current locale
|
||||
return "${numberFormat.format(readableDistance)} $unit"
|
||||
}
|
||||
|
||||
|
||||
/* 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 countryCode = Locale.getDefault().country
|
||||
return imperialSystemCountries.contains(countryCode)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,302 +0,0 @@
|
|||
/**
|
||||
* LocationHelper.java
|
||||
* Implements the LocationHelper class
|
||||
* A LocationHelper offers helper methods for dealing with location issues
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.Settings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* LocationHelper class
|
||||
*/
|
||||
public final class LocationHelper implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = LocationHelper.class.getSimpleName();
|
||||
|
||||
|
||||
/* Determines last known location */
|
||||
public static Location determineLastKnownLocation(LocationManager locationManager) {
|
||||
// define variables
|
||||
List locationProviders = locationManager.getProviders(true);
|
||||
Location gpsLocation = null;
|
||||
Location networkLocation = null;
|
||||
|
||||
// set location providers
|
||||
String gpsProvider = LocationManager.GPS_PROVIDER;
|
||||
String networkProvider = LocationManager.NETWORK_PROVIDER;
|
||||
|
||||
|
||||
if (locationProviders.contains(gpsProvider)) {
|
||||
// get last know location from gps
|
||||
try {
|
||||
gpsLocation = locationManager.getLastKnownLocation(gpsProvider);
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (locationProviders.contains(networkProvider)) {
|
||||
// get last known location from wifi and cell
|
||||
try {
|
||||
networkLocation = locationManager.getLastKnownLocation(networkProvider);
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (gpsLocation == null) {
|
||||
return networkLocation;
|
||||
} else if (networkLocation == null) {
|
||||
return gpsLocation;
|
||||
} else if (isBetterLocation(gpsLocation, networkLocation)) {
|
||||
return gpsLocation;
|
||||
} else {
|
||||
return networkLocation;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Determines whether one location reading is better than the current location fix */
|
||||
public static boolean isBetterLocation(Location location, Location currentBestLocation) {
|
||||
// credit: the isBetterLocation method was sample code from: https://developer.android.com/guide/topics/location/strategies.html
|
||||
|
||||
if (currentBestLocation == null) {
|
||||
// a new location is always better than no location
|
||||
return true;
|
||||
}
|
||||
|
||||
// check whether the new location fix is newer or older
|
||||
long timeDelta = location.getElapsedRealtimeNanos() - currentBestLocation.getElapsedRealtimeNanos();
|
||||
boolean isSignificantlyNewer = timeDelta > ONE_MINUTE_IN_NANOSECONDS;
|
||||
boolean isSignificantlyOlder = timeDelta < -ONE_MINUTE_IN_NANOSECONDS;
|
||||
boolean isNewer = timeDelta > 0;
|
||||
|
||||
// if it's been more than two minutes since the current location, use the new location because the user has likely moved
|
||||
if (isSignificantlyNewer) {
|
||||
return true;
|
||||
} else if (isSignificantlyOlder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check whether the new location fix is more or less accurate
|
||||
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
||||
boolean isLessAccurate = accuracyDelta > 0;
|
||||
boolean isMoreAccurate = accuracyDelta < 0;
|
||||
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
|
||||
|
||||
// check if the old and new location are from the same provider
|
||||
boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());
|
||||
|
||||
// determine location quality using a combination of timeliness and accuracy
|
||||
if (isMoreAccurate) {
|
||||
return true;
|
||||
} else if (isNewer && !isLessAccurate) {
|
||||
return true;
|
||||
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* Checks accuracy of given location */
|
||||
public static boolean isAccurate(Location location) {
|
||||
return location.getAccuracy() < FIFTY_METER_RADIUS;
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is newer than two minutes */
|
||||
public static boolean isCurrent(Location location) {
|
||||
if (location == null) {
|
||||
return false;
|
||||
} else {
|
||||
long locationAge = SystemClock.elapsedRealtimeNanos() - location.getElapsedRealtimeNanos();
|
||||
return locationAge < ONE_MINUTE_IN_NANOSECONDS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is a new WayPoint */
|
||||
public static boolean isNewWayPoint(Location lastLocation, Location newLocation, float averageSpeed) {
|
||||
float distance = newLocation.distanceTo(lastLocation);
|
||||
long timeDifference = newLocation.getElapsedRealtimeNanos() - lastLocation.getElapsedRealtimeNanos();
|
||||
|
||||
if (newLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER)) {
|
||||
// calculate speed difference
|
||||
float speedDifference;
|
||||
float currentSpeed = distance / ((float)timeDifference / ONE_SECOND_IN_NANOSECOND);
|
||||
if (currentSpeed > averageSpeed) {
|
||||
speedDifference = currentSpeed / averageSpeed;
|
||||
} else {
|
||||
speedDifference = averageSpeed / currentSpeed;
|
||||
}
|
||||
|
||||
// SPECIAL CASE network: plausibility check for network provider. looking for sudden location jump errors
|
||||
if (averageSpeed != 0f && currentSpeed > 10f && speedDifference > 2f) {
|
||||
// implausible location (speed is high (10 m/s == 36km/h) and has doubled)
|
||||
return false;
|
||||
}
|
||||
|
||||
// SPECIAL CASE network: if last location came from gps. only accept location fixes with decent accuracy
|
||||
if (lastLocation.getProvider().equals(LocationManager.GPS_PROVIDER) && newLocation.getAccuracy() < 66) {
|
||||
// network locations tend to be too in accurate
|
||||
return false;
|
||||
}
|
||||
|
||||
// DEFAULT network: distance is bigger than 30 meters and time difference bigger than 12 seconds
|
||||
return distance > 30 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND; // TODO add minimal accuracy
|
||||
|
||||
} else {
|
||||
// DEFAULT GPS: distance is bigger than 10 meters and time difference bigger than 12 seconds
|
||||
return distance > 10 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is a stop over */
|
||||
public static boolean isStopOver(@Nullable Location previousLocation, Location newLocation) {
|
||||
if (previousLocation != null) {
|
||||
long timeDifference = newLocation.getElapsedRealtimeNanos() - previousLocation.getElapsedRealtimeNanos();
|
||||
return timeDifference >= FIVE_MINUTES_IN_NANOSECONDS;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Registers gps and network location listeners */
|
||||
public static void registerLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) {
|
||||
LogHelper.v(LOG_TAG, "Registering location listeners.");
|
||||
|
||||
// get location providers
|
||||
List locationProviders = locationManager.getAllProviders();
|
||||
|
||||
// got GPS location provider?
|
||||
if (gpsListener != null && locationProviders.contains(LocationManager.GPS_PROVIDER)) {
|
||||
try {
|
||||
// register GPS location listener and request updates
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsListener);
|
||||
LogHelper.v(LOG_TAG, "Registering gps listener.");
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// got network location provider?
|
||||
if (networkListener != null && locationProviders.contains(LocationManager.NETWORK_PROVIDER)) {
|
||||
try {
|
||||
// register network location listener and request updates
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, networkListener);
|
||||
LogHelper.v(LOG_TAG, "Registering network listener.");
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Removes gps and network location listeners */
|
||||
public static void removeLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) {
|
||||
LogHelper.v(LOG_TAG, "Removing location listeners.");
|
||||
|
||||
// get location providers
|
||||
List locationProviders = locationManager.getAllProviders();
|
||||
|
||||
// got GPS location provider?
|
||||
if (locationProviders.contains(LocationManager.GPS_PROVIDER) && gpsListener != null) {
|
||||
try {
|
||||
// remove GPS listener
|
||||
locationManager.removeUpdates(gpsListener);
|
||||
LogHelper.v(LOG_TAG, "Removing gps listener.");
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// got network location provider?
|
||||
if (locationProviders.contains(LocationManager.NETWORK_PROVIDER) && networkListener != null) {
|
||||
try {
|
||||
// remove network listener
|
||||
locationManager.removeUpdates(networkListener);
|
||||
LogHelper.v(LOG_TAG, "Removing network listener.");
|
||||
} catch (SecurityException e) {
|
||||
// catches permission problems
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Converts milliseconds to mm:ss or hh:mm:ss */
|
||||
public static String convertToReadableTime(long milliseconds, boolean includeHours) {
|
||||
|
||||
if (includeHours) {
|
||||
// format hh:mm:ss
|
||||
return String.format(Locale.ENGLISH, "%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(milliseconds),
|
||||
TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1),
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1));
|
||||
} else if (TimeUnit.MILLISECONDS.toHours(milliseconds) < 1) {
|
||||
// format mm:ss
|
||||
return String.format(Locale.ENGLISH, "%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1),
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Check if any location provider is enabled */
|
||||
public static boolean checkLocationSystemSetting(Context context) {
|
||||
int locationSettingState = 0;
|
||||
try {
|
||||
locationSettingState = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE);
|
||||
} catch (Settings.SettingNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return locationSettingState != Settings.Secure.LOCATION_MODE_OFF;
|
||||
}
|
||||
|
||||
|
||||
/* Checks whether two location providers are the same */
|
||||
private static boolean isSameProvider(String provider1, String provider2) {
|
||||
// credit: the isSameProvider method was sample code from: https://developer.android.com/guide/topics/location/strategies.html
|
||||
if (provider1 == null) {
|
||||
return provider2 == null;
|
||||
}
|
||||
return provider1.equals(provider2);
|
||||
}
|
||||
|
||||
}
|
218
app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt
Normal file
218
app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* LocationHelper.kt
|
||||
* Implements the LocationHelper object
|
||||
* A LocationHelper offers helper methods for dealing with location issues
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.SystemClock
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.core.Track
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* Keys object
|
||||
*/
|
||||
object LocationHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(LocationHelper::class.java)
|
||||
|
||||
|
||||
/* Get default location */
|
||||
fun getDefaultLocation(): Location {
|
||||
val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
|
||||
defaultLocation.latitude = Keys.DEFAULT_LATITUDE
|
||||
defaultLocation.longitude = Keys.DEFAULT_LONGITUDE
|
||||
defaultLocation.accuracy = Keys.DEFAULT_ACCURACY
|
||||
defaultLocation.altitude = Keys.DEFAULT_ALTITUDE
|
||||
defaultLocation.time = Keys.DEFAULT_DATE.time
|
||||
return defaultLocation
|
||||
}
|
||||
|
||||
|
||||
/* Checks if a location is older than one minute */
|
||||
fun isOldLocation(location: Location): Boolean {
|
||||
// check how many milliseconds the given location is old
|
||||
return GregorianCalendar.getInstance().time.time - location.time > Keys.SIGNIFICANT_TIME_DIFFERENCE
|
||||
}
|
||||
|
||||
|
||||
/* Tries to return the last location that the system has stored */
|
||||
fun getLastKnownLocation(context: Context): Location {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return lastKnownLocation
|
||||
}
|
||||
|
||||
|
||||
/* Determines whether one Location reading is better than the current Location fix */
|
||||
fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean {
|
||||
// Credit: https://developer.android.com/guide/topics/location/strategies.html#BestEstimate
|
||||
|
||||
if (currentBestLocation == null) {
|
||||
// a new location is always better than no location
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
when {
|
||||
// if it's been more than two minutes since the current location, use the new location because the user has likely moved
|
||||
isSignificantlyNewer -> return true
|
||||
// if the new location is more than two minutes older, it must be worse
|
||||
isSignificantlyOlder -> return false
|
||||
}
|
||||
|
||||
// check whether the new location fix is more or less accurate
|
||||
val isNewer: Boolean = timeDelta > 0L
|
||||
val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy
|
||||
val isLessAccurate: Boolean = accuracyDelta > 0f
|
||||
val isMoreAccurate: Boolean = accuracyDelta < 0f
|
||||
val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f
|
||||
|
||||
// check if the old and new location are from the same provider
|
||||
val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider
|
||||
|
||||
// determine location quality using a combination of timeliness and accuracy
|
||||
return when {
|
||||
isMoreAccurate -> true
|
||||
isNewer && !isLessAccurate -> true
|
||||
isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
} else {
|
||||
return 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)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Checks if given location is new */
|
||||
fun isRecentEnough(location: Location): Boolean {
|
||||
val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos
|
||||
return locationAge < Keys.DEFAULT_THRESHOLD_LOCATION_AGE
|
||||
}
|
||||
|
||||
|
||||
/* 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 isAccurate
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is different enough compared to previous location */
|
||||
fun isDifferentEnough(previousLocation: Location?, location: Location): Boolean {
|
||||
// check if previous location is (not) available
|
||||
if (previousLocation == null) return true
|
||||
// check if distance between is large enough
|
||||
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
|
||||
} else {
|
||||
distanceThreshold = Keys.DEFAULT_THRESHOLD_DISTANCE
|
||||
}
|
||||
LogHelper.e(TAG, "distanceThreshold -> $distanceThreshold") // todo remove
|
||||
// location is different when far enough away from previous location
|
||||
return calculateDistance(previousLocation, location) > distanceThreshold
|
||||
}
|
||||
|
||||
|
||||
/* Calculates distance between two locations */
|
||||
fun calculateDistance(previousLocation: Location?, location: Location): Float {
|
||||
var distance: Float = 0f
|
||||
// two data points needed to calculate distance
|
||||
if (previousLocation != null) {
|
||||
// add up distance
|
||||
distance = previousLocation.distanceTo(location)
|
||||
}
|
||||
return distance
|
||||
}
|
||||
|
||||
|
||||
/* Calculate elevation differences */
|
||||
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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return Pair(positiveElevation, negativeElevation)
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is a stop over */
|
||||
fun isStopOver(previousLocation: Location?, location: Location): Boolean {
|
||||
if (previousLocation == null) return false
|
||||
// check how many milliseconds the given locations are apart
|
||||
return location.time - previousLocation.time > Keys.STOP_OVER_THRESHOLD
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* LogHelper.java
|
||||
* Implements the LogHelper class
|
||||
* A LogHelper wraps the logging calls to be able to strip them out of release versions
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.y20k.trackbook.BuildConfig;
|
||||
|
||||
|
||||
/**
|
||||
* LogHelper class
|
||||
*/
|
||||
public final class LogHelper {
|
||||
|
||||
private final static boolean mTesting = false;
|
||||
|
||||
public static void d(final String tag, String message) {
|
||||
// include logging only in debug versions
|
||||
if (BuildConfig.DEBUG || mTesting) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void v(final String tag, String message) {
|
||||
// include logging only in debug versions
|
||||
if (BuildConfig.DEBUG || mTesting) {
|
||||
Log.v(tag, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void e(final String tag, String message) {
|
||||
Log.e(tag, message);
|
||||
}
|
||||
|
||||
|
||||
public static void i(final String tag, String message) {
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
|
||||
public static void w(final String tag, String message) {
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
}
|
115
app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt
Normal file
115
app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* LogHelper.kt
|
||||
* Implements the LogHelper object
|
||||
* A LogHelper wraps the logging calls to be able to strip them out of release versions
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.util.Log
|
||||
import org.y20k.trackbook.BuildConfig
|
||||
|
||||
|
||||
/*
|
||||
* LogHelper object
|
||||
*/
|
||||
object LogHelper {
|
||||
|
||||
private const val TESTING: Boolean = true // set to "false"
|
||||
private const val LOG_PREFIX: String = "trackbook_"
|
||||
private const val MAX_LOG_TAG_LENGTH: Int = 64
|
||||
private const val LOG_PREFIX_LENGTH: Int = LOG_PREFIX.length
|
||||
|
||||
fun makeLogTag(str: String): String {
|
||||
return if (str.length > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) {
|
||||
LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1)
|
||||
} else LOG_PREFIX + str
|
||||
}
|
||||
|
||||
fun makeLogTag(cls: Class<*>): String {
|
||||
// don't use this when obfuscating class names
|
||||
return makeLogTag(cls.simpleName)
|
||||
}
|
||||
|
||||
fun v(tag: String, vararg messages: Any) {
|
||||
// Only log VERBOSE if build type is DEBUG or if TESTING is true
|
||||
if (BuildConfig.DEBUG || TESTING) {
|
||||
log(tag, Log.VERBOSE, null, *messages)
|
||||
}
|
||||
}
|
||||
|
||||
fun d(tag: String, vararg messages: Any) {
|
||||
// Only log DEBUG if build type is DEBUG or if TESTING is true
|
||||
if (BuildConfig.DEBUG || TESTING) {
|
||||
log(tag, Log.DEBUG, null, *messages)
|
||||
}
|
||||
}
|
||||
|
||||
fun i(tag: String, vararg messages: Any) {
|
||||
log(tag, Log.INFO, null, *messages)
|
||||
}
|
||||
|
||||
fun w(tag: String, vararg messages: Any) {
|
||||
log(tag, Log.WARN, null, *messages)
|
||||
}
|
||||
|
||||
fun w(tag: String, t: Throwable, vararg messages: Any) {
|
||||
log(tag, Log.WARN, t, *messages)
|
||||
}
|
||||
|
||||
fun e(tag: String, vararg messages: Any) {
|
||||
log(tag, Log.ERROR, null, *messages)
|
||||
}
|
||||
|
||||
fun e(tag: String, t: Throwable, vararg messages: Any) {
|
||||
log(tag, Log.ERROR, t, *messages)
|
||||
}
|
||||
|
||||
private fun log(tag: String, level: Int, t: Throwable?, vararg messages: Any) {
|
||||
val message: String
|
||||
if (t == null && messages.size == 1) {
|
||||
// handle this common case without the extra cost of creating a stringbuffer:
|
||||
message = messages[0].toString()
|
||||
} else {
|
||||
val sb = StringBuilder()
|
||||
for (m in messages) {
|
||||
sb.append(m)
|
||||
}
|
||||
if (t != null) {
|
||||
sb.append("\n").append(Log.getStackTraceString(t))
|
||||
}
|
||||
message = sb.toString()
|
||||
}
|
||||
Log.println(level, tag, message)
|
||||
|
||||
// if (Log.isLoggable(tag, level)) {
|
||||
// val message: String
|
||||
// if (t == null && messages != null && messages.size == 1) {
|
||||
// // handle this common case without the extra cost of creating a stringbuffer:
|
||||
// message = messages[0].toString()
|
||||
// } else {
|
||||
// val sb = StringBuilder()
|
||||
// if (messages != null)
|
||||
// for (m in messages) {
|
||||
// sb.append(m)
|
||||
// }
|
||||
// if (t != null) {
|
||||
// sb.append("\n").append(Log.getStackTraceString(t))
|
||||
// }
|
||||
// message = sb.toString()
|
||||
// }
|
||||
// Log.println(level, tag, message)
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -1,275 +0,0 @@
|
|||
/**
|
||||
* MapHelper.java
|
||||
* Implements the MapHelper class
|
||||
* A MapHelper offers helper methods for dealing with Trackbook's map
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.location.Location;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay;
|
||||
import org.osmdroid.views.overlay.OverlayItem;
|
||||
import org.y20k.trackbook.R;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.core.WayPoint;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
|
||||
/**
|
||||
* MapHelper class
|
||||
*/
|
||||
public final class MapHelper implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = MapHelper.class.getSimpleName();
|
||||
|
||||
|
||||
/* Creates icon overlay for current position (used in MainActivity Fragment) */
|
||||
public static ItemizedIconOverlay createMyLocationOverlay(final Context context, Location currentBestLocation, boolean locationIsNew, boolean trackingActive) {
|
||||
|
||||
final ArrayList<OverlayItem> overlayItems = new ArrayList<>();
|
||||
|
||||
// create marker
|
||||
Drawable newMarker;
|
||||
if (locationIsNew && !trackingActive) {
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_24dp);
|
||||
} else if (!locationIsNew && trackingActive) {
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_grey_24dp);
|
||||
} else {
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp);
|
||||
}
|
||||
|
||||
OverlayItem overlayItem = createOverlayItem(context, currentBestLocation);
|
||||
overlayItem.setMarker(newMarker);
|
||||
|
||||
// add marker to list of overlay items
|
||||
overlayItems.add(overlayItem);
|
||||
|
||||
// create and return overlay for current position
|
||||
return new ItemizedIconOverlay<>(overlayItems,
|
||||
new ItemizedIconOverlay.OnItemGestureListener<OverlayItem>() {
|
||||
@Override
|
||||
public boolean onItemSingleTapUp(final int index, final OverlayItem item) {
|
||||
// tap on My Location dot icon
|
||||
Toast.makeText(context, item.getTitle() + " | " + item.getSnippet(), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongPress(final int index, final OverlayItem item) {
|
||||
// long press on My Location dot icon
|
||||
return true;
|
||||
}
|
||||
}, context);
|
||||
}
|
||||
|
||||
|
||||
/* Creates icon overlay for track */
|
||||
public static ItemizedIconOverlay createTrackOverlay(final Context context, Track track, boolean trackingActive){
|
||||
|
||||
final ArrayList<OverlayItem> overlayItems = new ArrayList<>();
|
||||
boolean currentPosition;
|
||||
final int trackSize = track.getSize();
|
||||
final List<WayPoint> wayPoints = track.getWayPoints();
|
||||
WayPoint wayPoint;
|
||||
|
||||
for (int i = 0; i < track.getSize(); i++) {
|
||||
|
||||
// get WayPoint and check if it is current position
|
||||
wayPoint = wayPoints.get(i);
|
||||
currentPosition = i == trackSize - 1;
|
||||
|
||||
// create marker
|
||||
Drawable newMarker;
|
||||
|
||||
// CASE 1: Tracking active and WayPoint is not current position
|
||||
if (trackingActive && !currentPosition) {
|
||||
if (wayPoint.getIsStopOver()) {
|
||||
// stop over marker
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp);
|
||||
} else {
|
||||
// default marker for this case
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_red_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE 2: Tracking active and WayPoint is current position
|
||||
else if (trackingActive && currentPosition) {
|
||||
if (wayPoint.getIsStopOver()) {
|
||||
// stop over marker
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp);
|
||||
} else {
|
||||
// default marker for this case
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE 3: Tracking not active and WayPoint is not current position
|
||||
else if (!trackingActive && !currentPosition) {
|
||||
if (wayPoint.getIsStopOver()) {
|
||||
// stop over marker
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp);
|
||||
} else {
|
||||
// default marker for this case
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE 4: Tracking not active and WayPoint is current position
|
||||
else {
|
||||
// default marker
|
||||
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp);
|
||||
}
|
||||
|
||||
// create overlay item
|
||||
OverlayItem overlayItem = createOverlayItem(context, wayPoint.getLocation());
|
||||
overlayItem.setMarker(newMarker);
|
||||
|
||||
// add marker to list of overlay items
|
||||
overlayItems.add(overlayItem);
|
||||
}
|
||||
|
||||
// return overlay for current position
|
||||
return new ItemizedIconOverlay<>(overlayItems,
|
||||
new ItemizedIconOverlay.OnItemGestureListener<OverlayItem>() {
|
||||
@Override
|
||||
public boolean onItemSingleTapUp(final int index, final OverlayItem item) {
|
||||
// tap on waypoint
|
||||
Toast.makeText(context, item.getTitle(), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongPress(final int index, final OverlayItem item) {
|
||||
// long press on waypoint
|
||||
Toast.makeText(context, item.getSnippet(), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
}, context);
|
||||
}
|
||||
|
||||
|
||||
/* Creates a marker overlay item */
|
||||
private static OverlayItem createOverlayItem(Context context, Location location) {
|
||||
// create content of overlay item
|
||||
String time = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(location.getTime());
|
||||
final String title = context.getString(R.string.marker_description_source) + ": " + location.getProvider() + " | " + context.getString(R.string.marker_description_time) + ": " + time;
|
||||
final String description = context.getString(R.string.marker_description_accuracy) + ": " + location.getAccuracy();
|
||||
final GeoPoint position = new GeoPoint(location.getLatitude(),location.getLongitude());
|
||||
|
||||
return new OverlayItem(title, description, position);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code BoundingBox} for the collection of
|
||||
* {@code WayPoint}s, so that it would be possible to fit the map in
|
||||
* such box and see the whole {@code Track} in the map without
|
||||
* manual zooming.
|
||||
*
|
||||
* @return {@code BoundingBox} containing all {@code Waypoint}s
|
||||
*/
|
||||
public static BoundingBox calculateBoundingBox(List<WayPoint> wayPoints) {
|
||||
final ArrayList<GeoPoint> geoPoints = new ArrayList<>(wayPoints.size());
|
||||
|
||||
for (final WayPoint aWayPoint : wayPoints) {
|
||||
final GeoPoint aGeoPoint = new GeoPoint(aWayPoint.getLocation());
|
||||
geoPoints.add(aGeoPoint);
|
||||
}
|
||||
return BoundingBox.fromGeoPoints(geoPoints).increaseByScale(1.15f);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Calculates positive and negative elevation of track */
|
||||
public static Track calculateElevation(@Nullable Track track) {
|
||||
double maxAltitude = 0;
|
||||
double minAltitude = 0;
|
||||
double positiveElevation = 0;
|
||||
double negativeElevation = 0;
|
||||
|
||||
if (track != null && track.getWayPoints().size() > 0) {
|
||||
double previousLocationAltitude;
|
||||
double currentLocationAltitude;
|
||||
long previousTimeStamp;
|
||||
long currentTimeStamp;
|
||||
|
||||
// initial values for max height and min height - first waypoint
|
||||
maxAltitude = track.getWayPointLocation(0).getAltitude();
|
||||
minAltitude = maxAltitude;
|
||||
|
||||
// apply filter & smooth data
|
||||
// track = smoothTrack(track, 15f, 35f);
|
||||
|
||||
// iterate over track
|
||||
for (int i = 1; i < track.getWayPoints().size(); i++ ) {
|
||||
|
||||
// get time difference
|
||||
previousTimeStamp = track.getWayPointLocation(i -1).getTime();
|
||||
currentTimeStamp = track.getWayPointLocation(i).getTime();
|
||||
double timeDiff = (currentTimeStamp - previousTimeStamp);
|
||||
|
||||
// factor is bigger than 1 if the time stamp difference is larger than the movement recording interval (usually 15 seconds)
|
||||
double timeDiffFactor = timeDiff / FIFTEEN_SECONDS_IN_MILLISECONDS;
|
||||
|
||||
// height of previous and current waypoints
|
||||
previousLocationAltitude = track.getWayPointLocation(i -1).getAltitude();
|
||||
currentLocationAltitude = track.getWayPointLocation(i).getAltitude();
|
||||
|
||||
// check for new min and max heights
|
||||
if (currentLocationAltitude > maxAltitude) {
|
||||
maxAltitude = currentLocationAltitude;
|
||||
}
|
||||
if (minAltitude == 0 || currentLocationAltitude < minAltitude) {
|
||||
minAltitude = currentLocationAltitude;
|
||||
}
|
||||
|
||||
// get elevation difference and sum it up
|
||||
double altitudeDiff = currentLocationAltitude - previousLocationAltitude;
|
||||
if (altitudeDiff > 0 && altitudeDiff < MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) {
|
||||
positiveElevation = positiveElevation + altitudeDiff;
|
||||
}
|
||||
if (altitudeDiff < 0 && altitudeDiff > -MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) {
|
||||
negativeElevation = negativeElevation + altitudeDiff;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// store elevation data in track
|
||||
track.setMaxAltitude(maxAltitude);
|
||||
track.setMinAltitude(minAltitude);
|
||||
track.setPositiveElevation(positiveElevation);
|
||||
track.setNegativeElevation(negativeElevation);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
148
app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt
Normal file
148
app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* MapHelper.kt
|
||||
* Implements the MapHelper object
|
||||
* A MapHelper offers helper methods for manipulating osmdroid maps
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.location.Location
|
||||
import android.os.Vibrator
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay
|
||||
import org.osmdroid.views.overlay.OverlayItem
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* MapHelper object
|
||||
*/
|
||||
object MapHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val LOG_TAG = MapHelper::class.java.simpleName
|
||||
|
||||
|
||||
/* Creates icon overlay for current position (used in MapFragment) */
|
||||
fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem> {
|
||||
|
||||
val overlayItems = ArrayList<OverlayItem>()
|
||||
val locationIsOld = LocationHelper.isOldLocation(location)
|
||||
|
||||
// create marker
|
||||
val newMarker: Drawable
|
||||
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)!!
|
||||
}
|
||||
}
|
||||
// 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)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add marker to list of overlay items
|
||||
val overlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time)
|
||||
overlayItem.setMarker(newMarker)
|
||||
overlayItems.add(overlayItem)
|
||||
|
||||
// create and return overlay for current position
|
||||
return createOverlay(context, overlayItems)
|
||||
}
|
||||
|
||||
|
||||
/* Creates icon overlay for track */
|
||||
fun createTrackOverlay(context: Context, track: Track, trackingState: Int): ItemizedIconOverlay<OverlayItem> {
|
||||
|
||||
val overlayItems = ArrayList<OverlayItem>()
|
||||
val wayPoints = track.wayPoints
|
||||
|
||||
wayPoints.forEach { wayPoint ->
|
||||
// create marker
|
||||
val newMarker: Drawable
|
||||
|
||||
// get drawable
|
||||
when (trackingState) {
|
||||
// CASE: Recording is active
|
||||
Keys.STATE_TRACKING_ACTIVE -> {
|
||||
when (wayPoint.isStopOver) {
|
||||
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
|
||||
false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!!
|
||||
}
|
||||
}
|
||||
// CASE: Recording is paused/stopped
|
||||
else -> {
|
||||
when (wayPoint.isStopOver) {
|
||||
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
|
||||
false -> newMarker = 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)
|
||||
overlayItem.setMarker(newMarker)
|
||||
overlayItems.add(overlayItem)
|
||||
}
|
||||
|
||||
// create and return overlay for current position
|
||||
return createOverlay(context, overlayItems)
|
||||
}
|
||||
|
||||
|
||||
/* 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)}"
|
||||
val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
|
||||
val position: GeoPoint = GeoPoint(latitude, longitude)
|
||||
return OverlayItem(title, description, position)
|
||||
}
|
||||
|
||||
|
||||
/* Creates an overlay */
|
||||
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>): ItemizedIconOverlay<OverlayItem> {
|
||||
return ItemizedIconOverlay(overlayItems,
|
||||
object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
|
||||
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
|
||||
Toast.makeText(context, item.title, Toast.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
|
||||
val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
v.vibrate(50)
|
||||
Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
}, context)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
/**
|
||||
* NightModeHelper.java
|
||||
* Implements the NightModeHelper class
|
||||
* A NightModeHelper can toggle and restore the state of the theme's Night Mode
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import org.y20k.trackbook.R;
|
||||
|
||||
|
||||
/**
|
||||
* NightModeHelper class
|
||||
*/
|
||||
public final class NightModeHelper implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = NightModeHelper.class.getSimpleName();
|
||||
|
||||
|
||||
/* Switches between modes: day, night, undefined */
|
||||
public static void switchMode(Activity activity) {
|
||||
// SWITCH: undefined -> night / night -> day / day - undefined
|
||||
switch (AppCompatDelegate.getDefaultNightMode()) {
|
||||
case AppCompatDelegate.MODE_NIGHT_NO:
|
||||
// currently: day mode -> switch to: follow system
|
||||
displayDefaultStatusBar(activity); // necessary hack :-/
|
||||
activateFollowSystemMode(activity, true);
|
||||
break;
|
||||
case AppCompatDelegate.MODE_NIGHT_YES:
|
||||
// currently: night mode -> switch to: day mode
|
||||
displayLightStatusBar(activity); // necessary hack :-/
|
||||
activateDayMode(activity, true);
|
||||
break;
|
||||
default:
|
||||
// currently: follow system / undefined -> switch to: day mode
|
||||
displayLightStatusBar(activity); // necessary hack :-/
|
||||
activateNightMode(activity, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets night mode / dark theme */
|
||||
public static void restoreSavedState(Context context) {
|
||||
int savedNightModeState = loadNightModeState(context);
|
||||
int currentNightModeState = AppCompatDelegate.getDefaultNightMode();
|
||||
if (savedNightModeState != currentNightModeState) {
|
||||
switch (savedNightModeState) {
|
||||
case AppCompatDelegate.MODE_NIGHT_NO:
|
||||
// turn on day mode
|
||||
activateDayMode(context, false);
|
||||
break;
|
||||
case AppCompatDelegate.MODE_NIGHT_YES:
|
||||
// turn on night mode
|
||||
activateNightMode(context, false);
|
||||
break;
|
||||
default:
|
||||
// turn on mode "follow system"
|
||||
activateFollowSystemMode(context, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Return weather Night Mode is on, or not */
|
||||
public static Boolean getNightMode(Context context) {
|
||||
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
return nightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
|
||||
|
||||
/* Returns state of night mode */
|
||||
private static int getCurrentNightModeState(Context context) {
|
||||
return context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
}
|
||||
|
||||
|
||||
/* Activates Night Mode */
|
||||
private static void activateNightMode(Context context, Boolean notifyUser) {
|
||||
saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES);
|
||||
|
||||
// switch to Night Mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Activates Day Mode */
|
||||
private static void activateDayMode(Context context, Boolean notifyUser) {
|
||||
// save the new state
|
||||
saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO);
|
||||
|
||||
// switch to Day Mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Activate Mode "Follow System" */
|
||||
private static void activateFollowSystemMode(Context context, Boolean notifyUser) {
|
||||
// save the new state
|
||||
saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
|
||||
// switch to Undefined Mode / Follow System
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Displays the default status bar */
|
||||
private static void displayDefaultStatusBar(Activity activity) {
|
||||
View decorView = activity.getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(0);
|
||||
}
|
||||
|
||||
|
||||
/* Displays the light (inverted) status bar - if possible */
|
||||
private static void displayLightStatusBar(Activity activity) {
|
||||
View decorView = activity.getWindow().getDecorView();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
} else {
|
||||
decorView.setSystemUiVisibility(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Save state of night mode */
|
||||
private static void saveNightModeState(Context context, int currentState) {
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putInt(PREF_NIGHT_MODE_STATE, currentState);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
||||
/* Load state of Night Mode */
|
||||
private static int loadNightModeState(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
}
|
150
app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt
Normal file
150
app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* NightModeHelper.kt
|
||||
* Implements the NightModeHelper object
|
||||
* A NightModeHelper can toggle and restore the state of the theme's Night Mode
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import org.y20k.trackbook.R
|
||||
|
||||
|
||||
/*
|
||||
* NightModeHelper object
|
||||
*/
|
||||
object NightModeHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(NightModeHelper::class.java)
|
||||
|
||||
|
||||
/* Switches between modes: day, night, undefined */
|
||||
@SuppressLint("SwitchIntDef")
|
||||
fun switchMode(activity: Activity) {
|
||||
// SWITCH: undefined -> night / night -> day / day - undefined
|
||||
when (AppCompatDelegate.getDefaultNightMode()) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> {
|
||||
// currently: day mode -> switch to: follow system
|
||||
// displayDefaultStatusBar(activity) // necessary hack :-/
|
||||
activateFollowSystemMode(activity, true)
|
||||
}
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> {
|
||||
// currently: night mode -> switch to: day mode
|
||||
// displayLightStatusBar(activity) // necessary hack :-/
|
||||
activateDayMode(activity, true)
|
||||
}
|
||||
else -> {
|
||||
// currently: follow system / undefined -> switch to: day mode
|
||||
// displayLightStatusBar(activity) // necessary hack :-/
|
||||
activateNightMode(activity, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets night mode / dark theme */
|
||||
fun restoreSavedState(context: Context) {
|
||||
val savedNightModeState = PreferencesHelper.loadNightModeState(context)
|
||||
val currentNightModeState = AppCompatDelegate.getDefaultNightMode()
|
||||
if (savedNightModeState != currentNightModeState) {
|
||||
when (savedNightModeState) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO ->
|
||||
// turn on day mode
|
||||
activateDayMode(context, false)
|
||||
AppCompatDelegate.MODE_NIGHT_YES ->
|
||||
// turn on night mode
|
||||
activateNightMode(context, false)
|
||||
else ->
|
||||
// turn on mode "follow system"
|
||||
activateFollowSystemMode(context, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Return weather Night Mode is on, or not */
|
||||
fun isNightModeOn(context: Context): Boolean {
|
||||
val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
return nightMode == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
|
||||
|
||||
/* Activates Night Mode */
|
||||
private fun activateNightMode(context: Context, notifyUser: Boolean) {
|
||||
PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES)
|
||||
|
||||
// switch to Night Mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Activates Day Mode */
|
||||
private fun activateDayMode(context: Context, notifyUser: Boolean) {
|
||||
// save the new state
|
||||
PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO)
|
||||
|
||||
// switch to Day Mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Activate Mode "Follow System" */
|
||||
private fun activateFollowSystemMode(context: Context, notifyUser: Boolean) {
|
||||
// save the new state
|
||||
PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
// switch to Undefined Mode / Follow System
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
// notify user
|
||||
if (notifyUser) {
|
||||
Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Displays the default status bar */
|
||||
private fun displayDefaultStatusBar(activity: Activity) {
|
||||
val decorView = activity.window.decorView
|
||||
decorView.systemUiVisibility = 0
|
||||
}
|
||||
|
||||
|
||||
/* Displays the light (inverted) status bar */
|
||||
private fun displayLightStatusBar(activity: Activity) {
|
||||
val decorView = activity.window.decorView
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
/**
|
||||
* NotificationHelper.java
|
||||
* Implements the NotificationHelper class
|
||||
* A NotificationHelper creates and configures a notification
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
|
||||
import org.y20k.trackbook.MainActivity;
|
||||
import org.y20k.trackbook.R;
|
||||
import org.y20k.trackbook.TrackerService;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
|
||||
/**
|
||||
* NotificationHelper class
|
||||
*/
|
||||
public final class NotificationHelper implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = NotificationHelper.class.getSimpleName();
|
||||
|
||||
|
||||
/* Creates a notification builder */
|
||||
public static Notification getNotification(Context context, NotificationCompat.Builder builder, Track track, boolean tracking) {
|
||||
|
||||
// create notification channel
|
||||
createNotificationChannel(context);
|
||||
|
||||
// ACTION: NOTIFICATION TAP & BUTTON SHOW
|
||||
Intent tapActionIntent = new Intent(context, MainActivity.class);
|
||||
tapActionIntent.setAction(ACTION_SHOW_MAP);
|
||||
tapActionIntent.putExtra(EXTRA_TRACK, track);
|
||||
tapActionIntent.putExtra(EXTRA_TRACKING_STATE, tracking);
|
||||
// artificial back stack for started Activity (https://developer.android.com/training/notify-user/navigation.html#DirectEntry)
|
||||
TaskStackBuilder tapActionIntentBuilder = TaskStackBuilder.create(context);
|
||||
tapActionIntentBuilder.addParentStack(MainActivity.class);
|
||||
tapActionIntentBuilder.addNextIntent(tapActionIntent);
|
||||
// pending intent wrapper for notification tap
|
||||
PendingIntent tapActionPendingIntent = tapActionIntentBuilder.getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// ACTION: NOTIFICATION BUTTON STOP
|
||||
Intent stopActionIntent = new Intent(context, TrackerService.class);
|
||||
stopActionIntent.setAction(ACTION_STOP);
|
||||
// pending intent wrapper for notification stop action
|
||||
PendingIntent stopActionPendingIntent = PendingIntent.getService(context, 14, stopActionIntent, 0);
|
||||
|
||||
// ACTION: NOTIFICATION BUTTON RESUME
|
||||
Intent resumeActionIntent = new Intent(context, TrackerService.class);
|
||||
resumeActionIntent.setAction(ACTION_RESUME);
|
||||
// pending intent wrapper for notification resume action
|
||||
PendingIntent resuneActionPendingIntent = PendingIntent.getService(context, 16, resumeActionIntent, 0);
|
||||
|
||||
// construct notification in builder
|
||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
builder.setShowWhen(false);
|
||||
builder.setContentIntent(tapActionPendingIntent);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_small_24dp);
|
||||
builder.setLargeIcon(getNotificationIconLarge(context, tracking));
|
||||
if (tracking) {
|
||||
builder.addAction(R.drawable.ic_stop_white_24dp, context.getString(R.string.notification_stop), stopActionPendingIntent);
|
||||
builder.setContentTitle(context.getString(R.string.notification_title_trackbook_running));
|
||||
builder.setContentText(getContextString(context, track));
|
||||
} else {
|
||||
builder.addAction(R.drawable.ic_fiber_manual_record_white_24dp, context.getString(R.string.notification_resume), resuneActionPendingIntent);
|
||||
builder.addAction(R.drawable.ic_compass_needle_white_24dp, context.getString(R.string.notification_show), tapActionPendingIntent);
|
||||
builder.setContentTitle(context.getString(R.string.notification_title_trackbook_not_running));
|
||||
builder.setContentText(getContextString(context, track));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
/* Constructs an updated notification */
|
||||
public static Notification getUpdatedNotification(Context context, NotificationCompat.Builder builder, Track track) {
|
||||
builder.setContentText(getContextString(context, track));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
/* Create a notification channel */
|
||||
public static boolean createNotificationChannel(Context context) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// API level 26 ("Android O") supports notification channels.
|
||||
String id = NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL;
|
||||
CharSequence name = context.getString(R.string.notification_channel_recording_name);
|
||||
String description = context.getString(R.string.notification_channel_recording_description);
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
// create channel
|
||||
NotificationChannel channel = new NotificationChannel(id, name, importance);
|
||||
channel.setDescription(description);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get station image for notification's large icon */
|
||||
private static Bitmap getNotificationIconLarge(Context context, boolean tracking) {
|
||||
|
||||
// get dimensions
|
||||
Resources resources = context.getResources();
|
||||
int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
|
||||
int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
|
||||
|
||||
Bitmap bitmap;
|
||||
if (tracking) {
|
||||
bitmap = getBitmap(context, R.drawable.ic_notification_large_tracking_48dp);
|
||||
} else {
|
||||
bitmap = getBitmap(context, R.drawable.ic_notification_large_not_tracking_48dp);
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, false);
|
||||
}
|
||||
|
||||
|
||||
/* Return a bitmap for a given resource id of a vector drawable */
|
||||
private static Bitmap getBitmap(Context context, int resource) {
|
||||
VectorDrawableCompat drawable = VectorDrawableCompat.create(context.getResources(), resource, null);
|
||||
if (drawable != null) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Build context text for notification builder */
|
||||
private static String getContextString(Context context, Track track) {
|
||||
return context.getString(R.string.notification_content_distance) + ": " + LengthUnitHelper.convertDistanceToString(track.getTrackDistance()) + " | " +
|
||||
context.getString(R.string.notification_content_duration) + ": " + LocationHelper.convertToReadableTime(track.getTrackDuration(), true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* NotificationHelper.kt
|
||||
* Implements the NotificationHelper class
|
||||
* A NotificationHelper creates and configures a notification
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.MainActivity
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.TrackerService
|
||||
|
||||
|
||||
/*
|
||||
* NotificationHelper class
|
||||
*/
|
||||
class NotificationHelper(private val trackerService: TrackerService) {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(NotificationHelper::class.java)
|
||||
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private val notificationManager: NotificationManager = trackerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
|
||||
/* Creates notification */
|
||||
fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification {
|
||||
|
||||
// create notification channel if necessary
|
||||
if (shouldCreateNotificationChannel()) {
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
// Build notification
|
||||
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))
|
||||
|
||||
// add icon and actions for stop, resume and show
|
||||
when (trackingState) {
|
||||
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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 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)}"
|
||||
}
|
||||
|
||||
|
||||
/* Checks if notification channel should be created */
|
||||
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
|
||||
|
||||
|
||||
/* Create a notification channel */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel() {
|
||||
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.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
|
||||
|
||||
/* Notification pending intents */
|
||||
private val stopActionPendingIntent = PendingIntent.getService(
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
/* Notification actions */
|
||||
private val stopAction = NotificationCompat.Action(
|
||||
R.drawable.ic_notification_action_stop_24dp,
|
||||
trackerService.getString(R.string.notification_stop),
|
||||
stopActionPendingIntent)
|
||||
private val resumeAction = NotificationCompat.Action(
|
||||
R.drawable.ic_notification_action_resume_36dp,
|
||||
trackerService.getString(R.string.notification_resume),
|
||||
resumeActionPendingIntent)
|
||||
private val showAction = NotificationCompat.Action(
|
||||
R.drawable.ic_notification_action_show_36dp,
|
||||
trackerService.getString(R.string.notification_show),
|
||||
showActionPendingIntent)
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* PreferencesHelper.kt
|
||||
* Implements the PreferencesHelper object
|
||||
* A PreferencesHelper provides helper methods for the saving and loading values from shared preferences
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.extensions.getDouble
|
||||
import org.y20k.trackbook.extensions.putDouble
|
||||
|
||||
|
||||
/*
|
||||
* PreferencesHelper object
|
||||
*/
|
||||
object PreferencesHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(PreferencesHelper::class.java)
|
||||
|
||||
|
||||
/* Loads zoom level of map */
|
||||
fun loadZoomLevel(context: Context): Double {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// load zoom level
|
||||
return settings.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL)
|
||||
}
|
||||
|
||||
|
||||
/* Saves zoom level of map */
|
||||
fun saveZoomLevel(context: Context, zoomLevel: Double) {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val editor = settings.edit()
|
||||
// save zoom level
|
||||
editor.putDouble(Keys.PREF_MAP_ZOOM_LEVEL, zoomLevel)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
|
||||
/* Loads tracking state */
|
||||
fun loadTrackingState(context: Context): Int {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// load tracking state
|
||||
return settings.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_NOT_TRACKING)
|
||||
}
|
||||
|
||||
|
||||
/* Saves tracking state */
|
||||
fun saveTrackingState(context: Context, trackingState: Int) {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val editor = settings.edit()
|
||||
// save tracking state
|
||||
editor.putInt(Keys.PREF_TRACKING_STATE, trackingState)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
|
||||
/* Loads length unit system - metric or imperial */
|
||||
fun loadUseImperialUnits(context: Context): Boolean {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// load length unit system
|
||||
return settings.getBoolean(Keys.PREF_USE_IMPERIAL_UNITS, LengthUnitHelper.useImperialUnits())
|
||||
}
|
||||
|
||||
|
||||
/* Loads length unit system - metric or imperial */
|
||||
fun loadGpsOnly(context: Context): Boolean {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// load length unit system
|
||||
return settings.getBoolean(Keys.PREF_GPS_ONLY, false)
|
||||
}
|
||||
|
||||
/* Loads accuracy threshold used to determine if location is good enough */
|
||||
fun loadAccuracyThreshold(context: Context): Int {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// load tracking state
|
||||
return settings.getInt(Keys.PREF_LOCATION_ACCURACY_THRESHOLD, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY)
|
||||
}
|
||||
|
||||
|
||||
/* Loads the state of a map */
|
||||
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
|
||||
// create location
|
||||
val currentBestLocation: Location = 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)
|
||||
return currentBestLocation
|
||||
}
|
||||
|
||||
|
||||
/* Saves the state of a map */
|
||||
fun saveCurrentBestLocation(context: Context, currentBestLocation: Location) {
|
||||
// get preferences
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val editor = settings.edit()
|
||||
// save location
|
||||
editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, currentBestLocation.latitude)
|
||||
editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, currentBestLocation.longitude)
|
||||
editor.putFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, currentBestLocation.accuracy)
|
||||
editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, currentBestLocation.altitude)
|
||||
editor.putLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, currentBestLocation.time)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
|
||||
/* Load state of Night Mode */
|
||||
fun loadNightModeState(context: Context): Int {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(Keys.PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
|
||||
/* Save state of night mode */
|
||||
fun saveNightModeState(context: Context, currentState: Int) {
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val editor = settings.edit()
|
||||
editor.putInt(Keys.PREF_NIGHT_MODE_STATE, currentState)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
|
||||
/* Checks if housekeeping work needs to be done - used usually in DownloadWorker "REQUEST_UPDATE_COLLECTION" */
|
||||
fun isHouseKeepingNecessary(context: Context): Boolean {
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return settings.getBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, true)
|
||||
}
|
||||
|
||||
|
||||
/* Saves state of housekeeping */
|
||||
fun saveHouseKeepingNecessaryState(context: Context, state: Boolean = false) {
|
||||
val settings = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val editor = settings.edit()
|
||||
editor.putBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, state)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,405 +0,0 @@
|
|||
/**
|
||||
* StorageHelper.java
|
||||
* Implements the StorageHelper class
|
||||
* A StorageHelper deals with saving and loading recorded tracks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import org.y20k.trackbook.R;
|
||||
import org.y20k.trackbook.core.Track;
|
||||
import org.y20k.trackbook.core.TrackBuilder;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.EnvironmentCompat;
|
||||
|
||||
|
||||
/**
|
||||
* StorageHelper class
|
||||
*/
|
||||
public class StorageHelper implements TrackbookKeys {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = StorageHelper.class.getSimpleName();
|
||||
|
||||
/* Main class variables */
|
||||
private final Context mContext;
|
||||
private final File mFolder;
|
||||
private final File mTempFile;
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public StorageHelper(Context context) {
|
||||
// store activity
|
||||
mContext = context;
|
||||
|
||||
// get "tracks" folder
|
||||
mFolder = mContext.getExternalFilesDir(TRACKS_DIRECTORY_NAME);
|
||||
// mFolder = getTracksDirectory();
|
||||
|
||||
// create "tracks" folder if necessary
|
||||
if (mFolder != null && !mFolder.exists()) {
|
||||
LogHelper.v(LOG_TAG, "Creating new folder: " + mFolder.toString());
|
||||
mFolder.mkdirs();
|
||||
}
|
||||
|
||||
// create temp file object // todo check -> may produce NullPointerException
|
||||
String tempFilePathName = mFolder.toString() + "/" + FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION;
|
||||
mTempFile = new File(tempFilePathName);
|
||||
|
||||
// delete old track - exclude temp file
|
||||
deleteOldTracks(false);
|
||||
}
|
||||
|
||||
|
||||
/* Checks if a temp file exits */
|
||||
public boolean tempFileExists() {
|
||||
return mTempFile.exists();
|
||||
}
|
||||
|
||||
|
||||
/* Deletes temp file - if it exits */
|
||||
public boolean deleteTempFile() {
|
||||
return mTempFile.exists() && mTempFile.delete();
|
||||
}
|
||||
|
||||
|
||||
/* Saves track object to file */
|
||||
public boolean saveTrack(@Nullable Track track, int fileType) {
|
||||
|
||||
Date recordingStart = null;
|
||||
if (track != null) {
|
||||
recordingStart = track.getRecordingStart();
|
||||
}
|
||||
|
||||
if (mFolder != null && mFolder.exists() && mFolder.isDirectory() && mFolder.canWrite() && recordingStart != null && track != null) {
|
||||
|
||||
// create file object and calculate bounding box and elevation, if necessary
|
||||
String fileName;
|
||||
if (fileType == FILE_TEMP_TRACK) {
|
||||
// get the temp file name
|
||||
fileName = FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION;
|
||||
} else {
|
||||
// build a regular file name
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
|
||||
fileName = dateFormat.format(recordingStart) + FILE_TYPE_TRACKBOOK_EXTENSION;
|
||||
// calculate elevation and store it in track
|
||||
track = MapHelper.calculateElevation(track);
|
||||
// calculate bounding box and store it in track
|
||||
track.setBoundingBox(MapHelper.calculateBoundingBox(track.getWayPoints()));
|
||||
}
|
||||
File file = new File(mFolder.toString() + "/" + fileName);
|
||||
|
||||
// convert track to JSON
|
||||
Gson gson = getCustomGson();
|
||||
String json = gson.toJson(track);
|
||||
|
||||
// write track
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
LogHelper.v(LOG_TAG, "Saving track to external storage: " + file.toString());
|
||||
bw.write(json);
|
||||
} catch (IOException e) {
|
||||
LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + file.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
// if write was successful delete old track files - only if not a temp file
|
||||
if (fileType != FILE_TEMP_TRACK) {
|
||||
// include temp file if it exists
|
||||
deleteOldTracks(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
LogHelper.e(LOG_TAG, "Unable to save track to external storage.");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Loads given file into memory */
|
||||
public Track loadTrack(int fileType) {
|
||||
|
||||
// get file reference
|
||||
File trackFile;
|
||||
switch (fileType) {
|
||||
case FILE_TEMP_TRACK:
|
||||
trackFile = getTempFile();
|
||||
break;
|
||||
case FILE_MOST_CURRENT_TRACK:
|
||||
trackFile = getMostCurrentTrack();
|
||||
break;
|
||||
default:
|
||||
trackFile = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// read & parse file and return track
|
||||
return readTrackFromFile(trackFile);
|
||||
}
|
||||
|
||||
|
||||
/* Loads given file into memory */
|
||||
public Track loadTrack(File file) {
|
||||
|
||||
// get file reference
|
||||
File trackFile;
|
||||
if (file != null) {
|
||||
trackFile = file;
|
||||
} else {
|
||||
// fallback
|
||||
trackFile = getMostCurrentTrack();
|
||||
}
|
||||
|
||||
// read & parse file and return track
|
||||
return readTrackFromFile(trackFile);
|
||||
}
|
||||
|
||||
|
||||
/* Gets a list of .trackbook files - excluding the temp file */
|
||||
public File[] getListOfTrackbookFiles() {
|
||||
// TODO HANDLE CASE: EMPTY FILE LIST
|
||||
|
||||
// get files and sort them
|
||||
return sortFiles(mFolder.listFiles());
|
||||
}
|
||||
|
||||
|
||||
// /* Gets a list of tracks based on their file names */
|
||||
// public List<String> getListOfTracks() {
|
||||
// List<String> listOfTracks = new ArrayList<String>();
|
||||
//
|
||||
// // get files and sort them
|
||||
// File[] files = mFolder.listFiles();
|
||||
// files = sortFiles(files);
|
||||
//
|
||||
// for (File file : files) {
|
||||
// listOfTracks.add(file.getName());
|
||||
// }
|
||||
//
|
||||
// // TODO HANDLE CASE: EMPTY FILE LIST
|
||||
// return listOfTracks;
|
||||
// }
|
||||
|
||||
|
||||
// loads file and parses it into a track
|
||||
private Track readTrackFromFile(File file) {
|
||||
|
||||
// check if given file was null
|
||||
if (file == null) {
|
||||
LogHelper.e(LOG_TAG, "Did not receive a file object.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
||||
LogHelper.v(LOG_TAG, "Loading track from external storage: " + file.toString());
|
||||
|
||||
// read until last line reached
|
||||
String fileContent;
|
||||
String singleLine;
|
||||
StringBuilder sb = new StringBuilder("");
|
||||
while ((singleLine = br.readLine()) != null) {
|
||||
sb.append(singleLine);
|
||||
sb.append("\n");
|
||||
}
|
||||
fileContent = sb.toString();
|
||||
|
||||
// prepare custom Gson and return Track object
|
||||
Gson gson = getCustomGson();
|
||||
return gson.fromJson(fileContent, TrackBuilder.class).toTrack();
|
||||
|
||||
} catch (IOException e) {
|
||||
LogHelper.e(LOG_TAG, "Unable to read file from external storage: " + file.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates a Gson object */
|
||||
private Gson getCustomGson() {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.setDateFormat("M/d/yy hh:mm a");
|
||||
return gsonBuilder.create();
|
||||
}
|
||||
|
||||
/* Gets most current track from directory */
|
||||
private File getMostCurrentTrack() {
|
||||
|
||||
if (mFolder != null && mFolder.isDirectory()) {
|
||||
// get files and sort them
|
||||
File[] files = mFolder.listFiles();
|
||||
files = sortFiles(files);
|
||||
if (files.length > 0 && files[0].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[0].equals(mTempFile)){
|
||||
// return latest track
|
||||
return files[0];
|
||||
}
|
||||
}
|
||||
LogHelper.e(LOG_TAG, "Unable to get files from given folder. Folder is probably empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* Gets temp file - if it exists */
|
||||
private File getTempFile() {
|
||||
if (mTempFile.exists()) {
|
||||
return mTempFile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Gets the last track from directory */
|
||||
private void deleteOldTracks(boolean includeTempFile) {
|
||||
|
||||
if (mFolder != null && mFolder.isDirectory()) {
|
||||
LogHelper.v(LOG_TAG, "Deleting older recordings.");
|
||||
|
||||
// get files and sort them
|
||||
File[] files = mFolder.listFiles();
|
||||
files = sortFiles(files);
|
||||
|
||||
// store length of array
|
||||
int numberOfFiles = files.length;
|
||||
|
||||
// keep the latest ten (mMaxTrackFiles) track files
|
||||
int index = MAXIMUM_TRACK_FILES;
|
||||
// iterate through array
|
||||
while (index < numberOfFiles && files[index].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[index].equals(mTempFile)) {
|
||||
files[index].delete();
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// delete temp file if it exists
|
||||
if (includeTempFile && mTempFile.exists()) {
|
||||
mTempFile.delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Sorts array of files in a way that the newest files are at the top and non-.trackbook files are at the bottom */
|
||||
private File[] sortFiles(File[] files) {
|
||||
// sort array
|
||||
LogHelper.v(LOG_TAG, "Sorting files.");
|
||||
Arrays.sort(files, new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File file1, File file2) {
|
||||
|
||||
// discard temp file and files not ending with ".trackbook"
|
||||
boolean file1IsTrack = file1.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file1.equals(mTempFile);
|
||||
boolean file2IsTrack = file2.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file2.equals(mTempFile);
|
||||
|
||||
// note: "greater" means higher index in array
|
||||
if (!file1IsTrack && file2IsTrack) {
|
||||
// file1 is not a track, file1 is greater
|
||||
return 1;
|
||||
} else if (!file2IsTrack && file1IsTrack) {
|
||||
// file2 is not a track, file2 is greater
|
||||
return -1;
|
||||
} else {
|
||||
// "compareTo" compares abstract path names lexicographically | 0 == equal | -1 == file2 less than file1 | 1 == file2 greater than file1
|
||||
return file2.compareTo(file1);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// hand back sorted array of files
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
/* Return a write-able sub-directory from external storage */
|
||||
private File getTracksDirectory() {
|
||||
File[] storage = mContext.getExternalFilesDirs(TRACKS_DIRECTORY_NAME);
|
||||
for (File file : storage) {
|
||||
if (file != null) {
|
||||
String state = EnvironmentCompat.getStorageState(file);
|
||||
if (Environment.MEDIA_MOUNTED.equals(state)) {
|
||||
LogHelper.i(LOG_TAG, "External storage: " + file.toString());
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
Toast.makeText(mContext, R.string.toast_message_no_external_storage, Toast.LENGTH_LONG).show();
|
||||
LogHelper.e(LOG_TAG, "Unable to access external storage.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* Tries to smooth the elevation data using a low pass filter */
|
||||
private Track smoothTrack(Track input, float dt, float rc) {
|
||||
|
||||
// The following code is adapted from https://en.wikipedia.org/wiki/Low-pass_filter
|
||||
//
|
||||
// // Return RC low-pass filter output samples, given input samples,
|
||||
// // time interval dt, and time constant RC
|
||||
// function lowpass(real[0..n] x, real dt, real RC)
|
||||
// var real[0..n] y
|
||||
// var real α := dt / (RC + dt)
|
||||
// y[0] := α * x[0]
|
||||
// for i from 1 to n
|
||||
// y[i] := α * x[i] + (1-α) * y[i-1]
|
||||
// return y
|
||||
|
||||
// copy input track
|
||||
Track output = new Track(input);
|
||||
|
||||
// calculate alpha
|
||||
float alpha = dt / (rc + dt);
|
||||
|
||||
// set initial value for first waypoint
|
||||
double outputInitialAltitudeValue = alpha * input.getWayPoints().get(0).getLocation().getAltitude();
|
||||
output.getWayPoints().get(0).getLocation().setAltitude(outputInitialAltitudeValue);
|
||||
|
||||
double inputCurrentAltitudeValue;
|
||||
double outputPreviousAltitudeValue;
|
||||
double outputCurrentAltitudeValue;
|
||||
for (int i = 1; i < input.getSize(); i++) {
|
||||
inputCurrentAltitudeValue = input.getWayPoints().get(i).getLocation().getAltitude();
|
||||
outputPreviousAltitudeValue = output.getWayPoints().get(i-1).getLocation().getAltitude();
|
||||
|
||||
outputCurrentAltitudeValue = alpha * inputCurrentAltitudeValue + (1 - alpha) * outputPreviousAltitudeValue;
|
||||
|
||||
output.getWayPoints().get(i).getLocation().setAltitude(outputCurrentAltitudeValue);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
194
app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt
Normal file
194
app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* TrackHelper.kt
|
||||
* Implements the TrackHelper object
|
||||
* A TrackHelper offers helper methods for dealing with track objects
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.helpers
|
||||
|
||||
import android.location.Location
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.TracklistElement
|
||||
import org.y20k.trackbook.core.WayPoint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* TrackHelper object
|
||||
*/
|
||||
object TrackHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java)
|
||||
|
||||
|
||||
/* Returns unique ID for Track - currently the start date */
|
||||
fun getTrackId(track: Track): Long {
|
||||
return track.recordingStart.time
|
||||
}
|
||||
|
||||
|
||||
/* Returns unique ID for TracklistElement - currently the start date */
|
||||
fun getTrackId(tracklistElement: TracklistElement): Long {
|
||||
return tracklistElement.date.time
|
||||
}
|
||||
|
||||
|
||||
/* Adds given locatiom as waypoint to track */
|
||||
fun addWayPointToTrack(track: Track, location: Location, locationAccuracyThreshold: Int): Track {
|
||||
|
||||
// get previous location
|
||||
val previousLocation: Location?
|
||||
val numberOfWayPoints: Int = track.wayPoints.size
|
||||
if (numberOfWayPoints == 0) {
|
||||
previousLocation = null
|
||||
} else {
|
||||
previousLocation = track.wayPoints.get(numberOfWayPoints - 1).toLocation()
|
||||
}
|
||||
|
||||
// update duration
|
||||
val now: Date = GregorianCalendar.getInstance().time
|
||||
val difference: Long = now.time - track.recordingStop.time
|
||||
track.duration = track.duration + difference
|
||||
track.recordingStop = now
|
||||
|
||||
// add only if recent and accurate
|
||||
val shouldBeAdded: Boolean
|
||||
shouldBeAdded = (LocationHelper.isRecentEnough(location)
|
||||
&& LocationHelper.isAccurateEnough(location, locationAccuracyThreshold)
|
||||
&& LocationHelper.isDifferentEnough(previousLocation, location))
|
||||
|
||||
if (shouldBeAdded) {
|
||||
// update distance
|
||||
track.length = track.length + LocationHelper.calculateDistance(previousLocation, location)
|
||||
|
||||
if (location.altitude != 0.0) {
|
||||
// update altitude values
|
||||
if (numberOfWayPoints == 0) {
|
||||
track.maxAltitude = location.altitude
|
||||
track.minAltitude = location.altitude
|
||||
} else {
|
||||
// calculate elevation values
|
||||
val elevationDifferences: Pair<Double, Double> = LocationHelper.calculateElevationDifferences(previousLocation, location, track)
|
||||
// check if significant differences were calculated
|
||||
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
|
||||
// update elevation values
|
||||
track.positiveElevation = elevationDifferences.first
|
||||
track.negativeElevation = elevationDifferences.second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toggle stop over status, if necessary
|
||||
if (track.wayPoints.size < 0) {
|
||||
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)
|
||||
} else {
|
||||
numberOfSatellites = 0
|
||||
}
|
||||
|
||||
// add current location as point to center on for later display
|
||||
track.latitude = location.latitude
|
||||
track.longitude = location.longitude
|
||||
|
||||
// add location as new waypoint
|
||||
track.wayPoints.add(WayPoint(location.provider, location.latitude, location.longitude, location.altitude, location.accuracy, location.time, track.length, numberOfSatellites))
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
|
||||
/* 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=\"Transistor 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)
|
||||
|
||||
// add closing tag
|
||||
gpxString += "</gpx>\n"
|
||||
|
||||
return gpxString
|
||||
}
|
||||
|
||||
|
||||
/* Creates GPX formatted track */
|
||||
private fun createGpxTrk(track: Track): String {
|
||||
val gpxTrack = StringBuilder("")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
// add opening track tag
|
||||
gpxTrack.append("\t<trk>\n")
|
||||
|
||||
// add name to track
|
||||
gpxTrack.append("\t\t<name>")
|
||||
gpxTrack.append("Trackbook Recording: ${track.name}")
|
||||
gpxTrack.append("</name>\n")
|
||||
|
||||
// add opening track segment tag
|
||||
gpxTrack.append("\t\t<trkseg>\n")
|
||||
|
||||
// add route point
|
||||
track.wayPoints.forEach { wayPoint ->
|
||||
// add longitude and latitude
|
||||
gpxTrack.append("\t\t\t<trkpt lat=\"")
|
||||
gpxTrack.append(wayPoint.latitude)
|
||||
gpxTrack.append("\" lon=\"")
|
||||
gpxTrack.append(wayPoint.longitude)
|
||||
gpxTrack.append("\">\n")
|
||||
|
||||
// add time
|
||||
gpxTrack.append("\t\t\t\t<time>")
|
||||
gpxTrack.append(dateFormat.format(Date(wayPoint.time)))
|
||||
gpxTrack.append("</time>\n")
|
||||
|
||||
// add altitude
|
||||
gpxTrack.append("\t\t\t\t<ele>")
|
||||
gpxTrack.append(wayPoint.altitude)
|
||||
gpxTrack.append("</ele>\n")
|
||||
|
||||
// add closing tag
|
||||
gpxTrack.append("\t\t\t</trkpt>\n")
|
||||
}
|
||||
|
||||
// add closing track segment tag
|
||||
gpxTrack.append("\t\t</trkseg>\n")
|
||||
|
||||
// add closing track tag
|
||||
gpxTrack.append("\t</trk>\n")
|
||||
|
||||
return gpxTrack.toString()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/**
|
||||
* TrackbookKeys.java
|
||||
* Implements the keys used throughout the app
|
||||
* This interface hosts all keys used to control Trackbook's state
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.helpers;
|
||||
|
||||
|
||||
/**
|
||||
* TrackbookKeys.class
|
||||
*/
|
||||
public interface TrackbookKeys {
|
||||
|
||||
/* ACTIONS */
|
||||
String ACTION_START = "org.y20k.trackbook.action.START";
|
||||
String ACTION_STOP = "org.y20k.trackbook.action.STOP";
|
||||
String ACTION_DISMISS = "org.y20k.transistor.action.DISMISS";
|
||||
String ACTION_RESUME = "org.y20k.transistor.action.RESUME";
|
||||
String ACTION_CLEAR = "org.y20k.transistor.action.CLEAR";
|
||||
String ACTION_SAVE = "org.y20k.transistor.action.SAVE";
|
||||
String ACTION_DEFAULT = "DEFAULT";
|
||||
String ACTION_SHOW_MAP = "SHOW_MAP";
|
||||
String ACTION_TRACK_UPDATED = "TRACK_UPDATED";
|
||||
String ACTION_TRACK_REQUEST = "TRACK_REQUEST";
|
||||
String ACTION_TRACKING_STATE_CHANGED = "TRACKING_STATE_CHANGED";
|
||||
String ACTION_TRACK_SAVE = "TRACK_SAVE";
|
||||
|
||||
/* EXTRAS */
|
||||
String EXTRA_TRACK = "TRACK";
|
||||
String EXTRA_LAST_LOCATION = "LAST_LOCATION";
|
||||
String EXTRA_TRACKING_STATE = "TRACKING_STATE";
|
||||
String EXTRA_INFOSHEET_TITLE = "EXTRA_INFOSHEET_TITLE";
|
||||
String EXTRA_INFOSHEET_CONTENT = "INFOSHEET_CONTENT";
|
||||
String EXTRA_SAVE_FINISHED = "SAVE_FINISHED";
|
||||
|
||||
/* ARGS */
|
||||
String ARG_DIALOG_TITLE = "ArgDialogTitle";
|
||||
String ARG_DIALOG_MESSAGE = "ArgDialogMessage";
|
||||
String ARG_DIALOG_BUTTON_POSITIVE = "ArgDialogButtonPositive";
|
||||
String ARG_DIALOG_BUTTON_NEGATIVE = "ArgDialogButtonNegative";
|
||||
|
||||
/* PREFS */
|
||||
String PREFS_FAB_STATE = "fabStatePrefs";
|
||||
String PREFS_TRACKER_SERVICE_RUNNING = "trackerServiceRunning";
|
||||
String PREFS_CURRENT_TRACK_DURATION = "currentTrackDuration";
|
||||
String PREF_NIGHT_MODE_STATE = "prefNightModeState";
|
||||
|
||||
/* INSTANCE STATE */
|
||||
String INSTANCE_FIRST_START = "firstStart";
|
||||
String INSTANCE_TRACKING_STATE = "trackingState";
|
||||
String INSTANCE_SELECTED_TAB = "selectedTab";
|
||||
String INSTANCE_FAB_SUB_MENU_VISIBLE = "fabSubMenuVisible";
|
||||
String INSTANCE_LATITUDE_MAIN_MAP = "latitudeMainMap";
|
||||
String INSTANCE_LONGITUDE_MAIN_MAP = "longitudeMainMap";
|
||||
String INSTANCE_ZOOM_LEVEL_MAIN_MAP = "zoomLevelMainMap";
|
||||
String INSTANCE_TRACK_TRACK_MAP = "trackTrackMap";
|
||||
String INSTANCE_LATITUDE_TRACK_MAP = "latitudeTrackMap";
|
||||
String INSTANCE_LONGITUDE_TRACK_MAP = "longitudeTrackMap";
|
||||
String INSTANCE_ZOOM_LEVEL_TRACK_MAP = "zoomLevelTrackMap";
|
||||
String INSTANCE_CURRENT_LOCATION = "currentLocation";
|
||||
String INSTANCE_CURRENT_TRACK = "currentTrack";
|
||||
|
||||
/* FRAGMENT IDS */
|
||||
int FRAGMENT_ID_MAP = 0;
|
||||
int FRAGMENT_ID_TRACKS = 1;
|
||||
|
||||
/* RESULTS */
|
||||
int RESULT_SAVE_DIALOG = 1;
|
||||
int RESULT_CLEAR_DIALOG = 2;
|
||||
int RESULT_DELETE_DIALOG = 3;
|
||||
int RESULT_EXPORT_DIALOG = 4;
|
||||
int RESULT_EMPTY_RECORDING_DIALOG = 5;
|
||||
|
||||
/* CONSTANTS */
|
||||
long ONE_SECOND_IN_NANOSECOND = 1000000000L;
|
||||
long EIGHT_HOURS_IN_MILLISECONDS = 43200000; // maximum tracking duration
|
||||
long FIFTEEN_SECONDS_IN_MILLISECONDS = 15000; // timer interval for tracking
|
||||
long FIVE_MINUTES_IN_NANOSECONDS = 5L * 60000000000L; // determines a stop over
|
||||
long ONE_MINUTE_IN_NANOSECONDS = 1L * 60000000000L; // defines an old location
|
||||
int MAXIMUM_TRACK_FILES = 25;
|
||||
int FIFTY_METER_RADIUS = 50;
|
||||
|
||||
/* FILE */
|
||||
String FILE_TYPE_GPX_EXTENSION = ".gpx";
|
||||
String FILE_TYPE_TRACKBOOK_EXTENSION = ".trackbook";
|
||||
String FILE_NAME_TEMP = "temp";
|
||||
String TRACKS_DIRECTORY_NAME = "tracks";
|
||||
int FILE_TEMP_TRACK = 0;
|
||||
int FILE_MOST_CURRENT_TRACK = 1;
|
||||
|
||||
/* UNITS */
|
||||
int METRIC = 1;
|
||||
int IMPERIAL = -1;
|
||||
|
||||
/* FLOATING ACTION BUTTON */
|
||||
int FAB_STATE_DEFAULT = 0;
|
||||
int FAB_STATE_RECORDING = 1;
|
||||
int FAB_STATE_SAVE = 2;
|
||||
|
||||
/* NOTIFICATION */
|
||||
int TRACKER_SERVICE_NOTIFICATION_ID = 1;
|
||||
String NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL ="notificationChannelIdRecordingChannel";
|
||||
|
||||
/* MISC */
|
||||
int CURRENT_TRACK_FORMAT_VERSION = 3; // incremental version number to prevent issues in case the Track format evolves
|
||||
double DEFAULT_LATITUDE = 71.172500; // latitude Nordkapp, Norway
|
||||
double DEFAULT_LONGITUDE = 25.784444; // longitude Nordkapp, Norway
|
||||
int MEASUREMENT_ERROR_THRESHOLD = 10; // altitude changes of 10 meter or more (per 15 seconds) are being discarded
|
||||
int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
|
||||
|
||||
}
|
130
app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt
Normal file
130
app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* UiHelper.kt
|
||||
* Implements the UiHelper object
|
||||
* A UiHelper provides helper methods for User Interface related tasks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.escapepod.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
|
||||
/*
|
||||
* UiHelper object
|
||||
*/
|
||||
object UiHelper {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(UiHelper::class.java)
|
||||
|
||||
|
||||
/* 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) {
|
||||
val scalingFactor: Float = context.resources.displayMetrics.density
|
||||
val l: Int = (left * scalingFactor).toInt()
|
||||
val r: Int = (right * scalingFactor).toInt()
|
||||
val t: Int = (top * scalingFactor).toInt()
|
||||
val b: Int = (bottom * scalingFactor).toInt()
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
val p = view.layoutParams as ViewGroup.MarginLayoutParams
|
||||
p.setMargins(l, t, r, b)
|
||||
view.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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) {
|
||||
val l: Int = ((width / 100.0f) * left).toInt()
|
||||
val r: Int = ((width / 100.0f) * right).toInt()
|
||||
val t: Int = ((height / 100.0f) * top).toInt()
|
||||
val b: Int = ((height / 100.0f) * bottom).toInt()
|
||||
setViewMargins(context, view, l, r, t, b)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
|
||||
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) }
|
||||
|
||||
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) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// draw red delete background
|
||||
background.color = backgroundColor
|
||||
background.setBounds(
|
||||
itemView.right + dX.toInt(),
|
||||
itemView.top,
|
||||
itemView.right,
|
||||
itemView.bottom
|
||||
)
|
||||
background.draw(c)
|
||||
|
||||
// calculate position of delete icon
|
||||
val deleteIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2
|
||||
val deleteIconMargin = (itemHeight - intrinsicHeight) / 2
|
||||
val deleteIconLeft = itemView.right - deleteIconMargin - intrinsicWidth
|
||||
val deleteIconRight = itemView.right - deleteIconMargin
|
||||
val deleteIconBottom = deleteIconTop + intrinsicHeight
|
||||
|
||||
// draw delete icon
|
||||
deleteIcon?.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom)
|
||||
deleteIcon?.draw(c)
|
||||
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
}
|
||||
|
||||
private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
|
||||
c?.drawRect(left, top, right, bottom, clearPaint)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/**
|
||||
* DodgeAbleLayoutBehavior.java
|
||||
* Implements the DodgeAbleLayoutBehavior class
|
||||
* A DodgeAbleLayoutBehavior enables any element to be dodged up by a snackbar
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
|
||||
/**
|
||||
* DodgeAbleLayoutBehavior class
|
||||
* adapted from: http://stackoverflow.com/a/35904421
|
||||
*/
|
||||
public class DodgeAbleLayoutBehavior extends CoordinatorLayout.Behavior<View> {
|
||||
|
||||
/* Constructor (default) */
|
||||
public DodgeAbleLayoutBehavior() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
/* Constructor for context and attributes */
|
||||
public DodgeAbleLayoutBehavior(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
|
||||
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
|
||||
child.setTranslationY(translationY);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* NonSwipeableViewPager.java
|
||||
* Implements the NonSwipeableViewPager class
|
||||
* A NonSwipeableViewPager is a ViewPager with swiping gestures disabled
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-19 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
package org.y20k.trackbook.layout;
|
||||
|
||||
/**
|
||||
* NonSwipeableViewPager class
|
||||
* adapted from: http://stackoverflow.com/a/9650884
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import org.y20k.trackbook.helpers.LogHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
|
||||
public class NonSwipeableViewPager extends ViewPager {
|
||||
|
||||
/* Define log tag */
|
||||
private static final String LOG_TAG = NonSwipeableViewPager.class.getSimpleName();
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public NonSwipeableViewPager(Context context) {
|
||||
super(context);
|
||||
setMyScroller();
|
||||
}
|
||||
|
||||
|
||||
/* Constructor */
|
||||
public NonSwipeableViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setMyScroller();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
// Never allow swiping to switch between pages
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// Never allow swiping to switch between pages
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* Attaches a custom smooth scrolling scroller to a ViewPager */
|
||||
private void setMyScroller() {
|
||||
try {
|
||||
Class<?> viewpager = ViewPager.class;
|
||||
Field scroller = viewpager.getDeclaredField("mScroller");
|
||||
scroller.setAccessible(true);
|
||||
scroller.set(this, new MyScroller(getContext()));
|
||||
} catch (Exception e) {
|
||||
LogHelper.e(LOG_TAG, "Problem accessing or modifying the mScroller field. Exception: " + e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class: MyScroller is a custom Scroller
|
||||
*/
|
||||
public class MyScroller extends Scroller {
|
||||
public MyScroller(Context context) {
|
||||
super(context, new DecelerateInterpolator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
|
||||
super.startScroll(startX, startY, dx, dy, 350 /*1 secs*/);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* TracklistAdapter.kt
|
||||
* Implements the TracklistAdapter class
|
||||
* A TracklistAdapter is a custom adapter for a RecyclerView
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.tracklist
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.*
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Tracklist
|
||||
import org.y20k.trackbook.core.TracklistElement
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* TracklistAdapter class
|
||||
*/
|
||||
class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private val context: Context = fragment.activity as Context
|
||||
private lateinit var tracklistListener: TracklistAdapterListener
|
||||
private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits(context)
|
||||
private var tracklist: Tracklist = Tracklist()
|
||||
|
||||
|
||||
/* Listener Interface */
|
||||
interface TracklistAdapterListener {
|
||||
fun onTrackElementTapped(tracklistElement: TracklistElement) { }
|
||||
// fun onTrackElementStarred(trackId: Long, starred: Boolean)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onAttachedToRecyclerView from RecyclerView.Adapter */
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
// get reference to listener
|
||||
tracklistListener = fragment as TracklistAdapterListener
|
||||
// load tracklist
|
||||
tracklist = FileHelper.readTracklist(context)
|
||||
tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date }
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onCreateViewHolder from RecyclerView.Adapter */
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.track_element, parent, false)
|
||||
return TrackElementViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides getItemCount from RecyclerView.Adapter */
|
||||
override fun getItemCount(): Int {
|
||||
return tracklist.tracklistElements.size
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onBindViewHolder from RecyclerView.Adapter */
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val trackElementViewHolder: TrackElementViewHolder = holder as TrackElementViewHolder
|
||||
trackElementViewHolder.trackNameView.text = tracklist.tracklistElements[position].name
|
||||
trackElementViewHolder.trackDataView.text = createTrackDataString(position)
|
||||
when (tracklist.tracklistElements[position].starred) {
|
||||
true -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp))
|
||||
false -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp))
|
||||
}
|
||||
trackElementViewHolder.trackElement.setOnClickListener {
|
||||
tracklistListener.onTrackElementTapped(tracklist.tracklistElements[position])
|
||||
}
|
||||
trackElementViewHolder.starButton.setOnClickListener {
|
||||
toggleStarred(it, position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get track name for given position */
|
||||
fun getTrackName(position: Int): String {
|
||||
return tracklist.tracklistElements[position].name
|
||||
}
|
||||
|
||||
|
||||
/* Removes track and track files for given position - used by TracklistFragment */
|
||||
fun removeTrack(context: Context, position: Int) {
|
||||
val backgroundJob = Job()
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + backgroundJob)
|
||||
uiScope.launch {
|
||||
notifyItemRemoved(position)
|
||||
val deferred: Deferred<Tracklist> = async { FileHelper.deleteTrackSuspended(context, position, tracklist) }
|
||||
// wait for result and store in tracklist
|
||||
tracklist = deferred.await()
|
||||
backgroundJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Finds current position of track element in adapter list */
|
||||
fun findPosition(trackId: Long): Int {
|
||||
tracklist.tracklistElements.forEachIndexed {index, tracklistElement ->
|
||||
if (tracklistElement.getTrackId() == trackId) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
||||
/* Toggles the starred state of tracklist element - and saves tracklist */
|
||||
private fun toggleStarred(view: View, position: Int) {
|
||||
val starButton: ImageButton = view as ImageButton
|
||||
when (tracklist.tracklistElements[position].starred) {
|
||||
true -> {
|
||||
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp))
|
||||
tracklist.tracklistElements[position].starred = false
|
||||
}
|
||||
false -> {
|
||||
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp))
|
||||
tracklist.tracklistElements[position].starred = true
|
||||
}
|
||||
}
|
||||
GlobalScope.launch {
|
||||
FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Creates the track data string */
|
||||
private fun createTrackDataString(position: Int): String {
|
||||
val tracklistElement: TracklistElement = tracklist.tracklistElements[position]
|
||||
val trackDataString: String
|
||||
when (tracklistElement.name == tracklistElement.dateString) {
|
||||
// CASE: no individual name set - exclude date
|
||||
true -> trackDataString = "${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}"
|
||||
}
|
||||
return trackDataString
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: DiffUtil.Callback that determines changes in data - improves list performance
|
||||
*/
|
||||
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]
|
||||
val newItem = newList.tracklistElements[newItemPosition]
|
||||
return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem)
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.tracklistElements.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.tracklistElements.size
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.tracklistElements[oldItemPosition]
|
||||
val newItem = newList.tracklistElements[newItemPosition]
|
||||
return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) && oldItem.length == newItem.length
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: ViewHolder for a track element
|
||||
*/
|
||||
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)
|
||||
val starButton: ImageButton = trackElementLayout.findViewById(R.id.star_button)
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* MapFragmentLayoutHolder.kt
|
||||
* Implements the MapFragmentLayoutHolder class
|
||||
* A MapFragmentLayoutHolder hold references to the main views of a map fragment
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.osmdroid.api.IMapController
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay
|
||||
import org.osmdroid.views.overlay.OverlayItem
|
||||
import org.osmdroid.views.overlay.TilesOverlay
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.MapHelper
|
||||
import org.y20k.trackbook.helpers.NightModeHelper
|
||||
import org.y20k.trackbook.helpers.PreferencesHelper
|
||||
|
||||
|
||||
/*
|
||||
* MapFragmentLayoutHolder class
|
||||
*/
|
||||
data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, val startLocation: Location, 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 currentLocationButton: FloatingActionButton
|
||||
val recordingButton: FloatingActionButton
|
||||
val recordingButtonSubMenu: Group
|
||||
val saveButton: FloatingActionButton
|
||||
val clearButton: FloatingActionButton
|
||||
val resumeButton: FloatingActionButton
|
||||
var userInteraction: Boolean = false
|
||||
private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem>
|
||||
private var currentTrackOverlay: ItemizedIconOverlay<OverlayItem>?
|
||||
private var locationErrorBar: Snackbar
|
||||
private var controller: IMapController
|
||||
private var zoomLevel: Double
|
||||
|
||||
|
||||
/* 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)
|
||||
recordingButtonSubMenu = rootView.findViewById(R.id.fab_sub_menu)
|
||||
saveButton = rootView.findViewById(R.id.fab_sub_menu_button_save)
|
||||
clearButton = rootView.findViewById(R.id.fab_sub_menu_button_clear)
|
||||
resumeButton = rootView.findViewById(R.id.fab_sub_menu_button_resume)
|
||||
locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE)
|
||||
|
||||
// basic map setup
|
||||
controller = mapView.controller
|
||||
mapView.isTilesScaledToDpi = true
|
||||
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
||||
mapView.setMultiTouchControls(true)
|
||||
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
zoomLevel = PreferencesHelper.loadZoomLevel(context)
|
||||
controller.setZoom(zoomLevel)
|
||||
|
||||
// set dark map tiles, if necessary
|
||||
if (NightModeHelper.isNightModeOn(context as Activity)) {
|
||||
mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
|
||||
}
|
||||
|
||||
// add compass to map
|
||||
val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
|
||||
compassOverlay.enableCompass()
|
||||
compassOverlay.setCompassCenter(36f, 60f)
|
||||
|
||||
mapView.overlays.add(compassOverlay)
|
||||
|
||||
// add my location overlay
|
||||
currentPositionOverlay = MapHelper.createMyLocationOverlay(context, startLocation, trackingState)
|
||||
mapView.overlays.add(currentPositionOverlay)
|
||||
centerMap(startLocation)
|
||||
|
||||
// initialize track overlay
|
||||
currentTrackOverlay = null
|
||||
|
||||
// initialize recording button state
|
||||
updateRecordingButton(trackingState)
|
||||
|
||||
// add touch listeners
|
||||
addTouchListeners()
|
||||
|
||||
// listen for user interaction
|
||||
addInteractionListener()
|
||||
}
|
||||
|
||||
|
||||
/* Listen for user interaction */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun addInteractionListener() {
|
||||
mapView.setOnTouchListener { v, event ->
|
||||
userInteraction = true
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Set map center */
|
||||
fun centerMap(location: Location, animated: Boolean = false) {
|
||||
val position = GeoPoint(location.latitude, location.longitude)
|
||||
when (animated) {
|
||||
true -> controller.animateTo(position)
|
||||
false -> controller.setCenter(position)
|
||||
}
|
||||
userInteraction = false
|
||||
}
|
||||
|
||||
|
||||
/* Save current best location and state of map to shared preferences */
|
||||
fun saveState(currentBestLocation: Location) {
|
||||
PreferencesHelper.saveCurrentBestLocation(context, currentBestLocation)
|
||||
PreferencesHelper.saveZoomLevel(context, mapView.getZoomLevelDouble())
|
||||
// reset user interaction state
|
||||
userInteraction = false
|
||||
}
|
||||
|
||||
|
||||
/* Mark current position on map */
|
||||
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_NOT_TRACKING) {
|
||||
mapView.overlays.remove(currentPositionOverlay)
|
||||
currentPositionOverlay = MapHelper.createMyLocationOverlay(context, location, trackingState)
|
||||
mapView.overlays.add(currentPositionOverlay)
|
||||
}
|
||||
|
||||
|
||||
/* Overlay current track on map */
|
||||
fun overlayCurrentTrack(track: Track, trackingState: Int) {
|
||||
if (currentTrackOverlay != null) {
|
||||
mapView.overlays.remove(currentTrackOverlay)
|
||||
}
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
currentTrackOverlay = MapHelper.createTrackOverlay(context, track, trackingState)
|
||||
mapView.overlays.add(currentTrackOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Toggles state of recording button and sub menu_bottom_navigation */
|
||||
fun updateRecordingButton(trackingState: Int) {
|
||||
when (trackingState) {
|
||||
Keys.STATE_NOT_TRACKING -> {
|
||||
recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp)
|
||||
recordingButtonSubMenu.visibility = View.GONE
|
||||
}
|
||||
Keys.STATE_TRACKING_ACTIVE -> {
|
||||
recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_red_24dp)
|
||||
recordingButtonSubMenu.visibility = View.GONE
|
||||
}
|
||||
Keys.STATE_TRACKING_STOPPED -> {
|
||||
recordingButton.setImageResource(R.drawable.ic_save_white_24dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Toggles visibility of recording button sub menu_bottom_navigation */
|
||||
fun toggleRecordingButtonSubMenu() {
|
||||
when (recordingButtonSubMenu.visibility) {
|
||||
View.VISIBLE -> recordingButtonSubMenu.visibility = View.GONE
|
||||
else -> recordingButtonSubMenu.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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) {
|
||||
// CASE: Location permission not granted
|
||||
locationErrorBar.setText(R.string.snackbar_message_location_permission_denied)
|
||||
locationErrorBar.show()
|
||||
} else if (!gpsProviderActive && !networkProviderActive) {
|
||||
// CASE: Location setting is off
|
||||
locationErrorBar.setText(R.string.snackbar_message_location_offline)
|
||||
locationErrorBar.show()
|
||||
} else if (locationErrorBar.isShown) {
|
||||
// CASE: Snackbar is visible but unnecessary
|
||||
locationErrorBar.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets up views - adds touch listeners */
|
||||
private fun addTouchListeners() {
|
||||
currentLocationButton.setOnLongClickListener {
|
||||
NightModeHelper.switchMode(context as Activity)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* TrackFragmentLayoutHolder.kt
|
||||
* Implements the TrackFragmentLayoutHolder class
|
||||
* A TrackFragmentLayoutHolder hold references to the main views of a track fragment
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-20 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
||||
* https://github.com/osmdroid/osmdroid
|
||||
*/
|
||||
|
||||
|
||||
package org.y20k.trackbook.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Toast
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.osmdroid.api.IGeoPoint
|
||||
import org.osmdroid.api.IMapController
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.ItemizedIconOverlay
|
||||
import org.osmdroid.views.overlay.OverlayItem
|
||||
import org.osmdroid.views.overlay.TilesOverlay
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/*
|
||||
* TrackFragmentLayoutHolder class
|
||||
*/
|
||||
data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, var arguments: Bundle?) {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
val rootView: View
|
||||
val track: Track
|
||||
val shareButton: ImageButton
|
||||
val deleteButton: ImageButton
|
||||
val editButton: ImageButton
|
||||
val trackNameView: MaterialTextView
|
||||
private val mapView: MapView
|
||||
private var trackOverlay: ItemizedIconOverlay<OverlayItem>?
|
||||
private var controller: IMapController
|
||||
private var zoomLevel: Double
|
||||
private val statisticsSheetBehavior: BottomSheetBehavior<View>
|
||||
private val statisticsSheet: NestedScrollView
|
||||
private val statisticsView: View
|
||||
private val distanceView: MaterialTextView
|
||||
private val stepsView: MaterialTextView
|
||||
private val waypointsView: MaterialTextView
|
||||
private val durationView: MaterialTextView
|
||||
private val recordingStartView: MaterialTextView
|
||||
private val recordingStopView: MaterialTextView
|
||||
private val maxAltitudeView: MaterialTextView
|
||||
private val minAltitudeView: MaterialTextView
|
||||
private val positiveElevationView: MaterialTextView
|
||||
private val negativeElevationView: MaterialTextView
|
||||
private val elevationDataViews: Group
|
||||
private val trackManagementViews: Group
|
||||
private val useImperialUnits: Boolean
|
||||
|
||||
|
||||
/* 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.share_button)
|
||||
deleteButton = rootView.findViewById(R.id.delete_button)
|
||||
editButton = rootView.findViewById(R.id.edit_button)
|
||||
trackNameView = rootView.findViewById(R.id.statistics_track_name_headline)
|
||||
|
||||
// basic map setup
|
||||
controller = mapView.controller
|
||||
mapView.isTilesScaledToDpi = true
|
||||
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
||||
mapView.setMultiTouchControls(true)
|
||||
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
zoomLevel = Keys.DEFAULT_ZOOM_LEVEL
|
||||
controller.setZoom(zoomLevel)
|
||||
|
||||
// get views for statistics sheet
|
||||
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
|
||||
statisticsView = rootView.findViewById(R.id.statistics_view)
|
||||
distanceView = rootView.findViewById(R.id.statistics_data_distance)
|
||||
stepsView = rootView.findViewById(R.id.statistics_data_steps)
|
||||
waypointsView = rootView.findViewById(R.id.statistics_data_waypoints)
|
||||
durationView = rootView.findViewById(R.id.statistics_data_duration)
|
||||
recordingStartView = rootView.findViewById(R.id.statistics_data_recording_start)
|
||||
recordingStopView = rootView.findViewById(R.id.statistics_data_recording_stop)
|
||||
maxAltitudeView = rootView.findViewById(R.id.statistics_data_max_altitude)
|
||||
minAltitudeView = rootView.findViewById(R.id.statistics_data_min_altitude)
|
||||
positiveElevationView = rootView.findViewById(R.id.statistics_data_positive_elevation)
|
||||
negativeElevationView = rootView.findViewById(R.id.statistics_data_negative_elevation)
|
||||
elevationDataViews = rootView.findViewById(R.id.elevation_data)
|
||||
trackManagementViews = rootView.findViewById(R.id.management_icons)
|
||||
|
||||
// get measurement unit system
|
||||
useImperialUnits = PreferencesHelper.loadUseImperialUnits(context)
|
||||
|
||||
// set dark map tiles, if necessary
|
||||
if (NightModeHelper.isNightModeOn(context as Activity)) {
|
||||
mapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS)
|
||||
}
|
||||
|
||||
// add compass to map
|
||||
val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView)
|
||||
compassOverlay.enableCompass()
|
||||
compassOverlay.setCompassCenter(36f, 60f)
|
||||
mapView.overlays.add(compassOverlay)
|
||||
|
||||
// get track and create map overlay
|
||||
val fileUriString: String = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
|
||||
if (fileUriString.isNotBlank()) {
|
||||
track = FileHelper.readTrack(context, Uri.parse(fileUriString))
|
||||
} else {
|
||||
track = Track()
|
||||
}
|
||||
trackOverlay = MapHelper.createTrackOverlay(context, track, Keys.STATE_NOT_TRACKING)
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
mapView.overlays.add(trackOverlay)
|
||||
}
|
||||
|
||||
// set up and show statistics sheet
|
||||
statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet)
|
||||
statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
statisticsSheetBehavior.addBottomSheetCallback(getStatisticsSheetCallback())
|
||||
setupStatisticsViews()
|
||||
}
|
||||
|
||||
|
||||
/* Updates zoom level and center of this map */
|
||||
fun updateMapView() {
|
||||
val position = GeoPoint(track.latitude, track.longitude)
|
||||
controller.setCenter(position)
|
||||
controller.setZoom(track.zoomLevel)
|
||||
}
|
||||
|
||||
|
||||
/* Saves zoom level and center of this map */
|
||||
fun saveViewStateToTrack() {
|
||||
val center: IGeoPoint = mapView.mapCenter
|
||||
track.latitude = center.latitude
|
||||
track.longitude = center.longitude
|
||||
track.zoomLevel = mapView.zoomLevelDouble
|
||||
GlobalScope.launch { FileHelper.saveTrackSuspended(track, false) }
|
||||
}
|
||||
|
||||
|
||||
/* Sets up the statistics sheet */
|
||||
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()
|
||||
|
||||
// populate views
|
||||
trackNameView.text = track.name
|
||||
distanceView.text = LengthUnitHelper.convertDistanceToString(track.length, useImperialUnits)
|
||||
stepsView.text = steps
|
||||
waypointsView.text = track.wayPoints.size.toString()
|
||||
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration)
|
||||
recordingStartView.text = DateTimeHelper.convertToReadableDate(track.recordingStart)
|
||||
recordingStopView.text = DateTimeHelper.convertToReadableDate(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)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
// make track name on statistics sheet clickable
|
||||
trackNameView.setOnClickListener {
|
||||
toggleStatisticsSheetVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Shows/hides the statistics sheet */
|
||||
private fun toggleStatisticsSheetVisibility() {
|
||||
when (statisticsSheetBehavior.state) {
|
||||
BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Defines the behavior of the statistics sheet */
|
||||
private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback {
|
||||
return object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
when (newState) {
|
||||
BottomSheetBehavior.STATE_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)
|
||||
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)
|
||||
trackManagementViews.visibility = View.GONE
|
||||
shareButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded)
|
||||
trackManagementViews.visibility = View.VISIBLE
|
||||
shareButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
34
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
34
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0"/>
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1"/>
|
||||
</vector>
|
8
app/src/main/res/drawable/ic_account_circle_black.xml
Normal file
8
app/src/main/res/drawable/ic_account_circle_black.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="48dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="48dp">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
|
||||
</vector>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillColor="@color/trackbook_black"
|
||||
android:pathData="M21,3L3,10.53v0.98l6.84,2.65L12.48,21h0.98L21,3z"/>
|
||||
</vector>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/location_button_icon"
|
||||
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"/>
|
||||
</vector>
|
|
@ -5,5 +5,5 @@
|
|||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"
|
||||
android:fillColor="@color/track_management_icons" />
|
||||
android:fillColor="@color/icon_default" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_edit_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_edit_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<path
|
||||
android:fillColor="@color/icon_default"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_email_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_email_black_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
|
||||
</vector>
|
|
@ -5,5 +5,5 @@
|
|||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M11,17h2v-6h-2v6zM12,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,8zM11,9h2L13,7h-2v2z"
|
||||
android:fillColor="@color/statistic_sheet_icons" />
|
||||
android:fillColor="@color/icon_default" />
|
||||
</vector>
|
||||
|
|
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path android:fillColor="#008577"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="36dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="36dp">
|
||||
<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>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="36dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="36dp">
|
||||
<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"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="36dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="36dp">
|
||||
<path
|
||||
android:fillColor="@color/trackbook_black"
|
||||
android:pathData="M6,6h12v12H6z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_notifications_black.xml
Normal file
9
app/src/main/res/drawable/ic_notifications_black.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_person_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_person_black_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
</vector>
|
|
@ -0,0 +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">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,6h-2v9L6,15v2c0,0.55 0.45,1 1,1h11l4,4L22,7c0,-0.55 -0.45,-1 -1,-1zM17,12L17,3c0,-0.55 -0.45,-1 -1,-1L3,2c-0.55,0 -1,0.45 -1,1v14l4,-4h10c0.55,0 1,-0.45 1,-1z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_remove_circle_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_remove_circle_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<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"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_settings_black.xml
Normal file
9
app/src/main/res/drawable/ic_settings_black.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_settings_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_settings_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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"/>
|
||||
</vector>
|
|
@ -5,5 +5,5 @@
|
|||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"
|
||||
android:fillColor="@color/track_management_icons" />
|
||||
android:fillColor="@color/icon_default" />
|
||||
</vector>
|
||||
|
|
9
app/src/main/res/drawable/ic_star_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_star_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<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"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_star_border_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_star_border_24dp.xml
Normal file
|
@ -0,0 +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">
|
||||
<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"/>
|
||||
</vector>
|
|
@ -1,6 +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" />
|
||||
<item android:color="@color/trackbook_grey_lighter" />
|
||||
<item android:state_checked="true" android:color="@color/bottom_navigation_element_selected" />
|
||||
</selector>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:top="0dp"
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:bottom="-2dp">
|
||||
|
||||
<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"/>
|
||||
|
||||
</shape>
|
||||
|
||||
</item>
|
||||
|
||||
</layer-list>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:top="0dp"
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:bottom="-2dp">
|
||||
|
||||
<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"/>
|
||||
|
||||
</shape>
|
||||
|
||||
</item>
|
||||
|
||||
</layer-list>
|
236
app/src/main/res/layout/activity_main.xml
Executable file → Normal file
236
app/src/main/res/layout/activity_main.xml
Executable file → Normal file
|
@ -1,220 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="org.y20k.trackbook.MainActivity">
|
||||
tools:context=".MainActivity">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container"
|
||||
<fragment
|
||||
android:id="@+id/main_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<!-- BUTTON MY LOCATION -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabLocationButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/descr_fab_my_location"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/location_button_background"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fabMainButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/fabMainButton"
|
||||
app:srcCompat="@drawable/ic_my_location_24dp" />
|
||||
|
||||
|
||||
<!-- SAVE RESUME -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabSubMenuButtonSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="28dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/descr_fab_sub_menu_button_save"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/trackbook_green"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toTopOf="@+id/fabSubMenuButtonClear"
|
||||
app:layout_constraintEnd_toEndOf="@+id/fabSubMenuButtonClear"
|
||||
app:layout_constraintStart_toStartOf="@+id/fabSubMenuButtonClear"
|
||||
app:srcCompat="@drawable/ic_save_white_24dp" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/fabSubMenuLabelSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="@color/fab_button_card_background"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fabSubMenuButtonSave"
|
||||
app:layout_constraintEnd_toStartOf="@+id/fabSubMenuButtonSave"
|
||||
app:layout_constraintTop_toTopOf="@+id/fabSubMenuButtonSave">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingTop="2dp"
|
||||
android:text="@string/fab_sub_menu_save"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/fab_button_card_text"
|
||||
android:textStyle="bold" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<!-- BUTTON CLEAR -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabSubMenuButtonClear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="28dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/descr_fab_sub_menu_button_clear"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/trackbook_blue"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toTopOf="@+id/fabSubMenuButtonResume"
|
||||
app:layout_constraintEnd_toEndOf="@+id/fabSubMenuButtonResume"
|
||||
app:layout_constraintStart_toStartOf="@+id/fabSubMenuButtonResume"
|
||||
app:srcCompat="@drawable/ic_clear_white_24dp" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/fabSubMenuLabelClear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="@color/fab_button_card_background"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fabSubMenuButtonClear"
|
||||
app:layout_constraintEnd_toStartOf="@+id/fabSubMenuButtonClear"
|
||||
app:layout_constraintTop_toTopOf="@+id/fabSubMenuButtonClear">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingTop="2dp"
|
||||
android:text="@string/fab_sub_menu_clear"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/fab_button_card_text"
|
||||
android:textStyle="bold" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<!-- BUTTON RESUME -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabSubMenuButtonResume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/descr_fab_sub_menu_button_resume"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/trackbook_blue"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toTopOf="@+id/fabMainButton"
|
||||
app:layout_constraintEnd_toEndOf="@+id/fabMainButton"
|
||||
app:layout_constraintStart_toStartOf="@+id/fabMainButton"
|
||||
app:srcCompat="@drawable/ic_fiber_manual_record_white_24dp" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/fabSubMenuLabelResume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="@color/fab_button_card_background"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fabSubMenuButtonResume"
|
||||
app:layout_constraintEnd_toStartOf="@+id/fabSubMenuButtonResume"
|
||||
app:layout_constraintTop_toTopOf="@+id/fabSubMenuButtonResume">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingTop="2dp"
|
||||
android:text="@string/fab_sub_menu_resume"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/fab_button_card_text"
|
||||
android:textStyle="bold" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<!-- MAIN BUTTON -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabMainButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/descr_fab_main_start"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/trackbook_blue"
|
||||
app:fabSize="normal"
|
||||
app:layout_constraintBottom_toTopOf="@+id/navigation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_fiber_manual_record_white_24dp" />
|
||||
|
||||
|
||||
<!-- BOTTOM NAVIGATION -->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/navigation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/bottom_navigation_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:elevation="4dp"
|
||||
app:itemIconTint="@drawable/selector_bottom_navigation"
|
||||
app:itemTextColor="@drawable/selector_bottom_navigation"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:menu="@menu/menu_main" />
|
||||
|
||||
|
||||
<org.y20k.trackbook.layout.NonSwipeableViewPager
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/navigation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/nav_graph_main" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
app:itemBackground="@color/bottom_navigation_background"
|
||||
app:itemIconTint="@drawable/selector_bottom_navigation"
|
||||
app:itemTextColor="@drawable/selector_bottom_navigation"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:menu="@menu/menu_bottom_navigation" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView android:id="@android:id/text1"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingStart="@dimen/activity_horizontal_margin"
|
||||
android:paddingEnd="@dimen/activity_horizontal_margin"
|
||||
android:textColor="@color/track_management_text"
|
||||
android:backgroundTint="@color/track_management_background"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true" />
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView android:id="@android:id/text1"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingStart="@dimen/activity_horizontal_margin"
|
||||
android:paddingEnd="@dimen/activity_horizontal_margin"
|
||||
android:textColor="@color/track_management_text"
|
||||
android:backgroundTint="@color/track_management_background"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true" />
|
57
app/src/main/res/layout/dialog_generic_with_details.xml
Normal file
57
app/src/main/res/layout/dialog_generic_with_details.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/dialog_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="@color/text_default"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/dialog_details_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:scrollbars="vertical"
|
||||
android:text="@string/dialog_generic_details_button"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Button"
|
||||
android:textColor="@color/text_default"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialog_message"
|
||||
tools:text="@string/dialog_generic_details_button" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/dialog_details"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:scrollbars="vertical"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="@color/text_default"
|
||||
android:textIsSelectable="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialog_details_link" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
31
app/src/main/res/layout/dialog_rename_track.xml
Normal file
31
app/src/main/res/layout/dialog_rename_track.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/dialog_rename_track_input_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/dialog_rename_track_input_hint"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/dialog_rename_track_input_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue