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 CREATOR = new Creator() { - @Override - public Track createFromParcel(Parcel in) { - return new Track(in); - } - - @Override - public Track[] newArray(int size) { - return new Track[size]; - } - }; - - - /* Adds new WayPoint */ - public boolean addWayPoint(@Nullable Location previousLocation, Location newLocation) { - - // toggle stop over status, if necessary - boolean isStopOver = LocationHelper.isStopOver(previousLocation, newLocation); - if (isStopOver) { - int wayPointCount = mWayPoints.size(); - mWayPoints.get(wayPointCount-1).setIsStopOver(isStopOver); - } - - // create new WayPoint - WayPoint wayPoint = new WayPoint(newLocation, false, mTrackLength); - - // add new WayPoint to track - return mWayPoints.add(wayPoint); - } - - - /* Updates distance */ - public boolean updateDistance(@Nullable Location previousLocation, Location newLocation){ - // two data points needed to calculate distance - if (previousLocation != null) { - // add up distance - mTrackLength = mTrackLength + previousLocation.distanceTo(newLocation); - return true; - } else { - // this was the first waypoint - return false; - } - } - - - /* Toggles stop over status of last waypoint */ - public void toggleLastWayPointStopOverStatus(boolean stopOver) { - int wayPointCount = mWayPoints.size(); - mWayPoints.get(wayPointCount-1).setIsStopOver(stopOver); - } - - - /* Sets end time and date of recording */ - public void setRecordingEnd() { - mRecordingStop = GregorianCalendar.getInstance().getTime(); - } - - - /* Setter for duration of track */ - public void setDuration(long duration) { - mDuration = duration; - } - - - /* Setter for step count of track */ - public void setStepCount(float stepCount) { - mStepCount = stepCount; - } - - - /* Setter for maximum altitude of recording */ - public void setMaxAltitude(double maxAltitude) { - mMaxAltitude = maxAltitude; - } - - - /* Setter for lowest altitude of recording */ - public void setMinAltitude(double minAltitude) { - mMinAltitude = minAltitude; - } - - - /* Setter for positive elevation of recording (cumulative altitude difference) */ - public void setPositiveElevation(double positiveElevation) { - mPositiveElevation = positiveElevation; - } - - - /* Setter for negative elevation of recording (cumulative altitude difference) */ - public void setNegativeElevation(double negativeElevation) { - mNegativeElevation = negativeElevation; - } - - - /* Setter for this track's BoundingBox - a data structure describing the edge coordinates of a track */ - public void setBoundingBox(BoundingBox boundingBox) { - mBoundingBox = boundingBox; - } - - - /* Getter for file/track format version */ - public int getTrackFormatVersion() { - return mTrackFormatVersion; - } - - - /* Getter for mWayPoints */ - public List getWayPoints() { - return mWayPoints; - } - - - /* Getter size of Track / number of WayPoints */ - public int getSize() { - return mWayPoints.size(); - } - - - /* Getter for track length */ - public float getTrackLength() { - return mTrackLength; - } - - - /* Getter for duration of track */ - public long getTrackDuration() { - return mDuration; - } - - /* Getter for start date of recording */ - public Date getRecordingStart() { - return mRecordingStart; - } - - - /* Getter for stop date of recording */ - public Date getRecordingStop() { - return mRecordingStop; - } - - - /* Getter for step count of recording */ - public float getStepCount() { - return mStepCount; - } - - - /* Getter for maximum altitude of recording */ - public double getMaxAltitude() { - return mMaxAltitude; - } - - - /* Getter for lowest altitude of recording */ - public double getMinAltitude() { - return mMinAltitude; - } - - - /* Getter for positive elevation of recording (cumulative altitude difference) */ - public double getPositiveElevation() { - return mPositiveElevation; - } - - - /* Getter for negative elevation of recording (cumulative altitude difference) */ - public double getNegativeElevation() { - return mNegativeElevation; - } - - - /* Getter for this track's BoundingBox - a data structure describing the edge coordinates of a track */ - public BoundingBox getBoundingBox() { return mBoundingBox; } - - - /* Getter recorded distance */ - public Double getTrackDistance() { - int size = mWayPoints.size(); - if (size > 0) { - return (double)mWayPoints.get(size - 1).getDistanceToStartingPoint(); - } else { - return (double)0f; - } - } - - - /* Getter for location of specific WayPoint */ - public Location getWayPointLocation(int index) { - return mWayPoints.get(index).getLocation(); - } - - - @Override - public int describeContents() { - return 0; - } - - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeInt(mTrackFormatVersion); - parcel.writeTypedList(mWayPoints); - parcel.writeFloat(mTrackLength); - parcel.writeLong(mDuration); - parcel.writeFloat(mStepCount); - parcel.writeLong(mRecordingStart.getTime()); - parcel.writeLong(mRecordingStop.getTime()); - parcel.writeDouble(mMaxAltitude); - parcel.writeDouble(mMinAltitude); - parcel.writeDouble(mPositiveElevation); - parcel.writeDouble(mNegativeElevation); - parcel.writeDouble(mBoundingBox.getLatNorth()); - parcel.writeDouble(mBoundingBox.getLonEast()); - parcel.writeDouble(mBoundingBox.getLatSouth()); - parcel.writeDouble(mBoundingBox.getLonWest()); - // BoundingBox(double north, double east, double south, double west) - } - - -} diff --git a/app/src/main/java/org/y20k/trackbook/core/Track.kt b/app/src/main/java/org/y20k/trackbook/core/Track.kt new file mode 100644 index 0000000..7c566bd --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/Track.kt @@ -0,0 +1,77 @@ +/* + * Track.kt + * Implements the Track data class + * A Track stores a list of WayPoints + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.content.Context +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import org.y20k.trackbook.Keys +import org.y20k.trackbook.helpers.DateTimeHelper +import java.util.* + + +/* + * Track data class + */ +@Keep +@Parcelize +data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, + @Expose val wayPoints: MutableList = mutableListOf(), + @Expose var length: Float = 0f, + @Expose var duration: Long = 0L, + @Expose var stepCount: Float = 0f, + @Expose var recordingStart: Date = GregorianCalendar.getInstance().time, + @Expose var recordingStop: Date = recordingStart, + @Expose var maxAltitude: Double = 0.0, + @Expose var minAltitude: Double = 0.0, + @Expose var positiveElevation: Double = 0.0, + @Expose var negativeElevation: Double = 0.0, + @Expose var trackUriString: String = String(), + @Expose var gpxUriString: String = String(), + @Expose var latitude: Double = Keys.DEFAULT_LATITUDE, + @Expose var longitude: Double = Keys.DEFAULT_LONGITUDE, + @Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL, + @Expose var name: String = String()): Parcelable { + + + /* Creates a TracklistElement */ + fun toTracklistElement(context: Context): TracklistElement { + val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart) + val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration) + return TracklistElement( + name = name, + date = recordingStart, + dateString = readableDateString, + length = length, + durationString = readableDurationString, + trackUriString = trackUriString, + gpxUriString = gpxUriString, + starred = false + ) + } + + + /* Returns unique ID for Track - currently the start date */ + fun getTrackId(): Long { + return recordingStart.time + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java b/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java deleted file mode 100755 index 73d2306..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * TrackBuilder.java - * Implements a builder for the Track class - * A TrackBuilder can build a track object depending on the version of its file format - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.core; - -import org.osmdroid.util.BoundingBox; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.MapHelper; - -import java.util.Date; -import java.util.List; - - -/** - * TrackBuilder class - */ -public class TrackBuilder { - - /* Define log tag */ - private static final String LOG_TAG = TrackBuilder.class.getSimpleName(); - - - /* Main class variables */ - private final int mTrackFormatVersion; - private final List mWayPoints; - private final float mTrackLength; - private final long mDuration; - private final float mStepCount; - private final Date mRecordingStart; - private final Date mRecordingStop; - private final double mMaxAltitude; - private final double mMinAltitude; - private final double mPositiveElevation; - private final double mNegativeElevation; - private BoundingBox mBoundingBox; - - - /* Generic Constructor */ - public TrackBuilder(int trackFormatVersion, List wayPoints, float trackLength, long duration, float stepCount, Date recordingStart, Date recordingStop, double maxAltitude, double minAltitude, double positiveElevation, double negativeElevation, BoundingBox boundingBox) { - mTrackFormatVersion = trackFormatVersion; - mWayPoints = wayPoints; - mTrackLength = trackLength; - mDuration = duration; - mStepCount = stepCount; - mRecordingStart = recordingStart; - mRecordingStop = recordingStop; - mMaxAltitude = maxAltitude; - mMinAltitude = minAltitude; - mPositiveElevation = positiveElevation; - mNegativeElevation = negativeElevation; - mBoundingBox = boundingBox; - } - - - /* Builds and return a Track object */ - public Track toTrack() { - switch (mTrackFormatVersion) { - case 1: - // file format version 1 - does not have elevation data stored - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, 0f, 0f, 0f, 0f, new BoundingBox()); - case 2: - // file format version 2 - does not have edge coordinates stored - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, MapHelper.calculateBoundingBox(mWayPoints)); - case 3: - // file format version 3 (current version) - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, new BoundingBox()); - default: - LogHelper.e(LOG_TAG, "Unknown file format version: " + mTrackFormatVersion); - return null; - } - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java b/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java deleted file mode 100755 index 0f599df..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * TrackBundle.java - * Implements a TrackBundle - * TrackBundle is a container for file and corresponding name of a track - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.core; - -import org.y20k.trackbook.helpers.LogHelper; - -import java.io.File; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - - -/** - * TrackBundle class - */ -public class TrackBundle { - - /* Define log tag */ - private static final String LOG_TAG = TrackBundle.class.getSimpleName(); - - /* Main class variables */ - private final File mTrackFile; - private final String mTrackName; - - - /* Constructor */ - public TrackBundle(File file) { - mTrackFile = file; - mTrackName = buildTrackName(file); - } - - - /* Getter for track file */ - public File getTrackFile() { - return mTrackFile; - } - - - /* Getter for track name */ - public String getTrackName() { - return mTrackName; - } - - - /* Builds a readable track name from the track's file name */ - private String buildTrackName(File file) { - - // get file name without extension - String readableTrackName = file.getName(); - readableTrackName = readableTrackName.substring(0, readableTrackName.indexOf(".trackbook")); - - try { - // convert file name to date - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - Date trackDate = dateFormat.parse(readableTrackName); - - // convert date to track name string according to current locale - readableTrackName = DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault()).format(trackDate) + " - " + - DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(trackDate); - - } catch (ParseException e) { - LogHelper.w(LOG_TAG, "Unable to parse file name into date object (yyyy-MM-dd-HH-mm-ss): " + e); - } - - return readableTrackName; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt b/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt new file mode 100644 index 0000000..8944ed8 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt @@ -0,0 +1,53 @@ +/* + * Tracklist.kt + * Implements the Tracklist data class + * A Tracklist stores a list of Tracks + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import org.y20k.trackbook.Keys +import org.y20k.trackbook.helpers.TrackHelper +import java.util.* + + +/* + * Tracklist data class + */ +@Keep +@Parcelize +data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION, + @Expose val tracklistElements: MutableList = mutableListOf(), + @Expose var modificationDate: Date = Date()): Parcelable { + + /* Return trackelement for given track id */ + fun getTrackElement(trackId: Long): TracklistElement? { + tracklistElements.forEach { tracklistElement -> + if (TrackHelper.getTrackId(tracklistElement) == trackId) { + return tracklistElement + } + } + return null + } + + /* Create a deep copy */ + fun deepCopy(): Tracklist { + return Tracklist(tracklistFormatVersion, mutableListOf().apply { addAll(tracklistElements) }, modificationDate) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt b/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt new file mode 100644 index 0000000..baa7e5a --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt @@ -0,0 +1,46 @@ +/* + * TracklistElement.kt + * Implements the TracklistElement data class + * A TracklistElement data about a Track + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import java.util.* + + +/* + * TracklistElement data class + */ +@Keep +@Parcelize +data class TracklistElement(@Expose var name: String, + @Expose val date: Date, + @Expose val dateString: String, + @Expose val durationString: String, + @Expose val length: Float, + @Expose val trackUriString: String, + @Expose val gpxUriString: String, + @Expose var starred: Boolean = false): Parcelable { + + /* Returns unique ID for TracklistElement - currently the start date */ + fun getTrackId(): Long { + return date.time + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.java b/app/src/main/java/org/y20k/trackbook/core/WayPoint.java deleted file mode 100755 index de1b86c..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/WayPoint.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * WayPoint.java - * Implements the WayPoint class - * A WayPoint stores a location plus additional metadata - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.core; - -import android.location.Location; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; - - -/** - * WayPoint class - */ -public class WayPoint implements Parcelable { - - private Location mLocation; - private boolean mIsStopOver; - private float mDistanceToStartingPoint; - private final int mNumberSatellites; - - /* Constructor */ - public WayPoint(Location location, boolean isStopOver, float distanceToStartingPoint) { - mLocation = location; - mIsStopOver = isStopOver; - mDistanceToStartingPoint = distanceToStartingPoint; - - // save number of satellites - Bundle extras = location.getExtras(); - if (extras != null && extras.containsKey("satellites")) { - mNumberSatellites = extras.getInt("satellites", 0); - mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON - } else { - mNumberSatellites = 0; - } - - } - - /* Constructor used by CREATOR */ - protected WayPoint(Parcel in) { - mLocation = Location.CREATOR.createFromParcel(in); - mIsStopOver = in.readByte() != 0; - mDistanceToStartingPoint = in.readFloat(); - mNumberSatellites = in.readInt(); - } - - - /* CREATOR for WayPoint object used to do parcel related operations */ - public static final Creator CREATOR = new Creator() { - @Override - public WayPoint createFromParcel(Parcel in) { - return new WayPoint(in); - } - - @Override - public WayPoint[] newArray(int size) { - return new WayPoint[size]; - } - }; - - - /* Getter for mLocation */ - public Location getLocation() { - return mLocation; - } - - - /* Getter for mIsStopOver */ - public boolean getIsStopOver() { - return mIsStopOver; - } - - - /* Getter for mDistanceToStartingPoint */ - public float getDistanceToStartingPoint() { - return mDistanceToStartingPoint; - } - - - /* Setter for mLocation */ - public void setLocation(Location location) { - mLocation = location; - } - - - /* Setter for mIsStopOver */ - public void setIsStopOver(boolean isStopOver) { - mIsStopOver = isStopOver; - } - - - /* Setter for mDistanceToStartingPoint */ - public void setDistanceToStartingPoint(float distanceToStartingPoint) { - mDistanceToStartingPoint = distanceToStartingPoint; - } - - @Override - public int describeContents() { - return 0; - } - - - @Override - public void writeToParcel(Parcel parcel, int flags) { - mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON - mLocation.writeToParcel(parcel, flags); - parcel.writeByte((byte) (mIsStopOver ? 1 : 0)); - parcel.writeFloat(mDistanceToStartingPoint); - parcel.writeInt(mNumberSatellites); - } -} diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt new file mode 100644 index 0000000..cbc1dd0 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt @@ -0,0 +1,54 @@ +/* + * WayPoint.kt + * Implements the WayPoint data class + * A WayPoint stores a location plus additional metadata + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.location.Location +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize + + +/* + * WayPoint data class + */ +@Keep +@Parcelize +data class WayPoint(@Expose val provider: String, + @Expose val latitude: Double, + @Expose val longitude: Double, + @Expose val altitude: Double, + @Expose val accuracy: Float, + @Expose val time: Long, + @Expose val distanceToStartingPoint: Float = 0f, + @Expose val numberSatellites: Int = 0, + @Expose var isStopOver: Boolean = false): Parcelable { + + + /* Converts WayPoint into Location */ + fun toLocation(): Location { + val location: Location = Location(provider) + location.latitude = latitude + location.longitude = longitude + location.altitude = altitude + location.accuracy = accuracy + location.time = time + return location + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt new file mode 100644 index 0000000..48864c9 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt @@ -0,0 +1,92 @@ +/* + * ErrorDialog.kt + * Implements the ErrorDialog object + * A ErrorDialog shows an error dialog with details + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.dialogs + +import android.content.Context +import android.content.DialogInterface +import android.text.method.ScrollingMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * ErrorDialog object + */ +object ErrorDialog { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(ErrorDialog::class.java) + + + /* Construct and show dialog */ + fun show(context: Context, errorTitle: Int, errorMessage: Int, errorDetails: String = String()) { + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme) + + // set title + builder.setTitle(context.getString(errorTitle)) + + // get views + val inflater: LayoutInflater = LayoutInflater.from(context) + val view: View = inflater.inflate(R.layout.dialog_generic_with_details, null) + val errorMessageView: TextView = view.findViewById(R.id.dialog_message) as TextView + val errorDetailsLinkView: TextView = view.findViewById(R.id.dialog_details_link) as TextView + val errorDetailsView: TextView = view.findViewById(R.id.dialog_details) as TextView + + // set dialog view + builder.setView(view) + + // set detail view + if (errorDetails.isNotEmpty()) { + // show details link + errorDetailsLinkView.visibility = View.VISIBLE + + // allow scrolling on details view + errorDetailsView.movementMethod = ScrollingMovementMethod() + + // show and hide details on click + errorDetailsLinkView.setOnClickListener { + when (errorDetailsView.visibility) { + View.GONE -> errorDetailsView.visibility = View.VISIBLE + View.VISIBLE -> errorDetailsView.visibility = View.GONE + } + } + // set details text view + errorDetailsView.text = errorDetails + } else { + // hide details link + errorDetailsLinkView.visibility = View.GONE + } + + // set text views + errorMessageView.text = context.getString(errorMessage) + + // add okay button + builder.setPositiveButton(R.string.dialog_generic_button_okay, DialogInterface.OnClickListener { _, _ -> + // listen for click on okay button + // do nothing + }) + + // display error dialog + builder.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt new file mode 100644 index 0000000..ebd8403 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt @@ -0,0 +1,82 @@ +/* + * RenameTrackDialog.kt + * Implements the RenameTrackDialog class + * A RenameTrackDialog offers user to change name of track + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.dialogs + +import android.content.Context +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import android.widget.TextView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * RenameTrackDialog class + */ +class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) { + + /* Interface used to communicate back to activity */ + interface RenameTrackListener { + fun onRenameTrackDialog(textInput: String) { + } + } + + /* Define log tag */ + private val TAG = LogHelper.makeLogTag(RenameTrackDialog::class.java.simpleName) + + + /* Construct and show dialog */ + fun show(context: Context, trackName: String) { + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context) + + // get input field + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.dialog_rename_track, null) + val inputField = view.findViewById(R.id.dialog_rename_track_input_edit_text) as EditText + + // pre-fill with current track name + inputField.setText(trackName, TextView.BufferType.EDITABLE) + inputField.setSelection(trackName.length) + inputField.inputType = InputType.TYPE_CLASS_TEXT + + // set dialog view + builder.setView(view) + + // add "add" button + builder.setPositiveButton(R.string.dialog_rename_track_button) { _, _ -> + // hand text over to initiating activity + inputField.text?.let { + renameTrackListener.onRenameTrackDialog(it.toString()) + } + } + + // add cancel button + builder.setNegativeButton(R.string.dialog_generic_button_cancel) { _, _ -> + // listen for click on cancel button + // do nothing + } + + // display add dialog + builder.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt new file mode 100644 index 0000000..83c391e --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt @@ -0,0 +1,97 @@ +/* + * YesNoDialog + * Implements the YesNoDialog class + * A YesNoDialog asks the user if he/she wants to do something or notpackage org.y20k.trackbook.dialogs + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + +/* + * YesNoDialog class + */ +class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) { + + /* Interface used to communicate back to activity */ + interface YesNoDialogListener { + fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { + } + } + + + /* Define log tag */ + private val TAG = LogHelper.makeLogTag(YesNoDialog::class.java.simpleName) + + + + + + /* Construct and show dialog - variant: message from string */ + fun show(context: Context, + type: Int, + title: Int = Keys.EMPTY_STRING_RESOURCE, + message: Int, + yesButton: Int = R.string.dialog_yes_no_positive_button_default, + noButton: Int = R.string.dialog_generic_button_cancel, + payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT, + payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) { + // extract string from message resource and feed into main show method + show(context, type, title, context.getString(message), yesButton, noButton, payload, payloadString) + } + + + /* Construct and show dialog */ + fun show(context: Context, + type: Int, + title: Int = Keys.EMPTY_STRING_RESOURCE, + messageString: String, + yesButton: Int = R.string.dialog_yes_no_positive_button_default, + noButton: Int = R.string.dialog_generic_button_cancel, + payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT, + payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) { + + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme) + + // set title and message + builder.setMessage(messageString) + if (title != Keys.EMPTY_STRING_RESOURCE) { + builder.setTitle(context.getString(title)) + } + + + // add yes button + builder.setPositiveButton(yesButton) { _, _ -> + // listen for click on yes button + yesNoDialogListener.onYesNoDialog(type, true, payload, payloadString) + } + + // add no button + builder.setNegativeButton(noButton) { _, _ -> + // listen for click on no button + yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString) + } + + // handle outside-click as "no" + builder.setOnCancelListener(){ + yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString) + } + + // display dialog + builder.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt b/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt new file mode 100644 index 0000000..b4836eb --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt @@ -0,0 +1,29 @@ +/* + * SharedPreferencesExt.kt + * Implements the SharedPreferencesExt extension functions + * SharedPreferencesExt displays provides additional functions for dealing with shared preferences + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.extensions + + +import android.content.SharedPreferences + + +/* Puts a Double value in SharedPreferences */ +fun SharedPreferences.Editor.putDouble(key: String, double: Double) = putLong(key, java.lang.Double.doubleToRawLongBits(double)) + + +/* gets a Double value from SharedPreferences */ +fun SharedPreferences.getDouble(key: String, default: Double) = java.lang.Double.longBitsToDouble(getLong(key, java.lang.Double.doubleToRawLongBits(default))) \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt new file mode 100644 index 0000000..c02d380 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt @@ -0,0 +1,84 @@ +/* + * DateTimeHelper.kt + * Implements the DateTimeHelper object + * A DateTimeHelper provides helper methods for converting Date and Time objects + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + + +/* + * DateTimeHelper object + */ +object DateTimeHelper { + + /* Converts milliseconds to mm:ss or hh:mm:ss */ + fun convertToReadableTime(context: Context, milliseconds: Long): String { + var timeString: String = String() + val hours: Long = TimeUnit.MILLISECONDS.toHours(milliseconds) + val minutes: Long = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1) + val seconds: Long = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1) + val h: String = context.getString(R.string.abbreviation_hours) + val m: String = context.getString(R.string.abbreviation_minutes) + val s: String = context.getString(R.string.abbreviation_seconds) + + when (milliseconds >= Keys.ONE_HOUR_IN_MILLISECONDS) { + // CASE: format hh:mm:ss + true -> { + timeString = "$hours $h $minutes $m $seconds $s" + } + // CASE: format mm:ss + false -> { + timeString = "$minutes $m $seconds $s" + } + } + return timeString + } + + + /* Create sortable string from date - used for filenames */ + fun convertToSortableDateString(date: Date): String { + val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US) + return dateFormat.format(date) + } + + + /* Creates a readable string from date - used in the UI */ + fun convertToReadableDate(date: Date, dateStyle: Int = DateFormat.LONG): String { + return DateFormat.getDateInstance(dateStyle, Locale.getDefault()).format(date) + } + + + /* Calculates time difference between two locations */ + fun calculateTimeDistance(previousLocation: Location?, location: Location): Long { + var timeDifference: Long = 0L + // two data points needed to calculate time difference + if (previousLocation != null) { + // get time difference + timeDifference = location.time - previousLocation.time + } + return timeDifference + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java deleted file mode 100755 index 53e463b..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * DialogHelper.java - * Implements the DialogHelper class - * A DialogHelper creates a customizable alert dialog - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; - - -/** - * DialogHelper class - */ -public class DialogHelper extends DialogFragment implements TrackbookKeys { - - /* Constructs a new instance */ - public static DialogHelper newInstance(int title, String message, int positiveButton, int negativeButton) { - DialogHelper fragment = new DialogHelper(); - Bundle args = new Bundle(); - args.putInt(ARG_DIALOG_TITLE, title); - args.putString(ARG_DIALOG_MESSAGE, message); - args.putInt(ARG_DIALOG_BUTTON_POSITIVE, positiveButton); - args.putInt(ARG_DIALOG_BUTTON_NEGATIVE, negativeButton); - fragment.setArguments(args); - return fragment; - } - - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - - // get text elements - int title = args.getInt(ARG_DIALOG_TITLE); - String message = args.getString(ARG_DIALOG_MESSAGE); - int positiveButton = args.getInt(ARG_DIALOG_BUTTON_POSITIVE); - int negativeButton = args.getInt(ARG_DIALOG_BUTTON_NEGATIVE); - - // build dialog - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); - if (title != -1) { - dialogBuilder.setTitle(title); - } - dialogBuilder.setMessage(message); - dialogBuilder.setPositiveButton(positiveButton, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Fragment target = getTargetFragment(); - if (target != null) { - target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getActivity().getIntent()); - } - } - } - ); dialogBuilder.setNegativeButton(negativeButton, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Fragment target = getTargetFragment(); - if (target != null) { - target.onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, getActivity().getIntent()); - } - } - } - ); - - return dialogBuilder.create(); - } -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java b/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java deleted file mode 100755 index edf2c44..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * DropdownHelper.java - * Implements a dropdown menu - * The dropdown menu used to select tracks - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.content.res.Resources; -import android.database.DataSetObserver; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.TrackBundle; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.Nullable; -import androidx.appcompat.widget.ThemedSpinnerAdapter; - - -/** - * DropdownHelper class - */ -public class DropdownAdapter extends BaseAdapter implements ThemedSpinnerAdapter, TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = DropdownAdapter.class.getSimpleName(); - - - /* Main class variables */ - private final Activity mActivity; - private final ThemedSpinnerAdapter.Helper mDropdownAdapterHelper; - private List mTrackBundleList; - - - /* Constructor */ - public DropdownAdapter(Activity activity) { - // store activity - mActivity = activity; - - // fill list with track bundles - initializeTrackBundleList(); - - // create an adapter helper - mDropdownAdapterHelper = new ThemedSpinnerAdapter.Helper(activity); - } - - - @Override - public void setDropDownViewTheme(@Nullable Resources.Theme theme) { - mDropdownAdapterHelper.setDropDownViewTheme(theme); - } - - - @Nullable - @Override - public Resources.Theme getDropDownViewTheme() { - return mDropdownAdapterHelper.getDropDownViewTheme(); - } - - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // getView -> collapsed view of dropdown - View view = convertView; - if (view == null) { - LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater(); - view = inflater.inflate(R.layout.custom_dropdown_item_collapsed, parent, false); - } - ((TextView) view).setText(getItem(position).getTrackName()); - return view; - } - - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - // getDropDownView -> expanded view of dropdown - View view = convertView; - if (view == null) { - LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater(); -// view = inflater.inflate(R.layout.custom_dropdown_item_expanded, parent, false); - view = inflater.inflate(R.layout.support_simple_spinner_dropdown_item, parent, false); - } - ((TextView) view).setText(getItem(position).getTrackName()); - return view; - } - - - @Override - public void registerDataSetObserver(DataSetObserver dataSetObserver) { - - } - - - @Override - public void unregisterDataSetObserver(DataSetObserver dataSetObserver) { - - } - - - @Override - public int getCount() { - return mTrackBundleList.size(); - } - - - @Override - public TrackBundle getItem(int i) { - return mTrackBundleList.get(i); - } - - - @Override - public long getItemId(int i) { - return 0; - } - - - @Override - public boolean hasStableIds() { - return false; - } - - - @Override - public int getItemViewType(int i) { - return 0; - } - - - @Override - public int getViewTypeCount() { - return 1; - } - - - @Override - public boolean isEmpty() { - return mTrackBundleList.size() == 0; - } - - - /* Refreshes the adapter data */ - public void refresh() { - // re-initialize the adapter's array list - initializeTrackBundleList(); - } - - - /* Initializes list of track bundles */ - private void initializeTrackBundleList() { - - // get list of files from storage - StorageHelper storageHelper = new StorageHelper(mActivity); - File files[] = storageHelper.getListOfTrackbookFiles(); - - // fill list with track bundles - mTrackBundleList = new ArrayList<>(); - for (File file : files) { - String fileName = file.getName(); - if (fileName.endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !fileName.startsWith(FILE_NAME_TEMP)) { - mTrackBundleList.add(new TrackBundle(file)); - } - } - - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java deleted file mode 100755 index b39ce18..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java +++ /dev/null @@ -1,224 +0,0 @@ -/** - * ExportHelper.java - * Implements the ExportHelper class - * A ExportHelper can convert Track object into a GPX string - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.content.Intent; -import android.location.Location; -import android.os.Environment; -import android.widget.Toast; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.WayPoint; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -import androidx.core.content.FileProvider; - -/** - * ExportHelper class - */ -public final class ExportHelper extends FileProvider implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = ExportHelper.class.getSimpleName(); - - - /* Checks if a GPX file for given track is already present */ - public static boolean gpxFileExists(Track track) { - File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - return createFile(track, folder).exists(); - } - - - /* Exports given track to GPX */ - public static boolean exportToGpx(Context context, Track track) { - // get file for given track - File gpxFile = createFile(track, getDownloadFolder()); - - // get GPX string representation for given track - String gpxString = createGpxString(track); - - // write GPX file - if (writeGpxToFile(gpxString, gpxFile)) { - String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - return true; - } else { - String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - return false; - } - } - - - /* Creates Intent used to bring up an Android share sheet */ - public static Intent getGpxFileIntent(Context context, Track track) { - - // create file in Cache directory for given track - File gpxFile = createFile(track, context.getCacheDir()); - - // get GPX string representation for given track - String gpxString = createGpxString(track); - - // write GPX file - if (writeGpxToFile(gpxString, gpxFile)) { - String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - } else { - String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - } - - // create intent - String authority = "org.y20k.trackbook.exporthelper.provider"; - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.setDataAndType(FileProvider.getUriForFile(context, authority, gpxFile), "application/gpx+xml"); - intent.setType("application/gpx+xml"); - intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, authority, gpxFile)); - - return intent; - } - - - /* Empties the internal chache directory */ - public static void emptyCacheDirectory(Context context) { - // todo implement a date check - delete only stuff that is a week old - File[] cacheFiles = context.getCacheDir().listFiles(); - for (File file: cacheFiles) { - file.delete(); - } - } - - - /* Get "Download" folder */ - private static File getDownloadFolder() { - File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - if (folder != null && !folder.exists()) { - LogHelper.v(LOG_TAG, "Creating new folder: " + folder.toString()); - folder.mkdirs(); - } - return folder; - } - - - /* Return a GPX filepath for a given track */ - private static File createFile(Track track, File folder) { - Date recordingStart = track.getRecordingStart(); - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - - return new File(folder, dateFormat.format(recordingStart) + FILE_TYPE_GPX_EXTENSION); - } - - - /* Writes given GPX string to given file */ - private static boolean writeGpxToFile (String gpxString, File gpxFile) { - // write track - try (BufferedWriter bw = new BufferedWriter(new FileWriter(gpxFile))) { - LogHelper.v(LOG_TAG, "Saving track to external storage: " + gpxFile.toString()); - bw.write(gpxString); - return true; - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + gpxFile.toString()); - return false; - } - } - - - /* Creates GPX formatted string */ - private static String createGpxString(Track track) { - String gpxString; - - // add header - gpxString = "\n" + - "\n"; - - // add track - gpxString = gpxString + addTrack(track); - - // add closing tag - gpxString = gpxString + "\n"; - - return gpxString; - } - - - /* Creates Track */ - private static String addTrack(Track track) { - StringBuilder gpxTrack = new StringBuilder(""); - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - // add opening track tag - gpxTrack.append("\t\n"); - - // add name to track - gpxTrack.append("\t\t"); - gpxTrack.append("Trackbook Recording"); - gpxTrack.append("\n"); - - // add opening track segment tag - gpxTrack.append("\t\t\n"); - - // add route point - for (WayPoint wayPoint:track.getWayPoints()) { - // get location from waypoint - Location location = wayPoint.getLocation(); - - // add longitude and latitude - gpxTrack.append("\t\t\t\n"); - - // add time - gpxTrack.append("\t\t\t\t\n"); - - // add altitude - gpxTrack.append("\t\t\t\t"); - gpxTrack.append(location.getAltitude()); - gpxTrack.append("\n"); - - // add closing tag - gpxTrack.append("\t\t\t\n"); - } - - // add closing track segment tag - gpxTrack.append("\t\t\n"); - - // add closing track tag - gpxTrack.append("\t\n"); - - return gpxTrack.toString(); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt new file mode 100644 index 0000000..049e317 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt @@ -0,0 +1,431 @@ +/* + * FileHelper.kt + * Implements the FileHelper object + * A FileHelper provides helper methods for reading and writing files from and to device storage + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import android.provider.OpenableColumns +import androidx.core.net.toFile +import androidx.core.net.toUri +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.Tracklist +import org.y20k.trackbook.core.TracklistElement +import java.io.* +import java.text.NumberFormat +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +/* + * FileHelper object + */ +object FileHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java) + + + /* Return an InputStream for given Uri */ + fun getTextFileStream(context: Context, uri: Uri): InputStream? { + var stream : InputStream? = null + try { + stream = context.contentResolver.openInputStream(uri) + } catch (e : Exception) { + e.printStackTrace() + } + return stream + } + + + /* Get file size for given Uri */ + fun getFileSize(context: Context, uri: Uri): Long { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + val size: Long = cursor.getLong(sizeIndex) + cursor.close() + return size + } else { + return 0L + } + } + + + /* Get file name for given Uri */ + fun getFileName(context: Context, uri: Uri): String { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name: String = cursor.getString(nameIndex) + cursor.close() + return name + } else { + return String() + } + } + + + /* Clears given folder - keeps given number of files */ + fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) { + if (folder != null && folder.exists()) { + val files = folder.listFiles() + val fileCount: Int = files.size + files.sortBy { it.lastModified() } + for (fileNumber in files.indices) { + if (fileNumber < fileCount - keep) { + files[fileNumber].delete() + } + } + if (deleteFolder && keep == 0) { + folder.delete() + } + } + } + + + /* Reads tracklist from storage using GSON */ + fun readTracklist(context: Context): Tracklist { + LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}") + // get JSON from text file + val json: String = readTextFile(context, getTracklistFileUri(context)) + var tracklist: Tracklist = Tracklist() + when (json.isNotBlank()) { + // convert JSON and return as tracklist + true -> try { + tracklist = getCustomGson().fromJson(json, Tracklist::class.java) + } catch (e: Exception) { + e.printStackTrace() + } + } + return tracklist + } + + + /* Reads track from storage using GSON */ + fun readTrack(context: Context, fileUri: Uri): Track { + // get JSON from text file + val json: String = readTextFile(context, fileUri) + var track: Track = Track() + when (json.isNotEmpty()) { + // convert JSON and return as track + true -> try { + track = getCustomGson().fromJson(json, Track::class.java) + } catch (e: Exception) { + e.printStackTrace() + } + } + return track + } + + + /* Deletes temp track file */ + fun deleteTempFile(context: Context) { + getTempFileUri(context).toFile().delete() + } + + + /* Checks if temp track file exists */ + fun tempFileExists(context: Context): Boolean { + return getTempFileUri(context).toFile().exists() + } + + + /* Creates Uri for Gpx file of a track */ + fun getGpxFileUri(context: Context, track: Track): Uri { + val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION + return File(context.getExternalFilesDir(Keys.FOLDER_GPX), fileName).toUri() + } + + + /* Creates Uri for json track file */ + fun getTrackFileUri(context: Context, track: Track): Uri { + val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION + return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri() + } + + + /* Creates Uri for json temp track file */ + fun getTempFileUri(context: Context): Uri { + return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri() + } + + + /* Suspend function: Wrapper for saveTracklist */ + suspend fun addTrackAndSaveTracklistSuspended(context: Context, track: Track, modificationDate: Date = track.recordingStop) { + return suspendCoroutine { cont -> + val tracklist: Tracklist = readTracklist(context) + tracklist.tracklistElements.add(track.toTracklistElement(context)) + cont.resume(saveTracklist(context, tracklist, modificationDate)) + } + } + + + /* Suspend function: Wrapper for renameTrack */ + suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) { + return suspendCoroutine { cont -> + cont.resume(renameTrack(context, track, newName)) + } + } + + + /* Suspend function: Wrapper for saveTracklist */ + suspend fun saveTracklistSuspended(context: Context, tracklist: Tracklist, modificationDate: Date) { + return suspendCoroutine { cont -> + cont.resume(saveTracklist(context, tracklist, modificationDate)) + } + } + + + /* Suspend function: Wrapper for saveTrack */ + suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) { + return suspendCoroutine { cont -> + cont.resume(saveTrack(track, saveGpxToo)) + } + } + + + /* Suspend function: Wrapper for saveTempTrack */ + suspend fun saveTempTrackSuspended(context: Context, track: Track) { + return suspendCoroutine { cont -> + cont.resume(saveTempTrack(context, track)) + } + } + + + /* Suspend function: Wrapper for deleteTrack */ + suspend fun deleteTrackSuspended(context: Context, position: Int, tracklist: Tracklist): Tracklist { + return suspendCoroutine { cont -> + cont.resume(deleteTrack(context, position, tracklist)) + } + } + + + /* Suspend function: Wrapper for readTracklist */ + suspend fun readTracklistSuspended(context: Context): Tracklist { + return suspendCoroutine {cont -> + cont.resume(readTracklist(context)) + } + } + + + /* Suspend function: Wrapper for copyFile */ + suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri) { + return suspendCoroutine { cont -> + cont.resume(copyFile(context, originalFileUri, targetFileUri, deleteOriginal = true)) + } + } + + + /* Save Track as JSON to storage */ + private fun saveTrack(track: Track, saveGpxToo: Boolean) { + val jsonString: String = getTrackJsonString(track) + if (jsonString.isNotBlank()) { + // write track file + writeTextFile(jsonString, Uri.parse(track.trackUriString)) + } + if (saveGpxToo) { + val gpxString: String = TrackHelper.createGpxString(track) + if (gpxString.isNotBlank()) { + // write GPX file + writeTextFile(gpxString, Uri.parse(track.gpxUriString)) + } + } + } + + + /* Save Temp Track as JSON to storage */ + private fun saveTempTrack(context: Context, track: Track) { + val json: String = getTrackJsonString(track) + if (json.isNotBlank()) { + writeTextFile(json, getTempFileUri(context)) + } + } + + + /* Saves track tracklist as JSON text file */ + private fun saveTracklist(context: Context, tracklist: Tracklist, modificationDate: Date) { + tracklist.modificationDate = modificationDate + // convert to JSON + val gson: Gson = getCustomGson() + var json: String = String() + try { + json = gson.toJson(tracklist) + } catch (e: Exception) { + e.printStackTrace() + } + if (json.isNotBlank()) { + // write text file + writeTextFile(json, getTracklistFileUri(context)) + } + } + + + /* Creates Uri for tracklist file */ + private fun getTracklistFileUri(context: Context): Uri { + return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri() + } + + + + /* Renames track */ + private fun renameTrack(context: Context, track: Track, newName: String) { + // search track in tracklist + val tracklist: Tracklist = readTracklist(context) + var trackUriString: String = String() + tracklist.tracklistElements.forEach { tracklistElement -> + if (tracklistElement.getTrackId() == track.getTrackId()) { + // rename tracklist element + tracklistElement.name = newName + trackUriString = tracklistElement.trackUriString + } + } + if (trackUriString.isNotEmpty()) { + // save tracklist + saveTracklist(context, tracklist, GregorianCalendar.getInstance().time) + // rename track + track.name = newName + // save track + saveTrack(track, saveGpxToo = true) + } + } + + + /* Deletes track */ + private fun deleteTrack(context: Context, position: Int, tracklist: Tracklist): Tracklist { + val tracklistElement: TracklistElement = tracklist.tracklistElements[position] + // delete track files + tracklistElement.trackUriString.toUri().toFile().delete() + tracklistElement.gpxUriString.toUri().toFile().delete() + // remove track element from list + tracklist.tracklistElements.removeIf { TrackHelper.getTrackId(it) == TrackHelper.getTrackId(tracklistElement) } + saveTracklist(context, tracklist, GregorianCalendar.getInstance().time) + return tracklist + } + + + /* Copies file to specified target */ + private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { + val inputStream = context.contentResolver.openInputStream(originalFileUri) + val outputStream = context.contentResolver.openOutputStream(targetFileUri) + if (outputStream != null) { + inputStream?.copyTo(outputStream) + } + if (deleteOriginal) { + context.contentResolver.delete(originalFileUri, null, null) + } + } + + + /* Converts track to JSON */ + private fun getTrackJsonString(track: Track): String { + val gson: Gson = getCustomGson() + var json: String = String() + try { + json = gson.toJson(track) + } catch (e: Exception) { + e.printStackTrace() + } + return json + } + + + /* Creates a Gson object */ + private fun getCustomGson(): Gson { + val gsonBuilder = GsonBuilder() + gsonBuilder.setDateFormat("M/d/yy hh:mm a") + gsonBuilder.excludeFieldsWithoutExposeAnnotation() + return gsonBuilder.create() + } + + + + /* Converts byte value into a human readable format */ + // Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html + fun getReadableByteCount(bytes: Long, si: Boolean = true): String { + + // check if Decimal prefix symbol (SI) or Binary prefix symbol (IEC) requested + val unit: Long = if (si) 1000L else 1024L + + // just return bytes if file size is smaller than requested unit + if (bytes < unit) return "$bytes B" + + // calculate exp + val exp: Int = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() + + // determine prefix symbol + val prefix: String = ((if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i") + + // calculate result and set number format + val result: Double = bytes / Math.pow(unit.toDouble(), exp.toDouble()) + val numberFormat = NumberFormat.getNumberInstance() + numberFormat.maximumFractionDigits = 1 + + return numberFormat.format(result) + " " + prefix + "B" + } + + + /* Reads InputStream from file uri and returns it as String */ + private fun readTextFile(context: Context, fileUri: Uri): String { + // todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html + // https://developer.android.com/training/secure-file-sharing/retrieve-info + val file: File = fileUri.toFile() + // check if file exists + if (!file.exists()) { + return String() + } + // read until last line reached + val stream: InputStream = file.inputStream() + val reader: BufferedReader = BufferedReader(InputStreamReader(stream)) + val builder: StringBuilder = StringBuilder() + reader.forEachLine { + builder.append(it) + builder.append("\n") } + stream.close() + return builder.toString() + } + + + /* Writes given text to file on storage */ + private fun writeTextFile(text: String, fileUri: Uri) { + val file: File = fileUri.toFile() + file.writeText(text) + } + + + /* Writes given bitmap as image file to storage */ + private fun writeImageFile(context: Context, bitmap: Bitmap, file: File, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 75) { + if (file.exists()) file.delete () + try { + val out = FileOutputStream(file) + bitmap.compress(format, quality, out) + out.flush() + out.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt new file mode 100644 index 0000000..6b42d79 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt @@ -0,0 +1,165 @@ +/* + * ImportHelper.kt + * Implements the ImportHelper object + * A ImportHelper manages the one-time import of old .trackbook files + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import com.google.gson.GsonBuilder +import com.google.gson.annotations.SerializedName +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.WayPoint +import java.io.BufferedReader +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* + + +/* + * ImportHelper data class + */ +object ImportHelper { + + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(ImportHelper::class.java) + + + + /* Converts older tracks of type .trackbook into the new format */ + fun convertOldTracks(context: Context) { + val oldTracks: ArrayList = arrayListOf() + val trackFolder: File? = context.getExternalFilesDir(Keys.FOLDER_TRACKS) + + if (trackFolder != null && trackFolder.exists() && trackFolder.isDirectory) { + trackFolder.listFiles()?.forEach { file -> + if (file.name.endsWith(".trackbook")) { + // read until last line reached + val stream: InputStream = file.inputStream() + val reader: BufferedReader = BufferedReader(InputStreamReader(stream)) + val builder: StringBuilder = StringBuilder() + reader.forEachLine { + builder.append(it) + builder.append("\n") } + stream.close() + // get content of file + val fileContent: String = builder.toString() + // get LegacyTrack from JSON + val gsonBuilder = GsonBuilder() + gsonBuilder.setDateFormat("M/d/yy hh:mm a") + val oldTrack: LegacyTrack = gsonBuilder.create().fromJson(fileContent, LegacyTrack::class.java) + oldTracks.add(oldTrack.toTrack()) + } + } + } + + // save track using "deferred await" + if (oldTracks.isNotEmpty()) { + GlobalScope.launch { + oldTracks.forEach { oldTrack -> + // step 1: create and store filenames for json and gpx files + oldTrack.trackUriString = FileHelper.getTrackFileUri(context, oldTrack).toString() + oldTrack.gpxUriString = FileHelper.getGpxFileUri(context, oldTrack).toString() + // step 2: save track + FileHelper.saveTrackSuspended(oldTrack, saveGpxToo = true) + // step 3: save tracklist + FileHelper.addTrackAndSaveTracklistSuspended(context, oldTrack) + } + } + } + + } + + + /* + * Inner class: Legacy version of Track - used for one-time import only + * Warning: Works only as long as targetSdkVersion < 28 + */ + private data class LegacyTrack ( + @SerializedName("b") var mTrackFormatVersion: Int = 0, + @SerializedName("c") var mWayPoints: List, + @SerializedName("d") var mTrackLength: Float = 0f, + @SerializedName("e") var mDuration: Long = 0, + @SerializedName("f") var mStepCount: Float = 0f, + @SerializedName("g") var mRecordingStart: Date = GregorianCalendar.getInstance().time, + @SerializedName("h") var mRecordingStop: Date = mRecordingStart, + @SerializedName("i") var mMaxAltitude: Double = 0.0, + @SerializedName("j") var mMinAltitude: Double = 0.0, + @SerializedName("k") var mPositiveElevation: Double = 0.0, + @SerializedName("l") var mNegativeElevation: Double = 0.0) { + + + /* Converts */ + fun toTrack():Track { + val track: Track = Track() + track.trackFormatVersion = mTrackFormatVersion + mWayPoints.forEach { legacyWayPoint -> + val wayPoint: WayPoint= WayPoint( + provider = legacyWayPoint.mLocation.provider, + latitude = legacyWayPoint.mLocation.latitude, + longitude = legacyWayPoint.mLocation.longitude, + altitude = legacyWayPoint.mLocation.altitude, + accuracy = legacyWayPoint.mLocation.accuracy, + time = legacyWayPoint.mLocation.time, + distanceToStartingPoint = legacyWayPoint.mDistanceToStartingPoint, + numberSatellites = legacyWayPoint.mNumberSatellites, + isStopOver = legacyWayPoint.mIsStopOver + ) + track.wayPoints.add(wayPoint) + } + track.length = mTrackLength + track.duration = mDuration + track.stepCount = mStepCount + track.recordingStart = mRecordingStart + track.recordingStop = mRecordingStop + track.maxAltitude = mMaxAltitude + track.minAltitude = mMinAltitude + track.positiveElevation = mPositiveElevation + track.negativeElevation = mNegativeElevation + track.latitude = track.wayPoints[0].latitude + track.longitude = track.wayPoints[0].longitude + track.name = DateTimeHelper.convertToReadableDate(mRecordingStart) + return track + } + + } + /* + * End of inner class + */ + + + /* + * Inner class: Legacy version of WayPoint - used for one-time import only + * Warning: Works only as long as targetSdkVersion < 28 + */ + private data class LegacyWayPoint ( + var mLocation: Location, + var mIsStopOver: Boolean = false, + var mDistanceToStartingPoint: Float = 0f, + var mNumberSatellites: Int = 0) { + } + /* + * End of inner class + */ + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java deleted file mode 100755 index c6295d3..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * LengthUnitHelper.java - * Implements the LengthUnitHelper class - * A LengthUnitHelper offers helper methods for dealing with unit systems and locales - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import java.text.NumberFormat; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - - -/** - * LengthUnitHelper class - */ -public final class LengthUnitHelper implements TrackbookKeys { - - - /* Converts for the default locale a distance value to a readable string */ - public static String convertDistanceToString(double distance) { - return convertDistanceToString(distance, getUnitSystem()); - } - - - /* Converts for the given uni System a distance value to a readable string */ - public static String convertDistanceToString(double distance, int unitSystem) { - String unit; - NumberFormat numberFormat = NumberFormat.getNumberInstance(); - - // check for locale and set unit system accordingly - if (unitSystem == IMPERIAL) { - // miles and feet - if (distance > 1610) { - // convert distance to miles - distance = distance * 0.000621371192; - // set measurement unit - unit = "mi"; - // set number precision - numberFormat.setMaximumFractionDigits(2); - } else { - // convert distance to feet - distance = distance * 3.28084f; - // set measurement unit - unit = "ft"; - // set number precision - numberFormat.setMaximumFractionDigits(0); - } - - - } else { - // kilometer and meter - if (distance >= 1000) { - // convert distance to kilometer - distance = distance * 0.001f; - // set measurement unit - unit = "km"; - // set number precision - numberFormat.setMaximumFractionDigits(2); - } else { - // set measurement unit - unit = "m"; - // set number precision - numberFormat.setMaximumFractionDigits(0); - } - - } - // format distance according to current locale - return numberFormat.format(distance) + " " + unit; - } - - - /* Determines which unit system the device is using (metric or imperial) */ - public static int getUnitSystem() { - // America (US), Liberia (LR), Myanmar(MM) use the imperial system - List imperialSystemCountries = Arrays.asList("US", "LR", "MM"); - String countryCode = Locale.getDefault().getCountry(); - if (imperialSystemCountries.contains(countryCode)){ - return IMPERIAL; - } else { - return METRIC; - } - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt new file mode 100644 index 0000000..96f8106 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt @@ -0,0 +1,95 @@ +/* + * LengthUnitHelper.kt + * Implements the LengthUnitHelper object + * A LengthUnitHelper offers helper methods for dealing with unit systems and locales + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import java.text.NumberFormat +import java.util.* + + +/* + * LengthUnitHelper object + */ +object LengthUnitHelper { + + + /* Converts for the given uni System a distance value to a readable string */ + fun convertDistanceToString(distance: Float, useImperial: Boolean = false): String { + return convertDistanceToString(distance.toDouble(), useImperial) + } + + + /* Converts for the given uni System a distance value to a readable string */ + fun convertDistanceToString(distance: Double, useImperial: Boolean = false): String { + val readableDistance: Double + val unit: String + val numberFormat = NumberFormat.getNumberInstance() + + // check for locale and set unit system accordingly + when (useImperial) { + // CASE: miles and feet + true -> { + if (distance > 1610) { + // convert distance to miles + readableDistance = distance * 0.000621371192f + // set measurement unit + unit = "mi" + // set number precision + numberFormat.maximumFractionDigits = 2 + } else { + // convert distance to feet + readableDistance = distance * 3.28084f + // set measurement unit + unit = "ft" + // set number precision + numberFormat.maximumFractionDigits = 0 + } + } + // CASE: kilometer and meter + false -> { + if (distance >= 1000) { + // convert distance to kilometer + readableDistance = distance * 0.001f + // set measurement unit + unit = "km" + // set number precision + numberFormat.maximumFractionDigits = 2 + } else { + // no need to convert + readableDistance = distance + // set measurement unit + unit = "m" + // set number precision + numberFormat.maximumFractionDigits = 0 + } + } + } + + // format distance according to current locale + return "${numberFormat.format(readableDistance)} $unit" + } + + + /* Determines which unit system the device is using (metric or imperial) */ + fun useImperialUnits(): Boolean { + // America (US), Liberia (LR), Myanmar(MM) use the imperial system + val imperialSystemCountries = Arrays.asList("US", "LR", "MM") + val countryCode = Locale.getDefault().country + return imperialSystemCountries.contains(countryCode) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java deleted file mode 100755 index 41a4d6f..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java +++ /dev/null @@ -1,302 +0,0 @@ -/** - * LocationHelper.java - * Implements the LocationHelper class - * A LocationHelper offers helper methods for dealing with location issues - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.SystemClock; -import android.provider.Settings; - -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -import androidx.annotation.Nullable; - - -/** - * LocationHelper class - */ -public final class LocationHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = LocationHelper.class.getSimpleName(); - - - /* Determines last known location */ - public static Location determineLastKnownLocation(LocationManager locationManager) { - // define variables - List locationProviders = locationManager.getProviders(true); - Location gpsLocation = null; - Location networkLocation = null; - - // set location providers - String gpsProvider = LocationManager.GPS_PROVIDER; - String networkProvider = LocationManager.NETWORK_PROVIDER; - - - if (locationProviders.contains(gpsProvider)) { - // get last know location from gps - try { - gpsLocation = locationManager.getLastKnownLocation(gpsProvider); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - if (locationProviders.contains(networkProvider)) { - // get last known location from wifi and cell - try { - networkLocation = locationManager.getLastKnownLocation(networkProvider); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - if (gpsLocation == null) { - return networkLocation; - } else if (networkLocation == null) { - return gpsLocation; - } else if (isBetterLocation(gpsLocation, networkLocation)) { - return gpsLocation; - } else { - return networkLocation; - } - } - - - /* Determines whether one location reading is better than the current location fix */ - public static boolean isBetterLocation(Location location, Location currentBestLocation) { - // credit: the isBetterLocation method was sample code from: https://developer.android.com/guide/topics/location/strategies.html - - if (currentBestLocation == null) { - // a new location is always better than no location - return true; - } - - // check whether the new location fix is newer or older - long timeDelta = location.getElapsedRealtimeNanos() - currentBestLocation.getElapsedRealtimeNanos(); - boolean isSignificantlyNewer = timeDelta > ONE_MINUTE_IN_NANOSECONDS; - boolean isSignificantlyOlder = timeDelta < -ONE_MINUTE_IN_NANOSECONDS; - boolean isNewer = timeDelta > 0; - - // if it's been more than two minutes since the current location, use the new location because the user has likely moved - if (isSignificantlyNewer) { - return true; - } else if (isSignificantlyOlder) { - return false; - } - - // check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - boolean isLessAccurate = accuracyDelta > 0; - boolean isMoreAccurate = accuracyDelta < 0; - boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // check if the old and new location are from the same provider - boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); - - // determine location quality using a combination of timeliness and accuracy - if (isMoreAccurate) { - return true; - } else if (isNewer && !isLessAccurate) { - return true; - } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { - return true; - } - return false; - } - - - /* Checks accuracy of given location */ - public static boolean isAccurate(Location location) { - return location.getAccuracy() < FIFTY_METER_RADIUS; - } - - - /* Checks if given location is newer than two minutes */ - public static boolean isCurrent(Location location) { - if (location == null) { - return false; - } else { - long locationAge = SystemClock.elapsedRealtimeNanos() - location.getElapsedRealtimeNanos(); - return locationAge < ONE_MINUTE_IN_NANOSECONDS; - } - } - - - /* Checks if given location is a new WayPoint */ - public static boolean isNewWayPoint(Location lastLocation, Location newLocation, float averageSpeed) { - float distance = newLocation.distanceTo(lastLocation); - long timeDifference = newLocation.getElapsedRealtimeNanos() - lastLocation.getElapsedRealtimeNanos(); - - if (newLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER)) { - // calculate speed difference - float speedDifference; - float currentSpeed = distance / ((float)timeDifference / ONE_SECOND_IN_NANOSECOND); - if (currentSpeed > averageSpeed) { - speedDifference = currentSpeed / averageSpeed; - } else { - speedDifference = averageSpeed / currentSpeed; - } - - // SPECIAL CASE network: plausibility check for network provider. looking for sudden location jump errors - if (averageSpeed != 0f && currentSpeed > 10f && speedDifference > 2f) { - // implausible location (speed is high (10 m/s == 36km/h) and has doubled) - return false; - } - - // SPECIAL CASE network: if last location came from gps. only accept location fixes with decent accuracy - if (lastLocation.getProvider().equals(LocationManager.GPS_PROVIDER) && newLocation.getAccuracy() < 66) { - // network locations tend to be too in accurate - return false; - } - - // DEFAULT network: distance is bigger than 30 meters and time difference bigger than 12 seconds - return distance > 30 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND; // TODO add minimal accuracy - - } else { - // DEFAULT GPS: distance is bigger than 10 meters and time difference bigger than 12 seconds - return distance > 10 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND; - } - - } - - - /* Checks if given location is a stop over */ - public static boolean isStopOver(@Nullable Location previousLocation, Location newLocation) { - if (previousLocation != null) { - long timeDifference = newLocation.getElapsedRealtimeNanos() - previousLocation.getElapsedRealtimeNanos(); - return timeDifference >= FIVE_MINUTES_IN_NANOSECONDS; - } else { - return false; - } - } - - - /* Registers gps and network location listeners */ - public static void registerLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) { - LogHelper.v(LOG_TAG, "Registering location listeners."); - - // get location providers - List locationProviders = locationManager.getAllProviders(); - - // got GPS location provider? - if (gpsListener != null && locationProviders.contains(LocationManager.GPS_PROVIDER)) { - try { - // register GPS location listener and request updates - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsListener); - LogHelper.v(LOG_TAG, "Registering gps listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - // got network location provider? - if (networkListener != null && locationProviders.contains(LocationManager.NETWORK_PROVIDER)) { - try { - // register network location listener and request updates - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, networkListener); - LogHelper.v(LOG_TAG, "Registering network listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - } - - - /* Removes gps and network location listeners */ - public static void removeLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) { - LogHelper.v(LOG_TAG, "Removing location listeners."); - - // get location providers - List locationProviders = locationManager.getAllProviders(); - - // got GPS location provider? - if (locationProviders.contains(LocationManager.GPS_PROVIDER) && gpsListener != null) { - try { - // remove GPS listener - locationManager.removeUpdates(gpsListener); - LogHelper.v(LOG_TAG, "Removing gps listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - // got network location provider? - if (locationProviders.contains(LocationManager.NETWORK_PROVIDER) && networkListener != null) { - try { - // remove network listener - locationManager.removeUpdates(networkListener); - LogHelper.v(LOG_TAG, "Removing network listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - } - - - /* Converts milliseconds to mm:ss or hh:mm:ss */ - public static String convertToReadableTime(long milliseconds, boolean includeHours) { - - if (includeHours) { - // format hh:mm:ss - return String.format(Locale.ENGLISH, "%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(milliseconds), - TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1), - TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)); - } else if (TimeUnit.MILLISECONDS.toHours(milliseconds) < 1) { - // format mm:ss - return String.format(Locale.ENGLISH, "%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1), - TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)); - } else { - return null; - } - - } - - - /* Check if any location provider is enabled */ - public static boolean checkLocationSystemSetting(Context context) { - int locationSettingState = 0; - try { - locationSettingState = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE); - } catch (Settings.SettingNotFoundException e) { - e.printStackTrace(); - } - return locationSettingState != Settings.Secure.LOCATION_MODE_OFF; - } - - - /* Checks whether two location providers are the same */ - private static boolean isSameProvider(String provider1, String provider2) { - // credit: the isSameProvider method was sample code from: https://developer.android.com/guide/topics/location/strategies.html - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt new file mode 100644 index 0000000..888369e --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt @@ -0,0 +1,218 @@ +/* + * LocationHelper.kt + * Implements the LocationHelper object + * A LocationHelper offers helper methods for dealing with location issues + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.os.SystemClock +import androidx.core.content.ContextCompat +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import java.util.* + + +/* + * Keys object + */ +object LocationHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(LocationHelper::class.java) + + + /* Get default location */ + fun getDefaultLocation(): Location { + val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER) + defaultLocation.latitude = Keys.DEFAULT_LATITUDE + defaultLocation.longitude = Keys.DEFAULT_LONGITUDE + defaultLocation.accuracy = Keys.DEFAULT_ACCURACY + defaultLocation.altitude = Keys.DEFAULT_ALTITUDE + defaultLocation.time = Keys.DEFAULT_DATE.time + return defaultLocation + } + + + /* Checks if a location is older than one minute */ + fun isOldLocation(location: Location): Boolean { + // check how many milliseconds the given location is old + return GregorianCalendar.getInstance().time.time - location.time > Keys.SIGNIFICANT_TIME_DIFFERENCE + } + + + /* Tries to return the last location that the system has stored */ + fun getLastKnownLocation(context: Context): Location { + // get last location that Trackbook has stored + var lastKnownLocation: Location = PreferencesHelper.loadCurrentBestLocation(context) + // try to get the last location the system has stored - it is probably more recent + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val lastKnownLocationGps: Location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) ?: lastKnownLocation + val lastKnownLocationNetwork: Location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) ?: lastKnownLocation + when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) { + true -> lastKnownLocation = lastKnownLocationGps + false -> lastKnownLocation = lastKnownLocationNetwork + } + } + return lastKnownLocation + } + + + /* Determines whether one Location reading is better than the current Location fix */ + fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean { + // Credit: https://developer.android.com/guide/topics/location/strategies.html#BestEstimate + + if (currentBestLocation == null) { + // a new location is always better than no location + return true + } + + // check whether the new location fix is newer or older + val timeDelta: Long = location.time - currentBestLocation.time + val isSignificantlyNewer: Boolean = timeDelta > Keys.SIGNIFICANT_TIME_DIFFERENCE + val isSignificantlyOlder:Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE + + when { + // if it's been more than two minutes since the current location, use the new location because the user has likely moved + isSignificantlyNewer -> return true + // if the new location is more than two minutes older, it must be worse + isSignificantlyOlder -> return false + } + + // check whether the new location fix is more or less accurate + val isNewer: Boolean = timeDelta > 0L + val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy + val isLessAccurate: Boolean = accuracyDelta > 0f + val isMoreAccurate: Boolean = accuracyDelta < 0f + val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f + + // check if the old and new location are from the same provider + val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider + + // determine location quality using a combination of timeliness and accuracy + return when { + isMoreAccurate -> true + isNewer && !isLessAccurate -> true + isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true + else -> false + } + } + + + /* Checks if GPS location provider is available and enabled */ + fun isGpsEnabled(locationManager: LocationManager): Boolean { + if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) { + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } else { + return false + } + } + + + /* Checks if Network location provider is available and enabled */ + fun isNetworkEnabled(locationManager: LocationManager): Boolean { + if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER)) { + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } else { + return false + } + } + + + + /* Checks if given location is new */ + fun isRecentEnough(location: Location): Boolean { + val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos + return locationAge < Keys.DEFAULT_THRESHOLD_LOCATION_AGE + } + + + /* Checks if given location is accurate */ + fun isAccurateEnough(location: Location, locationAccuracyThreshold: Int): Boolean { + val isAccurate: Boolean + when (location.provider) { + LocationManager.GPS_PROVIDER -> isAccurate = location.accuracy < locationAccuracyThreshold + else -> isAccurate = location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider + } + return isAccurate + } + + + /* Checks if given location is different enough compared to previous location */ + fun isDifferentEnough(previousLocation: Location?, location: Location): Boolean { + // check if previous location is (not) available + if (previousLocation == null) return true + // check if distance between is large enough + val distanceThreshold: Float + val averageAccuracy: Float = (previousLocation.accuracy + location.accuracy) / 2 + // increase the distance threshold if one or both locations are + if (averageAccuracy > Keys.DEFAULT_THRESHOLD_DISTANCE) { + distanceThreshold = averageAccuracy + } else { + distanceThreshold = Keys.DEFAULT_THRESHOLD_DISTANCE + } + LogHelper.e(TAG, "distanceThreshold -> $distanceThreshold") // todo remove + // location is different when far enough away from previous location + return calculateDistance(previousLocation, location) > distanceThreshold + } + + + /* Calculates distance between two locations */ + fun calculateDistance(previousLocation: Location?, location: Location): Float { + var distance: Float = 0f + // two data points needed to calculate distance + if (previousLocation != null) { + // add up distance + distance = previousLocation.distanceTo(location) + } + return distance + } + + + /* Calculate elevation differences */ + fun calculateElevationDifferences(previousLocation: Location?, location: Location, track: Track): Pair { + // store current values + var positiveElevation: Double = track.positiveElevation + var negativeElevation: Double = track.negativeElevation + if (previousLocation != null) { + // factor is bigger than 1 if the time stamp difference is larger than the movement recording interval + val timeDifferenceFactor: Long = (location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL + // get elevation difference and sum it up + val altitudeDifference: Double = location.altitude - previousLocation.altitude + if (altitudeDifference > 0 && altitudeDifference < Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) { + positiveElevation = track.positiveElevation + altitudeDifference // upwards movement + } + if (altitudeDifference < 0 && altitudeDifference > -Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) { + negativeElevation = track.negativeElevation + altitudeDifference // downwards movement + } + } + return Pair(positiveElevation, negativeElevation) + } + + + /* Checks if given location is a stop over */ + fun isStopOver(previousLocation: Location?, location: Location): Boolean { + if (previousLocation == null) return false + // check how many milliseconds the given locations are apart + return location.time - previousLocation.time > Keys.STOP_OVER_THRESHOLD + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java deleted file mode 100755 index eae3ae4..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * LogHelper.java - * Implements the LogHelper class - * A LogHelper wraps the logging calls to be able to strip them out of release versions - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.helpers; - -import android.util.Log; - -import org.y20k.trackbook.BuildConfig; - - -/** - * LogHelper class - */ -public final class LogHelper { - - private final static boolean mTesting = false; - - public static void d(final String tag, String message) { - // include logging only in debug versions - if (BuildConfig.DEBUG || mTesting) { - Log.d(tag, message); - } - } - - - public static void v(final String tag, String message) { - // include logging only in debug versions - if (BuildConfig.DEBUG || mTesting) { - Log.v(tag, message); - } - } - - - public static void e(final String tag, String message) { - Log.e(tag, message); - } - - - public static void i(final String tag, String message) { - Log.i(tag, message); - } - - - public static void w(final String tag, String message) { - Log.w(tag, message); - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt new file mode 100644 index 0000000..5ca9427 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt @@ -0,0 +1,115 @@ +/* + * LogHelper.kt + * Implements the LogHelper object + * A LogHelper wraps the logging calls to be able to strip them out of release versions + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.util.Log +import org.y20k.trackbook.BuildConfig + + +/* + * LogHelper object + */ +object LogHelper { + + private const val TESTING: Boolean = true // set to "false" + private const val LOG_PREFIX: String = "trackbook_" + private const val MAX_LOG_TAG_LENGTH: Int = 64 + private const val LOG_PREFIX_LENGTH: Int = LOG_PREFIX.length + + fun makeLogTag(str: String): String { + return if (str.length > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) { + LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1) + } else LOG_PREFIX + str + } + + fun makeLogTag(cls: Class<*>): String { + // don't use this when obfuscating class names + return makeLogTag(cls.simpleName) + } + + fun v(tag: String, vararg messages: Any) { + // Only log VERBOSE if build type is DEBUG or if TESTING is true + if (BuildConfig.DEBUG || TESTING) { + log(tag, Log.VERBOSE, null, *messages) + } + } + + fun d(tag: String, vararg messages: Any) { + // Only log DEBUG if build type is DEBUG or if TESTING is true + if (BuildConfig.DEBUG || TESTING) { + log(tag, Log.DEBUG, null, *messages) + } + } + + fun i(tag: String, vararg messages: Any) { + log(tag, Log.INFO, null, *messages) + } + + fun w(tag: String, vararg messages: Any) { + log(tag, Log.WARN, null, *messages) + } + + fun w(tag: String, t: Throwable, vararg messages: Any) { + log(tag, Log.WARN, t, *messages) + } + + fun e(tag: String, vararg messages: Any) { + log(tag, Log.ERROR, null, *messages) + } + + fun e(tag: String, t: Throwable, vararg messages: Any) { + log(tag, Log.ERROR, t, *messages) + } + + private fun log(tag: String, level: Int, t: Throwable?, vararg messages: Any) { + val message: String + if (t == null && messages.size == 1) { + // handle this common case without the extra cost of creating a stringbuffer: + message = messages[0].toString() + } else { + val sb = StringBuilder() + for (m in messages) { + sb.append(m) + } + if (t != null) { + sb.append("\n").append(Log.getStackTraceString(t)) + } + message = sb.toString() + } + Log.println(level, tag, message) + +// if (Log.isLoggable(tag, level)) { +// val message: String +// if (t == null && messages != null && messages.size == 1) { +// // handle this common case without the extra cost of creating a stringbuffer: +// message = messages[0].toString() +// } else { +// val sb = StringBuilder() +// if (messages != null) +// for (m in messages) { +// sb.append(m) +// } +// if (t != null) { +// sb.append("\n").append(Log.getStackTraceString(t)) +// } +// message = sb.toString() +// } +// Log.println(level, tag, message) +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java deleted file mode 100755 index 67e1760..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java +++ /dev/null @@ -1,275 +0,0 @@ -/** - * MapHelper.java - * Implements the MapHelper class - * A MapHelper offers helper methods for dealing with Trackbook's map - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.location.Location; -import android.widget.Toast; - -import org.osmdroid.util.BoundingBox; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.overlay.ItemizedIconOverlay; -import org.osmdroid.views.overlay.OverlayItem; -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.WayPoint; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - - -/** - * MapHelper class - */ -public final class MapHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = MapHelper.class.getSimpleName(); - - - /* Creates icon overlay for current position (used in MainActivity Fragment) */ - public static ItemizedIconOverlay createMyLocationOverlay(final Context context, Location currentBestLocation, boolean locationIsNew, boolean trackingActive) { - - final ArrayList overlayItems = new ArrayList<>(); - - // create marker - Drawable newMarker; - if (locationIsNew && !trackingActive) { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_24dp); - } else if (!locationIsNew && trackingActive) { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_grey_24dp); - } else { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp); - } - - OverlayItem overlayItem = createOverlayItem(context, currentBestLocation); - overlayItem.setMarker(newMarker); - - // add marker to list of overlay items - overlayItems.add(overlayItem); - - // create and return overlay for current position - return new ItemizedIconOverlay<>(overlayItems, - new ItemizedIconOverlay.OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(final int index, final OverlayItem item) { - // tap on My Location dot icon - Toast.makeText(context, item.getTitle() + " | " + item.getSnippet(), Toast.LENGTH_LONG).show(); - return true; - } - - @Override - public boolean onItemLongPress(final int index, final OverlayItem item) { - // long press on My Location dot icon - return true; - } - }, context); - } - - - /* Creates icon overlay for track */ - public static ItemizedIconOverlay createTrackOverlay(final Context context, Track track, boolean trackingActive){ - - final ArrayList overlayItems = new ArrayList<>(); - boolean currentPosition; - final int trackSize = track.getSize(); - final List wayPoints = track.getWayPoints(); - WayPoint wayPoint; - - for (int i = 0; i < track.getSize(); i++) { - - // get WayPoint and check if it is current position - wayPoint = wayPoints.get(i); - currentPosition = i == trackSize - 1; - - // create marker - Drawable newMarker; - - // CASE 1: Tracking active and WayPoint is not current position - if (trackingActive && !currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_red_24dp); - } - } - - // CASE 2: Tracking active and WayPoint is current position - else if (trackingActive && currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_24dp); - } - } - - // CASE 3: Tracking not active and WayPoint is not current position - else if (!trackingActive && !currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp); - } - } - - // CASE 4: Tracking not active and WayPoint is current position - else { - // default marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp); - } - - // create overlay item - OverlayItem overlayItem = createOverlayItem(context, wayPoint.getLocation()); - overlayItem.setMarker(newMarker); - - // add marker to list of overlay items - overlayItems.add(overlayItem); - } - - // return overlay for current position - return new ItemizedIconOverlay<>(overlayItems, - new ItemizedIconOverlay.OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(final int index, final OverlayItem item) { - // tap on waypoint - Toast.makeText(context, item.getTitle(), Toast.LENGTH_LONG).show(); - return true; - } - - @Override - public boolean onItemLongPress(final int index, final OverlayItem item) { - // long press on waypoint - Toast.makeText(context, item.getSnippet(), Toast.LENGTH_LONG).show(); - return true; - } - - }, context); - } - - - /* Creates a marker overlay item */ - private static OverlayItem createOverlayItem(Context context, Location location) { - // create content of overlay item - String time = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(location.getTime()); - final String title = context.getString(R.string.marker_description_source) + ": " + location.getProvider() + " | " + context.getString(R.string.marker_description_time) + ": " + time; - final String description = context.getString(R.string.marker_description_accuracy) + ": " + location.getAccuracy(); - final GeoPoint position = new GeoPoint(location.getLatitude(),location.getLongitude()); - - return new OverlayItem(title, description, position); - } - - - - /** - * Create a {@code BoundingBox} for the collection of - * {@code WayPoint}s, so that it would be possible to fit the map in - * such box and see the whole {@code Track} in the map without - * manual zooming. - * - * @return {@code BoundingBox} containing all {@code Waypoint}s - */ - public static BoundingBox calculateBoundingBox(List wayPoints) { - final ArrayList geoPoints = new ArrayList<>(wayPoints.size()); - - for (final WayPoint aWayPoint : wayPoints) { - final GeoPoint aGeoPoint = new GeoPoint(aWayPoint.getLocation()); - geoPoints.add(aGeoPoint); - } - return BoundingBox.fromGeoPoints(geoPoints).increaseByScale(1.15f); - } - - - - /* Calculates positive and negative elevation of track */ - public static Track calculateElevation(@Nullable Track track) { - double maxAltitude = 0; - double minAltitude = 0; - double positiveElevation = 0; - double negativeElevation = 0; - - if (track != null && track.getWayPoints().size() > 0) { - double previousLocationAltitude; - double currentLocationAltitude; - long previousTimeStamp; - long currentTimeStamp; - - // initial values for max height and min height - first waypoint - maxAltitude = track.getWayPointLocation(0).getAltitude(); - minAltitude = maxAltitude; - - // apply filter & smooth data -// track = smoothTrack(track, 15f, 35f); - - // iterate over track - for (int i = 1; i < track.getWayPoints().size(); i++ ) { - - // get time difference - previousTimeStamp = track.getWayPointLocation(i -1).getTime(); - currentTimeStamp = track.getWayPointLocation(i).getTime(); - double timeDiff = (currentTimeStamp - previousTimeStamp); - - // factor is bigger than 1 if the time stamp difference is larger than the movement recording interval (usually 15 seconds) - double timeDiffFactor = timeDiff / FIFTEEN_SECONDS_IN_MILLISECONDS; - - // height of previous and current waypoints - previousLocationAltitude = track.getWayPointLocation(i -1).getAltitude(); - currentLocationAltitude = track.getWayPointLocation(i).getAltitude(); - - // check for new min and max heights - if (currentLocationAltitude > maxAltitude) { - maxAltitude = currentLocationAltitude; - } - if (minAltitude == 0 || currentLocationAltitude < minAltitude) { - minAltitude = currentLocationAltitude; - } - - // get elevation difference and sum it up - double altitudeDiff = currentLocationAltitude - previousLocationAltitude; - if (altitudeDiff > 0 && altitudeDiff < MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) { - positiveElevation = positiveElevation + altitudeDiff; - } - if (altitudeDiff < 0 && altitudeDiff > -MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) { - negativeElevation = negativeElevation + altitudeDiff; - } - - } - - // store elevation data in track - track.setMaxAltitude(maxAltitude); - track.setMinAltitude(minAltitude); - track.setPositiveElevation(positiveElevation); - track.setNegativeElevation(negativeElevation); - } - return track; - } - - - - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt new file mode 100644 index 0000000..1d191c8 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt @@ -0,0 +1,148 @@ +/* + * MapHelper.kt + * Implements the MapHelper object + * A MapHelper offers helper methods for manipulating osmdroid maps + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + + +import android.content.Context +import android.graphics.drawable.Drawable +import android.location.Location +import android.os.Vibrator +import android.widget.Toast +import androidx.core.content.ContextCompat +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.* + + +/* + * MapHelper object + */ +object MapHelper { + + /* Define log tag */ + private val LOG_TAG = MapHelper::class.java.simpleName + + + /* Creates icon overlay for current position (used in MapFragment) */ + fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay { + + val overlayItems = ArrayList() + val locationIsOld = LocationHelper.isOldLocation(location) + + // create marker + val newMarker: Drawable + when (trackingState) { + // CASE: Tracking active + Keys.STATE_TRACKING_ACTIVE -> { + when (locationIsOld) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_24dp)!! + } + } + // CASE. Tracking is NOT active + else -> { + when (locationIsOld) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_24dp)!! + } + } + } + + // add marker to list of overlay items + val overlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time) + overlayItem.setMarker(newMarker) + overlayItems.add(overlayItem) + + // create and return overlay for current position + return createOverlay(context, overlayItems) + } + + + /* Creates icon overlay for track */ + fun createTrackOverlay(context: Context, track: Track, trackingState: Int): ItemizedIconOverlay { + + val overlayItems = ArrayList() + val wayPoints = track.wayPoints + + wayPoints.forEach { wayPoint -> + // create marker + val newMarker: Drawable + + // get drawable + when (trackingState) { + // CASE: Recording is active + Keys.STATE_TRACKING_ACTIVE -> { + when (wayPoint.isStopOver) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!! + } + } + // CASE: Recording is paused/stopped + else -> { + when (wayPoint.isStopOver) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_blue_24dp)!! + } + } + } + + // create overlay item and add to list of overlay items + val overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) + overlayItem.setMarker(newMarker) + overlayItems.add(overlayItem) + } + + // create and return overlay for current position + return createOverlay(context, overlayItems) + } + + + /* Creates a marker overlay item */ + private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem { + val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}" + val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})" + val position: GeoPoint = GeoPoint(latitude, longitude) + return OverlayItem(title, description, position) + } + + + /* Creates an overlay */ + private fun createOverlay(context: Context, overlayItems: ArrayList): ItemizedIconOverlay { + return ItemizedIconOverlay(overlayItems, + object : ItemizedIconOverlay.OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { + Toast.makeText(context, item.title, Toast.LENGTH_LONG).show() + return true + } + override fun onItemLongPress(index: Int, item: OverlayItem): Boolean { + val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + v.vibrate(50) + Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show() + return true + } + }, context) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java deleted file mode 100755 index 4b8131c..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java +++ /dev/null @@ -1,177 +0,0 @@ -/** - * NightModeHelper.java - * Implements the NightModeHelper class - * A NightModeHelper can toggle and restore the state of the theme's Night Mode - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.os.Build; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatDelegate; - -import org.y20k.trackbook.R; - - -/** - * NightModeHelper class - */ -public final class NightModeHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = NightModeHelper.class.getSimpleName(); - - - /* Switches between modes: day, night, undefined */ - public static void switchMode(Activity activity) { - // SWITCH: undefined -> night / night -> day / day - undefined - switch (AppCompatDelegate.getDefaultNightMode()) { - case AppCompatDelegate.MODE_NIGHT_NO: - // currently: day mode -> switch to: follow system - displayDefaultStatusBar(activity); // necessary hack :-/ - activateFollowSystemMode(activity, true); - break; - case AppCompatDelegate.MODE_NIGHT_YES: - // currently: night mode -> switch to: day mode - displayLightStatusBar(activity); // necessary hack :-/ - activateDayMode(activity, true); - break; - default: - // currently: follow system / undefined -> switch to: day mode - displayLightStatusBar(activity); // necessary hack :-/ - activateNightMode(activity, true); - break; - } - } - - - /* Sets night mode / dark theme */ - public static void restoreSavedState(Context context) { - int savedNightModeState = loadNightModeState(context); - int currentNightModeState = AppCompatDelegate.getDefaultNightMode(); - if (savedNightModeState != currentNightModeState) { - switch (savedNightModeState) { - case AppCompatDelegate.MODE_NIGHT_NO: - // turn on day mode - activateDayMode(context, false); - break; - case AppCompatDelegate.MODE_NIGHT_YES: - // turn on night mode - activateNightMode(context, false); - break; - default: - // turn on mode "follow system" - activateFollowSystemMode(context, false); - break; - } - } - } - - - /* Return weather Night Mode is on, or not */ - public static Boolean getNightMode(Context context) { - int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - return nightMode == Configuration.UI_MODE_NIGHT_YES; - } - - - /* Returns state of night mode */ - private static int getCurrentNightModeState(Context context) { - return context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - } - - - /* Activates Night Mode */ - private static void activateNightMode(Context context, Boolean notifyUser) { - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES); - - // switch to Night Mode - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_SHORT).show(); - } - } - - - /* Activates Day Mode */ - private static void activateDayMode(Context context, Boolean notifyUser) { - // save the new state - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO); - - // switch to Day Mode - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show(); - } - } - - - /* Activate Mode "Follow System" */ - private static void activateFollowSystemMode(Context context, Boolean notifyUser) { - // save the new state - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - - // switch to Undefined Mode / Follow System - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show(); - } - } - - - /* Displays the default status bar */ - private static void displayDefaultStatusBar(Activity activity) { - View decorView = activity.getWindow().getDecorView(); - decorView.setSystemUiVisibility(0); - } - - - /* Displays the light (inverted) status bar - if possible */ - private static void displayLightStatusBar(Activity activity) { - View decorView = activity.getWindow().getDecorView(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } else { - decorView.setSystemUiVisibility(0); - } - } - - - /* Save state of night mode */ - private static void saveNightModeState(Context context, int currentState) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(PREF_NIGHT_MODE_STATE, currentState); - editor.apply(); - } - - - /* Load state of Night Mode */ - private static int loadNightModeState(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt(PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt new file mode 100644 index 0000000..594d131 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt @@ -0,0 +1,150 @@ +/* + * NightModeHelper.kt + * Implements the NightModeHelper object + * A NightModeHelper can toggle and restore the state of the theme's Night Mode + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatDelegate +import org.y20k.trackbook.R + + +/* + * NightModeHelper object + */ +object NightModeHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(NightModeHelper::class.java) + + + /* Switches between modes: day, night, undefined */ + @SuppressLint("SwitchIntDef") + fun switchMode(activity: Activity) { + // SWITCH: undefined -> night / night -> day / day - undefined + when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_NO -> { + // currently: day mode -> switch to: follow system + // displayDefaultStatusBar(activity) // necessary hack :-/ + activateFollowSystemMode(activity, true) + } + AppCompatDelegate.MODE_NIGHT_YES -> { + // currently: night mode -> switch to: day mode + // displayLightStatusBar(activity) // necessary hack :-/ + activateDayMode(activity, true) + } + else -> { + // currently: follow system / undefined -> switch to: day mode + // displayLightStatusBar(activity) // necessary hack :-/ + activateNightMode(activity, true) + } + } + } + + + /* Sets night mode / dark theme */ + fun restoreSavedState(context: Context) { + val savedNightModeState = PreferencesHelper.loadNightModeState(context) + val currentNightModeState = AppCompatDelegate.getDefaultNightMode() + if (savedNightModeState != currentNightModeState) { + when (savedNightModeState) { + AppCompatDelegate.MODE_NIGHT_NO -> + // turn on day mode + activateDayMode(context, false) + AppCompatDelegate.MODE_NIGHT_YES -> + // turn on night mode + activateNightMode(context, false) + else -> + // turn on mode "follow system" + activateFollowSystemMode(context, false) + } + } + } + + + /* Return weather Night Mode is on, or not */ + fun isNightModeOn(context: Context): Boolean { + val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return nightMode == Configuration.UI_MODE_NIGHT_YES + } + + + /* Activates Night Mode */ + private fun activateNightMode(context: Context, notifyUser: Boolean) { + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES) + + // switch to Night Mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_LONG).show() + } + } + + + /* Activates Day Mode */ + private fun activateDayMode(context: Context, notifyUser: Boolean) { + // save the new state + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO) + + // switch to Day Mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show() + } + } + + + /* Activate Mode "Follow System" */ + private fun activateFollowSystemMode(context: Context, notifyUser: Boolean) { + // save the new state + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + // switch to Undefined Mode / Follow System + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show() + } + } + + + /* Displays the default status bar */ + private fun displayDefaultStatusBar(activity: Activity) { + val decorView = activity.window.decorView + decorView.systemUiVisibility = 0 + } + + + /* Displays the light (inverted) status bar */ + private fun displayLightStatusBar(activity: Activity) { + val decorView = activity.window.decorView + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java deleted file mode 100755 index a86d277..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java +++ /dev/null @@ -1,171 +0,0 @@ -/** - * NotificationHelper.java - * Implements the NotificationHelper class - * A NotificationHelper creates and configures a notification - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.os.Build; - -import org.y20k.trackbook.MainActivity; -import org.y20k.trackbook.R; -import org.y20k.trackbook.TrackerService; -import org.y20k.trackbook.core.Track; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.TaskStackBuilder; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - - -/** - * NotificationHelper class - */ -public final class NotificationHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = NotificationHelper.class.getSimpleName(); - - - /* Creates a notification builder */ - public static Notification getNotification(Context context, NotificationCompat.Builder builder, Track track, boolean tracking) { - - // create notification channel - createNotificationChannel(context); - - // ACTION: NOTIFICATION TAP & BUTTON SHOW - Intent tapActionIntent = new Intent(context, MainActivity.class); - tapActionIntent.setAction(ACTION_SHOW_MAP); - tapActionIntent.putExtra(EXTRA_TRACK, track); - tapActionIntent.putExtra(EXTRA_TRACKING_STATE, tracking); - // artificial back stack for started Activity (https://developer.android.com/training/notify-user/navigation.html#DirectEntry) - TaskStackBuilder tapActionIntentBuilder = TaskStackBuilder.create(context); - tapActionIntentBuilder.addParentStack(MainActivity.class); - tapActionIntentBuilder.addNextIntent(tapActionIntent); - // pending intent wrapper for notification tap - PendingIntent tapActionPendingIntent = tapActionIntentBuilder.getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT); - - // ACTION: NOTIFICATION BUTTON STOP - Intent stopActionIntent = new Intent(context, TrackerService.class); - stopActionIntent.setAction(ACTION_STOP); - // pending intent wrapper for notification stop action - PendingIntent stopActionPendingIntent = PendingIntent.getService(context, 14, stopActionIntent, 0); - - // ACTION: NOTIFICATION BUTTON RESUME - Intent resumeActionIntent = new Intent(context, TrackerService.class); - resumeActionIntent.setAction(ACTION_RESUME); - // pending intent wrapper for notification resume action - PendingIntent resuneActionPendingIntent = PendingIntent.getService(context, 16, resumeActionIntent, 0); - - // construct notification in builder - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - builder.setShowWhen(false); - builder.setContentIntent(tapActionPendingIntent); - builder.setSmallIcon(R.drawable.ic_notification_small_24dp); - builder.setLargeIcon(getNotificationIconLarge(context, tracking)); - if (tracking) { - builder.addAction(R.drawable.ic_stop_white_24dp, context.getString(R.string.notification_stop), stopActionPendingIntent); - builder.setContentTitle(context.getString(R.string.notification_title_trackbook_running)); - builder.setContentText(getContextString(context, track)); - } else { - builder.addAction(R.drawable.ic_fiber_manual_record_white_24dp, context.getString(R.string.notification_resume), resuneActionPendingIntent); - builder.addAction(R.drawable.ic_compass_needle_white_24dp, context.getString(R.string.notification_show), tapActionPendingIntent); - builder.setContentTitle(context.getString(R.string.notification_title_trackbook_not_running)); - builder.setContentText(getContextString(context, track)); - } - - return builder.build(); - } - - - /* Constructs an updated notification */ - public static Notification getUpdatedNotification(Context context, NotificationCompat.Builder builder, Track track) { - builder.setContentText(getContextString(context, track)); - return builder.build(); - } - - - /* Create a notification channel */ - public static boolean createNotificationChannel(Context context) { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // API level 26 ("Android O") supports notification channels. - String id = NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL; - CharSequence name = context.getString(R.string.notification_channel_recording_name); - String description = context.getString(R.string.notification_channel_recording_description); - int importance = NotificationManager.IMPORTANCE_LOW; - - // create channel - NotificationChannel channel = new NotificationChannel(id, name, importance); - channel.setDescription(description); - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(channel); - return true; - - } else { - return false; - } - } - - - /* Get station image for notification's large icon */ - private static Bitmap getNotificationIconLarge(Context context, boolean tracking) { - - // get dimensions - Resources resources = context.getResources(); - int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height); - int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width); - - Bitmap bitmap; - if (tracking) { - bitmap = getBitmap(context, R.drawable.ic_notification_large_tracking_48dp); - } else { - bitmap = getBitmap(context, R.drawable.ic_notification_large_not_tracking_48dp); - } - - return Bitmap.createScaledBitmap(bitmap, width, height, false); - } - - - /* Return a bitmap for a given resource id of a vector drawable */ - private static Bitmap getBitmap(Context context, int resource) { - VectorDrawableCompat drawable = VectorDrawableCompat.create(context.getResources(), resource, null); - if (drawable != null) { - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } else { - return null; - } - } - - - /* Build context text for notification builder */ - private static String getContextString(Context context, Track track) { - return context.getString(R.string.notification_content_distance) + ": " + LengthUnitHelper.convertDistanceToString(track.getTrackDistance()) + " | " + - context.getString(R.string.notification_content_duration) + ": " + LocationHelper.convertToReadableTime(track.getTrackDuration(), true); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt new file mode 100644 index 0000000..2f2fe02 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt @@ -0,0 +1,135 @@ +/* + * NotificationHelper.kt + * Implements the NotificationHelper class + * A NotificationHelper creates and configures a notification + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.app.* +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.toBitmap +import org.y20k.trackbook.Keys +import org.y20k.trackbook.MainActivity +import org.y20k.trackbook.R +import org.y20k.trackbook.TrackerService + + +/* + * NotificationHelper class + */ +class NotificationHelper(private val trackerService: TrackerService) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(NotificationHelper::class.java) + + + + /* Main class variables */ + private val notificationManager: NotificationManager = trackerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + + /* Creates notification */ + fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification { + + // create notification channel if necessary + if (shouldCreateNotificationChannel()) { + createNotificationChannel() + } + + // Build notification + val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING) + builder.setContentIntent(showActionPendingIntent) + builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp) + builder.setContentText(getContentString(trackerService, duration, trackLength, useImperial)) + + // add icon and actions for stop, resume and show + when (trackingState) { + Keys.STATE_TRACKING_ACTIVE -> { + builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_running)) + builder.addAction(stopAction) + builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_active_48dp)!!.toBitmap()) + } + else -> { + builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_not_running)) + builder.addAction(resumeAction) + builder.addAction(showAction) + builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_stopped_48dp)!!.toBitmap()) + } + } + + return builder.build() + + } + + + /* Build context text for notification builder */ + private fun getContentString(context: Context, duration: Long, trackLength: Float, useImperial: Boolean): String { + return "${LengthUnitHelper.convertDistanceToString(trackLength, useImperial)} • ${DateTimeHelper.convertToReadableTime(context, duration)}" + } + + + /* Checks if notification channel should be created */ + private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists() + + + /* Checks if notification channel exists */ + @RequiresApi(Build.VERSION_CODES.O) + private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null + + + /* Create a notification channel */ + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val notificationChannel = NotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING, + trackerService.getString(R.string.notification_channel_recording_name), + NotificationManager.IMPORTANCE_LOW) + .apply { description = trackerService.getString(R.string.notification_channel_recording_description) } + notificationManager.createNotificationChannel(notificationChannel) + } + + + /* Notification pending intents */ + private val stopActionPendingIntent = PendingIntent.getService( + trackerService,14, + Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),0) + private val resumeActionPendingIntent = PendingIntent.getService( + trackerService, 16, + Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),0) + private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run { + addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java)) + getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT) + } + + + /* Notification actions */ + private val stopAction = NotificationCompat.Action( + R.drawable.ic_notification_action_stop_24dp, + trackerService.getString(R.string.notification_stop), + stopActionPendingIntent) + private val resumeAction = NotificationCompat.Action( + R.drawable.ic_notification_action_resume_36dp, + trackerService.getString(R.string.notification_resume), + resumeActionPendingIntent) + private val showAction = NotificationCompat.Action( + R.drawable.ic_notification_action_show_36dp, + trackerService.getString(R.string.notification_show), + showActionPendingIntent) + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt new file mode 100644 index 0000000..5ab9774 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt @@ -0,0 +1,167 @@ +/* + * PreferencesHelper.kt + * Implements the PreferencesHelper object + * A PreferencesHelper provides helper methods for the saving and loading values from shared preferences + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import org.y20k.trackbook.Keys +import org.y20k.trackbook.extensions.getDouble +import org.y20k.trackbook.extensions.putDouble + + +/* + * PreferencesHelper object + */ +object PreferencesHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(PreferencesHelper::class.java) + + + /* Loads zoom level of map */ + fun loadZoomLevel(context: Context): Double { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load zoom level + return settings.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL) + } + + + /* Saves zoom level of map */ + fun saveZoomLevel(context: Context, zoomLevel: Double) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save zoom level + editor.putDouble(Keys.PREF_MAP_ZOOM_LEVEL, zoomLevel) + editor.apply() + } + + + /* Loads tracking state */ + fun loadTrackingState(context: Context): Int { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load tracking state + return settings.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_NOT_TRACKING) + } + + + /* Saves tracking state */ + fun saveTrackingState(context: Context, trackingState: Int) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save tracking state + editor.putInt(Keys.PREF_TRACKING_STATE, trackingState) + editor.apply() + } + + + /* Loads length unit system - metric or imperial */ + fun loadUseImperialUnits(context: Context): Boolean { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load length unit system + return settings.getBoolean(Keys.PREF_USE_IMPERIAL_UNITS, LengthUnitHelper.useImperialUnits()) + } + + + /* Loads length unit system - metric or imperial */ + fun loadGpsOnly(context: Context): Boolean { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load length unit system + return settings.getBoolean(Keys.PREF_GPS_ONLY, false) + } + + /* Loads accuracy threshold used to determine if location is good enough */ + fun loadAccuracyThreshold(context: Context): Int { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load tracking state + return settings.getInt(Keys.PREF_LOCATION_ACCURACY_THRESHOLD, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) + } + + + /* Loads the state of a map */ + fun loadCurrentBestLocation(context: Context): Location { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val provider: String = settings.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER + // create location + val currentBestLocation: Location = Location(provider) + // load location attributes + currentBestLocation.latitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, Keys.DEFAULT_LATITUDE) + currentBestLocation.longitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, Keys.DEFAULT_LONGITUDE) + currentBestLocation.accuracy = settings.getFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, Keys.DEFAULT_ACCURACY) + currentBestLocation.altitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, Keys.DEFAULT_ALTITUDE) + currentBestLocation.time = settings.getLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, Keys.DEFAULT_TIME) + return currentBestLocation + } + + + /* Saves the state of a map */ + fun saveCurrentBestLocation(context: Context, currentBestLocation: Location) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save location + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, currentBestLocation.latitude) + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, currentBestLocation.longitude) + editor.putFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, currentBestLocation.accuracy) + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, currentBestLocation.altitude) + editor.putLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, currentBestLocation.time) + editor.apply() + } + + + /* Load state of Night Mode */ + fun loadNightModeState(context: Context): Int { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(Keys.PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + + + /* Save state of night mode */ + fun saveNightModeState(context: Context, currentState: Int) { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + editor.putInt(Keys.PREF_NIGHT_MODE_STATE, currentState) + editor.apply() + } + + + /* Checks if housekeeping work needs to be done - used usually in DownloadWorker "REQUEST_UPDATE_COLLECTION" */ + fun isHouseKeepingNecessary(context: Context): Boolean { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + return settings.getBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, true) + } + + + /* Saves state of housekeeping */ + fun saveHouseKeepingNecessaryState(context: Context, state: Boolean = false) { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + editor.putBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, state) + editor.apply() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java deleted file mode 100755 index 400b340..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java +++ /dev/null @@ -1,405 +0,0 @@ -/** - * StorageHelper.java - * Implements the StorageHelper class - * A StorageHelper deals with saving and loading recorded tracks - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.os.Environment; -import android.widget.Toast; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.TrackBuilder; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Date; -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.core.os.EnvironmentCompat; - - -/** - * StorageHelper class - */ -public class StorageHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = StorageHelper.class.getSimpleName(); - - /* Main class variables */ - private final Context mContext; - private final File mFolder; - private final File mTempFile; - - - /* Constructor */ - public StorageHelper(Context context) { - // store activity - mContext = context; - - // get "tracks" folder - mFolder = mContext.getExternalFilesDir(TRACKS_DIRECTORY_NAME); - // mFolder = getTracksDirectory(); - - // create "tracks" folder if necessary - if (mFolder != null && !mFolder.exists()) { - LogHelper.v(LOG_TAG, "Creating new folder: " + mFolder.toString()); - mFolder.mkdirs(); - } - - // create temp file object // todo check -> may produce NullPointerException - String tempFilePathName = mFolder.toString() + "/" + FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION; - mTempFile = new File(tempFilePathName); - - // delete old track - exclude temp file - deleteOldTracks(false); - } - - - /* Checks if a temp file exits */ - public boolean tempFileExists() { - return mTempFile.exists(); - } - - - /* Deletes temp file - if it exits */ - public boolean deleteTempFile() { - return mTempFile.exists() && mTempFile.delete(); - } - - - /* Saves track object to file */ - public boolean saveTrack(@Nullable Track track, int fileType) { - - Date recordingStart = null; - if (track != null) { - recordingStart = track.getRecordingStart(); - } - - if (mFolder != null && mFolder.exists() && mFolder.isDirectory() && mFolder.canWrite() && recordingStart != null && track != null) { - - // create file object and calculate bounding box and elevation, if necessary - String fileName; - if (fileType == FILE_TEMP_TRACK) { - // get the temp file name - fileName = FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION; - } else { - // build a regular file name - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - fileName = dateFormat.format(recordingStart) + FILE_TYPE_TRACKBOOK_EXTENSION; - // calculate elevation and store it in track - track = MapHelper.calculateElevation(track); - // calculate bounding box and store it in track - track.setBoundingBox(MapHelper.calculateBoundingBox(track.getWayPoints())); - } - File file = new File(mFolder.toString() + "/" + fileName); - - // convert track to JSON - Gson gson = getCustomGson(); - String json = gson.toJson(track); - - // write track - try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { - LogHelper.v(LOG_TAG, "Saving track to external storage: " + file.toString()); - bw.write(json); - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + file.toString()); - return false; - } - - // if write was successful delete old track files - only if not a temp file - if (fileType != FILE_TEMP_TRACK) { - // include temp file if it exists - deleteOldTracks(true); - } - - return true; - - } else { - LogHelper.e(LOG_TAG, "Unable to save track to external storage."); - return false; - } - - } - - - /* Loads given file into memory */ - public Track loadTrack(int fileType) { - - // get file reference - File trackFile; - switch (fileType) { - case FILE_TEMP_TRACK: - trackFile = getTempFile(); - break; - case FILE_MOST_CURRENT_TRACK: - trackFile = getMostCurrentTrack(); - break; - default: - trackFile = null; - break; - } - - // read & parse file and return track - return readTrackFromFile(trackFile); - } - - - /* Loads given file into memory */ - public Track loadTrack(File file) { - - // get file reference - File trackFile; - if (file != null) { - trackFile = file; - } else { - // fallback - trackFile = getMostCurrentTrack(); - } - - // read & parse file and return track - return readTrackFromFile(trackFile); - } - - - /* Gets a list of .trackbook files - excluding the temp file */ - public File[] getListOfTrackbookFiles() { - // TODO HANDLE CASE: EMPTY FILE LIST - - // get files and sort them - return sortFiles(mFolder.listFiles()); - } - - -// /* Gets a list of tracks based on their file names */ -// public List getListOfTracks() { -// List listOfTracks = new ArrayList(); -// -// // get files and sort them -// File[] files = mFolder.listFiles(); -// files = sortFiles(files); -// -// for (File file : files) { -// listOfTracks.add(file.getName()); -// } -// -// // TODO HANDLE CASE: EMPTY FILE LIST -// return listOfTracks; -// } - - - // loads file and parses it into a track - private Track readTrackFromFile(File file) { - - // check if given file was null - if (file == null) { - LogHelper.e(LOG_TAG, "Did not receive a file object."); - return null; - } - - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - LogHelper.v(LOG_TAG, "Loading track from external storage: " + file.toString()); - - // read until last line reached - String fileContent; - String singleLine; - StringBuilder sb = new StringBuilder(""); - while ((singleLine = br.readLine()) != null) { - sb.append(singleLine); - sb.append("\n"); - } - fileContent = sb.toString(); - - // prepare custom Gson and return Track object - Gson gson = getCustomGson(); - return gson.fromJson(fileContent, TrackBuilder.class).toTrack(); - - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to read file from external storage: " + file.toString()); - return null; - } - } - - - /* Creates a Gson object */ - private Gson getCustomGson() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setDateFormat("M/d/yy hh:mm a"); - return gsonBuilder.create(); - } - - /* Gets most current track from directory */ - private File getMostCurrentTrack() { - - if (mFolder != null && mFolder.isDirectory()) { - // get files and sort them - File[] files = mFolder.listFiles(); - files = sortFiles(files); - if (files.length > 0 && files[0].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[0].equals(mTempFile)){ - // return latest track - return files[0]; - } - } - LogHelper.e(LOG_TAG, "Unable to get files from given folder. Folder is probably empty."); - return null; - } - - - /* Gets temp file - if it exists */ - private File getTempFile() { - if (mTempFile.exists()) { - return mTempFile; - } else { - return null; - } - } - - - /* Gets the last track from directory */ - private void deleteOldTracks(boolean includeTempFile) { - - if (mFolder != null && mFolder.isDirectory()) { - LogHelper.v(LOG_TAG, "Deleting older recordings."); - - // get files and sort them - File[] files = mFolder.listFiles(); - files = sortFiles(files); - - // store length of array - int numberOfFiles = files.length; - - // keep the latest ten (mMaxTrackFiles) track files - int index = MAXIMUM_TRACK_FILES; - // iterate through array - while (index < numberOfFiles && files[index].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[index].equals(mTempFile)) { - files[index].delete(); - index++; - } - } - - // delete temp file if it exists - if (includeTempFile && mTempFile.exists()) { - mTempFile.delete(); - } - - } - - - /* Sorts array of files in a way that the newest files are at the top and non-.trackbook files are at the bottom */ - private File[] sortFiles(File[] files) { - // sort array - LogHelper.v(LOG_TAG, "Sorting files."); - Arrays.sort(files, new Comparator() { - @Override - public int compare(File file1, File file2) { - - // discard temp file and files not ending with ".trackbook" - boolean file1IsTrack = file1.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file1.equals(mTempFile); - boolean file2IsTrack = file2.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file2.equals(mTempFile); - - // note: "greater" means higher index in array - if (!file1IsTrack && file2IsTrack) { - // file1 is not a track, file1 is greater - return 1; - } else if (!file2IsTrack && file1IsTrack) { - // file2 is not a track, file2 is greater - return -1; - } else { - // "compareTo" compares abstract path names lexicographically | 0 == equal | -1 == file2 less than file1 | 1 == file2 greater than file1 - return file2.compareTo(file1); - } - - } - }); - - // hand back sorted array of files - return files; - } - - - /* Return a write-able sub-directory from external storage */ - private File getTracksDirectory() { - File[] storage = mContext.getExternalFilesDirs(TRACKS_DIRECTORY_NAME); - for (File file : storage) { - if (file != null) { - String state = EnvironmentCompat.getStorageState(file); - if (Environment.MEDIA_MOUNTED.equals(state)) { - LogHelper.i(LOG_TAG, "External storage: " + file.toString()); - return file; - } - } - } - Toast.makeText(mContext, R.string.toast_message_no_external_storage, Toast.LENGTH_LONG).show(); - LogHelper.e(LOG_TAG, "Unable to access external storage."); - - return null; - } - - - /* Tries to smooth the elevation data using a low pass filter */ - private Track smoothTrack(Track input, float dt, float rc) { - - // The following code is adapted from https://en.wikipedia.org/wiki/Low-pass_filter - // - // // Return RC low-pass filter output samples, given input samples, - // // time interval dt, and time constant RC - // function lowpass(real[0..n] x, real dt, real RC) - // var real[0..n] y - // var real α := dt / (RC + dt) - // y[0] := α * x[0] - // for i from 1 to n - // y[i] := α * x[i] + (1-α) * y[i-1] - // return y - - // copy input track - Track output = new Track(input); - - // calculate alpha - float alpha = dt / (rc + dt); - - // set initial value for first waypoint - double outputInitialAltitudeValue = alpha * input.getWayPoints().get(0).getLocation().getAltitude(); - output.getWayPoints().get(0).getLocation().setAltitude(outputInitialAltitudeValue); - - double inputCurrentAltitudeValue; - double outputPreviousAltitudeValue; - double outputCurrentAltitudeValue; - for (int i = 1; i < input.getSize(); i++) { - inputCurrentAltitudeValue = input.getWayPoints().get(i).getLocation().getAltitude(); - outputPreviousAltitudeValue = output.getWayPoints().get(i-1).getLocation().getAltitude(); - - outputCurrentAltitudeValue = alpha * inputCurrentAltitudeValue + (1 - alpha) * outputPreviousAltitudeValue; - - output.getWayPoints().get(i).getLocation().setAltitude(outputCurrentAltitudeValue); - } - - return output; - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt new file mode 100644 index 0000000..00d073f --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt @@ -0,0 +1,194 @@ +/* + * TrackHelper.kt + * Implements the TrackHelper object + * A TrackHelper offers helper methods for dealing with track objects + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.location.Location +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.core.WayPoint +import java.text.SimpleDateFormat +import java.util.* + + +/* + * TrackHelper object + */ +object TrackHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java) + + + /* Returns unique ID for Track - currently the start date */ + fun getTrackId(track: Track): Long { + return track.recordingStart.time + } + + + /* Returns unique ID for TracklistElement - currently the start date */ + fun getTrackId(tracklistElement: TracklistElement): Long { + return tracklistElement.date.time + } + + + /* Adds given locatiom as waypoint to track */ + fun addWayPointToTrack(track: Track, location: Location, locationAccuracyThreshold: Int): Track { + + // get previous location + val previousLocation: Location? + val numberOfWayPoints: Int = track.wayPoints.size + if (numberOfWayPoints == 0) { + previousLocation = null + } else { + previousLocation = track.wayPoints.get(numberOfWayPoints - 1).toLocation() + } + + // update duration + val now: Date = GregorianCalendar.getInstance().time + val difference: Long = now.time - track.recordingStop.time + track.duration = track.duration + difference + track.recordingStop = now + + // add only if recent and accurate + val shouldBeAdded: Boolean + shouldBeAdded = (LocationHelper.isRecentEnough(location) + && LocationHelper.isAccurateEnough(location, locationAccuracyThreshold) + && LocationHelper.isDifferentEnough(previousLocation, location)) + + if (shouldBeAdded) { + // update distance + track.length = track.length + LocationHelper.calculateDistance(previousLocation, location) + + if (location.altitude != 0.0) { + // update altitude values + if (numberOfWayPoints == 0) { + track.maxAltitude = location.altitude + track.minAltitude = location.altitude + } else { + // calculate elevation values + val elevationDifferences: Pair = LocationHelper.calculateElevationDifferences(previousLocation, location, track) + // check if significant differences were calculated + if (elevationDifferences != Pair(track.positiveElevation, track.negativeElevation)) { + // update altitude values + if (location.altitude > track.maxAltitude) track.maxAltitude = location.altitude + if (location.altitude < track.minAltitude) track.minAltitude = location.altitude + // update elevation values + track.positiveElevation = elevationDifferences.first + track.negativeElevation = elevationDifferences.second + } + } + } + + // toggle stop over status, if necessary + if (track.wayPoints.size < 0) { + track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location) + } + + // save number of satellites + val numberOfSatellites: Int + val extras = location.extras + if (extras != null && extras.containsKey("satellites")) { + numberOfSatellites = extras.getInt("satellites", 0) + } else { + numberOfSatellites = 0 + } + + // add current location as point to center on for later display + track.latitude = location.latitude + track.longitude = location.longitude + + // add location as new waypoint + track.wayPoints.add(WayPoint(location.provider, location.latitude, location.longitude, location.altitude, location.accuracy, location.time, track.length, numberOfSatellites)) + } + + return track + } + + + /* Creates GPX string for given track */ + fun createGpxString(track: Track): String { + var gpxString: String + + // add header + gpxString = "\n" + + "\n" + + // add track + gpxString += createGpxTrk(track) + + // add closing tag + gpxString += "\n" + + return gpxString + } + + + /* Creates GPX formatted track */ + private fun createGpxTrk(track: Track): String { + val gpxTrack = StringBuilder("") + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + dateFormat.timeZone = TimeZone.getTimeZone("UTC") + + // add opening track tag + gpxTrack.append("\t\n") + + // add name to track + gpxTrack.append("\t\t") + gpxTrack.append("Trackbook Recording: ${track.name}") + gpxTrack.append("\n") + + // add opening track segment tag + gpxTrack.append("\t\t\n") + + // add route point + track.wayPoints.forEach { wayPoint -> + // add longitude and latitude + gpxTrack.append("\t\t\t\n") + + // add time + gpxTrack.append("\t\t\t\t\n") + + // add altitude + gpxTrack.append("\t\t\t\t") + gpxTrack.append(wayPoint.altitude) + gpxTrack.append("\n") + + // add closing tag + gpxTrack.append("\t\t\t\n") + } + + // add closing track segment tag + gpxTrack.append("\t\t\n") + + // add closing track tag + gpxTrack.append("\t\n") + + return gpxTrack.toString() + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java b/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java deleted file mode 100755 index 9a7d902..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * TrackbookKeys.java - * Implements the keys used throughout the app - * This interface hosts all keys used to control Trackbook's state - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - - -/** - * TrackbookKeys.class - */ -public interface TrackbookKeys { - - /* ACTIONS */ - String ACTION_START = "org.y20k.trackbook.action.START"; - String ACTION_STOP = "org.y20k.trackbook.action.STOP"; - String ACTION_DISMISS = "org.y20k.transistor.action.DISMISS"; - String ACTION_RESUME = "org.y20k.transistor.action.RESUME"; - String ACTION_CLEAR = "org.y20k.transistor.action.CLEAR"; - String ACTION_SAVE = "org.y20k.transistor.action.SAVE"; - String ACTION_DEFAULT = "DEFAULT"; - String ACTION_SHOW_MAP = "SHOW_MAP"; - String ACTION_TRACK_UPDATED = "TRACK_UPDATED"; - String ACTION_TRACK_REQUEST = "TRACK_REQUEST"; - String ACTION_TRACKING_STATE_CHANGED = "TRACKING_STATE_CHANGED"; - String ACTION_TRACK_SAVE = "TRACK_SAVE"; - - /* EXTRAS */ - String EXTRA_TRACK = "TRACK"; - String EXTRA_LAST_LOCATION = "LAST_LOCATION"; - String EXTRA_TRACKING_STATE = "TRACKING_STATE"; - String EXTRA_INFOSHEET_TITLE = "EXTRA_INFOSHEET_TITLE"; - String EXTRA_INFOSHEET_CONTENT = "INFOSHEET_CONTENT"; - String EXTRA_SAVE_FINISHED = "SAVE_FINISHED"; - - /* ARGS */ - String ARG_DIALOG_TITLE = "ArgDialogTitle"; - String ARG_DIALOG_MESSAGE = "ArgDialogMessage"; - String ARG_DIALOG_BUTTON_POSITIVE = "ArgDialogButtonPositive"; - String ARG_DIALOG_BUTTON_NEGATIVE = "ArgDialogButtonNegative"; - - /* PREFS */ - String PREFS_FAB_STATE = "fabStatePrefs"; - String PREFS_TRACKER_SERVICE_RUNNING = "trackerServiceRunning"; - String PREFS_CURRENT_TRACK_DURATION = "currentTrackDuration"; - String PREF_NIGHT_MODE_STATE = "prefNightModeState"; - - /* INSTANCE STATE */ - String INSTANCE_FIRST_START = "firstStart"; - String INSTANCE_TRACKING_STATE = "trackingState"; - String INSTANCE_SELECTED_TAB = "selectedTab"; - String INSTANCE_FAB_SUB_MENU_VISIBLE = "fabSubMenuVisible"; - String INSTANCE_LATITUDE_MAIN_MAP = "latitudeMainMap"; - String INSTANCE_LONGITUDE_MAIN_MAP = "longitudeMainMap"; - String INSTANCE_ZOOM_LEVEL_MAIN_MAP = "zoomLevelMainMap"; - String INSTANCE_TRACK_TRACK_MAP = "trackTrackMap"; - String INSTANCE_LATITUDE_TRACK_MAP = "latitudeTrackMap"; - String INSTANCE_LONGITUDE_TRACK_MAP = "longitudeTrackMap"; - String INSTANCE_ZOOM_LEVEL_TRACK_MAP = "zoomLevelTrackMap"; - String INSTANCE_CURRENT_LOCATION = "currentLocation"; - String INSTANCE_CURRENT_TRACK = "currentTrack"; - - /* FRAGMENT IDS */ - int FRAGMENT_ID_MAP = 0; - int FRAGMENT_ID_TRACKS = 1; - - /* RESULTS */ - int RESULT_SAVE_DIALOG = 1; - int RESULT_CLEAR_DIALOG = 2; - int RESULT_DELETE_DIALOG = 3; - int RESULT_EXPORT_DIALOG = 4; - int RESULT_EMPTY_RECORDING_DIALOG = 5; - - /* CONSTANTS */ - long ONE_SECOND_IN_NANOSECOND = 1000000000L; - long EIGHT_HOURS_IN_MILLISECONDS = 43200000; // maximum tracking duration - long FIFTEEN_SECONDS_IN_MILLISECONDS = 15000; // timer interval for tracking - long FIVE_MINUTES_IN_NANOSECONDS = 5L * 60000000000L; // determines a stop over - long ONE_MINUTE_IN_NANOSECONDS = 1L * 60000000000L; // defines an old location - int MAXIMUM_TRACK_FILES = 25; - int FIFTY_METER_RADIUS = 50; - - /* FILE */ - String FILE_TYPE_GPX_EXTENSION = ".gpx"; - String FILE_TYPE_TRACKBOOK_EXTENSION = ".trackbook"; - String FILE_NAME_TEMP = "temp"; - String TRACKS_DIRECTORY_NAME = "tracks"; - int FILE_TEMP_TRACK = 0; - int FILE_MOST_CURRENT_TRACK = 1; - - /* UNITS */ - int METRIC = 1; - int IMPERIAL = -1; - - /* FLOATING ACTION BUTTON */ - int FAB_STATE_DEFAULT = 0; - int FAB_STATE_RECORDING = 1; - int FAB_STATE_SAVE = 2; - - /* NOTIFICATION */ - int TRACKER_SERVICE_NOTIFICATION_ID = 1; - String NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL ="notificationChannelIdRecordingChannel"; - - /* MISC */ - int CURRENT_TRACK_FORMAT_VERSION = 3; // incremental version number to prevent issues in case the Track format evolves - double DEFAULT_LATITUDE = 71.172500; // latitude Nordkapp, Norway - double DEFAULT_LONGITUDE = 25.784444; // longitude Nordkapp, Norway - int MEASUREMENT_ERROR_THRESHOLD = 10; // altitude changes of 10 meter or more (per 15 seconds) are being discarded - int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124; - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt new file mode 100644 index 0000000..edad419 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt @@ -0,0 +1,130 @@ +/* + * UiHelper.kt + * Implements the UiHelper object + * A UiHelper provides helper methods for User Interface related tasks + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.escapepod.helpers + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * UiHelper object + */ +object UiHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(UiHelper::class.java) + + + /* Sets layout margins for given view in DP */ + fun setViewMargins(context: Context, view: View, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) { + val scalingFactor: Float = context.resources.displayMetrics.density + val l: Int = (left * scalingFactor).toInt() + val r: Int = (right * scalingFactor).toInt() + val t: Int = (top * scalingFactor).toInt() + val b: Int = (bottom * scalingFactor).toInt() + if (view.layoutParams is ViewGroup.MarginLayoutParams) { + val p = view.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(l, t, r, b) + view.requestLayout() + } + } + + + /* Sets layout margins for given view in percent */ + fun setViewMarginsPercentage(context: Context, view: View, height: Int, width: Int, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) { + val l: Int = ((width / 100.0f) * left).toInt() + val r: Int = ((width / 100.0f) * right).toInt() + val t: Int = ((height / 100.0f) * top).toInt() + val b: Int = ((height / 100.0f) * bottom).toInt() + setViewMargins(context, view, l, r, t, b) + } + + + /* + * Inner class: Callback that detects a left swipe + * Credit: https://github.com/kitek/android-rv-swipe-delete/blob/master/app/src/main/java/pl/kitek/rvswipetodelete/SwipeToDeleteCallback.kt + */ + abstract class SwipeToDeleteCallback(context: Context): ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { + + private val deleteIcon = ContextCompat.getDrawable(context, R.drawable.ic_remove_circle_24dp) + private val intrinsicWidth: Int = deleteIcon?.intrinsicWidth ?: 0 + private val intrinsicHeight: Int = deleteIcon?.intrinsicHeight ?: 0 + private val background: ColorDrawable = ColorDrawable() + private val backgroundColor = context.resources.getColor(R.color.list_card_delete_background, null) + private val clearPaint: Paint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + // do nothing + return false + } + + override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { + val itemView = viewHolder.itemView + val itemHeight = itemView.bottom - itemView.top + val isCanceled = dX == 0f && !isCurrentlyActive + + if (isCanceled) { + clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat()) + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + return + } + + // draw red delete background + background.color = backgroundColor + background.setBounds( + itemView.right + dX.toInt(), + itemView.top, + itemView.right, + itemView.bottom + ) + background.draw(c) + + // calculate position of delete icon + val deleteIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2 + val deleteIconMargin = (itemHeight - intrinsicHeight) / 2 + val deleteIconLeft = itemView.right - deleteIconMargin - intrinsicWidth + val deleteIconRight = itemView.right - deleteIconMargin + val deleteIconBottom = deleteIconTop + intrinsicHeight + + // draw delete icon + deleteIcon?.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom) + deleteIcon?.draw(c) + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) { + c?.drawRect(left, top, right, bottom, clearPaint) + } + } + /* + * End of inner class + */ + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java b/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java deleted file mode 100755 index e1eb7bc..0000000 --- a/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * DodgeAbleLayoutBehavior.java - * Implements the DodgeAbleLayoutBehavior class - * A DodgeAbleLayoutBehavior enables any element to be dodged up by a snackbar - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.layout; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; - - -/** - * DodgeAbleLayoutBehavior class - * adapted from: http://stackoverflow.com/a/35904421 - */ -public class DodgeAbleLayoutBehavior extends CoordinatorLayout.Behavior { - - /* Constructor (default) */ - public DodgeAbleLayoutBehavior() { - super(); - } - - - /* Constructor for context and attributes */ - public DodgeAbleLayoutBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - @Override - public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { - return dependency instanceof Snackbar.SnackbarLayout; - } - - - @Override - public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { - float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight()); - child.setTranslationY(translationY); - return true; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java b/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java deleted file mode 100755 index 842d1b0..0000000 --- a/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * NonSwipeableViewPager.java - * Implements the NonSwipeableViewPager class - * A NonSwipeableViewPager is a ViewPager with swiping gestures disabled - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.layout; - -/** - * NonSwipeableViewPager class - * adapted from: http://stackoverflow.com/a/9650884 - */ - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.animation.DecelerateInterpolator; -import android.widget.Scroller; - -import org.y20k.trackbook.helpers.LogHelper; - -import java.lang.reflect.Field; - -import androidx.viewpager.widget.ViewPager; - - -public class NonSwipeableViewPager extends ViewPager { - - /* Define log tag */ - private static final String LOG_TAG = NonSwipeableViewPager.class.getSimpleName(); - - - /* Constructor */ - public NonSwipeableViewPager(Context context) { - super(context); - setMyScroller(); - } - - - /* Constructor */ - public NonSwipeableViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - setMyScroller(); - } - - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - - /* Attaches a custom smooth scrolling scroller to a ViewPager */ - private void setMyScroller() { - try { - Class viewpager = ViewPager.class; - Field scroller = viewpager.getDeclaredField("mScroller"); - scroller.setAccessible(true); - scroller.set(this, new MyScroller(getContext())); - } catch (Exception e) { - LogHelper.e(LOG_TAG, "Problem accessing or modifying the mScroller field. Exception: " + e); - e.printStackTrace(); - } - } - - - /** - * Inner class: MyScroller is a custom Scroller - */ - public class MyScroller extends Scroller { - public MyScroller(Context context) { - super(context, new DecelerateInterpolator()); - } - - @Override - public void startScroll(int startX, int startY, int dx, int dy, int duration) { - super.startScroll(startX, startY, dx, dy, 350 /*1 secs*/); - } - } - /** - * End of inner class - */ - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt new file mode 100644 index 0000000..98feb30 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt @@ -0,0 +1,208 @@ +/* + * TracklistAdapter.kt + * Implements the TracklistAdapter class + * A TracklistAdapter is a custom adapter for a RecyclerView + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.tracklist + + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.* +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Tracklist +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.helpers.* +import java.util.* + + +/* + * TracklistAdapter class + */ +class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter() { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java) + + + /* Main class variables */ + private val context: Context = fragment.activity as Context + private lateinit var tracklistListener: TracklistAdapterListener + private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits(context) + private var tracklist: Tracklist = Tracklist() + + + /* Listener Interface */ + interface TracklistAdapterListener { + fun onTrackElementTapped(tracklistElement: TracklistElement) { } + // fun onTrackElementStarred(trackId: Long, starred: Boolean) + } + + + /* Overrides onAttachedToRecyclerView from RecyclerView.Adapter */ + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + // get reference to listener + tracklistListener = fragment as TracklistAdapterListener + // load tracklist + tracklist = FileHelper.readTracklist(context) + tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date } + } + + + /* Overrides onCreateViewHolder from RecyclerView.Adapter */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val v = LayoutInflater.from(parent.context).inflate(R.layout.track_element, parent, false) + return TrackElementViewHolder(v) + } + + + /* Overrides getItemCount from RecyclerView.Adapter */ + override fun getItemCount(): Int { + return tracklist.tracklistElements.size + } + + + /* Overrides onBindViewHolder from RecyclerView.Adapter */ + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val trackElementViewHolder: TrackElementViewHolder = holder as TrackElementViewHolder + trackElementViewHolder.trackNameView.text = tracklist.tracklistElements[position].name + trackElementViewHolder.trackDataView.text = createTrackDataString(position) + when (tracklist.tracklistElements[position].starred) { + true -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp)) + false -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp)) + } + trackElementViewHolder.trackElement.setOnClickListener { + tracklistListener.onTrackElementTapped(tracklist.tracklistElements[position]) + } + trackElementViewHolder.starButton.setOnClickListener { + toggleStarred(it, position) + } + } + + + /* Get track name for given position */ + fun getTrackName(position: Int): String { + return tracklist.tracklistElements[position].name + } + + + /* Removes track and track files for given position - used by TracklistFragment */ + fun removeTrack(context: Context, position: Int) { + val backgroundJob = Job() + val uiScope = CoroutineScope(Dispatchers.Main + backgroundJob) + uiScope.launch { + notifyItemRemoved(position) + val deferred: Deferred = async { FileHelper.deleteTrackSuspended(context, position, tracklist) } + // wait for result and store in tracklist + tracklist = deferred.await() + backgroundJob.cancel() + } + } + + + /* Finds current position of track element in adapter list */ + fun findPosition(trackId: Long): Int { + tracklist.tracklistElements.forEachIndexed {index, tracklistElement -> + if (tracklistElement.getTrackId() == trackId) return index + } + return -1 + } + + + /* Toggles the starred state of tracklist element - and saves tracklist */ + private fun toggleStarred(view: View, position: Int) { + val starButton: ImageButton = view as ImageButton + when (tracklist.tracklistElements[position].starred) { + true -> { + starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp)) + tracklist.tracklistElements[position].starred = false + } + false -> { + starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp)) + tracklist.tracklistElements[position].starred = true + } + } + GlobalScope.launch { + FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time) + } + } + + + /* Creates the track data string */ + private fun createTrackDataString(position: Int): String { + val tracklistElement: TracklistElement = tracklist.tracklistElements[position] + val trackDataString: String + when (tracklistElement.name == tracklistElement.dateString) { + // CASE: no individual name set - exclude date + true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)} • ${tracklistElement.durationString}" + // CASE: no individual name set - include date + false -> trackDataString = "${tracklistElement.dateString} • ${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)} • ${tracklistElement.durationString}" + } + return trackDataString + } + + + /* + * Inner class: DiffUtil.Callback that determines changes in data - improves list performance + */ + private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() { + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList.tracklistElements[oldItemPosition] + val newItem = newList.tracklistElements[newItemPosition] + return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) + } + + override fun getOldListSize(): Int { + return oldList.tracklistElements.size + } + + override fun getNewListSize(): Int { + return newList.tracklistElements.size + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList.tracklistElements[oldItemPosition] + val newItem = newList.tracklistElements[newItemPosition] + return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) && oldItem.length == newItem.length + } + } + /* + * End of inner class + */ + + + /* + * Inner class: ViewHolder for a track element + */ + private inner class TrackElementViewHolder (trackElementLayout: View): RecyclerView.ViewHolder(trackElementLayout) { + val trackElement: ConstraintLayout = trackElementLayout.findViewById(R.id.track_element) + val trackNameView: TextView = trackElementLayout.findViewById(R.id.track_name) + val trackDataView: TextView = trackElementLayout.findViewById(R.id.track_data) + val starButton: ImageButton = trackElementLayout.findViewById(R.id.star_button) + } + /* + * End of inner class + */ + +} diff --git a/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt b/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt new file mode 100644 index 0000000..bb65884 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt @@ -0,0 +1,233 @@ +/* + * MapFragmentLayoutHolder.kt + * Implements the MapFragmentLayoutHolder class + * A MapFragmentLayoutHolder hold references to the main views of a map fragment + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Group +import androidx.core.content.ContextCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar +import org.osmdroid.api.IMapController +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.TilesOverlay +import org.osmdroid.views.overlay.compass.CompassOverlay +import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.helpers.MapHelper +import org.y20k.trackbook.helpers.NightModeHelper +import org.y20k.trackbook.helpers.PreferencesHelper + + +/* + * MapFragmentLayoutHolder class + */ +data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, val startLocation: Location, val trackingState: Int) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(MapFragmentLayoutHolder::class.java) + + + /* Main class variables */ + val rootView: View + val mapView: MapView + val currentLocationButton: FloatingActionButton + val recordingButton: FloatingActionButton + val recordingButtonSubMenu: Group + val saveButton: FloatingActionButton + val clearButton: FloatingActionButton + val resumeButton: FloatingActionButton + var userInteraction: Boolean = false + private var currentPositionOverlay: ItemizedIconOverlay + private var currentTrackOverlay: ItemizedIconOverlay? + private var locationErrorBar: Snackbar + private var controller: IMapController + private var zoomLevel: Double + + + /* Init block */ + init { + // find views + rootView = inflater.inflate(R.layout.fragment_map, container, false) + mapView = rootView.findViewById(R.id.map) + currentLocationButton = rootView.findViewById(R.id.fab_location_button) + recordingButton = rootView.findViewById(R.id.fab_main_button) + recordingButtonSubMenu = rootView.findViewById(R.id.fab_sub_menu) + saveButton = rootView.findViewById(R.id.fab_sub_menu_button_save) + clearButton = rootView.findViewById(R.id.fab_sub_menu_button_clear) + resumeButton = rootView.findViewById(R.id.fab_sub_menu_button_resume) + locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE) + + // basic map setup + controller = mapView.controller + mapView.isTilesScaledToDpi = true + mapView.setTileSource(TileSourceFactory.MAPNIK) + mapView.setMultiTouchControls(true) + mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) + zoomLevel = PreferencesHelper.loadZoomLevel(context) + controller.setZoom(zoomLevel) + + // set dark map tiles, if necessary + if (NightModeHelper.isNightModeOn(context as Activity)) { + mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) + } + + // add compass to map + val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView) + compassOverlay.enableCompass() + compassOverlay.setCompassCenter(36f, 60f) + + mapView.overlays.add(compassOverlay) + + // add my location overlay + currentPositionOverlay = MapHelper.createMyLocationOverlay(context, startLocation, trackingState) + mapView.overlays.add(currentPositionOverlay) + centerMap(startLocation) + + // initialize track overlay + currentTrackOverlay = null + + // initialize recording button state + updateRecordingButton(trackingState) + + // add touch listeners + addTouchListeners() + + // listen for user interaction + addInteractionListener() + } + + + /* Listen for user interaction */ + @SuppressLint("ClickableViewAccessibility") + private fun addInteractionListener() { + mapView.setOnTouchListener { v, event -> + userInteraction = true + false + } + } + + + /* Set map center */ + fun centerMap(location: Location, animated: Boolean = false) { + val position = GeoPoint(location.latitude, location.longitude) + when (animated) { + true -> controller.animateTo(position) + false -> controller.setCenter(position) + } + userInteraction = false + } + + + /* Save current best location and state of map to shared preferences */ + fun saveState(currentBestLocation: Location) { + PreferencesHelper.saveCurrentBestLocation(context, currentBestLocation) + PreferencesHelper.saveZoomLevel(context, mapView.getZoomLevelDouble()) + // reset user interaction state + userInteraction = false + } + + + /* Mark current position on map */ + fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_NOT_TRACKING) { + mapView.overlays.remove(currentPositionOverlay) + currentPositionOverlay = MapHelper.createMyLocationOverlay(context, location, trackingState) + mapView.overlays.add(currentPositionOverlay) + } + + + /* Overlay current track on map */ + fun overlayCurrentTrack(track: Track, trackingState: Int) { + if (currentTrackOverlay != null) { + mapView.overlays.remove(currentTrackOverlay) + } + if (track.wayPoints.isNotEmpty()) { + currentTrackOverlay = MapHelper.createTrackOverlay(context, track, trackingState) + mapView.overlays.add(currentTrackOverlay) + } + } + + + /* Toggles state of recording button and sub menu_bottom_navigation */ + fun updateRecordingButton(trackingState: Int) { + when (trackingState) { + Keys.STATE_NOT_TRACKING -> { + recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp) + recordingButtonSubMenu.visibility = View.GONE + } + Keys.STATE_TRACKING_ACTIVE -> { + recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_red_24dp) + recordingButtonSubMenu.visibility = View.GONE + } + Keys.STATE_TRACKING_STOPPED -> { + recordingButton.setImageResource(R.drawable.ic_save_white_24dp) + } + } + } + + + /* Toggles visibility of recording button sub menu_bottom_navigation */ + fun toggleRecordingButtonSubMenu() { + when (recordingButtonSubMenu.visibility) { + View.VISIBLE -> recordingButtonSubMenu.visibility = View.GONE + else -> recordingButtonSubMenu.visibility = View.VISIBLE + } + } + + + + /* Toggles content and visibility of the location error snackbar */ + fun toggleLocationErrorBar(gpsProviderActive: Boolean, networkProviderActive: Boolean) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { + // CASE: Location permission not granted + locationErrorBar.setText(R.string.snackbar_message_location_permission_denied) + locationErrorBar.show() + } else if (!gpsProviderActive && !networkProviderActive) { + // CASE: Location setting is off + locationErrorBar.setText(R.string.snackbar_message_location_offline) + locationErrorBar.show() + } else if (locationErrorBar.isShown) { + // CASE: Snackbar is visible but unnecessary + locationErrorBar.dismiss() + } + } + + + /* Sets up views - adds touch listeners */ + private fun addTouchListeners() { + currentLocationButton.setOnLongClickListener { + NightModeHelper.switchMode(context as Activity) + return@setOnLongClickListener true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt new file mode 100644 index 0000000..1c6fef5 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt @@ -0,0 +1,254 @@ +/* + * TrackFragmentLayoutHolder.kt + * Implements the TrackFragmentLayoutHolder class + * A TrackFragmentLayoutHolder hold references to the main views of a track fragment + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.ui + +import android.app.Activity +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Toast +import androidx.constraintlayout.widget.Group +import androidx.core.widget.NestedScrollView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.textview.MaterialTextView +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.osmdroid.api.IGeoPoint +import org.osmdroid.api.IMapController +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.TilesOverlay +import org.osmdroid.views.overlay.compass.CompassOverlay +import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.helpers.* +import kotlin.math.roundToInt + + +/* + * TrackFragmentLayoutHolder class + */ +data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, var arguments: Bundle?) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java) + + + /* Main class variables */ + val rootView: View + val track: Track + val shareButton: ImageButton + val deleteButton: ImageButton + val editButton: ImageButton + val trackNameView: MaterialTextView + private val mapView: MapView + private var trackOverlay: ItemizedIconOverlay? + private var controller: IMapController + private var zoomLevel: Double + private val statisticsSheetBehavior: BottomSheetBehavior + private val statisticsSheet: NestedScrollView + private val statisticsView: View + private val distanceView: MaterialTextView + private val stepsView: MaterialTextView + private val waypointsView: MaterialTextView + private val durationView: MaterialTextView + private val recordingStartView: MaterialTextView + private val recordingStopView: MaterialTextView + private val maxAltitudeView: MaterialTextView + private val minAltitudeView: MaterialTextView + private val positiveElevationView: MaterialTextView + private val negativeElevationView: MaterialTextView + private val elevationDataViews: Group + private val trackManagementViews: Group + private val useImperialUnits: Boolean + + + /* Init block */ + init { + // find views + rootView = inflater.inflate(R.layout.fragment_track, container, false) + mapView = rootView.findViewById(R.id.map) + shareButton = rootView.findViewById(R.id.share_button) + deleteButton = rootView.findViewById(R.id.delete_button) + editButton = rootView.findViewById(R.id.edit_button) + trackNameView = rootView.findViewById(R.id.statistics_track_name_headline) + + // basic map setup + controller = mapView.controller + mapView.isTilesScaledToDpi = true + mapView.setTileSource(TileSourceFactory.MAPNIK) + mapView.setMultiTouchControls(true) + mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) + zoomLevel = Keys.DEFAULT_ZOOM_LEVEL + controller.setZoom(zoomLevel) + + // get views for statistics sheet + statisticsSheet = rootView.findViewById(R.id.statistics_sheet) + statisticsView = rootView.findViewById(R.id.statistics_view) + distanceView = rootView.findViewById(R.id.statistics_data_distance) + stepsView = rootView.findViewById(R.id.statistics_data_steps) + waypointsView = rootView.findViewById(R.id.statistics_data_waypoints) + durationView = rootView.findViewById(R.id.statistics_data_duration) + recordingStartView = rootView.findViewById(R.id.statistics_data_recording_start) + recordingStopView = rootView.findViewById(R.id.statistics_data_recording_stop) + maxAltitudeView = rootView.findViewById(R.id.statistics_data_max_altitude) + minAltitudeView = rootView.findViewById(R.id.statistics_data_min_altitude) + positiveElevationView = rootView.findViewById(R.id.statistics_data_positive_elevation) + negativeElevationView = rootView.findViewById(R.id.statistics_data_negative_elevation) + elevationDataViews = rootView.findViewById(R.id.elevation_data) + trackManagementViews = rootView.findViewById(R.id.management_icons) + + // get measurement unit system + useImperialUnits = PreferencesHelper.loadUseImperialUnits(context) + + // set dark map tiles, if necessary + if (NightModeHelper.isNightModeOn(context as Activity)) { + mapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS) + } + + // add compass to map + val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView) + compassOverlay.enableCompass() + compassOverlay.setCompassCenter(36f, 60f) + mapView.overlays.add(compassOverlay) + + // get track and create map overlay + val fileUriString: String = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String() + if (fileUriString.isNotBlank()) { + track = FileHelper.readTrack(context, Uri.parse(fileUriString)) + } else { + track = Track() + } + trackOverlay = MapHelper.createTrackOverlay(context, track, Keys.STATE_NOT_TRACKING) + if (track.wayPoints.isNotEmpty()) { + mapView.overlays.add(trackOverlay) + } + + // set up and show statistics sheet + statisticsSheetBehavior = BottomSheetBehavior.from(statisticsSheet) + statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + statisticsSheetBehavior.addBottomSheetCallback(getStatisticsSheetCallback()) + setupStatisticsViews() + } + + + /* Updates zoom level and center of this map */ + fun updateMapView() { + val position = GeoPoint(track.latitude, track.longitude) + controller.setCenter(position) + controller.setZoom(track.zoomLevel) + } + + + /* Saves zoom level and center of this map */ + fun saveViewStateToTrack() { + val center: IGeoPoint = mapView.mapCenter + track.latitude = center.latitude + track.longitude = center.longitude + track.zoomLevel = mapView.zoomLevelDouble + GlobalScope.launch { FileHelper.saveTrackSuspended(track, false) } + } + + + /* Sets up the statistics sheet */ + private fun setupStatisticsViews() { + + // get step count string + val steps: String + if (track.stepCount == -1f) steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer) + else steps = track.stepCount.roundToInt().toString() + + // populate views + trackNameView.text = track.name + distanceView.text = LengthUnitHelper.convertDistanceToString(track.length, useImperialUnits) + stepsView.text = steps + waypointsView.text = track.wayPoints.size.toString() + durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration) + recordingStartView.text = DateTimeHelper.convertToReadableDate(track.recordingStart) + recordingStopView.text = DateTimeHelper.convertToReadableDate(track.recordingStart) + maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits) + minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits) + positiveElevationView.text = LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits) + negativeElevationView.text = LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits) + + // inform user about possible accuracy issues with altitude measurements + elevationDataViews.referencedIds.forEach { id -> + (rootView.findViewById(id) as View).setOnClickListener{ + Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show() + } + } + // make track name on statistics sheet clickable + trackNameView.setOnClickListener { + toggleStatisticsSheetVisibility() + } + } + + + /* Shows/hides the statistics sheet */ + private fun toggleStatisticsSheetVisibility() { + when (statisticsSheetBehavior.state) { + BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + + /* Defines the behavior of the statistics sheet */ + private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback { + return object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_EXPANDED -> { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded) + trackManagementViews.visibility = View.VISIBLE + shareButton.visibility = View.GONE + // bottomSheet.setPadding(0,24,0,0) + } + else -> { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed) + trackManagementViews.visibility = View.GONE + shareButton.visibility = View.VISIBLE + // bottomSheet.setPadding(0,0,0,0) + } + } + } + override fun onSlide(bottomSheet: View, slideOffset: Float) { + if (slideOffset < 0.125f) { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed) + trackManagementViews.visibility = View.GONE + shareButton.visibility = View.VISIBLE + } else { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded) + trackManagementViews.visibility = View.VISIBLE + shareButton.visibility = View.GONE + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..6348baa --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_account_circle_black.xml b/app/src/main/res/drawable/ic_account_circle_black.xml new file mode 100644 index 0000000..7de6442 --- /dev/null +++ b/app/src/main/res/drawable/ic_account_circle_black.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml b/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml index fbd8e47..df07973 100755 --- a/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml +++ b/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_my_location_24dp.xml b/app/src/main/res/drawable/ic_current_location_24dp.xml similarity index 90% rename from app/src/main/res/drawable/ic_my_location_24dp.xml rename to app/src/main/res/drawable/ic_current_location_24dp.xml index 609c58d..46e57f3 100755 --- a/app/src/main/res/drawable/ic_my_location_24dp.xml +++ b/app/src/main/res/drawable/ic_current_location_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_delete_forever_24dp.xml b/app/src/main/res/drawable/ic_delete_24dp.xml similarity index 88% rename from app/src/main/res/drawable/ic_delete_forever_24dp.xml rename to app/src/main/res/drawable/ic_delete_24dp.xml index d61792d..651f304 100755 --- a/app/src/main/res/drawable/ic_delete_forever_24dp.xml +++ b/app/src/main/res/drawable/ic_delete_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml new file mode 100644 index 0000000..1a9a294 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_email_black_24dp.xml b/app/src/main/res/drawable/ic_email_black_24dp.xml new file mode 100644 index 0000000..ce97ab8 --- /dev/null +++ b/app/src/main/res/drawable/ic_email_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_24dp.xml b/app/src/main/res/drawable/ic_info_24dp.xml index b701d0e..0da35e2 100755 --- a/app/src/main/res/drawable/ic_info_24dp.xml +++ b/app/src/main/res/drawable/ic_info_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..a0ad202 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_my_location_dot_blue_24dp.xml b/app/src/main/res/drawable/ic_marker_location_blue_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_blue_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_blue_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_blue_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_location_blue_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_blue_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_blue_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_red_24dp.xml b/app/src/main/res/drawable/ic_marker_location_red_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_red_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_red_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_red_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_location_red_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_red_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_red_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_blue_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_blue_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_blue_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_blue_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_red_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_red_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_red_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_red_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_transparent_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_transparent_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_transparent_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_transparent_24dp.xml diff --git a/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml b/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml new file mode 100644 index 0000000..f1017e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification_action_show_36dp.xml b/app/src/main/res/drawable/ic_notification_action_show_36dp.xml new file mode 100644 index 0000000..4c1ec9c --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_show_36dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml b/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml new file mode 100644 index 0000000..cecebba --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification_large_tracking_48dp.xml b/app/src/main/res/drawable/ic_notification_icon_large_tracking_active_48dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_large_tracking_48dp.xml rename to app/src/main/res/drawable/ic_notification_icon_large_tracking_active_48dp.xml diff --git a/app/src/main/res/drawable/ic_notification_large_not_tracking_48dp.xml b/app/src/main/res/drawable/ic_notification_icon_large_tracking_stopped_48dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_large_not_tracking_48dp.xml rename to app/src/main/res/drawable/ic_notification_icon_large_tracking_stopped_48dp.xml diff --git a/app/src/main/res/drawable/ic_notification_small_24dp.xml b/app/src/main/res/drawable/ic_notification_icon_small_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_small_24dp.xml rename to app/src/main/res/drawable/ic_notification_icon_small_24dp.xml diff --git a/app/src/main/res/drawable/ic_notifications_black.xml b/app/src/main/res/drawable/ic_notifications_black.xml new file mode 100644 index 0000000..41d88a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 0000000..b2cb337b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_question_answer_black_24dp.xml b/app/src/main/res/drawable/ic_question_answer_black_24dp.xml new file mode 100644 index 0000000..26eda09 --- /dev/null +++ b/app/src/main/res/drawable/ic_question_answer_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_remove_circle_24dp.xml b/app/src/main/res/drawable/ic_remove_circle_24dp.xml new file mode 100644 index 0000000..31439c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove_circle_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black.xml b/app/src/main/res/drawable/ic_settings_black.xml new file mode 100644 index 0000000..c44b9ff --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..48597cc --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_24dp.xml b/app/src/main/res/drawable/ic_share_24dp.xml index c19e95c..24bf669 100755 --- a/app/src/main/res/drawable/ic_share_24dp.xml +++ b/app/src/main/res/drawable/ic_share_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_star_24dp.xml b/app/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000..5e1387f --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_24dp.xml b/app/src/main/res/drawable/ic_star_border_24dp.xml new file mode 100644 index 0000000..81babd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_bottom_navigation.xml b/app/src/main/res/drawable/selector_bottom_navigation.xml index dfc4cc0..1c4350c 100755 --- a/app/src/main/res/drawable/selector_bottom_navigation.xml +++ b/app/src/main/res/drawable/selector_bottom_navigation.xml @@ -1,6 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/shape_statistics_background_collapsed.xml b/app/src/main/res/drawable/shape_statistics_background_collapsed.xml new file mode 100644 index 0000000..3f75958 --- /dev/null +++ b/app/src/main/res/drawable/shape_statistics_background_collapsed.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_statistics_background_expanded.xml b/app/src/main/res/drawable/shape_statistics_background_expanded.xml new file mode 100644 index 0000000..682b9e8 --- /dev/null +++ b/app/src/main/res/drawable/shape_statistics_background_expanded.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml old mode 100755 new mode 100644 index b4f0a6c..39039c1 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,220 +1,34 @@ - + tools:context=".MainActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/nav_graph_main" /> + + diff --git a/app/src/main/res/layout/custom_dropdown_item_collapsed.xml b/app/src/main/res/layout/custom_dropdown_item_collapsed.xml deleted file mode 100755 index 214bdca..0000000 --- a/app/src/main/res/layout/custom_dropdown_item_collapsed.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_dropdown_item_expanded.xml b/app/src/main/res/layout/custom_dropdown_item_expanded.xml deleted file mode 100755 index 214bdca..0000000 --- a/app/src/main/res/layout/custom_dropdown_item_expanded.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_generic_with_details.xml b/app/src/main/res/layout/dialog_generic_with_details.xml new file mode 100644 index 0000000..89af16d --- /dev/null +++ b/app/src/main/res/layout/dialog_generic_with_details.xml @@ -0,0 +1,57 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rename_track.xml b/app/src/main/res/layout/dialog_rename_track.xml new file mode 100644 index 0000000..69058e5 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_track.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_map.xml b/app/src/main/res/layout/fragment_main_map.xml deleted file mode 100755 index ef50963..0000000 --- a/app/src/main/res/layout/fragment_main_map.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml new file mode 100644 index 0000000..60e023b --- /dev/null +++ b/app/src/main/res/layout/fragment_map.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_main_track.xml b/app/src/main/res/layout/fragment_track.xml similarity index 54% rename from app/src/main/res/layout/fragment_main_track.xml rename to app/src/main/res/layout/fragment_track.xml index 9da09f3..5611711 100755 --- a/app/src/main/res/layout/fragment_main_track.xml +++ b/app/src/main/res/layout/fragment_track.xml @@ -5,41 +5,35 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivityMapFragment"> + tools:context=".MainActivity"> - - - - - - - - - - - - + + + + + + - diff --git a/app/src/main/res/layout/fragment_tracklist.xml b/app/src/main/res/layout/fragment_tracklist.xml new file mode 100644 index 0000000..9945328 --- /dev/null +++ b/app/src/main/res/layout/fragment_tracklist.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tracklist_land.xml b/app/src/main/res/layout/fragment_tracklist_land.xml new file mode 100644 index 0000000..d9b49d2 --- /dev/null +++ b/app/src/main/res/layout/fragment_tracklist_land.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_onboarding.xml b/app/src/main/res/layout/main_onboarding.xml index 95247fd..a20a16a 100755 --- a/app/src/main/res/layout/main_onboarding.xml +++ b/app/src/main/res/layout/main_onboarding.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_vertical_margin" android:textAppearance="@style/TextAppearance.AppCompat.Large" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h1_welcome" android:id="@+id/h1_welcome" /> @@ -49,7 +49,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h2_app_name" android:id="@+id/h2_app_name" /> @@ -57,7 +57,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_p_app_claim" android:id="@+id/p_app_claim" /> @@ -69,7 +69,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h2_request_permissions" android:id="@+id/h2_request_permissions" android:layout_marginTop="@dimen/activity_vertical_margin"/> @@ -78,7 +78,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:textStyle="bold" android:text="@string/layout_onboarding_h3_permission_location" android:id="@+id/h3_permission_location" @@ -87,7 +87,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_p_permission_location" android:id="@+id/p_permission_location" /> diff --git a/app/src/main/res/layout/track_element.xml b/app/src/main/res/layout/track_element.xml new file mode 100644 index 0000000..e5fac85 --- /dev/null +++ b/app/src/main/res/layout/track_element.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/track_management.xml b/app/src/main/res/layout/track_management.xml deleted file mode 100755 index fbee446..0000000 --- a/app/src/main/res/layout/track_management.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/track_onboarding.xml b/app/src/main/res/layout/track_onboarding.xml index a12abb3..d42787b 100755 --- a/app/src/main/res/layout/track_onboarding.xml +++ b/app/src/main/res/layout/track_onboarding.xml @@ -1,35 +1,48 @@ - + android:id="@+id/track_list_onboarding"> - + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="16dp" + android:text="@string/track_list_onboarding_h1_part_1" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + android:textColor="@color/text_default" + app:layout_constraintBottom_toTopOf="@+id/trackbook_icon" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.532" + app:layout_constraintStart_toStartOf="parent" /> - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:text="@string/track_list_onboarding_h1_part_2" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + android:textColor="@color/text_default" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/trackbook_icon" /> - + diff --git a/app/src/main/res/layout/track_statistics.xml b/app/src/main/res/layout/track_statistics.xml index 3b7ef90..b4f2e95 100755 --- a/app/src/main/res/layout/track_statistics.xml +++ b/app/src/main/res/layout/track_statistics.xml @@ -1,9 +1,10 @@ @@ -11,270 +12,299 @@ android:id="@+id/elevation_data" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="statistics_p_positive_elevation, statistics_data_positive_elevation, statistics_p_negative_elevation, statistics_data_negative_elevation, statistics_p_max_altitude, statistics_data_max_altitude, statistics_p_min_altitude, statistics_data_min_altitude" /> + app:constraint_referenced_ids="statistics_p_positive_elevation,statistics_data_positive_elevation,statistics_p_negative_elevation,statistics_data_negative_elevation,statistics_p_max_altitude,statistics_data_max_altitude,statistics_p_min_altitude,statistics_data_min_altitude" /> + android:visibility="gone" + app:constraint_referenced_ids="delete_button,edit_button" /> - + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/sample_text_track_name" /> - + android:contentDescription="@string/descr_statistics_sheet_delete_button" + app:layout_constraintBottom_toBottomOf="@+id/edit_button" + app:layout_constraintEnd_toStartOf="@+id/share_button" + app:layout_constraintTop_toTopOf="@+id/edit_button" + app:srcCompat="@drawable/ic_delete_24dp" /> - + + + + + android:textAllCaps="false" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" + android:textColor="@color/text_lightweight" + app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline" + app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" /> - + app:layout_constraintTop_toTopOf="@+id/statistics_p_distance" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_steps" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_waypoints" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_duration" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_recording_start" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_recording_stop" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_positive_elevation" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_negative_elevation" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_max_altitude" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_min_altitude" + tools:text="@string/sample_text_default_data" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_bottom_navigation.xml old mode 100755 new mode 100644 similarity index 50% rename from app/src/main/res/menu/menu_main.xml rename to app/src/main/res/menu/menu_bottom_navigation.xml index 701fc84..a1fdb8d --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_bottom_navigation.xml @@ -2,13 +2,18 @@ + android:title="@string/tab_tracks" /> + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index f1b9414..c486148 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index aefb6ac..66dca23 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index ec7f9ff..a25a833 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index a1c02df..2d77960 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index e59cd5b..61de4af 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 257fc8a..a781fbc 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index c003095..da5f47d 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 15dbe13..6a90b82 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 21e86a6..a7999ae 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index ab09819..02999b1 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index d9971ba..583031c 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index d763e82..b303b13 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 524dad2..4bdccc5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index c47904b..0d2aa47 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 3689a32..afc28df 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml new file mode 100644 index 0000000..7d020f5 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/nav_graph_tracklist.xml b/app/src/main/res/navigation/nav_graph_tracklist.xml new file mode 100644 index 0000000..b8e3edf --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_tracklist.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 89886d7..5abcb18 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -20,7 +20,7 @@ Sporing startet Sporing genoptaget Lokalisering er slået fra. Trackbook kan ikke virke. - + Ryd Gem Fortsæt @@ -39,7 +39,7 @@ Eksporter og overskriv Del GPX fil med Kan ikke gemme - Trackbook har ingen rutepunkter endnu. + Trackbook har ingen rutepunkter endnu. Fortsæt optagelse Tilladelse givet. @@ -89,8 +89,8 @@ "Trackbook bruger osmdroid, til at lagre kortfliser på Androids eksterne hukommelse. Du kan finde dem i osmdroid mappen i roden af filsystemet." Forstået! - Dine optagede ture - ... vil dukke op her. + Dine optagede ture + ... vil dukke op her. Kortlægning af nuværende tur Kortlægning af sidste tur @@ -102,9 +102,8 @@ Ryd knap Fortsæt knap Overskrift for statistikker - Skift mellem optagelse af data visning Liste med yderligere ture - Eksporter tur knap - Slet tur knap - Del eksport som GPX knap + Eksporter tur knap + Slet tur knap + Del eksport som GPX knap diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d757c67..a097c96 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,7 +21,7 @@ Aufzeichnung gestartet Aufzeichnung fortgesetzt Standortdienste sind deaktiviert. Trackbook wird nicht funktionieren. - + Zurücksetzen Speichern Fortsetzen @@ -40,7 +40,7 @@ Exportieren und überschreiben GPX-Datei öffnen mit Speichern nicht möglich - Trackbook hat noch keine Wegpunkte aufgezeichnet. + Trackbook hat noch keine Wegpunkte aufgezeichnet. Aufzeichnung fortsetzen Berechtigungen erteilt. @@ -90,8 +90,8 @@ Trackbook nutzt osmdroid; osmdroid speichert Kartendaten im Externen Speicher von Android. Der Karten-Cache befindet sich im Ordner osmdroid auf der obersten Ebene des für Nutzer sichtbaren Dateisystems. Alles klar! - Bewegungsaufzeichnungen - … werden hier erscheinen. + Bewegungsaufzeichnungen + … werden hier erscheinen. Kartierung der aktuellen Aufzeichnung Kartierung der letzten Aufzeichnung @@ -103,9 +103,8 @@ Zurücksetzen-Knopf Fortsetzen-Knopf Überschrift der Statistik-Einblendung - Umschalten der Anzeige der Aufzeichnungsdaten Auswahl-Menü für weitere Aufzeichnungen - Schaltfläche „Aufzeichnung exportieren“ - Schaltfläche „Aufzeichnung löschen” - Share-Taste, die den Export als GPX anbietet + Schaltfläche „Aufzeichnung exportieren“ + Schaltfläche „Aufzeichnung löschen” + Share-Taste, die den Export als GPX anbietet \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c40a7a3..406ac89 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -21,7 +21,7 @@ Pistage repris Pistage démarré Pistage arrêté - + Effacer Poursuivre Sauvegarder @@ -33,7 +33,7 @@ Supprimer cet enregistrement : Supprimer l\'enregistrement \? Poursuivre l\'enregistrement - Trackbook n\'a enregistré aucun point de parcours jusqu\'à présent. + Trackbook n\'a enregistré aucun point de parcours jusqu\'à présent. Sauvegarde impossible Exporter Exporter et écraser @@ -90,11 +90,11 @@ Trackbook a besoin des données GPS précises pour enregistrer vos déplacements. Si les données GPS ne sont pas disponibles ou insuffisament précises, Trackbook utilisera la localisation à partir du réseau cellulaire et la triangulation Wi-Fi. Trackbook utilise osmdroid, qui nécessite la conservation de tuiles cartographiques dans l\'espace de stockage Android. Vous pouvez retrouver ces tuiles cartographiques dans le dossier osmdroid au plus haut niveau du système de fichiers de l\'utilisateur. - Vos parcours enregistrés - … seront affichés ici. + Vos parcours enregistrés + … seront affichés ici. - Bouton « Supprimer le parcours » - Bouton « Exporter le parcours » + Bouton « Supprimer le parcours » + Bouton « Exporter le parcours » Bouton « Enregistrement des paramètres » Bouton « Démarrer l\'enregistrement » Bouton « Arrêter l\'enregistrement » @@ -104,8 +104,7 @@ Bouton « Sauvegarder » Affichage du parcours actuel Affichage du dernier parcours - Bouton « Partager l\'export GPX » + Bouton « Partager l\'export GPX » Entête de la page des statistiques - Baculer l\'affichage de l\'enregistrement des déplacements Liste déroulante pour la sélection d\'autres parcours \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index a7fd780..30b026c 100755 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -24,7 +24,7 @@ Tracking resumed - + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fd778d7..dd6e399 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -21,7 +21,7 @@ Tracciamento avviato Tracciamento ripreso La posizione è disattivata. Trackbook non funzionerà. - + Cancella Salva Riprendi @@ -40,7 +40,7 @@ Esporta e sovrascrivi Condividi il file GPX con Impossibile salvare - Trackbook non ha registrato nessun waypoint finora. + Trackbook non ha registrato nessun waypoint finora. Riprendi la registrazione Autorizzazioni concesse. @@ -90,8 +90,8 @@ Trackbook usa osmdroid, che memorizza porzioni di mappa sull\'SD esterna di Android. Puoi trovare la cache della mappa nella cartella osmdroid al livello più alto del file system accessibile all\'utente. Capito! - Le tue tracce registrate - … verranno mostrate qui. + Le tue tracce registrate + … verranno mostrate qui. Mappatura della traccia corrente Mappatura dell\'ultima traccia @@ -103,9 +103,8 @@ Pulsante Cancella Pulsante Riprendi Titolo della scheda delle statistiche - Attiva/disattiva la visualizzazione dei dati di registrazione Menu a tendina per altre tracce - Pulsante Esporta traccia - Pulsante Elimina traccia - Pulsante Condividi che permette di esportare come GPX + Pulsante Esporta traccia + Pulsante Elimina traccia + Pulsante Condividi che permette di esportare come GPX \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 6dfeccf..a475f88 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,7 +21,7 @@ トレースを開始しました トレースを再開しました 位置情報がオフです。Trackbook は動作しません。 - + クリア 保存してクリア 再開 @@ -40,7 +40,7 @@ エクスポートして上書き GPX ファイルを共有... 保存できません - トラックブックはこれまでウェイポイントを記録していません。 + トラックブックはこれまでウェイポイントを記録していません。 記録を再開 アクセス許可を付与しました。 @@ -87,8 +87,8 @@ Trackbook は、Android の外部ストレージに地図タイルをキャッシュする osmdroid を使用します。地図キャッシュは、ユーザー向けファイル・システムの最上位レベルの osmdroid フォルダーで見つけることができます。 完了! - 記録したトレース - … ここに表示されます。 + 記録したトレース + … ここに表示されます。 現在のトレースのマッピング 最後のトレースのマッピング @@ -100,11 +100,10 @@ クリアボタン 再開ボタン 統計情報シートの見出し - 記録データの表示切り替え ドロップダウン メニューでさらにトレース - トレースのエクスポート ボタン - トレース削除ボタン - GPX としてエクスポートする共有ボタン + トレースのエクスポート ボタン + トレース削除ボタン + GPX としてエクスポートする共有ボタン 夜間モードに切り替えています (長押しを検出しました) 日中モードに切り替えています (長押しを検出しました) システム設定モードにしたがって切り替えています (長押しを検出しました) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 98acbec..9a83ade 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -21,7 +21,7 @@ Sporing startet Sporing gjenopptatt Plassering avskrudd. Trackbook bil ikke fungere. - + Tøm Lagre og tøm Fortsett @@ -40,7 +40,7 @@ Eksporter og overskriv Del GPX-fil med Kunne ikke lagre - Trackbook har ikke registrert noen veipunkter så langt. + Trackbook har ikke registrert noen veipunkter så langt. Gjenoppta opptak Tilgang gitt. @@ -90,8 +90,8 @@ Trackbook brukerosmdroid, som hurtiglagrer kartfliser på Androids eksterne lagringsmedie. Du kan finne dette karthurtiglageret i osmdroid-mappa på topnivået av brukergrensesnittet. Skjønner! - Dine registrerte spor - … vil vises her. + Dine registrerte spor + … vil vises her. Kartlegging av nåværende spor Kartlegging av forrige spor @@ -103,9 +103,8 @@ Tøm-knapp Fortsett-knapp Overskrift for statistikkarket - Veksle opptaksdatavisning Nedtrekksmeny for ytterligere spor - Sporeksport-knapp - Sporslettingsknapp - Delingsknapp som muliggjør eksport som GPX + Sporeksport-knapp + Sporslettingsknapp + Delingsknapp som muliggjør eksport som GPX \ No newline at end of file diff --git a/app/src/main/res/values-night-v23/styles.xml b/app/src/main/res/values-night-v23/styles.xml deleted file mode 100755 index 5af2c59..0000000 --- a/app/src/main/res/values-night-v23/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d16b819..f1023bd 100755 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,26 +1,41 @@ - + + + @color/trackbook_grey_darker @color/trackbook_black + @color/trackbook_black + @color/trackbook_black - @color/trackbook_white - @color/trackbook_white + @color/trackbook_grey_light + @color/trackbook_blue @color/trackbook_white @color/trackbook_grey_dark @color/trackbook_white - @color/trackbook_grey_dark + @color/trackbook_black + + @color/trackbook_white + @color/trackbook_grey_very_light + + @color/trackbook_white + @color/trackbook_grey_very_light + @color/trackbook_grey_lighter + @color/trackbook_blue + + @color/trackbook_grey_dark + @color/trackbook_red @color/trackbook_white @color/trackbook_white @color/trackbook_black - @color/trackbook_white @color/trackbook_white @color/trackbook_white @color/trackbook_black_85percent - @color/trackbook_black_95percent + @color/trackbook_black + @color/trackbook_grey_dark diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index a5c339b..7b3da0b 100755 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -2,18 +2,17 @@ - - - diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 070e07d..4a37252 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -21,7 +21,7 @@ GPX-opname gestart GPX-opname hervat Locatie is uitgeschakeld; Trackbook zal niet werken. - + Wissen Opslaan en wissen Hervatten @@ -40,7 +40,7 @@ Exporteren en overschijven Deel GPX bestand met Opslaan niet mogelijk - Trackbook heeft nog geen locaties opgenomen. + Trackbook heeft nog geen locaties opgenomen. Opname voortzetten Rechten verleend. @@ -90,8 +90,8 @@ Trackbook gebruikt osmdroid. osmdroid slaat kaarttegels op in een cache op Android\'s externe opslag. U kunt de kaartcache vinden in de osmdroid-map op het hoogste niveau van het gebruikersbestandssysteem. Ik snap het! - Opgenomen tracks - ... zal hier verschijnen. + Opgenomen tracks + ... zal hier verschijnen. Kaart van de huidige track Kaart van de laatste track @@ -103,9 +103,8 @@ Wissen-knop Voortzetten-knop Kop van het statistiekenblad - Opnamegegevensweergave tonen/verbergen Uitrolmenu voor verdere tracks - Track exporteerknop - Track verwijderknop - Deelknop met ondersteuning voor het exporteren naar GPX + Track exporteerknop + Track verwijderknop + Deelknop met ondersteuning voor het exporteren naar GPX \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 35dc049..5d5e79f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -24,7 +24,7 @@ Spårningen återupptogs Plats är avstängt. Trackbook kommer inte fungera. - + Rensa Spara Återuppta @@ -44,7 +44,7 @@ Exportera och skriv över Dela GPX-fil med Kunde inte spara - Trackbook spelade inte in några vägpunkter så här långt. + Trackbook spelade inte in några vägpunkter så här långt. Återuppta inspelning diff --git a/app/src/main/res/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml new file mode 100644 index 0000000..8e66f10 --- /dev/null +++ b/app/src/main/res/values-sw600dp/bools.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml deleted file mode 100755 index fad0a76..0000000 --- a/app/src/main/res/values-v23/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml deleted file mode 100755 index 544ea84..0000000 --- a/app/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 24dp - 600dp - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9beeb70..eeb26b2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,7 +32,7 @@ 导出并覆盖 分享GPX文件到 无法保存 - Trackbook目前没有记录到任何航点。 + Trackbook目前没有记录到任何航点。 恢复记录 授予权限。 无法启动Ttrackbook。 @@ -78,8 +78,8 @@ 存储 Trackbook使用osmdroid,它在Android的外部存储上缓存地图图块。你可以在 /sdcard/Android/data/org.y20k.trackbook/files/tiles 中找到地图缓存。 授权! - 你的运动记录 - ... 会显示在这里。 + 你的运动记录 + ... 会显示在这里。 当前轨道的映射 最后一个轨道的映射 我的位置按钮 @@ -90,9 +90,8 @@ 删除按钮 恢复按钮 统计表的标题 - 切换录制数据显示 记录信息的下拉菜单 - 记录导出按钮 - 删除记录按钮 - 分享按钮,提供导出GPX文件 + 记录导出按钮 + 删除记录按钮 + 分享按钮,提供导出GPX文件 diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 0000000..c2dcd8b --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml old mode 100755 new mode 100644 index 73443ad..3d65f69 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,10 +2,14 @@ + + @color/trackbook_grey_very_light @color/trackbook_white + @color/trackbook_white @color/trackbook_white - @color/trackbook_grey - @color/trackbook_grey + + @color/trackbook_grey_light + @color/trackbook_blue @color/trackbook_grey_dark @color/trackbook_white @@ -13,23 +17,47 @@ @color/trackbook_grey @color/trackbook_white + @color/trackbook_grey_dark + @color/trackbook_grey + + @color/trackbook_grey + @color/trackbook_grey_light + @color/trackbook_grey_lighter + @color/trackbook_blue + + @color/trackbook_white + @color/trackbook_red + @color/trackbook_grey_dark @color/trackbook_grey_dark @color/trackbook_white - @color/trackbook_grey @color/trackbook_grey_dark @color/trackbook_grey_dark @color/trackbook_white_85percent - @color/trackbook_white_95percent + @color/trackbook_white + @color/trackbook_grey_lighter - #FFDC3D33 + #FF595959 + #FF7D7D7D + #FFDADADA + #FFF2F2F2 + #FF414141 + #FF2D2D2D + + #DC3D33 #FFE15950 #FFCA2D23 #FFAD261E + #FF121212 + #FFFFFFFF + #00ffffff + + + #FFE6BA64 #FF3C98DB @@ -38,20 +66,10 @@ #FF4CAF50 - #FFFFFFFF #D9FFFFFF #F2FFFFFF - #FF000000 - #D9000000 - #F2000000 - - #FFD2D6DA - #FFBDC1C6 - #FF5F6368 - #FF3C4043 - #FF17181A - - #00000000 + #D9121212 + #F2121212 #FF3C98DB diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml old mode 100755 new mode 100644 index 60eedb6..5edf8bf --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,20 @@ - + + Map + Tracks + Track + Settings + + Trackbook - + + Map - Last Tracks - + Tracks + Settings + + Trackbook running Trackbook not running Stop @@ -15,33 +24,53 @@ Distance Movement Recording State Display duration and distance. Option to stop movement recording. - + + Tracking stopped Tracking started Tracking resumed Location is turned off. Trackbook will not work. - + Location permission not granted. Trackbook will not work. + + Clear Save Resume - + + + Cancel + OK + Show details + No details available Cancel Clear Recording? Clear Delete Recording? Delete this recording: Delete + Unable to save + Trackbook did not record any waypoints so far. + Resume Recording Export Recording as GPX? Export this recording as GPX track. Export Export and Overwrite? File already exists. Export and overwrite this recording as GPX track. Export and Overwrite + Rename + Enter a new name + Rename Recording Share GPX file with - Unable to save - Trackbook did not record any waypoints so far. - Resume Recording - + Do you really want to do this? + Cancel + No + Yes + Remove + Remove this recording? + Decide + + + Permissions granted. Unable to start Trackbook. Unable to access external storage. @@ -59,11 +88,13 @@ Switching to Night mode (long press detected) Switching to Day mode (long press detected) Switching to Follow System Setting mode (long press detected) - + + Source Time Accuracy - + + Statistics track data missing Total distance: @@ -77,7 +108,13 @@ Lowest waypoint: Elevation (uphill): Elevation (downhill): - + + + Delete + Rename + Share + + Hello Trackbook App Icon Trackbook @@ -88,10 +125,31 @@ STORAGE Trackbook uses osmdroid, which caches map tiles on Android\'s external storage. You can find the map cache in the osmdroid folder on the top level of the user-facing file system. Got it! - - Your recorded tracks - … will show up here. - + + + Your recorded tracks + … will show up here. + + + Discard location fixes with an accuracy larger than + Accuracy Threshold + Advanced + General + Restrict to GPS + Currently using GPS and Network for localization. + Currently using only GPS for localization. + Currently using metric units (Kilometer, Meter). + Currently using imperial units (Miles, Feet). + Enable Imperial Measurements + Set Advanced settings to their default values. + Reset + + + hrs + min + sec + + Mapping of current track Mapping of last track My Location button @@ -101,10 +159,17 @@ Save button Clear button Resume button + Mark as starred button + Track export button Headline of the statistics sheet - Toggle recording data display + Track delete button + Track edit button + Share as GPX button Dropdown menu for further tracks - Track export button - Track delete button - Share button that offers to export as GPX + + + 23.0 km • 5 hrs 23 min 42 sec + July 20, 1969 + track data missing + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml old mode 100755 new mode 100644 index 632bb10..3967dce --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,18 +2,30 @@ - - - + + + diff --git a/app/src/main/res/xml/backupscheme.xml b/app/src/main/res/xml/backupscheme.xml deleted file mode 100755 index de7b18d..0000000 --- a/app/src/main/res/xml/backupscheme.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml old mode 100755 new mode 100644 index 8dcfc50..4495c28 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,10 +1,4 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/trackbook-app-icon-current.svg b/assets/trackbook-app-icon-current.svg index fb338b4..e61f0fe 100644 --- a/assets/trackbook-app-icon-current.svg +++ b/assets/trackbook-app-icon-current.svg @@ -5,7 +5,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" id="svg2" xml:space="preserve" width="108pt" height="108pt" viewBox="0 0 144.00001 144.00001" sodipodi:docname="trackbook-app-icon-current.svg" - inkscape:version="0.92.2 5c3e80d, 2017-08-06" inkscape:export-filename="/Users/solaris/Desktop/trackbook/assets/trackbook-app-icon-current-background.png" + inkscape:version="0.92.4 5da689c313, 2019-01-14" inkscape:export-filename="/Users/solaris/Desktop/trackbook/assets/trackbook-app-icon-current-background.png" inkscape:export-xdpi="600" inkscape:export-ydpi="600">image/svg+xml