diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 37fcb95ea..e5726f2d2 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,8 +27,8 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build with Gradle # skip tests, coverage, etc. - run: ./gradlew assembleDebug --stacktrace -x test + - name: Build with Gradle + run: ./gradlew assembleDebug --stacktrace - name: Rename output APK run: | @@ -41,3 +41,25 @@ jobs: with: name: debug-${{ env.ARTIFACT_DATE }} path: app/build/outputs/apk/debug/OSMTracker-debug-*.apk + + - name: Run unit tests and jacoco coverage + run: ./gradlew testDebugUnitTest jacocoTestReport --stacktrace + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run connected tests + uses: ReactiveCircus/android-emulator-runner@v2 + with: + api-level: 26 + script: | + adb logcat & + ./gradlew connectedCheck --no-parallel + + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + format: jacoco diff --git a/README.md b/README.md index 47bf24e02..335238300 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,28 @@ +# OSMTracker for Android™ + +![Build](https://github.com/labexp/osmtracker-android/actions/workflows/android.yml/badge.svg?branch=develop) +[![Coverage Status](https://coveralls.io/repos/github/labexp/osmtracker-android/badge.svg?branch=develop)](https://coveralls.io/github/labexp/osmtracker-android?branch=develop) + +**OSMTracker for Android™** is a mobile app designed for OpenStreetMap mappers and outdoor adventurers. It lets you log a GPS track to document your journey. Its customizable buttons let you simply add POIs as track points directly inside your GPX track. + +It also supports voice recording, picture taking, and note-taking. This is the perfect app to survey a place or a path whether you are hiking, cycling, or exploring new areas. + +![Main screen](https://wiki.openstreetmap.org/w/images/thumb/7/7b/OSMTracker-Android-main-screen-en.jpg/200px-OSMTracker-Android-main-screen-en.jpg) + +Here is a screenshot of the main screen with its default buttons. You can [customize](https://github.com/labexp/osmtracker-android/wiki/Custom-buttons-layouts) these buttons to your liking. + +## Get the App 📲 + [Get it on Google Play](https://play.google.com/store/apps/details?id=net.osmtracker) [Get it on F-Droid](https://f-droid.org/app/net.osmtracker) -OSMTracker for Android™ official source code repository is [https://github.com/labexp/osmtracker-android](https://github.com/labexp/osmtracker-android). +## More Info ℹ️ -[![Build Status](https://travis-ci.org/labexp/osmtracker-android.svg?branch=master)](https://travis-ci.org/labexp/osmtracker-android) +- Find more information in the [documentation](https://github.com/labexp/osmtracker-android/wiki) +- Submit bug reports in the [issue tracker](https://github.com/labexp/osmtracker-android/issues) +- Contributions are welcome, please visit our [contributor guide](https://github.com/labexp/osmtracker-android/blob/master/CONTRIBUTING.md) +- Translations can be done on [Transifex](https://explore.transifex.com/labexp/osmtracker-android/) -For more information about the project, documentation and bug reports please visit https://github.com/labexp/osmtracker-android/wiki +## Note 📝 -If you are interested in contribute to this project, please visit https://github.com/labexp/osmtracker-android/blob/master/CONTRIBUTING.md to know the way you could do it. - -To help translate OSMTracker, please visit https://www.transifex.com/projects/p/osmtracker-android/ +OSMTracker for Android™ official source code repository is [https://github.com/labexp/osmtracker-android](https://github.com/labexp/osmtracker-android). diff --git a/app/build.gradle b/app/build.gradle index 2087fcec9..7f0d40d6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,25 +1,47 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'jacoco' +} android { - - useLibrary 'org.apache.http.legacy' - compileSdk 33 + namespace 'net.osmtracker' + compileSdk 35 defaultConfig { applicationId "net.osmtracker" - minSdkVersion 25 - targetSdkVersion 33 + minSdk 25 + targetSdk 35 multiDexEnabled true + // Version code should be increased after each release + versionCode 65 + versionName new Date().format('yyyy.MM.dd') + testApplicationId "net.osmtracker.test" - testInstrumentationRunner "android.test.InstrumentationTestRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.findByName('release') + } + debug { + versionNameSuffix "-dev" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 } signingConfigs { - // Create a file $HOME/.gradle/gradle.properties - // containing the values signing.storeFile=..., etc. if (project.hasProperty('signing.storeFile')) { release { storeFile file(project.property('signing.storeFile')) @@ -30,92 +52,72 @@ android { } } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - if (signingConfigs.hasProperty('release')) { - signingConfig signingConfigs.release - } - - - } - } - packagingOptions { - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } - android { - lintOptions { - abortOnError false - } + resources.excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE.txt' + ] } - - defaultConfig { - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - } - testOptions { unitTests.returnDefaultValues = true + unitTests.all { + it.jvmArgs = [ + '--add-opens', 'java.base/java.io=ALL-UNNAMED', + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens', 'java.base/java.util=ALL-UNNAMED' + ] + } animationsDisabled = true } - namespace 'net.osmtracker' - } dependencies { - - //implementation 'org.apache.james:apache-mime4j-core:0.8.3' - - //implementation 'org.apache.httpcomponents:httpmime:4.5.6' - - implementation 'org.osmdroid:osmdroid-android:6.1.5' - //implementation 'org.apache.httpcomponents:httpcore:4.4.13' - - //implementation 'oauth.signpost:signpost-commonshttp4:1.2.1.2' - implementation 'org.slf4j:slf4j-android:1.7.30' - implementation 'androidx.core:core:1.6.0' - //implementation 'org.apache.commons:commons-io:1.3.2' - - // For upload traces to osm server + // Lib to show OSM map as background + implementation 'org.osmdroid:osmdroid-android:6.1.20' + // OAuth implementation 'net.openid:appauth:0.11.1' - implementation 'de.westnordost:osmapi-traces:3.1' - - // Required for local unit tests (JUnit 4 framework) - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.4.0' + // For upload traces to osm server + implementation('de.westnordost:osmapi-traces:3.1') { + // Already included in Android + exclude group: 'net.sf.kxml', module: 'kxml2' + exclude group: 'xmlpull', module: 'xmlpull' + } + // App intro + implementation 'com.github.AppIntro:AppIntro:6.3.1' - testImplementation 'org.powermock:powermock-core:1.7.0RC2' - testImplementation 'org.powermock:powermock-module-junit4:1.7.0RC2' - testImplementation 'org.powermock:powermock-api-mockito2:1.7.0RC2' + implementation 'com.google.android.material:material:1.12.0' + implementation 'org.slf4j:slf4j-android:1.7.30' + implementation 'org.apache.commons:commons-io:1.3.2' + implementation 'androidx.core:core:1.15.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.7.0' + + // Required -- JUnit 4 framework + testImplementation 'junit:junit:4.13.2' + // Robolectric environment + testImplementation "androidx.test:core:1.6.1" + // Mockito framework + testImplementation "org.mockito:mockito-core:3.12.4" + + testImplementation 'org.powermock:powermock-core:2.0.9' + testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' + testImplementation 'org.powermock:powermock-module-junit4:2.0.9' + // Required for local unit tests. Prevent null in JSONObject, JSONArray, etc. + testImplementation 'org.json:json:20240303' // Required for instrumented tests - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.3.0' - - //compileOnly 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2' - - // AndroidX Capable version - implementation 'com.github.AppIntro:AppIntro:6.0.0' - implementation 'com.google.android.material:material:1.4.0' - - -} - -configurations.all { - // it's already included in Android and not need from osmapi - exclude group:'net.sf.kxml', module:'kxml2' - exclude group: 'xmlpull', module: 'xmlpull' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test:rules:1.6.1' } repositories { + google() mavenCentral() maven { url "https://jitpack.io" } } + +apply from: "jacoco.gradle" diff --git a/app/jacoco.gradle b/app/jacoco.gradle new file mode 100644 index 000000000..8bcded24b --- /dev/null +++ b/app/jacoco.gradle @@ -0,0 +1,25 @@ +jacoco { + toolVersion = jacocoVersion +} + +tasks.withType(Test).configureEach { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + +tasks.register("jacocoTestReport", JacocoReport) { + dependsOn testDebugUnitTest + + reports { + xml.required = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*'] + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(fileTree(dir: layout.buildDirectory.dir("intermediates/javac/debug").get().asFile, excludes: fileFilter)) + executionData.setFrom(fileTree(dir: layout.buildDirectory.get(), includes: [ + 'jacoco/testDebugUnitTest.exec', 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec' + ])) +} \ No newline at end of file diff --git a/app/src/androidTest/java/net/osmtracker/layouts/DeleteLayoutTest.java b/app/src/androidTest/java/net/osmtracker/layouts/DeleteLayoutTest.java index 59b16cc2d..63fd0a464 100644 --- a/app/src/androidTest/java/net/osmtracker/layouts/DeleteLayoutTest.java +++ b/app/src/androidTest/java/net/osmtracker/layouts/DeleteLayoutTest.java @@ -8,6 +8,7 @@ import net.osmtracker.activity.ButtonsPresets; import net.osmtracker.activity.Preferences; import net.osmtracker.util.CustomLayoutsUtils; +import net.osmtracker.util.TestUtils; import org.junit.Rule; import org.junit.Test; @@ -32,6 +33,9 @@ public class DeleteLayoutTest { + @Rule + public GrantPermissionRule storagePermission = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE); + @Rule public ActivityTestRule mRule = new ActivityTestRule(ButtonsPresets.class) { @Override @@ -47,10 +51,6 @@ protected void beforeActivityLaunched() { } }; - // Storage permissions are required - @Rule - public GrantPermissionRule writePermission = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE); - private static String layoutName = "mock"; private static String ISOLanguageCode = "es"; diff --git a/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java b/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java index 57040fe94..1c20d9c7e 100644 --- a/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java +++ b/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java @@ -1,11 +1,27 @@ package net.osmtracker.layouts; +import static androidx.test.espresso.Espresso.onData; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.scrollTo; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.PreferenceMatchers.withTitleText; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static junit.framework.TestCase.fail; +import static net.osmtracker.util.WaitForView.waitForView; + +import android.Manifest; import android.content.SharedPreferences; import android.preference.PreferenceManager; import androidx.test.espresso.Espresso; -import androidx.test.espresso.assertion.ViewAssertions; import androidx.test.rule.ActivityTestRule; +import androidx.test.rule.GrantPermissionRule; import net.osmtracker.OSMTracker; import net.osmtracker.R; @@ -18,19 +34,17 @@ import java.util.Locale; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static androidx.test.espresso.Espresso.onData; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.scrollTo; -import static androidx.test.espresso.matcher.PreferenceMatchers.withTitleText; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static junit.framework.TestCase.fail; - public class DownloadLayoutTest { + + private final int WAIT_VIEW_TIMEOUT = 5000; + + @Rule + public GrantPermissionRule fineLocationPermission = GrantPermissionRule.grant(Manifest.permission.ACCESS_FINE_LOCATION); + @Rule + public GrantPermissionRule coarseLocationPermission = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION); + @Rule + public GrantPermissionRule writeStoragePermission = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE); + @Rule public ActivityTestRule mRule = new ActivityTestRule(TrackManager.class) { @Override @@ -71,13 +85,16 @@ public void deleteLayoutsDirectory(){ /** * Assuming being in TrackManager */ - public void navigateToAvailableLayouts(){ + public void navigateToAvailableLayouts() { + // Open options menu in the Action Bar openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); - + // Click on "Settings" in this menu onView(withText(TestUtils.getStringResource(R.string.menu_settings))).perform(click()); - + // Click on "Buttons presets" settings onData(withTitleText(TestUtils.getStringResource(R.string.prefs_ui_buttons_layout))).perform(scrollTo(), click()); - + // Wait for "+" to be visible + onView(isRoot()).perform(waitForView(R.id.launch_available, WAIT_VIEW_TIMEOUT)); + // Perform a click action on the "+" button onView(withId(R.id.launch_available)).perform(click()); } @@ -91,7 +108,7 @@ private void makePostDownloadAssertions(String layoutName) { Espresso.pressBack(); // Check the layout appears as a new option in AvailableLayouts - onView(withText(layoutName.toLowerCase())).check(ViewAssertions.matches(isDisplayed())); + onView(withText(layoutName.toLowerCase())).check(matches(isDisplayed())); // Select the layout onView(withText(layoutName.toLowerCase())).perform(click()); @@ -104,7 +121,7 @@ private void makePostDownloadAssertions(String layoutName) { // Check the buttons are loaded correctly String expectedButtonsLabels[] = new String[]{"A", "B", "C"}; for(String label : expectedButtonsLabels) - onView(withText(label)).check(ViewAssertions.matches(isDisplayed())); + onView(withText(label)).check(matches(isDisplayed())); } diff --git a/app/src/androidTest/java/net/osmtracker/layouts/RepositorySettingsDialogTest.java b/app/src/androidTest/java/net/osmtracker/layouts/RepositorySettingsDialogTest.java index 4fc9c8729..607587fc0 100644 --- a/app/src/androidTest/java/net/osmtracker/layouts/RepositorySettingsDialogTest.java +++ b/app/src/androidTest/java/net/osmtracker/layouts/RepositorySettingsDialogTest.java @@ -1,16 +1,5 @@ package net.osmtracker.layouts; -import androidx.test.espresso.Espresso; -import androidx.test.espresso.ViewAssertion; -import androidx.test.rule.ActivityTestRule; - -import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.activity.AvailableLayouts; - -import org.hamcrest.Matcher; -import org.junit.Rule; -import org.junit.Test; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.clearText; import static androidx.test.espresso.action.ViewActions.click; @@ -27,6 +16,17 @@ import static net.osmtracker.util.TestUtils.getStringResource; import static org.hamcrest.core.IsNot.not; +import androidx.test.espresso.ViewAssertion; +import androidx.test.rule.ActivityTestRule; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.activity.AvailableLayouts; + +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; + public class RepositorySettingsDialogTest { @Rule @@ -66,7 +66,7 @@ public void checkStateAfterToggle(int expectedActiveId, int expectedInactiveId){ onView(withId(expectedInactiveId)).check(matches(isEnabled())); } - public void checkRepositoryValidity(String user, String repo, String branch, boolean isValid){ + public void checkRepositoryValidity(String user, String repo, String branch, boolean isValid) { onView(withId(R.id.github_config)).perform(click()); onView(withId(R.id.custom_server)).perform(click(), closeSoftKeyboard()); @@ -84,7 +84,6 @@ public void checkRepositoryValidity(String user, String repo, String branch, boo ViewAssertion expectedDialogState = (isValid) ? doesNotExist() : matches(isDisplayed()); checkDialogState(expectedDialogState); - } /** diff --git a/app/src/androidTest/java/net/osmtracker/util/LogcatHelper.java b/app/src/androidTest/java/net/osmtracker/util/LogcatHelper.java new file mode 100644 index 000000000..8049c6dbd --- /dev/null +++ b/app/src/androidTest/java/net/osmtracker/util/LogcatHelper.java @@ -0,0 +1,29 @@ +package net.osmtracker.util; + +import android.util.Log; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.regex.Pattern; + +public class LogcatHelper { + + public static boolean checkLogForMessage(String tag, String message) { + try { + Process process = Runtime.getRuntime().exec("logcat -d " + tag + ":I *:S"); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + String line; + Pattern pattern = Pattern.compile(".*\\b" + Pattern.quote(message) + "\\b.*"); + + while ((line = bufferedReader.readLine()) != null) { + if (pattern.matcher(line).matches()) { + return true; + } + } + } catch (Exception e) { + Log.e("LogcatHelper", "Error reading logcat output", e); + } + + return false; + } +} diff --git a/app/src/androidTest/java/net/osmtracker/util/TestUtils.java b/app/src/androidTest/java/net/osmtracker/util/TestUtils.java index 15b621190..2c733c75f 100644 --- a/app/src/androidTest/java/net/osmtracker/util/TestUtils.java +++ b/app/src/androidTest/java/net/osmtracker/util/TestUtils.java @@ -1,9 +1,11 @@ package net.osmtracker.util; +import static net.osmtracker.util.LogcatHelper.checkLogForMessage; + import android.content.Context; import android.content.SharedPreferences; -import android.os.Environment; import android.preference.PreferenceManager; + import androidx.test.platform.app.InstrumentationRegistry; import net.osmtracker.OSMTracker; @@ -14,11 +16,6 @@ import java.io.FileWriter; import java.util.ArrayList; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - /** * Contains common and reusable static methods used for tests */ @@ -118,8 +115,10 @@ public static File getLayoutsDirectory(){ } public static void checkToastIsShownWith(String text){ - onView(withText(text)).inRoot(new ToastMatcher()) - .check(matches(isDisplayed())); + // Espresso can not check Toast for android >= 11 + // https://github.com/android/android-test/issues/803 + //onView(withText(text)).inRoot(new ToastMatcher()).check(matches(isDisplayed())); + checkLogForMessage("TOAST", text); } public static String getStringResource(int resourceId){ diff --git a/app/src/androidTest/java/net/osmtracker/util/ToastMatcher.java b/app/src/androidTest/java/net/osmtracker/util/ToastMatcher.java index 0a1f1433a..f5e3c19f6 100644 --- a/app/src/androidTest/java/net/osmtracker/util/ToastMatcher.java +++ b/app/src/androidTest/java/net/osmtracker/util/ToastMatcher.java @@ -9,7 +9,7 @@ /** * Used to search for Toasts in the UI - * This class was taken from https://stackoverflow.com/questions/28390574/checking-toast-message-in-android-espresso + * This class was taken from stackoverflow */ public class ToastMatcher extends TypeSafeMatcher { diff --git a/app/src/androidTest/java/net/osmtracker/util/WaitForView.java b/app/src/androidTest/java/net/osmtracker/util/WaitForView.java new file mode 100644 index 000000000..2cbc9aed6 --- /dev/null +++ b/app/src/androidTest/java/net/osmtracker/util/WaitForView.java @@ -0,0 +1,65 @@ +package net.osmtracker.util; + +import android.view.View; +import androidx.test.espresso.PerformException; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.matcher.ViewMatchers; +import androidx.test.espresso.util.HumanReadables; +import androidx.test.espresso.util.TreeIterables; + +import org.hamcrest.Matcher; + +import java.util.concurrent.TimeoutException; + +public class WaitForView implements ViewAction { + private final int viewId; + private final long timeout; + + /** + * This ViewAction tells espresso to wait till a certain view is found in the view hierarchy. + * @param viewId The id of the view to wait for. + * @param timeout The maximum time which espresso will wait for the view to show up (in milliseconds) + */ + public WaitForView(int viewId, long timeout) { + this.viewId = viewId; + this.timeout = timeout; + } + + @Override + public Matcher getConstraints() { + return ViewMatchers.isRoot(); + } + + @Override + public String getDescription() { + return "wait for a specific view with id " + viewId + " during " + timeout + " millis."; + } + + @Override + public void perform(UiController uiController, View rootView) { + uiController.loopMainThreadUntilIdle(); + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeout; + Matcher viewMatcher = ViewMatchers.withId(viewId); + + do { + for (View child : TreeIterables.breadthFirstViewTraversal(rootView)) { + if (viewMatcher.matches(child)) { + return; + } + } + uiController.loopMainThreadForAtLeast(100); + } while (System.currentTimeMillis() < endTime); + + throw new PerformException.Builder() + .withCause(new TimeoutException()) + .withActionDescription(this.getDescription()) + .withViewDescription(HumanReadables.describe(rootView)) + .build(); + } + + public static ViewAction waitForView(final int viewId, final long timeout) { + return new WaitForView(viewId, timeout); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2b63e2b8..053700c60 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,21 @@ + android:installLocation="auto"> + - + @@ -127,4 +126,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/net/osmtracker/OSMTracker.java b/app/src/main/java/net/osmtracker/OSMTracker.java index f8c18b75d..046a4d23a 100644 --- a/app/src/main/java/net/osmtracker/OSMTracker.java +++ b/app/src/main/java/net/osmtracker/OSMTracker.java @@ -25,6 +25,7 @@ public static final class Preferences { public final static String KEY_GPS_LOGGING_MIN_DISTANCE = "gps.logging.min_distance"; public final static String KEY_USE_BAROMETER = "gpx.use_barometer"; public final static String KEY_OUTPUT_FILENAME = "gpx.filename"; + public final static String KEY_OUTPUT_FILENAME_LABEL = "gpx.filename.label"; public final static String KEY_OUTPUT_ACCURACY = "gpx.accuracy"; public final static String KEY_OUTPUT_GPX_HDOP_APPROXIMATION = "gpx.hdop.approximation"; public final static String KEY_OUTPUT_DIR_PER_TRACK = "gpx.directory_per_track"; @@ -62,8 +63,10 @@ public static final class Preferences { public final static String VAL_OUTPUT_FILENAME_NAME = "name"; public final static String VAL_OUTPUT_FILENAME_NAME_DATE = "name_date"; + public final static String VAL_OUTPUT_FILENAME_DATE_NAME = "date_name"; public final static String VAL_OUTPUT_FILENAME_DATE = "date"; public final static String VAL_OUTPUT_FILENAME = VAL_OUTPUT_FILENAME_NAME_DATE; + public final static String VAL_OUTPUT_FILENAME_LABEL = ""; public final static String VAL_OUTPUT_ACCURACY_NONE = "none"; public final static String VAL_OUTPUT_ACCURACY_WPT_NAME = "wpt_name"; diff --git a/app/src/main/java/net/osmtracker/activity/About.java b/app/src/main/java/net/osmtracker/activity/About.java index 8386ef4f9..84aa7804c 100644 --- a/app/src/main/java/net/osmtracker/activity/About.java +++ b/app/src/main/java/net/osmtracker/activity/About.java @@ -10,6 +10,7 @@ import android.app.Dialog; import android.app.ProgressDialog; import android.content.DialogInterface; +import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; @@ -47,7 +48,7 @@ protected void onCreate(Bundle savedInstanceState) { } catch (NameNotFoundException nnfe) { // Should not occur } - + findViewById(R.id.about_debug_info_button).setOnClickListener( new OnClickListener() { @@ -76,7 +77,7 @@ public void onClick(View view) { File dbFile = getDatabasePath(DatabaseHelper.DB_NAME); File targetFolder = new File( - view.getContext().getExternalFilesDir(null), + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), //Environment.getExternalStorageDirectory(), PreferenceManager.getDefaultSharedPreferences(About.this).getString( OSMTracker.Preferences.KEY_STORAGE_DIR, @@ -128,10 +129,17 @@ public ProgressDialog getExportDbProgressDialog() { private String getDebugInfo() { File externalStorageDir = this.getExternalFilesDir(null); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + String exportDirectoryNameInPreferences = preferences.getString( + OSMTracker.Preferences.KEY_STORAGE_DIR, OSMTracker.Preferences.VAL_STORAGE_DIR); + File baseExportDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), + exportDirectoryNameInPreferences); return "External Storage Directory: '" + externalStorageDir + "'\n" + "External Storage State: '" + Environment.getExternalStorageState() + "'\n" + "Can write to external storage: " - + Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) + "\n"; + + Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) + "\n" + + "Export External Public Storage Directory: '" + + baseExportDirectory + "'\n"; } } diff --git a/app/src/main/java/net/osmtracker/activity/AvailableLayouts.java b/app/src/main/java/net/osmtracker/activity/AvailableLayouts.java index 30553259f..eb215767c 100644 --- a/app/src/main/java/net/osmtracker/activity/AvailableLayouts.java +++ b/app/src/main/java/net/osmtracker/activity/AvailableLayouts.java @@ -151,12 +151,13 @@ public void setAvailableLayouts(List options) { Button layoutButton = new Button(this); layoutButton.setHeight(150); layoutButton.setText(CustomLayoutsUtils.convertFileName(option)); - layoutButton.setTextSize((float)22 ); + layoutButton.setTextSize(16f); layoutButton.setTextColor(Color.WHITE); + layoutButton.setSingleLine(false); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - layoutParams.setMargins(110, 0, 110, 0); + layoutParams.setMargins(50, 10, 50, 10); layoutButton.setLayoutParams(layoutParams); - layoutButton.setPadding(10,20,10,20); + layoutButton.setPadding(40, 30, 40, 30); layoutButton.setOnClickListener(listener); rootLayout.addView(layoutButton,AT_START); } @@ -247,7 +248,9 @@ public void onClick(DialogInterface dialog, int which) { protected void onPostExecute(Boolean result){ //validating the github repository if(result){ - Toast.makeText(AvailableLayouts.this, getResources().getString(R.string.github_repository_settings_valid_server), Toast.LENGTH_SHORT).show(); + String message = getResources().getString(R.string.github_repository_settings_valid_server); + Log.i("TOAST", message); + Toast.makeText(AvailableLayouts.this, message, Toast.LENGTH_SHORT).show(); //save the entered options into the shared preferences file editor.putString(OSMTracker.Preferences.KEY_GITHUB_USERNAME, repositoryCustomOptions[0]); editor.putString(OSMTracker.Preferences.KEY_REPOSITORY_NAME, repositoryCustomOptions[1]); @@ -257,7 +260,9 @@ protected void onPostExecute(Boolean result){ tmpSharedPref.edit().putBoolean("isCallBack", false).commit(); retrieveAvailableLayouts(); }else{ - Toast.makeText(AvailableLayouts.this, getResources().getString(R.string.github_repository_settings_invalid_server), Toast.LENGTH_SHORT).show(); + String message = getResources().getString(R.string.github_repository_settings_invalid_server); + Log.e("TOAST", message); + Toast.makeText(AvailableLayouts.this, message, Toast.LENGTH_SHORT).show(); tmpSharedPref.edit().putString(OSMTracker.Preferences.KEY_GITHUB_USERNAME, repositoryCustomOptions[0]).commit(); tmpSharedPref.edit().putString(OSMTracker.Preferences.KEY_REPOSITORY_NAME, repositoryCustomOptions[1]).commit(); tmpSharedPref.edit().putString(OSMTracker.Preferences.KEY_BRANCH_NAME, repositoryCustomOptions[2]).commit(); @@ -321,8 +326,8 @@ private void toggleRepositoryOptions(boolean status){ //setting the custom options into text fields else{ etxGithubUsername.setText(sharedPrefs.getString(OSMTracker.Preferences.KEY_GITHUB_USERNAME, "")); - etxRepositoryName.setText(sharedPrefs.getString(OSMTracker.Preferences.KEY_REPOSITORY_NAME,"")); - etxBranchName.setText(sharedPrefs.getString(OSMTracker.Preferences.KEY_BRANCH_NAME, "")); + etxRepositoryName.setText(sharedPrefs.getString(OSMTracker.Preferences.KEY_REPOSITORY_NAME, OSMTracker.Preferences.VAL_REPOSITORY_NAME)); + etxBranchName.setText(sharedPrefs.getString(OSMTracker.Preferences.KEY_BRANCH_NAME, OSMTracker.Preferences.VAL_BRANCH_NAME)); } } @@ -492,9 +497,11 @@ protected void onPostExecute(Boolean status){ String message=""; if (status) { message = getResources().getString(R.string.available_layouts_successful_download); + Log.i("TOAST", message); } else { message = getResources().getString(R.string.available_layouts_unsuccessful_download); + Log.e("TOAST", message); } Toast.makeText(getApplicationContext(),message,Toast.LENGTH_LONG).show(); dialog.dismiss(); diff --git a/app/src/main/java/net/osmtracker/activity/ButtonsPresets.java b/app/src/main/java/net/osmtracker/activity/ButtonsPresets.java index ed6a9fb46..2d5f864dd 100644 --- a/app/src/main/java/net/osmtracker/activity/ButtonsPresets.java +++ b/app/src/main/java/net/osmtracker/activity/ButtonsPresets.java @@ -285,8 +285,11 @@ public void onClick(DialogInterface dialog, int which) { int messageToShowId = (successfulDeletion) ? R.string.buttons_presets_successful_delete : R.string.buttons_presets_unsuccessful_delete; + String message = getResources().getString(messageToShowId); - Toast.makeText(getApplicationContext(), getResources().getString(messageToShowId), Toast.LENGTH_SHORT).show(); + Log.println(successfulDeletion ? Log.INFO : Log.ERROR, "TOAST", message); + + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show(); //reload the activity refreshActivity(); diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index c0e8f3d64..707fefbc2 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -1,24 +1,5 @@ package net.osmtracker.activity; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.db.TrackContentProvider; -import net.osmtracker.overlay.WayPointsOverlay; - -import org.osmdroid.api.IMapController; -import org.osmdroid.config.Configuration; -import org.osmdroid.tileprovider.tilesource.ITileSource; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.PathOverlay; -import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; -import org.osmdroid.views.overlay.ScaleBarOverlay; - import android.app.Activity; import android.content.ContentUris; import android.content.Intent; @@ -26,6 +7,7 @@ import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Color; +import android.graphics.Paint; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; @@ -35,49 +17,62 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.overlay.WayPointsOverlay; + +import org.osmdroid.api.IMapController; +import org.osmdroid.config.Configuration; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.CustomZoomButtonsController; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Polyline; +import org.osmdroid.views.overlay.ScaleBarOverlay; +import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; + +import java.util.ArrayList; +import java.util.List; /** * Display current track over an OSM map. - * Based on osmdroid code http://osmdroid.googlecode.com/ + * Based on osmdroid code *

* Used only if {@link OSMTracker.Preferences#KEY_UI_DISPLAYTRACK_OSM} is set. * Otherwise {@link DisplayTrack} is used (track only, no OSM background tiles). - * + * * @author Viesturs Zarins * */ public class DisplayTrackMap extends Activity { private static final String TAG = DisplayTrackMap.class.getSimpleName(); - + /** * Key for keeping the zoom level in the saved instance bundle */ private static final String CURRENT_ZOOM = "currentZoom"; /** - * Key for keeping scrolled left position of OSM view activity re-creation - * + * Key for keeping scrolled left position of OSM view activity re-creation */ private static final String CURRENT_SCROLL_X = "currentScrollX"; - /** + /** * Key for keeping scrolled top position of OSM view across activity re-creation - * - */ + */ private static final String CURRENT_SCROLL_Y = "currentScrollY"; /** - * Key for keeping whether the map display should be centered to the gps location - * + * Key for keeping whether the map display should be centered to the gps location */ private static final String CURRENT_CENTER_TO_GPS_POS = "currentCenterToGpsPos"; /** - * Key for keeping whether the map display was zoomed and centered - * on an old track id loaded from the database (boolean {@link #zoomedToTrackAlready}) + * Key for keeping whether the map display was zoomed and centered + * on an old track id loaded from the database (boolean {@link #zoomedToTrackAlready}) */ private static final String CURRENT_ZOOMED_TO_TRACK = "currentZoomedToTrack"; @@ -89,30 +84,40 @@ public class DisplayTrackMap extends Activity { /** * Default zoom level */ - private static final int DEFAULT_ZOOM = 16; + private static final int DEFAULT_ZOOM = 16; + + /** + * Default zoom level for center with zoom + */ + private static final double CENTER_DEFAULT_ZOOM_LEVEL = 18; + + /** + * Animation duration in milliseconds for center with zoom + */ + private static final long ANIMATION_DURATION_MS = 1000; /** * Main OSM view */ private MapView osmView; - + /** * Controller to interact with view */ private IMapController osmViewController; - + /** * OSM view overlay that displays current location */ private SimpleLocationOverlay myLocationOverlay; - + /** * OSM view overlay that displays current path */ - private PathOverlay pathOverlay; + private Polyline polyline; /** - * OSM view overlay that displays waypoints + * OSM view overlay that displays waypoints */ private WayPointsOverlay wayPointsOverlay; @@ -120,23 +125,23 @@ public class DisplayTrackMap extends Activity { * OSM view overlay for the map scale bar */ private ScaleBarOverlay scaleBarOverlay; - + /** * Current track id */ private long currentTrackId; - + /** - * whether the map display should be centered to the gps location + * whether the map display should be centered to the gps location */ private boolean centerToGpsPos = true; - + /** * whether the map display was already zoomed and centered * on an old track loaded from the database (should be done only once). */ private boolean zoomedToTrackAlready = false; - + /** * the last position we know */ @@ -145,12 +150,12 @@ public class DisplayTrackMap extends Activity { /** * The row id of the last location read from the database that has been added to the * list of layout points. Using this we to reduce DB load by only reading new points. - * Initially null, to indicate that no data has yet been read. + * Initially null, to indicate that no data has yet been read. */ private Integer lastTrackPointIdProcessed = null; - + /** - * Observes changes on trackpoints + * Observes changes on track points */ private ContentObserver trackpointContentObserver; @@ -173,14 +178,17 @@ public void onCreate(Bundle savedInstanceState) { // Initialize OSM view Configuration.getInstance().load(this, prefs); - osmView = (MapView) findViewById(R.id.displaytrackmap_osmView); - osmView.setMultiTouchControls(true); // pinch to zoom + + osmView = findViewById(R.id.displaytrackmap_osmView); + // pinch to zoom + osmView.setMultiTouchControls(true); + osmView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); // we'll use osmView to define if the screen is always on or not osmView.setKeepScreenOn(prefs.getBoolean(OSMTracker.Preferences.KEY_UI_DISPLAY_KEEP_ON, OSMTracker.Preferences.VAL_UI_DISPLAY_KEEP_ON)); osmViewController = osmView.getController(); // Check if there is a saved zoom level - if(savedInstanceState != null) { + if (savedInstanceState != null) { osmViewController.setZoom(savedInstanceState.getInt(CURRENT_ZOOM, DEFAULT_ZOOM)); osmView.scrollTo(savedInstanceState.getInt(CURRENT_SCROLL_X, 0), savedInstanceState.getInt(CURRENT_SCROLL_Y, 0)); @@ -194,11 +202,11 @@ public void onCreate(Bundle savedInstanceState) { selectTileSource(); - setTileDpiScaling(); + setTileDpiScaling(); createOverlays(); - // Create content observer for trackpoints + // Create content observer for track points trackpointContentObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { @@ -207,16 +215,12 @@ public void onChange(boolean selfChange) { }; // Register listeners for zoom buttons - findViewById(R.id.displaytrackmap_imgZoomIn).setOnClickListener( new OnClickListener() { - @Override - public void onClick(View v) { - osmViewController.zoomIn(); - } - }); - findViewById(R.id.displaytrackmap_imgZoomOut).setOnClickListener( new OnClickListener() { - @Override - public void onClick(View v) { - osmViewController.zoomOut(); + findViewById(R.id.displaytrackmap_imgZoomIn).setOnClickListener(v -> osmViewController.zoomIn()); + findViewById(R.id.displaytrackmap_imgZoomOut).setOnClickListener(v -> osmViewController.zoomOut()); + findViewById(R.id.displaytrackmap_imgZoomCenter).setOnClickListener(view -> { + centerToGpsPos = true; + if (currentPosition != null) { + osmViewController.animateTo(currentPosition,CENTER_DEFAULT_ZOOM_LEVEL, ANIMATION_DURATION_MS); } }); } @@ -231,14 +235,14 @@ public void selectTileSource() { osmView.setTileSource(TileSourceFactory.DEFAULT_TILE_SOURCE); } - /** - * Make text on map better readable on high DPI displays - */ - public void setTileDpiScaling () { - osmView.setTilesScaledToDpi(true); - } + /** + * Make text on map better readable on high DPI displays + */ + public void setTileDpiScaling() { + osmView.setTilesScaledToDpi(true); + } + - // /** // * Returns a ITileSource for the map according to the selected mapTile // * String. The default is mapnik. @@ -268,20 +272,17 @@ protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); } - @Override protected void onResume() { - super.onResume(); resumeActivity(); - } - private void resumeActivity(){ + private void resumeActivity() { // setKeepScreenOn depending on user's preferences osmView.setKeepScreenOn(prefs.getBoolean(OSMTracker.Preferences.KEY_UI_DISPLAY_KEEP_ON, OSMTracker.Preferences.VAL_UI_DISPLAY_KEEP_ON)); - // Register content observer for any trackpoint changes + // Register content observer for any track point changes getContentResolver().registerContentObserver( TrackContentProvider.trackPointsUri(currentTrackId), true, trackpointContentObserver); @@ -297,19 +298,18 @@ private void resumeActivity(){ selectTileSource(); setTileDpiScaling(); - + // Refresh way points wayPointsOverlay.refresh(); - } @Override protected void onPause() { // Unregister content observer getContentResolver().unregisterContentObserver(trackpointContentObserver); - + // Clear the points list. - pathOverlay.clearPath(); + polyline.setPoints(new ArrayList<>()); super.onPause(); } @@ -322,46 +322,43 @@ protected void onStop() { SharedPreferences settings = getPreferences(MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); editor.putInt(LAST_ZOOM, osmView.getZoomLevel()); - editor.commit(); + editor.apply(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.displaytrackmap_menu, menu); + inflater.inflate(R.menu.displaytrackmap_menu, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.displaytrackmap_menu_center_to_gps).setEnabled( (!centerToGpsPos && currentPosition != null ) ); + menu.findItem(R.id.displaytrackmap_menu_center_to_gps).setEnabled((!centerToGpsPos && currentPosition != null)); return super.onPrepareOptionsMenu(menu); } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()){ - case R.id.displaytrackmap_menu_center_to_gps: - centerToGpsPos = true; - if(currentPosition != null){ - osmViewController.animateTo(currentPosition); - } - break; - case R.id.displaytrackmap_menu_settings: - // Start settings activity - startActivity(new Intent(this, Preferences.class)); - break; + switch (item.getItemId()) { + case R.id.displaytrackmap_menu_center_to_gps: + centerToGpsPos = true; + if (currentPosition != null) { + osmViewController.animateTo(currentPosition); + } + break; + case R.id.displaytrackmap_menu_settings: + // Start settings activity + startActivity(new Intent(this, Preferences.class)); + break; } return super.onOptionsItemSelected(item); } - @Override public boolean onTouchEvent(MotionEvent event) { - switch(event.getAction()){ + switch (event.getAction()) { case MotionEvent.ACTION_MOVE: if (currentPosition != null) centerToGpsPos = false; @@ -370,7 +367,6 @@ public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } - /** * Creates overlays over the OSM view */ @@ -379,20 +375,22 @@ private void createOverlays() { this.getWindowManager().getDefaultDisplay().getMetrics(metrics); // set with to hopefully DPI independent 0.5mm - pathOverlay = new PathOverlay(Color.BLUE, (float)(metrics.densityDpi / 25.4 / 2),this); + polyline = new Polyline(); + Paint paint = polyline.getOutlinePaint(); + paint.setColor(Color.BLUE); + paint.setStrokeWidth((float) (metrics.densityDpi / 25.4 / 2)); + osmView.getOverlayManager().add(polyline); - osmView.getOverlays().add(pathOverlay); - myLocationOverlay = new SimpleLocationOverlay(this); osmView.getOverlays().add(myLocationOverlay); - + wayPointsOverlay = new WayPointsOverlay(this, currentTrackId); osmView.getOverlays().add(wayPointsOverlay); scaleBarOverlay = new ScaleBarOverlay(osmView); osmView.getOverlays().add(scaleBarOverlay); } - + /** * On track path changed, update the two overlays and repaint view. * If {@link #lastTrackPointIdProcessed} is null, this is the initial call @@ -404,7 +402,7 @@ private void pathChanged() { if (isFinishing()) { return; } - + // See if the track is active. // If not, we'll calculate initial track bounds // while retrieving from the database. @@ -412,18 +410,21 @@ private void pathChanged() { boolean doInitialBoundsCalc = false; double minLat = 91.0, minLon = 181.0; double maxLat = -91.0, maxLon = -181.0; - if ((! zoomedToTrackAlready) && (lastTrackPointIdProcessed == null)) { + if ((!zoomedToTrackAlready) && (lastTrackPointIdProcessed == null)) { final String[] proj_active = {TrackContentProvider.Schema.COL_ACTIVE}; Cursor cursor = getContentResolver().query( - ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, currentTrackId), - proj_active, null, null, null); - if (cursor.moveToFirst()) { - doInitialBoundsCalc = - (cursor.getInt(cursor.getColumnIndex(TrackContentProvider.Schema.COL_ACTIVE)) == TrackContentProvider.Schema.VAL_TRACK_INACTIVE); + ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, currentTrackId), + proj_active, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int colIndex = cursor.getColumnIndex(TrackContentProvider.Schema.COL_ACTIVE); + if (colIndex != -1) { + doInitialBoundsCalc = + (cursor.getInt(colIndex) == TrackContentProvider.Schema.VAL_TRACK_INACTIVE); + } + cursor.close(); } - cursor.close(); } - + // Projection: The columns to retrieve. Here, we want the latitude, // longitude and primary key only String[] projection = {TrackContentProvider.Schema.COL_LATITUDE, TrackContentProvider.Schema.COL_LONGITUDE, TrackContentProvider.Schema.COL_ID}; @@ -431,71 +432,69 @@ private void pathChanged() { String selection = null; // SelectionArgs: The parameter replacements to use for the '?' in the selection String[] selectionArgs = null; - - // Only request the track points that we have not seen yet + + // Only request the track points that we have not seen yet // If we have processed any track points in this session then // lastTrackPointIdProcessed will not be null. We only want // to see data from rows with a primary key greater than lastTrackPointIdProcessed if (lastTrackPointIdProcessed != null) { selection = TrackContentProvider.Schema.COL_ID + " > ?"; - List selectionArgsList = new ArrayList(); + List selectionArgsList = new ArrayList<>(); selectionArgsList.add(lastTrackPointIdProcessed.toString()); - - selectionArgs = selectionArgsList.toArray(new String[1]); + selectionArgs = selectionArgsList.toArray(new String[1]); } // Retrieve any points we have not yet seen Cursor c = getContentResolver().query( - TrackContentProvider.trackPointsUri(currentTrackId), - projection, selection, selectionArgs, TrackContentProvider.Schema.COL_ID + " asc"); - - int numberOfPointsRetrieved = c.getCount(); - if (numberOfPointsRetrieved > 0 ) { - c.moveToFirst(); - double lastLat = 0; - double lastLon = 0; - int primaryKeyColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_ID); - int latitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE); - int longitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE); - - // Add each new point to the track - while(!c.isAfterLast()) { - lastLat = c.getDouble(latitudeColumnIndex); - lastLon = c.getDouble(longitudeColumnIndex); - lastTrackPointIdProcessed = c.getInt(primaryKeyColumnIndex); - pathOverlay.addPoint((int)(lastLat * 1e6), (int)(lastLon * 1e6)); - if (doInitialBoundsCalc) { - if (lastLat < minLat) minLat = lastLat; - if (lastLon < minLon) minLon = lastLon; - if (lastLat > maxLat) maxLat = lastLat; - if (lastLon > maxLon) maxLon = lastLon; + TrackContentProvider.trackPointsUri(currentTrackId), + projection, selection, selectionArgs, TrackContentProvider.Schema.COL_ID + " asc"); + + if (c != null) { + int numberOfPointsRetrieved = c.getCount(); + if (numberOfPointsRetrieved > 0) { + c.moveToFirst(); + double lastLat = 0; + double lastLon = 0; + int primaryKeyColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_ID); + int latitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE); + int longitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE); + + // Add each new point to the track + while (!c.isAfterLast()) { + lastLat = c.getDouble(latitudeColumnIndex); + lastLon = c.getDouble(longitudeColumnIndex); + lastTrackPointIdProcessed = c.getInt(primaryKeyColumnIndex); + polyline.addPoint(new GeoPoint(lastLat, lastLon)); + if (doInitialBoundsCalc) { + if (lastLat < minLat) minLat = lastLat; + if (lastLon < minLon) minLon = lastLon; + if (lastLat > maxLat) maxLat = lastLat; + if (lastLon > maxLon) maxLon = lastLon; + } + c.moveToNext(); } - c.moveToNext(); - } - - // Last point is current position. - currentPosition = new GeoPoint(lastLat, lastLon); - myLocationOverlay.setLocation(currentPosition); - if(centerToGpsPos) { - osmViewController.setCenter(currentPosition); - } - - // Repaint - osmView.invalidate(); - if (doInitialBoundsCalc && (numberOfPointsRetrieved > 1)) { - // osmdroid-3.0.8 hangs if we directly call zoomToSpan during initial onResume, - // so post a Runnable instead for after it's done initializing. - final double north = maxLat, east = maxLon, south = minLat, west = minLon; - osmView.post(new Runnable() { - @Override - public void run() { - osmViewController.zoomToSpan((int) (north-south), (int) (east-west)); + + // Last point is current position. + currentPosition = new GeoPoint(lastLat, lastLon); + myLocationOverlay.setLocation(currentPosition); + if (centerToGpsPos) { + osmViewController.setCenter(currentPosition); + } + + // Repaint + osmView.invalidate(); + if (doInitialBoundsCalc && (numberOfPointsRetrieved > 1)) { + // osmdroid-3.0.8 hangs if we directly call zoomToSpan during initial onResume, + // so post a Runnable instead for after it's done initializing. + final double north = maxLat, east = maxLon, south = minLat, west = minLon; + osmView.post(() -> { + osmViewController.zoomToSpan((int) (north - south), (int) (east - west)); osmViewController.setCenter(new GeoPoint((north + south) / 2, (east + west) / 2)); zoomedToTrackAlready = true; - } - }); + }); + } } + c.close(); } - c.close(); } } diff --git a/app/src/main/java/net/osmtracker/activity/Intro.kt b/app/src/main/java/net/osmtracker/activity/Intro.kt index 013f88914..073ac2f6a 100644 --- a/app/src/main/java/net/osmtracker/activity/Intro.kt +++ b/app/src/main/java/net/osmtracker/activity/Intro.kt @@ -15,16 +15,18 @@ class Intro : AppIntro() { // Call addSlide passing your Fragments. // You can use AppIntroFragment to use a pre-built fragment - addSlide(AppIntroFragment.newInstance( + addSlide(AppIntroFragment.createInstance( title = getString(R.string.app_intro_slide1_title), imageDrawable = R.drawable.icon_100x100, + backgroundColorRes = R.color.appintro_background_color, description = getString(R.string.app_intro_slide1_description) )) //TODO: change the image of slide number 2. - addSlide(AppIntroFragment.newInstance( + addSlide(AppIntroFragment.createInstance( title = getString(R.string.app_intro_slide2_title), imageDrawable = R.drawable.icon_100x100, + backgroundColorRes = R.color.appintro_background_color, description = getString(R.string.app_intro_slide2_description) )) } diff --git a/app/src/main/java/net/osmtracker/activity/Preferences.java b/app/src/main/java/net/osmtracker/activity/Preferences.java index b32133503..6ccb65aed 100644 --- a/app/src/main/java/net/osmtracker/activity/Preferences.java +++ b/app/src/main/java/net/osmtracker/activity/Preferences.java @@ -27,6 +27,7 @@ import android.text.TextWatcher; import android.widget.Button; import android.widget.EditText; +import android.widget.ListView; /** @@ -63,11 +64,25 @@ public class Preferences extends PreferenceActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); + ListView listView = getListView(); + listView.setFitsSystemWindows(true); + listView.setClipToPadding(false); + listView.setPadding(0, 48, 0, 0); // Set summary of some preferences to their actual values // and register a change listener to set again the summary in case of change final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + // Explicit execution of buttons presets window + Preference buttonLayoutPref = findPreference("prefs_ui_buttons_layout"); + if (buttonLayoutPref != null) { + buttonLayoutPref.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(this, ButtonsPresets.class); + startActivity(intent); + return true; + }); + } + // External storage directory EditTextPreference storageDirPref = (EditTextPreference) findPreference(OSMTracker.Preferences.KEY_STORAGE_DIR); storageDirPref.setSummary(prefs.getString(OSMTracker.Preferences.KEY_STORAGE_DIR, OSMTracker.Preferences.VAL_STORAGE_DIR)); diff --git a/app/src/main/java/net/osmtracker/activity/TrackDetail.java b/app/src/main/java/net/osmtracker/activity/TrackDetail.java index 5f4eb6d95..5cb640f0c 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackDetail.java +++ b/app/src/main/java/net/osmtracker/activity/TrackDetail.java @@ -15,12 +15,14 @@ import net.osmtracker.util.MercatorProjection; import android.Manifest; +import android.app.AlertDialog; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Paint; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import androidx.core.app.ActivityCompat; @@ -181,7 +183,7 @@ protected void onResume() { if (cursor.isNull(cursor.getColumnIndex(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE))) { map.put(ITEM_VALUE, getResources().getString(R.string.trackdetail_osm_upload_notyet)); } else { - map.put(ITEM_VALUE, DateFormat.getDateTimeInstance().format(new Date(cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_EXPORT_DATE))))); + map.put(ITEM_VALUE, DateFormat.getDateTimeInstance().format(new Date(cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE))))); } data.add(map); @@ -237,32 +239,10 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(i); break; case R.id.trackdetail_menu_export: - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - - // Should we show an explanation? - if (ActivityCompat.shouldShowRequestPermissionRationale(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - - // Show an expanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - // TODO: explain why we need permission. - Log.w(TAG, "we should explain why we need write permission"); - - } else { - - // No explanation needed, we can request the permission. - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - RC_WRITE_PERMISSIONS); - break; - } - - } else { + if (writeExternalStoragePermissionGranted()) { exportTrack(); - break; } + break; case R.id.trackdetail_menu_osm_upload: i = new Intent(this, OpenStreetMapUpload.class); i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, trackId); @@ -272,6 +252,51 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + /** + * Checks if the external storage write permission is granted. + * If not, it requests the permission and may display a rationale dialog explaining why it is needed. + * + *

For devices running Android R (API level 30) and above, this permission is not required, + * so the method will return {@code true} immediately.

+ * + * @return {@code true} if the write permission is already granted or not required (Android R+), + * {@code false} otherwise. + */ + private boolean writeExternalStoragePermissionGranted() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return true; + } + else if (ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + // Should we show an explanation? + if (ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + // Show an expanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(this) + .setTitle(R.string.permission_required) + .setMessage(R.string.storage_permission_for_export_GPX) + .setPositiveButton(R.string.acccept, (dialog, which) -> { + // Request the permission again + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + RC_WRITE_PERMISSIONS); + }) + .setNegativeButton(R.string.menu_cancel, (dialog, which) -> dialog.dismiss()) + .show(); + } else { + // No explanation needed, we can request the permission. + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + RC_WRITE_PERMISSIONS); + } + } + + return ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + /** * Invoke the export track task after external write permissions request. diff --git a/app/src/main/java/net/osmtracker/activity/TrackLogger.java b/app/src/main/java/net/osmtracker/activity/TrackLogger.java index f384d9d05..0cbb5f405 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackLogger.java +++ b/app/src/main/java/net/osmtracker/activity/TrackLogger.java @@ -1,36 +1,12 @@ package net.osmtracker.activity; -import java.io.File; - -import java.util.Date; -import java.util.HashSet; - -import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.db.DataHelper; -import net.osmtracker.layout.GpsStatusRecord; -import net.osmtracker.layout.UserDefinedLayout; -import net.osmtracker.listener.SensorListener; -import net.osmtracker.listener.PressureListener; -import net.osmtracker.receiver.MediaButtonReceiver; -import net.osmtracker.service.gps.GPSLogger; -import net.osmtracker.service.gps.GPSLoggerServiceConnection; -import net.osmtracker.util.CustomLayoutsUtils; -import net.osmtracker.util.FileSystemUtils; -import net.osmtracker.util.ThemeValidator; -import net.osmtracker.view.TextNoteDialog; -import net.osmtracker.view.VoiceRecDialog; -import net.osmtracker.db.TrackContentProvider; - import android.Manifest; - import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ComponentName; import android.content.ContentUris; import android.content.ContentValues; - import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -42,17 +18,11 @@ import android.location.LocationManager; import android.media.AudioManager; import android.net.Uri; - import android.os.Bundle; -import android.os.Environment; import android.os.StrictMode; import android.preference.PreferenceManager; import android.provider.MediaStore; -import android.provider.MediaStore.Images.ImageColumns; import android.provider.Settings; -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; @@ -62,6 +32,33 @@ import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.db.DataHelper; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.layout.GpsStatusRecord; +import net.osmtracker.layout.UserDefinedLayout; +import net.osmtracker.listener.PressureListener; +import net.osmtracker.listener.SensorListener; +import net.osmtracker.receiver.MediaButtonReceiver; +import net.osmtracker.service.gps.GPSLogger; +import net.osmtracker.service.gps.GPSLoggerServiceConnection; +import net.osmtracker.util.CustomLayoutsUtils; +import net.osmtracker.util.FileSystemUtils; +import net.osmtracker.util.ThemeValidator; +import net.osmtracker.view.TextNoteDialog; +import net.osmtracker.view.VoiceRecDialog; + +import java.io.File; +import java.util.Date; +import java.util.HashSet; +import java.util.UUID; + /** * Main track logger activity. Communicate with the GPS service to display GPS @@ -75,7 +72,6 @@ public class TrackLogger extends Activity { private static final String TAG = TrackLogger.class.getSimpleName(); final private int RC_STORAGE_AUDIO_PERMISSIONS = 1; - final private int RC_STORAGE_CAMERA_PERMISSIONS = 2; /** * Request code for callback after the camera application had taken a @@ -124,11 +120,11 @@ public class TrackLogger extends Activity { * goes to settings/about/other screen. */ private boolean checkGPSFlag = true; - + /** * Keeps track of the image file when taking a picture. */ - private File currentImageFile; + private File currentPhotoFile; /** * Keeps track of the current track id. @@ -180,6 +176,10 @@ public class TrackLogger extends Activity { */ private HashSet layoutNameTags = new HashSet(); + public boolean getButtonsEnabled() { + return buttonsEnabled; + } + @Override protected void onCreate(Bundle savedInstanceState) { @@ -476,6 +476,7 @@ public boolean onOptionsItemSelected(MenuItem item) { saveTagsForTrack(); Intent intent = new Intent(OSMTracker.INTENT_STOP_TRACKING); + intent.setPackage(getPackageName()); sendBroadcast(intent); ((GpsStatusRecord) findViewById(R.id.gpsStatus)).manageRecordingIndicator(false); finish(); @@ -526,34 +527,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { case KeyEvent.KEYCODE_CAMERA: Log.d(TAG, "click on camera button"); if (gpsLogger.isTracking()) { - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "camera permission isn't granted, will request"); - // Should we show an explanation? - if ( (ActivityCompat.shouldShowRequestPermissionRationale(this, - Manifest.permission.CAMERA)) ) { - - // Show an expanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - // TODO: explain why we need permission. - Log.w(TAG, "we should explain why we need write and record audio permission"); - - } else { - - // No explanation needed, we can request the permission. - ActivityCompat.requestPermissions(this, - new String[]{ - Manifest.permission.CAMERA}, - RC_STORAGE_CAMERA_PERMISSIONS); - break; - } - - } else { - requestStillImage(); - //return true; - } - + requestStillImage(); } break; case KeyEvent.KEYCODE_DPAD_CENTER: @@ -580,92 +554,101 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { */ public void requestStillImage() { if (gpsLogger.isTracking()) { - final File imageFile = pushImageFile(); - if (null != imageFile) { - - - final String pictureSource = prefs.getString(OSMTracker.Preferences.KEY_UI_PICTURE_SOURCE, - OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE); - if (OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE_CAMERA.equals(pictureSource)) { - startCamera(imageFile); - } else if (OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE_GALLERY.equals(pictureSource)) { - startGallery(); - } else { - // Let the user choose between using the camera - // or selecting a picture from the gallery - - AlertDialog.Builder getImageFrom = new AlertDialog.Builder(TrackLogger.this); - getImageFrom.setTitle("Select:"); - final CharSequence[] opsChars = { getString(R.string.tracklogger_camera), getString(R.string.tracklogger_gallery) }; - getImageFrom.setItems(opsChars, new android.content.DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - startCamera(imageFile); - } else if (which == 1) { - startGallery(); - } - dialog.dismiss(); - } - }); - - getImageFrom.show(); - } + final String pictureSource = prefs.getString(OSMTracker.Preferences.KEY_UI_PICTURE_SOURCE, + OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE); + if (OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE_CAMERA.equals(pictureSource)) { + startCamera(); + } else if (OSMTracker.Preferences.VAL_UI_PICTURE_SOURCE_GALLERY.equals(pictureSource)) { + startGallery(); } else { - Toast.makeText(getBaseContext(), - getResources().getString(R.string.error_externalstorage_not_writable), - Toast.LENGTH_SHORT).show(); + // Let the user choose between using the camera + // or selecting a picture from the gallery + + AlertDialog.Builder getImageFrom = new AlertDialog.Builder(TrackLogger.this); + getImageFrom.setTitle("Select:"); + final CharSequence[] opsChars = { getString(R.string.tracklogger_camera), getString(R.string.tracklogger_gallery) }; + getImageFrom.setItems(opsChars, new android.content.DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + startCamera(); + } else if (which == 1) { + startGallery(); + } + dialog.dismiss(); + } + }); + + getImageFrom.show(); } + } else { + Toast.makeText(getBaseContext(), + getResources().getString(R.string.error_externalstorage_not_writable), + Toast.LENGTH_SHORT).show(); } } + /** + * Get file path from android media URI + * + * @param contentUri the android media URI + * @return the filepath of the file + */ + public String getRealPathFromURI(Uri contentUri) { + Cursor cursor = getContentResolver().query(contentUri, null, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA); + cursor.moveToFirst(); + return cursor.getString(column_index); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.v(TAG, "Activity result: " + requestCode + ", resultCode=" + resultCode + ", Intent=" + data); switch (requestCode) { case REQCODE_IMAGE_CAPTURE: if (resultCode == RESULT_OK) { - // A still image has been captured, track the corresponding waypoint - // Send an intent to inform service to track the waypoint. - File imageFile = popImageFile(); - if (imageFile != null) { + if (currentPhotoFile != null && currentPhotoFile.exists()) { Intent intent = new Intent(OSMTracker.INTENT_TRACK_WP); + intent.putExtra(OSMTracker.INTENT_KEY_UUID, UUID.randomUUID().toString()); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); intent.putExtra(OSMTracker.INTENT_KEY_NAME, getResources().getString(R.string.wpt_stillimage)); - intent.putExtra(OSMTracker.INTENT_KEY_LINK, imageFile.getName()); + intent.putExtra(OSMTracker.INTENT_KEY_LINK, currentPhotoFile.getName()); + intent.setPackage(this.getPackageName()); sendBroadcast(intent); + } else { + Log.e(TAG, "Cannot get image path from camera intent"); } } break; case REQCODE_GALLERY_CHOSEN: if (resultCode == RESULT_OK) { - // An image has been selected from the gallery, track the corresponding waypoint - File imageFile = popImageFile(); - if (imageFile != null) { + // Get imagePath from Gallery Uri + String imagePath = getRealPathFromURI(data.getData()); + File imageFile = new File(imagePath != null ? imagePath : ""); + if (imageFile.exists()) { // Copy the file from the gallery - Cursor c = getContentResolver().query(data.getData(), null, null, null, null); - c.moveToFirst(); - String f = c.getString(c.getColumnIndex(ImageColumns.DATA)); - c.close(); - Log.d(TAG, "Copying gallery file '"+f+"' into '"+imageFile+"'"); - FileSystemUtils.copyFile(imageFile.getParentFile(), new File(f), imageFile.getName()); + File destFile = createImageFile(); + Log.d(TAG, "Copying gallery file '" + imagePath + "' into '" + destFile.getAbsolutePath() + "'"); + FileSystemUtils.copyFile(destFile.getParentFile(), new File(imagePath), destFile.getName()); // Send an intent to inform service to track the waypoint. Intent intent = new Intent(OSMTracker.INTENT_TRACK_WP); + intent.putExtra(OSMTracker.INTENT_KEY_UUID, UUID.randomUUID().toString()); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); intent.putExtra(OSMTracker.INTENT_KEY_NAME, getResources().getString(R.string.wpt_stillimage)); - intent.putExtra(OSMTracker.INTENT_KEY_LINK, imageFile.getName()); + intent.putExtra(OSMTracker.INTENT_KEY_LINK, destFile.getName()); + intent.setPackage(this.getPackageName()); sendBroadcast(intent); + } else { + Log.e(TAG, "Cannot get image path from gallery intent"); } } } super.onActivityResult(requestCode, resultCode, data); } - - - + /** * Getter for gpsLogger * @@ -684,45 +667,25 @@ public GPSLogger getGpsLogger() { public void setGpsLogger(GPSLogger l) { this.gpsLogger = l; } - + /** - * Gets a File for storing an image in the current track dir - * and stores it in a class variable. - * - * @return A File pointing to an image file inside the current track directory + * Create Image file according to DataHelper format and location + * + * @return a File */ - public File pushImageFile() { - currentImageFile = null; - - // Query for current track directory + public File createImageFile() { File trackDir = DataHelper.getTrackDirectory(currentTrackId, this); - - // Create the track storage directory if it does not yet exist - if (!trackDir.exists()) { - if ( !trackDir.mkdirs() ) { - Log.w(TAG, "Directory [" + trackDir.getAbsolutePath() + "] does not exist and cannot be created"); - } + if (!trackDir.exists() && !trackDir.mkdirs()) { + Log.w(TAG, "Directory [" + trackDir.getAbsolutePath() + "] does not exist and cannot be created"); + return null; } - - // Ensure that this location can be written to if (trackDir.exists() && trackDir.canWrite()) { - currentImageFile = new File(trackDir, - DataHelper.FILENAME_FORMATTER.format(new Date()) + DataHelper.EXTENSION_JPG); - } else { - Log.w(TAG, "The directory [" + trackDir.getAbsolutePath() + "] will not allow files to be created"); + File imageFile = new File(trackDir, DataHelper.FILENAME_FORMATTER.format(new Date()) + DataHelper.EXTENSION_JPG); + Log.d(TAG, "New Image File: " + imageFile); + return imageFile; } - - Log.d(TAG, "currentImage File: " + currentImageFile); - return currentImageFile; - } - - /** - * @return The current image file, and clear the internal variable. - */ - public File popImageFile() { - File imageFile = currentImageFile; - currentImageFile = null; - return imageFile; + Log.w(TAG, "The directory [" + trackDir.getAbsolutePath() + "] will not allow files to be created"); + return null; } @Override @@ -795,13 +758,21 @@ public long getCurrentTrackId() { /** * Starts the camera app. to take a picture - * @param imageFile File to save the picture to */ - private void startCamera(File imageFile) { + private void startCamera() { StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build()); + + currentPhotoFile = createImageFile(); + if (currentPhotoFile == null) { + Log.e(TAG, "imageFile is NULL in startCamera"); + return; + } + Uri imageUriContent = FileProvider.getUriForFile(this, DataHelper.FILE_PROVIDER_AUTHORITY, currentPhotoFile); + Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(imageFile)); + cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUriContent); startActivityForResult(cameraIntent, REQCODE_IMAGE_CAPTURE); } @@ -809,10 +780,10 @@ private void startCamera(File imageFile) { * Starts the gallery app. to choose a picture */ private void startGallery() { - Intent intent = new Intent(); - intent.setType("image/*"); - intent.setAction(Intent.ACTION_GET_CONTENT); - startActivityForResult(Intent.createChooser(intent, getString(R.string.tracklogger_choose_gallery_camera)), REQCODE_GALLERY_CHOSEN); + Intent galleryIntent = new Intent(); + galleryIntent.setType(DataHelper.MIME_TYPE_IMAGE); + galleryIntent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(galleryIntent, REQCODE_GALLERY_CHOSEN); } public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @@ -837,27 +808,6 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } return; } - - case RC_STORAGE_CAMERA_PERMISSIONS: { - Log.d(TAG, "camera case"); - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 1) { - // TODO: fix permission management - //&& grantResults[0] == PackageManager.PERMISSION_GRANTED - //&& grantResults[1] == PackageManager.PERMISSION_GRANTED) { - - // permission was granted, yay! - requestStillImage(); - - } else { - - // permission denied, boo! Disable the - // functionality that depends on this permission. - //TODO: add an informative message. - Log.v(TAG, "Camera permission is denied."); - } - return; - } } } diff --git a/app/src/main/java/net/osmtracker/activity/TrackManager.java b/app/src/main/java/net/osmtracker/activity/TrackManager.java index 337485293..57e317444 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackManager.java +++ b/app/src/main/java/net/osmtracker/activity/TrackManager.java @@ -40,6 +40,7 @@ import net.osmtracker.exception.CreateTrackException; import net.osmtracker.gpx.ExportToStorageTask; import net.osmtracker.gpx.ExportToTempFileTask; +import net.osmtracker.gpx.ZipHelper; import net.osmtracker.util.FileSystemUtils; import java.io.File; @@ -84,6 +85,9 @@ public class TrackManager extends AppCompatActivity private TrackListRVAdapter recyclerViewAdapter; + // To check if the RecyclerView already has a DividerItemDecoration added + private boolean hasDivider; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -112,6 +116,14 @@ public void onClick(View view) { Intent intro = new Intent(this, Intro.class); startActivity(intro); } + RecyclerView recyclerView = findViewById(R.id.recyclerview); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + + // Adding a horizontal divider + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); + dividerItemDecoration.setDrawable(ContextCompat.getDrawable(this, R.drawable.divider)); // Using a custom drawable + + recyclerView.addItemDecoration(dividerItemDecoration); } @Override @@ -139,7 +151,7 @@ protected void onResume() { /** - * + * Configures and initializes the RecyclerView for displaying the list of tracks. */ private void setRecyclerView() { RecyclerView recyclerView = findViewById(R.id.recyclerview); @@ -147,11 +159,13 @@ private void setRecyclerView() { LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); recyclerView.setLayoutManager(layoutManager); - - DividerItemDecoration did = new DividerItemDecoration(recyclerView.getContext(), - layoutManager.getOrientation()); - recyclerView.addItemDecoration(did); - + // adds a divider decoration if not already present + if (!hasDivider) { + DividerItemDecoration did = new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation()); + recyclerView.addItemDecoration(did); + hasDivider = true; + } recyclerView.setHasFixedSize(true); Cursor cursor = getContentResolver().query( TrackContentProvider.CONTENT_URI_TRACK, null, null, null, @@ -281,7 +295,7 @@ private void tryStartTrackLogger(Intent intent){ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { Log.i(TAG,"Should explain"); - Toast.makeText(this, "Can't continue without GPS permission", + Toast.makeText(this, R.string.gps_perms_required, Toast.LENGTH_LONG).show(); } @@ -396,10 +410,13 @@ public boolean onContextItemSelected(MenuItem item) { // stop the active track stopActiveTrack(); break; - case R.id.trackmgr_contextmenu_resume: - // let's activate the track and start the TrackLogger activity - setActiveTrack(contextMenuSelectedTrackid); + // Activate the selected track if it is different from the currently active one + // (or if no track is currently active) + if (currentTrackId != contextMenuSelectedTrackid) { + setActiveTrack(contextMenuSelectedTrackid); + } + // Start the TrackLogger activity to begin logging the selected track i = new Intent(this, TrackLogger.class); i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, contextMenuSelectedTrackid); tryStartTrackLogger(i); @@ -565,7 +582,9 @@ private static void prepareAndShareTrack(final long trackId, Context context) { new ExportToTempFileTask(context, trackId){ @Override protected void executionCompleted(){ - shareFile(this.getTmpFile(), context); + // Creates a zip file with the trace and its multimedia files + File zipFile = ZipHelper.zipCacheFiles(context, trackId, this.getTmpFile()); + shareFile(zipFile, context); } @Override @@ -688,6 +707,7 @@ private void stopActiveTrack(){ if(currentTrackId != TRACK_ID_NO_TRACK){ // we send a broadcast to inform all registered services to stop tracking Intent intent = new Intent(OSMTracker.INTENT_STOP_TRACKING); + intent.setPackage(this.getPackageName()); sendBroadcast(intent); // need to get sure, that the database is up to date @@ -720,7 +740,7 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], // functionality that depends on this permission. //TODO: add an informative message. Log.w(TAG, "we should explain why we need write permission_EXPORT_ALL"); - Toast.makeText(this, "To export the GPX trace we need to write on the storage.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.storage_permission_for_export_GPX, Toast.LENGTH_LONG).show(); } break; } @@ -738,7 +758,7 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], // functionality that depends on this permission. //TODO: add an informative message. Log.w(TAG, "we should explain why we need write permission_EXPORT_ONE"); - Toast.makeText(this, "To export the GPX trace we need to write on the storage.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.storage_permission_for_export_GPX, Toast.LENGTH_LONG).show(); } break; } @@ -755,7 +775,7 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], // functionality that depends on this permission. //TODO: add an informative message. Log.w(TAG, "Permission not granted"); - Toast.makeText(this, "To display the track properly we need access to the storage.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.storage_permission_for_display_track, Toast.LENGTH_LONG).show(); } break; } @@ -773,7 +793,7 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], // functionality that depends on this permission. //TODO: add an informative message. Log.w(TAG, "Permission not granted"); - Toast.makeText(this, "To share the track properly we need access to the storage.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.storage_permission_for_share_track, Toast.LENGTH_LONG).show(); } break; } @@ -790,7 +810,7 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], // functionality that depends on this permission. //TODO: add an informative message. Log.w(TAG, "Permission not granted"); - Toast.makeText(this, "To upload the track to OSM we need access to the storage.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.storage_permission_for_upload_to_OSM, Toast.LENGTH_LONG).show(); } break; } diff --git a/app/src/main/java/net/osmtracker/activity/WaypointList.java b/app/src/main/java/net/osmtracker/activity/WaypointList.java index f337e7c51..0c046c7ee 100644 --- a/app/src/main/java/net/osmtracker/activity/WaypointList.java +++ b/app/src/main/java/net/osmtracker/activity/WaypointList.java @@ -1,11 +1,28 @@ package net.osmtracker.activity; -import net.osmtracker.db.TrackContentProvider; -import net.osmtracker.db.WaypointListAdapter; - +import android.app.AlertDialog; import android.app.ListActivity; +import android.content.DialogInterface; +import android.content.Intent; import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; import android.widget.CursorAdapter; +import android.widget.EditText; +import android.widget.ListView; +import androidx.core.content.FileProvider; +import net.osmtracker.R; +import net.osmtracker.db.DataHelper; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.db.WaypointListAdapter; +import net.osmtracker.listener.EditWaypointDialogOnClickListener; + +import java.io.File; /** * Activity that lists the previous waypoints tracked by the user. @@ -15,6 +32,17 @@ */ public class WaypointList extends ListActivity { + private static final String TAG = WaypointList.class.getSimpleName(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ListView listView = getListView(); + listView.setFitsSystemWindows(true); + listView.setClipToPadding(false); + listView.setPadding(0, 48, 0, 0); + } + @Override protected void onResume() { Long trackId = getIntent().getExtras().getLong(TrackContentProvider.Schema.COL_TRACK_ID); @@ -41,4 +69,152 @@ protected void onPause() { super.onPause(); } + /** + * Handles the selection of a waypoint from the list and opens an edit dialog. + * This dialog allows the user to update the waypoint's name and preview attached files (images or audio). + * + * @param l The ListView where the item was clicked. + * @param v The view that was clicked. + * @param position The position of the clicked item. + * @param id The ID of the clicked waypoint. + */ + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor(); + final DataHelper dataHelper = new DataHelper(l.getContext()); + LayoutInflater inflater = this.getLayoutInflater(); + + // Inflate the waypoint edit dialog layout + final View editWaypointDialog = inflater.inflate(R.layout.edit_waypoint_dialog, null); + final EditText editWaypointName = editWaypointDialog.findViewById(R.id.edit_waypoint_et_name); + + Button buttonPreview = editWaypointDialog.findViewById(R.id.edit_waypoint_button_preview); + Button buttonUpdate = editWaypointDialog.findViewById(R.id.edit_waypoint_button_update); + Button buttonDelete = editWaypointDialog.findViewById(R.id.edit_waypoint_button_delete); + Button buttonCancel = editWaypointDialog.findViewById(R.id.edit_waypoint_button_cancel); + + // Retrieve existing waypoint name + String oldName = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME)); + editWaypointName.setText(oldName); + editWaypointName.setSelection(oldName.length()); + + // Retrieve waypoint details + final long trackId = cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_TRACK_ID)); + final String uuid = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_UUID)); + final String link = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LINK)); + + final String filePath = (link != null) ? DataHelper.getTrackDirectory(trackId, l.getContext()) + "/" + link : null; + File file = (filePath != null) ? new File(filePath) : null; + + if (file != null && file.exists()) { + try { + if (isImageFile(filePath) || isAudioFile(filePath)) { + buttonPreview.setVisibility(View.VISIBLE); + } + } catch (Exception e) { + Log.e(TAG, "Error handling file: " + filePath, e); + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + AlertDialog alert = builder.create(); + + // Preview button + buttonPreview.setOnClickListener(new EditWaypointDialogOnClickListener(alert, null) { + @Override + public void onClick(View view) { + if (filePath != null) { + File file = new File(filePath); + Uri fileUri = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) ? + FileProvider.getUriForFile(getApplicationContext(), DataHelper.FILE_PROVIDER_AUTHORITY, file) : + Uri.fromFile(file); + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + if (isImageFile(filePath)) { + intent.setDataAndType(fileUri, DataHelper.MIME_TYPE_IMAGE); + } else if (isAudioFile(filePath)) { + intent.setDataAndType(fileUri, DataHelper.MIME_TYPE_AUDIO); + } + + if (intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); + } + } + alert.dismiss(); + } + }); + + // Update waypoint name + buttonUpdate.setOnClickListener(new EditWaypointDialogOnClickListener(alert, null) { + @Override + public void onClick(View view) { + String newName = editWaypointName.getText().toString(); + dataHelper.updateWayPoint(trackId, uuid, newName, link); + alert.dismiss(); + } + }); + + // Delete waypoint + buttonDelete.setOnClickListener(new EditWaypointDialogOnClickListener(alert, cursor) { + @Override + public void onClick(View view) { + new AlertDialog.Builder(WaypointList.this) + .setTitle(getString(R.string.delete_waypoint_confirm_dialog_title)) + .setMessage(getString(R.string.delete_waypoint_confirm_dialog_msg)) + .setPositiveButton(getString(R.string.delete_waypoint_confirm_bt_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dataHelper.deleteWayPoint(uuid, filePath); + cursor.requery(); + alert.dismiss(); + dialog.dismiss(); + } + }) + .setNegativeButton(getString(R.string.delete_waypoint_confirm_bt_cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .show(); + } + }); + + // Cancel button + buttonCancel.setOnClickListener(new EditWaypointDialogOnClickListener(alert, null) { + @Override + public void onClick(View view) { + alert.dismiss(); + } + }); + + alert.setView(editWaypointDialog); + alert.show(); + + super.onListItemClick(l, v, position, id); + } + + /** + * Checks if a given file path corresponds to an image. + * + * @param path The file path. + * @return True if the file is an image, false otherwise. + */ + private boolean isImageFile(String path) { + return path.endsWith(DataHelper.EXTENSION_JPG); + } + + /** + * Checks if a given file path corresponds to an audio file. + * + * @param path The file path. + * @return True if the file is an audio file, false otherwise. + */ + private boolean isAudioFile(String path) { + return path.endsWith(DataHelper.EXTENSION_3GPP); + } + } diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java index 3dd174563..aafdbb751 100644 --- a/app/src/main/java/net/osmtracker/db/DataHelper.java +++ b/app/src/main/java/net/osmtracker/db/DataHelper.java @@ -48,11 +48,26 @@ public class DataHelper { */ public static final String EXTENSION_JPG = ".jpg"; + /** + * ZIP file extension + */ + public static final String EXTENSION_ZIP = ".zip"; + /** * GPX Files MIME standard for sharing */ public static final String MIME_TYPE_GPX = "application/gpx+xml"; + /** + * Audio Files MIME + */ + public static final String MIME_TYPE_AUDIO = "audio/*"; + + /** + * Image Files MIME + */ + public static final String MIME_TYPE_IMAGE = "image/*"; + /** * APP sign plus FileProvider = authority */ @@ -253,16 +268,25 @@ public void updateWayPoint(long trackId, String uuid, String name, String link) } /** - * Deletes a waypoint + * Deletes a waypoint and its file associated (if exists) * * @param uuid * Unique ID of the target waypoint + * + * @param filepath + * file attached to the waypoint */ - public void deleteWayPoint(String uuid) { + public void deleteWayPoint(String uuid, String filepath) { Log.v(TAG, "Deleting waypoint with uuid '" + uuid); if (uuid != null) { contentResolver.delete(Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, uuid), null, null); } + + // delete file if exists + File file = (filepath != null) ? new File(filepath) : null; + if (file != null && file.exists() && file.delete()) { + Log.v(TAG, "File deleted: " + filepath); + } } @@ -396,33 +420,12 @@ public static File getTrackDirectory(long trackId, Context context) { File _return = null; String trackStorageDirectory = context.getExternalFilesDir(null) - + "/osmtracker/track" + trackId; + + OSMTracker.Preferences.VAL_STORAGE_DIR + File.separator + "track" + trackId; _return = new File(trackStorageDirectory); return _return; } - /* method not in use. TODO: delete code. - public static File getGPXTrackFile(long trackId, ContentResolver contentResolver, Context context) { - - String trackName = getTrackNameInDB(trackId, contentResolver); - - File sdRoot = Environment.getExternalStorageDirectory(); - - // The location where the user has specified gpx files and associated content to be written - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String userGPXExportDirectoryName = prefs.getString( - OSMTracker.Preferences.KEY_STORAGE_DIR, OSMTracker.Preferences.VAL_STORAGE_DIR); - - // Build storage track path for file creation - String completeGPXTrackPath = sdRoot + userGPXExportDirectoryName.trim() + - File.separator + trackName.trim() + File.separator + - trackName.trim() + DataHelper.EXTENSION_GPX; - - return new File(completeGPXTrackPath); - } - */ - public static String getTrackNameInDB(long trackId, ContentResolver contentResolver) { String trackName = ""; Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java index ffb551ed0..38f344221 100644 --- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java +++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java @@ -471,7 +471,6 @@ public static final class Schema { public static final String TBL_TRACKPOINT = "trackpoint"; public static final String TBL_WAYPOINT = "waypoint"; public static final String TBL_TRACK = "track"; - public static final String COL_ID = "_id"; public static final String COL_TRACK_ID = "track_id"; public static final String COL_UUID = "uuid"; diff --git a/app/src/main/java/net/osmtracker/db/WaypointListAdapter.java b/app/src/main/java/net/osmtracker/db/WaypointListAdapter.java index b8f847a45..2f8ebc75c 100644 --- a/app/src/main/java/net/osmtracker/db/WaypointListAdapter.java +++ b/app/src/main/java/net/osmtracker/db/WaypointListAdapter.java @@ -13,6 +13,7 @@ import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.RelativeLayout; +import android.widget.TableLayout; import android.widget.TextView; /** @@ -45,15 +46,15 @@ public WaypointListAdapter(Context context, Cursor c) { @Override public void bindView(View view, Context context, Cursor cursor) { - RelativeLayout rl = (RelativeLayout) view; - bind(cursor, rl, context); + TableLayout tl = (TableLayout) view; + bind(cursor, tl, context); } @Override public View newView(Context context, Cursor cursor, ViewGroup vg) { - RelativeLayout rl = (RelativeLayout) LayoutInflater.from(vg.getContext()).inflate(R.layout.waypointlist_item, + TableLayout tl = (TableLayout) LayoutInflater.from(vg.getContext()).inflate(R.layout.waypointlist_item, vg, false); - return bind(cursor, rl, context); + return bind(cursor, tl, context); } /** @@ -61,16 +62,16 @@ public View newView(Context context, Cursor cursor, ViewGroup vg) { * * @param cursor * Cursor to pull data - * @param rl + * @param tl * RelativeView representing one item * @param context * Context, to get resources * @return The relative view with data bound. */ - private View bind(Cursor cursor, RelativeLayout rl, Context context) { - TextView vName = (TextView) rl.findViewById(R.id.wplist_item_name); - TextView vLocation = (TextView) rl.findViewById(R.id.wplist_item_location); - TextView vTimestamp = (TextView) rl.findViewById(R.id.wplist_item_timestamp); + private View bind(Cursor cursor, TableLayout tl, Context context) { + TextView vName = (TextView) tl.findViewById(R.id.wplist_item_name); + TextView vLocation = (TextView) tl.findViewById(R.id.wplist_item_location); + TextView vTimestamp = (TextView) tl.findViewById(R.id.wplist_item_timestamp); // Bind name String name = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME)); @@ -103,8 +104,7 @@ private View bind(Cursor cursor, RelativeLayout rl, Context context) { // Bind timestamp Date ts = new Date(cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_TIMESTAMP))); vTimestamp.setText(DATE_FORMATTER.format(ts)); - - return rl; + return tl; } } diff --git a/app/src/main/java/net/osmtracker/gpx/ExportToStorageTask.java b/app/src/main/java/net/osmtracker/gpx/ExportToStorageTask.java index 32d97718d..855929f85 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportToStorageTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportToStorageTask.java @@ -1,5 +1,7 @@ package net.osmtracker.gpx; +import static net.osmtracker.util.FileSystemUtils.getUniqueChildNameFor; + import android.content.Context; import android.content.SharedPreferences; import android.os.Environment; @@ -15,34 +17,57 @@ import java.io.File; import java.util.Date; -import static net.osmtracker.util.FileSystemUtils.getUniqueChildNameFor; - -import androidx.core.content.ContextCompat; - /** - * Exports to the external storage / SD card - * in a folder defined by the user. + * ExportToStorageTask is responsible for exporting track data to the device's storage. + * It extends the ExportTrackTask class and provides specific implementations for + * exporting track data to a directory on the external storage. */ public class ExportToStorageTask extends ExportTrackTask { private static final String TAG = ExportToStorageTask.class.getSimpleName(); - private String ERROR_MESSAGE; - - + private final String ERROR_MESSAGE; + private final DataHelper dataHelper; + private final SharedPreferences sharedPreferences; + + /** + * Constructor for ExportToStorageTask. + * + * @param context the context of the application + * @param trackId the IDs of the tracks to be exported + */ public ExportToStorageTask(Context context, long... trackId) { + this(context, new DataHelper(context), trackId); + } + + /** + * Constructor for ExportToStorageTask with a DataHelper instance. + * + * @param context the context of the application + * @param dataHelper the DataHelper instance for accessing track data + * @param trackId the IDs of the tracks to be exported + */ + public ExportToStorageTask(Context context, DataHelper dataHelper, long... trackId) { super(context, trackId); + this.dataHelper = dataHelper; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); ERROR_MESSAGE = context.getString(R.string.error_create_track_dir); } + /** + * Gets the directory where the track data will be exported. + * + * @param startDate the start date of the track + * @return the directory where the track data will be exported + * @throws ExportTrackException if the directory cannot be created + */ @Override protected File getExportDirectory(Date startDate) throws ExportTrackException { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - String trackName = getSanitizedTrackNameByStartDate(startDate); - boolean shouldCreateDirectoryPerTrack = shouldCreateDirectoryPerTrack(preferences); - File finalExportDirectory = getBaseExportDirectory(preferences); + boolean shouldCreateDirectoryPerTrack = shouldCreateDirectoryPerTrack(); + File finalExportDirectory = getBaseExportDirectory(); + Log.d(TAG, "absolute dir: " + finalExportDirectory.getAbsolutePath()); - if( shouldCreateDirectoryPerTrack && trackName.length() >= 1){ + if( shouldCreateDirectoryPerTrack && !trackName.isEmpty()){ String uniqueFolderName = getUniqueChildNameFor(finalExportDirectory, trackName, ""); finalExportDirectory = new File(finalExportDirectory, uniqueFolderName); finalExportDirectory.mkdirs(); @@ -53,65 +78,76 @@ protected File getExportDirectory(Date startDate) throws ExportTrackException { return finalExportDirectory; } + /** + * Gets a sanitized track name based on the start date. + * + * @param startDate the start date of the track + * @return the sanitized track name + */ + public String getSanitizedTrackNameByStartDate(Date startDate) { + Track track = dataHelper.getTrackByStartDate(startDate); + + String trackName = ""; + if (track != null) { + trackName = track.getName(); + } + if (trackName != null && !trackName.isEmpty()) { + trackName = trackName.replace("/", "_").trim(); + } + return trackName; + } - /** - * - * @param startDate - * @return - */ - public String getSanitizedTrackNameByStartDate(Date startDate){ - - DataHelper dh = new DataHelper(context); - Track track = dh.getTrackByStartDate(startDate); - - String trackName = ""; - if(track != null){ - trackName = track.getName(); - } - if(trackName != null && trackName.length() >= 1) { - trackName = trackName.replace("/", "_").trim(); - } - return trackName; - } - - public boolean shouldCreateDirectoryPerTrack(SharedPreferences prefs){ - return prefs.getBoolean(OSMTracker.Preferences.KEY_OUTPUT_DIR_PER_TRACK, + /** + * Determines whether a separate directory should be created for each track. + * + * @return true if a separate directory should be created for each track, false otherwise + */ + public boolean shouldCreateDirectoryPerTrack(){ + return sharedPreferences.getBoolean(OSMTracker.Preferences.KEY_OUTPUT_DIR_PER_TRACK, OSMTracker.Preferences.VAL_OUTPUT_GPX_OUTPUT_DIR_PER_TRACK); - } - // Checks if a volume containing external storage is available for read and write. + /** + * Checks if external storage is writable. + * + * @return true if external storage is writable, false otherwise + */ private boolean isExternalStorageWritable() { return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); } - // Create before returning if not exists - public File getBaseExportDirectory(SharedPreferences prefs) throws ExportTrackException { + /** + * Gets the base directory where the track data will be exported. + * Creates the directory if it does not exist. + * + * @return the base directory where the track data will be exported + * @throws ExportTrackException if the directory cannot be created or is not writable + */ + public File getBaseExportDirectory() throws ExportTrackException { if (!isExternalStorageWritable()) { throw new ExportTrackException( context.getResources().getString(R.string.error_externalstorage_not_writable)); } - - String exportDirectoryNameInPreferences = prefs.getString( + String exportDirectoryNameInPreferences = sharedPreferences.getString( OSMTracker.Preferences.KEY_STORAGE_DIR, OSMTracker.Preferences.VAL_STORAGE_DIR); Log.d(TAG,"exportDirectoryNameInPreferences: " + exportDirectoryNameInPreferences); - File baseExportDirectory = new File(context.getExternalFilesDir(null), + File baseExportDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), exportDirectoryNameInPreferences); - - if(! baseExportDirectory.exists()){ + + // if folder not exists, create it + if (!baseExportDirectory.exists()) { boolean ok = baseExportDirectory.mkdirs(); if (!ok) { throw new ExportTrackException( - context.getResources().getString( - R.string.error_externalstorage_not_writable)); + context.getResources().getString(R.string.error_externalstorage_not_writable)); } } - Log.d(TAG, "BaseExportDirectory: " + baseExportDirectory); + Log.d(TAG, "BaseExportDirectory: " + baseExportDirectory.getAbsolutePath()); return baseExportDirectory; - } + } @Override protected boolean exportMediaFiles() { diff --git a/app/src/main/java/net/osmtracker/gpx/ExportToTempFileTask.java b/app/src/main/java/net/osmtracker/gpx/ExportToTempFileTask.java index 8c1a3d30d..fdc971180 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportToTempFileTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportToTempFileTask.java @@ -2,12 +2,14 @@ import android.content.Context; import android.database.Cursor; +import android.preference.PreferenceManager; import android.util.Log; +import net.osmtracker.OSMTracker; +import net.osmtracker.db.DataHelper; import net.osmtracker.exception.ExportTrackException; import java.io.File; -import java.io.IOException; import java.util.Date; /** @@ -24,10 +26,21 @@ public abstract class ExportToTempFileTask extends ExportTrackTask { public ExportToTempFileTask(Context context, long trackId) { super(context, trackId); + String desiredOutputFormat = PreferenceManager.getDefaultSharedPreferences(context).getString( + OSMTracker.Preferences.KEY_OUTPUT_FILENAME, + OSMTracker.Preferences.VAL_OUTPUT_FILENAME); + try { - tmpFile = File.createTempFile("osm-upload", ".gpx", context.getCacheDir()); - Log.d(TAG, "Temporary file: " + tmpFile.getAbsolutePath()); - } catch (IOException ioe) { + String trackName = new DataHelper(context).getTrackById(trackId).getName(); + + long startDate = new DataHelper(context).getTrackById(trackId).getTrackDate(); + String formattedTrackStartDate = DataHelper.FILENAME_FORMATTER.format(new Date(startDate)); + + // Create temporary file + String tmpFilename = super.formatGpxFilename(desiredOutputFormat, trackName, formattedTrackStartDate); + tmpFile = new File(context.getCacheDir(),tmpFilename + DataHelper.EXTENSION_GPX); + Log.d(TAG, "Temporary file: "+ tmpFile.getAbsolutePath()); + } catch (Exception ioe) { Log.e(TAG, "Could not create temporary file", ioe); throw new IllegalStateException("Could not create temporary file", ioe); } diff --git a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java index 5e419a42f..a9dec64da 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java @@ -1,6 +1,5 @@ package net.osmtracker.gpx; -import android.Manifest; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ContentResolver; @@ -8,13 +7,10 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; -import android.content.pm.PackageManager; import android.database.Cursor; import android.media.MediaScannerConnection; import android.os.AsyncTask; -import android.os.Environment; import android.preference.PreferenceManager; -import androidx.core.content.ContextCompat; import android.util.Log; import android.widget.Toast; @@ -569,6 +565,12 @@ public String buildGPXFilename(Cursor cursor, File parentDirectory) { public String formatGpxFilename(String desiredOutputFormat, String sanitizedTrackName, String formattedTrackStartDate){ String result = ""; + String exportLabelName = PreferenceManager.getDefaultSharedPreferences(context).getString( + OSMTracker.Preferences.KEY_OUTPUT_FILENAME_LABEL, OSMTracker.Preferences.VAL_OUTPUT_FILENAME_LABEL); + // If is required to avoid Unit Test fail + if(exportLabelName == null){ + exportLabelName = OSMTracker.Preferences.VAL_OUTPUT_FILENAME_LABEL; + } boolean thereIsTrackName = sanitizedTrackName != null && sanitizedTrackName.length() >= 1; switch(desiredOutputFormat){ @@ -576,18 +578,36 @@ public String formatGpxFilename(String desiredOutputFormat, String sanitizedTrac if(thereIsTrackName) result += sanitizedTrackName; else - result += formattedTrackStartDate; // fallback case + result += formattedTrackStartDate; break; case OSMTracker.Preferences.VAL_OUTPUT_FILENAME_NAME_DATE: if(thereIsTrackName) - result += sanitizedTrackName + "_" + formattedTrackStartDate; + if(sanitizedTrackName.equals(formattedTrackStartDate)) { + result += sanitizedTrackName; + }else{ + result += sanitizedTrackName + "_" + formattedTrackStartDate; // name is not equal + } else result += formattedTrackStartDate; break; + case OSMTracker.Preferences.VAL_OUTPUT_FILENAME_DATE_NAME: + if(thereIsTrackName){ + if(sanitizedTrackName.equals(formattedTrackStartDate)){ + result += formattedTrackStartDate; + }else{ + result += formattedTrackStartDate + "_" + sanitizedTrackName; + } + }else{ + result += formattedTrackStartDate; + } + break; case OSMTracker.Preferences.VAL_OUTPUT_FILENAME_DATE: result += formattedTrackStartDate; break; } + if(!(exportLabelName.equals(""))) { + result += "_" + exportLabelName; + } return result; } diff --git a/app/src/main/java/net/osmtracker/gpx/ZipHelper.java b/app/src/main/java/net/osmtracker/gpx/ZipHelper.java new file mode 100644 index 000000000..20b1eb8ad --- /dev/null +++ b/app/src/main/java/net/osmtracker/gpx/ZipHelper.java @@ -0,0 +1,94 @@ +package net.osmtracker.gpx; + +import android.content.Context; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import net.osmtracker.db.DataHelper; + +public class ZipHelper { + + private static final String TAG = "ZipHelper"; + + /** + * Compresses all files associated with a track into a ZIP file. + * + * @param context Application context. + * @param trackId Track ID. + * @param fileGPX GPX file. + * @return The created ZIP file or null if an error occurred. + */ + public static File zipCacheFiles(Context context, long trackId, File fileGPX) { + + String name = fileGPX.getName(); + File zipFile = new File(context.getCacheDir(), + name.substring(0, name.lastIndexOf(".")) + DataHelper.EXTENSION_ZIP); + + File traceFilesDirectory = DataHelper.getTrackDirectory(trackId, context); + + try (FileOutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zos = new ZipOutputStream(fos)) { + + // Add gpx file + addFileToZip(fileGPX, zos); + + if(!traceFilesDirectory.exists()){ + return zipFile; + } + + for (File multimediaFile : Objects.requireNonNull(traceFilesDirectory.listFiles())) { + if (!multimediaFile.isDirectory()) { // Avoid adding empty folders + // only add files that are not .zip files + if (!multimediaFile.getName().endsWith(DataHelper.EXTENSION_ZIP)) { + addFileToZip(multimediaFile, zos); + } else { + Log.d(TAG, "Multimedia file: " + multimediaFile.getAbsolutePath() + " ignored. "); + } + } else { + Log.d(TAG, "Folder " + multimediaFile.getAbsolutePath() + " ignored. "); + } + } + + Log.d(TAG, "ZIP file created: " + zipFile.getAbsolutePath()); + return zipFile; + + } catch (IOException e) { + Log.e(TAG, "Error creating ZIP file", e); + return null; + } + } + + + /** + * Adds a file to the ZIP archive. + * + * @param file The file to add. + * @param zos The ZipOutputStream to which the file will be added. + */ + private static void addFileToZip(File file, ZipOutputStream zos) throws IOException { + if (!file.exists()) { + Log.e(TAG, "File does not exist: " + file.getAbsolutePath()); + return; + } + + try (FileInputStream fis = new FileInputStream(file)) { + ZipEntry zipEntry = new ZipEntry(file.getName()); + zos.putNextEntry(zipEntry); + + byte[] buffer = new byte[1024]; + int length; + while ((length = fis.read(buffer)) > 0) { + zos.write(buffer, 0, length); + } + + zos.closeEntry(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java b/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java index a7b3a4caa..f2b264d9d 100644 --- a/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java +++ b/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java @@ -8,7 +8,9 @@ import android.Manifest; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.location.GnssStatus; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -114,15 +116,42 @@ public void requestLocationUpdates(boolean request) { if (request) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { lmgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + lmgr.registerGnssStatusCallback(mStatusCallback); } else { ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_GPS_PERMISSIONS); } } else { lmgr.removeUpdates(this); + lmgr.unregisterGnssStatusCallback(mStatusCallback); } } + private GnssStatus.Callback mStatusCallback = new GnssStatus.Callback() { + @Override + public void onSatelliteStatusChanged(GnssStatus status) { + satCount = status.getSatelliteCount(); + fixCount = 0; + + for (int i = 0; i < satCount; i++) { + if (status.usedInFix(i)) { + fixCount++; + } + } + + if (fixCount == 0) { + TextView tvAccuracy = findViewById(R.id.gpsstatus_record_tvAccuracy); + tvAccuracy.setText(getResources().getString(R.string.various_waiting_gps_fix) + .replace("{0}", Long.toString(fixCount)) + .replace("{1}", Long.toString(satCount))); + + ((ImageView) findViewById(R.id.gpsstatus_record_imgSatIndicator)).setImageResource(R.drawable.sat_indicator_unknown); + } + + Log.v(TAG, "Found " + satCount + " satellites. " + fixCount + " used in fix."); + } + }; + @Override public void onLocationChanged(Location location) { // first of all we check if the time from the last used fix to the current fix is greater than the logging interval @@ -135,9 +164,10 @@ public void onLocationChanged(Location location) { activity.onGpsEnabled(); manageRecordingIndicator(true); } - - //TODO: get number of satellites used and visible. - //Log.v(TAG, "Found " + satCount + " satellites. " + fixCount + " used in fix. + else if (gpsActive && !activity.getButtonsEnabled()) { + activity.onGpsEnabled(); + manageRecordingIndicator(true); + } TextView tvAccuracy = findViewById(R.id.gpsstatus_record_tvAccuracy); if (location.hasAccuracy()) { @@ -145,11 +175,8 @@ public void onLocationChanged(Location location) { tvAccuracy.setText(getResources().getString(R.string.various_accuracy_with_sats) .replace("{0}", ACCURACY_FORMAT.format(location.getAccuracy())) .replace("{1}", getResources().getString(R.string.various_unit_meters)) - //TODO: use the number of satellites used and visible - //.replace("{2}", Long.toString(fixCount)) - //.replace("{3}", Long.toString(satCount))); - .replace("{2}", "?") - .replace("{3}", "?")); + .replace("{2}", Long.toString(fixCount)) + .replace("{3}", Long.toString(satCount))); manageSatelliteStatusIndicator((int) location.getAccuracy()); diff --git a/app/src/main/java/net/osmtracker/listener/EditWaypointDialogOnClickListener.java b/app/src/main/java/net/osmtracker/listener/EditWaypointDialogOnClickListener.java new file mode 100644 index 000000000..181a11ba2 --- /dev/null +++ b/app/src/main/java/net/osmtracker/listener/EditWaypointDialogOnClickListener.java @@ -0,0 +1,24 @@ +package net.osmtracker.listener; + +import android.app.AlertDialog; +import android.database.Cursor; +import android.view.View; + +/** + * Class that implements an OnClickListener to display an edit waypoint dialog. + */ +public class EditWaypointDialogOnClickListener implements View.OnClickListener { + + private Cursor cursor; + + protected AlertDialog alert; + + protected EditWaypointDialogOnClickListener(AlertDialog alert, Cursor cu) { + this.cursor = cu; // Assigns the received cursor to the class attribute + this.alert = alert; // Assigns the received alert to the class attribute + } + + @Override + public void onClick(View view) { + } +} diff --git a/app/src/main/java/net/osmtracker/listener/PressureListener.java b/app/src/main/java/net/osmtracker/listener/PressureListener.java index 4f74b9d8c..c6a1ab7ed 100644 --- a/app/src/main/java/net/osmtracker/listener/PressureListener.java +++ b/app/src/main/java/net/osmtracker/listener/PressureListener.java @@ -9,9 +9,7 @@ /** * Listener for pressure sensor. - * * Register the listener with your context using the register/unregister functions - * * Should be possible to enable/disable via preferences due to power consumption. * */ diff --git a/app/src/main/java/net/osmtracker/listener/SensorListener.java b/app/src/main/java/net/osmtracker/listener/SensorListener.java index 467c6d103..3eb287b68 100644 --- a/app/src/main/java/net/osmtracker/listener/SensorListener.java +++ b/app/src/main/java/net/osmtracker/listener/SensorListener.java @@ -17,9 +17,7 @@ /** * Listener for sensors. In particular for the acceleration and magnetic sensors to provide compass * heading. - * * Register the listener with your context using the register/unregister functions - * * most recent reading from the sensor is always available from azimuth, pitch, roll, accuracy and valid fields * * @author Christoph Gohle diff --git a/app/src/main/java/net/osmtracker/listener/StillImageOnClickListener.java b/app/src/main/java/net/osmtracker/listener/StillImageOnClickListener.java index c53cbd936..bfd963b5c 100644 --- a/app/src/main/java/net/osmtracker/listener/StillImageOnClickListener.java +++ b/app/src/main/java/net/osmtracker/listener/StillImageOnClickListener.java @@ -2,11 +2,6 @@ import net.osmtracker.activity.TrackLogger; -import android.Manifest; -import android.content.pm.PackageManager; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - import android.view.View; import android.view.View.OnClickListener; @@ -22,8 +17,6 @@ public class StillImageOnClickListener implements OnClickListener { * Parent activity */ TrackLogger activity; - - final private int RC_STORAGE_CAMERA_PERMISSIONS = 2; public StillImageOnClickListener(TrackLogger parent) { activity = parent; @@ -31,59 +24,6 @@ public StillImageOnClickListener(TrackLogger parent) { @Override public void onClick(View v) { - if (ContextCompat.checkSelfPermission(activity, - Manifest.permission.WRITE_EXTERNAL_STORAGE) + ContextCompat.checkSelfPermission(activity, - Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - - // Should we show an explanation? - if ( (ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.WRITE_EXTERNAL_STORAGE)) - || (ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.CAMERA)) ) { - - // Show an expanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - // TODO: explain why we need permission. - //"we should explain why we need write and record audio permission" - - } else { - - // No explanation needed, we can request the permission. - ActivityCompat.requestPermissions(activity, - new String[]{ - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.CAMERA}, - RC_STORAGE_CAMERA_PERMISSIONS); - } - - } else { - activity.requestStillImage(); - } - + activity.requestStillImage(); } - - public void onRequestPermissionsResult(int requestCode, - String permissions[], int[] grantResults) { - switch (requestCode) { - case RC_STORAGE_CAMERA_PERMISSIONS: { - // If request is cancelled, the result arrays are empty. - if (grantResults.length == 2 - && grantResults[0] == PackageManager.PERMISSION_GRANTED - && grantResults[1] == PackageManager.PERMISSION_GRANTED) { - - // permission was granted, yay! - activity.requestStillImage(); - - } else { - - // permission denied, boo! Disable the - // functionality that depends on this permission. - //TODO: add an informative message. - } - return; - } - } - } - } diff --git a/app/src/main/java/net/osmtracker/listener/TagButtonOnClickListener.java b/app/src/main/java/net/osmtracker/listener/TagButtonOnClickListener.java index b4a8da875..4b5f35b4c 100644 --- a/app/src/main/java/net/osmtracker/listener/TagButtonOnClickListener.java +++ b/app/src/main/java/net/osmtracker/listener/TagButtonOnClickListener.java @@ -1,16 +1,18 @@ package net.osmtracker.listener; -import net.osmtracker.OSMTracker; -import net.osmtracker.R; - import android.content.Intent; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.Toast; +import java.util.UUID; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; import net.osmtracker.db.TrackContentProvider; + /** * Listener for standard waypoint tag button. * Sends an Intent to track waypoint. Waypoint name is the @@ -36,6 +38,11 @@ public void onClick(View view) { Intent intent = new Intent(OSMTracker.INTENT_TRACK_WP); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); intent.putExtra(OSMTracker.INTENT_KEY_NAME, label); + intent.putExtra(OSMTracker.INTENT_KEY_UUID, UUID.randomUUID().toString()); + + String packageName = view.getContext().getPackageName(); + intent.setPackage(packageName); + view.getContext().sendBroadcast(intent); // Inform user that the waypoint was tracked diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java index d02ecdb24..fb785d279 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java @@ -1,13 +1,5 @@ package net.osmtracker.service.gps; -import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.activity.TrackLogger; -import net.osmtracker.db.DataHelper; -import net.osmtracker.db.TrackContentProvider; -import net.osmtracker.listener.PressureListener; -import net.osmtracker.listener.SensorListener; - import android.Manifest; import android.app.Notification; import android.app.NotificationChannel; @@ -27,9 +19,18 @@ import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.util.Log; + import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; -import android.util.Log; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.activity.TrackLogger; +import net.osmtracker.db.DataHelper; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.listener.PressureListener; +import net.osmtracker.listener.SensorListener; /** * GPS logging service. @@ -147,8 +148,15 @@ public void onReceive(Context context, Intent intent) { // Delete an existing waypoint Bundle extras = intent.getExtras(); if (extras != null) { + Long trackId = extras.getLong(TrackContentProvider.Schema.COL_TRACK_ID); String uuid = extras.getString(OSMTracker.INTENT_KEY_UUID); - dataHelper.deleteWayPoint(uuid); + String link = extras.getString(OSMTracker.INTENT_KEY_LINK); + String filePath = null; + try { + filePath = link.equals("null") ? null : DataHelper.getTrackDirectory(trackId, context) + "/" + link; + } + catch(NullPointerException ne){} + dataHelper.deleteWayPoint(uuid, filePath); } } else if (OSMTracker.INTENT_START_TRACKING.equals(intent.getAction())) { Bundle extras = intent.getExtras(); @@ -222,7 +230,11 @@ public void onCreate() { filter.addAction(OSMTracker.INTENT_DELETE_WP); filter.addAction(OSMTracker.INTENT_START_TRACKING); filter.addAction(OSMTracker.INTENT_STOP_TRACKING); - registerReceiver(receiver, filter); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(receiver, filter); + } // Register ourselves for location updates lmgr = (LocationManager) getSystemService(Context.LOCATION_SERVICE); diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java index fcf26a7c6..e7d22f67b 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java @@ -50,6 +50,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { activity.setEnabledActionButtons(false); Intent intent = new Intent(OSMTracker.INTENT_START_TRACKING); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, activity.getCurrentTrackId()); + intent.setPackage(activity.getPackageName()); activity.sendBroadcast(intent); } } diff --git a/app/src/main/java/net/osmtracker/util/CustomLayoutsUtils.java b/app/src/main/java/net/osmtracker/util/CustomLayoutsUtils.java index ca12f4e8f..b07f7884f 100644 --- a/app/src/main/java/net/osmtracker/util/CustomLayoutsUtils.java +++ b/app/src/main/java/net/osmtracker/util/CustomLayoutsUtils.java @@ -16,6 +16,8 @@ import java.io.Reader; import java.io.StringWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; + /** * Created by adma9717 on 12/8/17. */ @@ -65,16 +67,18 @@ public static String createFileName(String layoutName, String iso) { /** * FIXME: Create a util class with this method. This method is a copy&paste of the one in {@link GetStringResponseTask} - * @param stream - * @return all the characters in the stream as a single String - * @throws IOException + * Converts an InputStream to a String using the UTF-8 charset. + * + * @param stream the InputStream to read from + * @return a String containing all characters read from the stream + * @throws IOException if an I/O error occurs */ public static String getStringFromStream(InputStream stream) throws IOException { if (stream != null) { Writer writer = new StringWriter(); char[] buffer = new char[2048]; try { - Reader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + Reader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); int counter; while ((counter = reader.read(buffer)) != -1) { writer.write(buffer, 0, counter); diff --git a/app/src/main/java/net/osmtracker/util/FileSystemUtils.java b/app/src/main/java/net/osmtracker/util/FileSystemUtils.java index 49d30ad40..04e3e0fa1 100644 --- a/app/src/main/java/net/osmtracker/util/FileSystemUtils.java +++ b/app/src/main/java/net/osmtracker/util/FileSystemUtils.java @@ -1,5 +1,7 @@ package net.osmtracker.util; +import android.util.Log; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -8,8 +10,6 @@ import java.util.ArrayList; import java.util.List; -import android.util.Log; - public final class FileSystemUtils { private static final String TAG = FileSystemUtils.class.getSimpleName(); @@ -148,7 +148,7 @@ public static boolean delete(File fileToDelete, boolean recursive) { * @param fileToDelete the file/directory to be deleted * @param recursive if the deletion should be recursive * @param recursionDepth takes care of the depth of recursion and aborts deletion if DELETE_MAX_RECURSION_DEPTH is reached - * @return + * @return true if file was deleted */ private static boolean delete(File fileToDelete, boolean recursive, int recursionDepth){ // if we're deeper than one recursion/subfolder, we'll cancel deletion diff --git a/app/src/main/java/net/osmtracker/util/MercatorProjection.java b/app/src/main/java/net/osmtracker/util/MercatorProjection.java index c3ba0ea36..3da1193b1 100644 --- a/app/src/main/java/net/osmtracker/util/MercatorProjection.java +++ b/app/src/main/java/net/osmtracker/util/MercatorProjection.java @@ -84,7 +84,7 @@ public MercatorProjection(double minLat, double minLon, double maxLat, double ma * @param latitude * Latitude to project * @return An array of 2 int projected coordinates (use - * {@link MercatorProjection.X} and {@link MercatorProjection.Y} for + * {@link MercatorProjection}.X and {@link MercatorProjection}.Y for * access. */ public int[] project(double longitude, double latitude) { diff --git a/app/src/main/java/net/osmtracker/util/UserDefinedLayoutReader.java b/app/src/main/java/net/osmtracker/util/UserDefinedLayoutReader.java index f42ec9dbb..74262bad2 100644 --- a/app/src/main/java/net/osmtracker/util/UserDefinedLayoutReader.java +++ b/app/src/main/java/net/osmtracker/util/UserDefinedLayoutReader.java @@ -87,7 +87,7 @@ public class UserDefinedLayoutReader { /** * representing ScreenOrientation - * see {@link Configuration.orientation} + * see {@link Configuration}.orientation */ private int orientation; diff --git a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java index bf38bd573..6064dea12 100644 --- a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java +++ b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java @@ -1,10 +1,5 @@ package net.osmtracker.view; -import java.util.UUID; - -import net.osmtracker.OSMTracker; -import net.osmtracker.R; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -13,8 +8,12 @@ import android.view.WindowManager.LayoutParams; import android.widget.EditText; +import net.osmtracker.OSMTracker; +import net.osmtracker.R; import net.osmtracker.db.TrackContentProvider; +import java.util.UUID; + public class TextNoteDialog extends AlertDialog { /** @@ -63,7 +62,7 @@ public TextNoteDialog(Context context, long trackId) { this.setCancelable(true); this.setView(input); - this.setButton(context.getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() { + this.setButton(context.getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Track waypoint with user input text @@ -71,11 +70,12 @@ public void onClick(DialogInterface dialog, int which) { intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, TextNoteDialog.this.wayPointTrackId); intent.putExtra(OSMTracker.INTENT_KEY_NAME, input.getText().toString()); intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid); - TextNoteDialog.this.context.sendBroadcast(intent); + intent.setPackage(getContext().getPackageName()); + context.sendBroadcast(intent); } }); - this.setButton2(context.getResources().getString(android.R.string.cancel), new DialogInterface.OnClickListener() { + this.setButton2(context.getResources().getString(android.R.string.cancel), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // cancel the dialog @@ -89,7 +89,8 @@ public void onCancel(DialogInterface dialog) { // delete the waypoint because user canceled this dialog Intent intent = new Intent(OSMTracker.INTENT_DELETE_WP); intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid); - TextNoteDialog.this.context.sendBroadcast(intent); + intent.setPackage(getContext().getPackageName()); + context.sendBroadcast(intent); } }); @@ -100,7 +101,7 @@ public void onCancel(DialogInterface dialog) { */ @Override protected void onStart() { - if(wayPointUuid == null){ + if (wayPointUuid == null) { // there is no UUID set for the waypoint we're working on // so we need to generate a UUID and track this point wayPointUuid = UUID.randomUUID().toString(); @@ -108,6 +109,7 @@ protected void onStart() { intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId); intent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid); intent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote)); + intent.setPackage(getContext().getPackageName()); context.sendBroadcast(intent); } @@ -120,18 +122,18 @@ protected void onStart() { * resets values of this dialog * such as the input fields text and the waypoints uuid */ - public void resetValues(){ + public void resetValues() { wayPointUuid = null; input.setText(""); } /** - * restoring values from the savedInstaceState + * restoring values from the savedInstanceState */ @Override public void onRestoreInstanceState(Bundle savedInstanceState) { String text = savedInstanceState.getString(KEY_INPUT_TEXT); - if(text != null){ + if (text != null) { input.setText(text); } wayPointUuid = savedInstanceState.getString(KEY_WAYPOINT_UUID); @@ -150,8 +152,4 @@ public Bundle onSaveInstanceState() { extras.putString(KEY_WAYPOINT_UUID, wayPointUuid); return extras; } - - - - } diff --git a/app/src/main/java/net/osmtracker/view/VoiceRecDialog.java b/app/src/main/java/net/osmtracker/view/VoiceRecDialog.java index 085e6551e..97018ffc0 100644 --- a/app/src/main/java/net/osmtracker/view/VoiceRecDialog.java +++ b/app/src/main/java/net/osmtracker/view/VoiceRecDialog.java @@ -1,14 +1,5 @@ package net.osmtracker.view; -import java.io.File; -import java.util.Date; -import java.util.UUID; - -import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.db.DataHelper; -import net.osmtracker.db.TrackContentProvider.Schema; - import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; @@ -24,6 +15,15 @@ import android.view.KeyEvent; import android.widget.Toast; +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.db.DataHelper; +import net.osmtracker.db.TrackContentProvider.Schema; + +import java.io.File; +import java.util.Date; +import java.util.UUID; + public class VoiceRecDialog extends ProgressDialog implements OnInfoListener{ private final static String TAG = VoiceRecDialog.class.getSimpleName(); @@ -151,6 +151,7 @@ public void onStart() { intent.putExtra(Schema.COL_TRACK_ID, wayPointTrackId); intent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid); intent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.wpt_voicerec)); + intent.setPackage(getContext().getPackageName()); context.sendBroadcast(intent); } @@ -215,6 +216,7 @@ public void onStart() { intent.putExtra(Schema.COL_TRACK_ID, wayPointTrackId); intent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid); intent.putExtra(OSMTracker.INTENT_KEY_LINK, audioFile.getName()); + intent.setPackage(getContext().getPackageName()); context.sendBroadcast(intent); } else { Log.w(TAG,"onStart() no suitable audioFile could be created"); diff --git a/app/src/main/res/drawable-mdpi/gps_center.png b/app/src/main/res/drawable-mdpi/gps_center.png new file mode 100644 index 000000000..38f81f17e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/gps_center.png differ diff --git a/app/src/main/res/drawable-mdpi/zoom_in.png b/app/src/main/res/drawable-mdpi/zoom_in.png index 3e63eae2a..8aaa8fa98 100644 Binary files a/app/src/main/res/drawable-mdpi/zoom_in.png and b/app/src/main/res/drawable-mdpi/zoom_in.png differ diff --git a/app/src/main/res/drawable-mdpi/zoom_out.png b/app/src/main/res/drawable-mdpi/zoom_out.png index f3079fd64..2a56507cf 100644 Binary files a/app/src/main/res/drawable-mdpi/zoom_out.png and b/app/src/main/res/drawable-mdpi/zoom_out.png differ diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml new file mode 100644 index 000000000..b19ee985b --- /dev/null +++ b/app/src/main/res/drawable/divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/map_btn_style.xml b/app/src/main/res/drawable/map_btn_style.xml new file mode 100644 index 000000000..a8c33d11f --- /dev/null +++ b/app/src/main/res/drawable/map_btn_style.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/layout-iw/waypointlist_item.xml b/app/src/main/res/layout-iw/waypointlist_item.xml index 953fc6650..96fa5dc97 100644 --- a/app/src/main/res/layout-iw/waypointlist_item.xml +++ b/app/src/main/res/layout-iw/waypointlist_item.xml @@ -1,10 +1,10 @@ - + - - diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml index 9234145c3..c6288176a 100644 --- a/app/src/main/res/layout/about.xml +++ b/app/src/main/res/layout/about.xml @@ -1,125 +1,112 @@ + android:id="@+id/about_root" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="10dp" + android:fitsSystemWindows="true"> - - - - - + android:layout_alignParentTop="true"> - - - - - + - - - + + + + + + + + + + + + - + android:text="@string/about_text" /> - - - - + android:text="@string/about_link" /> - + android:text="@string/about_translate_text" /> - - + android:text="@string/about_translate_link" /> + - + -