diff --git a/.gitignore b/.gitignore
index a090e65..f8d5865 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
+.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
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
index c5f69d4..0f1b989 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -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
diff --git a/README.md b/README.md
index c39548f..bee390a 100644
--- a/README.md
+++ b/README.md
@@ -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/).
diff --git a/app/build.gradle b/app/build.gradle
index d85e175..d01fd94 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 55837c4..f1b4245 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -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
\ No newline at end of file
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 11e6b47..1199564 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,10 +1,11 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="org.y20k.trackbook">
-
+
+
@@ -18,27 +19,19 @@
-
-
-
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ tools:ignore="GoogleAppIndexingWarning">
+
+
-
-
+
+
-
@@ -53,17 +46,20 @@
-
+
+ android:name="androidx.core.content.FileProvider"
+ android:authorities="${applicationId}.provider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/provider_paths"/>
-
\ No newline at end of file
+
+
+
+
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
index 6e0beb4..dbd5712 100644
Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/org/y20k/trackbook/Keys.kt b/app/src/main/java/org/y20k/trackbook/Keys.kt
new file mode 100644
index 0000000..99702f2
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/Keys.kt
@@ -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"
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/MainActivity.java b/app/src/main/java/org/y20k/trackbook/MainActivity.java
deleted file mode 100755
index f8b1659..0000000
--- a/app/src/main/java/org/y20k/trackbook/MainActivity.java
+++ /dev/null
@@ -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 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 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 checkPermissions() {
- List 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> 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 wr = instantiatedFragments.get(position);
- if (wr != null) {
- return wr.get();
- } else {
- return null;
- }
- }
-
- }
- /**
- * End of inner class
- */
-
-
-}
diff --git a/app/src/main/java/org/y20k/trackbook/MainActivity.kt b/app/src/main/java/org/y20k/trackbook/MainActivity.kt
new file mode 100644
index 0000000..d341827
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/MainActivity.kt
@@ -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(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)
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java b/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java
deleted file mode 100755
index ebf6950..0000000
--- a/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java
+++ /dev/null
@@ -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 {
-
- @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 {
-
- @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
- */
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java b/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java
deleted file mode 100755
index c87ffc3..0000000
--- a/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java
+++ /dev/null
@@ -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 {
-
- @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
- */
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/MapFragment.kt b/app/src/main/java/org/y20k/trackbook/MapFragment.kt
new file mode 100644
index 0000000..da1747e
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/MapFragment.kt
@@ -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, 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
+ */
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
new file mode 100644
index 0000000..d09417f
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt
@@ -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
+ }
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt
new file mode 100644
index 0000000..706fbe1
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt
@@ -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()
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook.java b/app/src/main/java/org/y20k/trackbook/Trackbook.java
deleted file mode 100755
index 3be8999..0000000
--- a/app/src/main/java/org/y20k/trackbook/Trackbook.java
+++ /dev/null
@@ -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.");
- }
-
-}
diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook2.kt b/app/src/main/java/org/y20k/trackbook/Trackbook2.kt
new file mode 100644
index 0000000..ff04f7a
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/Trackbook2.kt
@@ -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.")
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.java b/app/src/main/java/org/y20k/trackbook/TrackerService.java
deleted file mode 100755
index 121685a..0000000
--- a/app/src/main/java/org/y20k/trackbook/TrackerService.java
+++ /dev/null
@@ -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 {
-
- @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
- */
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt
new file mode 100644
index 0000000..22098b3
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt
@@ -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
+ */
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
new file mode 100644
index 0000000..4c52350
--- /dev/null
+++ b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt
@@ -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
+ */
+
+}
diff --git a/app/src/main/java/org/y20k/trackbook/core/Track.java b/app/src/main/java/org/y20k/trackbook/core/Track.java
deleted file mode 100755
index 6e0183b..0000000
--- a/app/src/main/java/org/y20k/trackbook/core/Track.java
+++ /dev/null
@@ -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 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 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();
- 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