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()
|
||||