Kotlin Rewrite - everything is new

This commit is contained in:
y20k 2020-01-02 18:00:37 +01:00
parent b09259594f
commit 99265afe58
No known key found for this signature in database
GPG key ID: 824D4259F41FAFF6
155 changed files with 6115 additions and 6802 deletions

74
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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/).

View file

@ -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
}

View file

@ -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

View file

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.y20k.trackbook">
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

View 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"
}

View file

@ -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
*/
}

View 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)
}
}
}

View file

@ -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
*/
}

View file

@ -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
*/
}

View 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
*/
}

View 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
}
}

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

View file

@ -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.");
}
}

View 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.")
}
}

View file

@ -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
*/
}

View 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
*/
}

View 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
*/
}

View file

@ -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)
}
}

View 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
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View 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)
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View 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
}
}

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

View file

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

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

View file

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

View file

@ -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
}
}

View file

@ -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();
}
}

View file

@ -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));
}
}
}
}

View 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();
}
}

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

View 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
*/
}

View file

@ -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;
}
}
}

View file

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