Compare commits

...

44 Commits

Author SHA1 Message Date
Dennis Guse 7c0069394f Release v3.1.0 2019-09-02 19:42:40 +02:00
Dennis Guse 379913fb3e Cleanup. 2019-09-02 19:39:58 +02:00
Dennis Guse 542d9ccb77 Request storage permission on import all and export all. 2019-09-02 19:37:55 +02:00
Dennis Guse c8ae3b83b5 Added option to show statistics while recording without unlocking the device. 2019-09-02 18:36:31 +02:00
Dennis Guse 0a046fc100 Cleanup. 2019-09-02 18:02:43 +02:00
Dennis Guse c551a38a8b Show full version name based upon git describe --tags on startup and also in AboutActivity. 2019-09-02 16:05:24 +02:00
Dennis Guse 72b1ca82ed Moved debug warnings into Startup. 2019-09-02 15:50:35 +02:00
Dennis Guse 1ef81c45db Build: do not minify code for release.
The amount of space saved is too small to be of a real benefit while stack traces become unreadable.
2019-09-02 15:50:35 +02:00
Dennis Guse 08e6ae76f2 Fix TravisCI for Android-29 (#34) 2019-09-02 15:44:21 +02:00
Dennis Guse 6e410e049d Bugfix: creating waypoint from camera works now.
Introduced in commit 677b691ef1
2019-08-26 22:57:00 +02:00
Dennis Guse 22b7962940 Upgraded to Gradle 5.6 and AndroidStudio 3.5.0. 2019-08-26 21:55:40 +02:00
Dennis Guse 5788f99b20 Catch ActivityNotFound exception for starting geo-data intents. 2019-08-25 22:20:08 +02:00
Dennis Guse 8685e43dc2 Fixed ShareContentProvider and KMZ mime type. 2019-08-25 21:37:47 +02:00
Dennis Guse b8bea27155 Added AndroidStudio coding style configuration. 2019-08-25 16:21:20 +02:00
Dennis Guse c9226d3b79 Fixed CustomContentProvider tests. 2019-08-25 16:20:27 +02:00
Dennis Guse 9701feff04 Removed duplicate registration of images at MediaScanner. 2019-08-25 08:42:36 +02:00
Dennis Guse b8b40a88ef Show Marker menu entry at defined position. 2019-08-25 08:41:11 +02:00
Dennis Guse adcf7b388b Finish MarkerDetailActivity after storing marker. 2019-08-25 00:26:04 +02:00
Dennis Guse 3a7562d96c Modified "show on map" functionality to only share locations and no other track data. 2019-08-25 00:21:36 +02:00
Dennis Guse 2753cf7d0b Removed search suggestion configuration. 2019-08-24 18:48:50 +02:00
Dennis Guse 5841415a0b Refactored TrackWriter instantiation. 2019-08-24 18:14:32 +02:00
Dennis Guse d79fa07834 Share multiple tracks from TrackListActivity. 2019-08-24 17:51:57 +02:00
Dennis Guse ff6caf706e Added show on map to TrackDetailActivity. 2019-08-24 17:51:57 +02:00
Dennis Guse c953fbc67a Implemented "Share" from TrackListActivity's context menu. 2019-08-24 09:37:38 +02:00
Dennis Guse 55870cca85 Bugfix: import/export all now react to back button. 2019-08-24 08:49:59 +02:00
Dennis Guse 0123242be6 Bugfix: use correct entries/values for track name. 2019-08-24 08:44:43 +02:00
Dennis Guse 81a6d7b53a Added release recipe for FDroid. 2019-08-24 08:39:13 +02:00
Dennis Guse f423b07cee Added Changelogs for v3.0.0, v3.0.1, and the upcoming release. 2019-08-24 08:27:31 +02:00
Dennis Guse 33231bdbb1 Renaming to AbstractListActivity. 2019-08-23 07:37:03 +02:00
Dennis Guse 677b691ef1 Moved special intent creation to IntentUtils. 2019-08-23 07:33:58 +02:00
Dennis Guse 5e74e80a9a Use correct mime type for KML and KMZ. 2019-08-23 07:19:40 +02:00
Dennis Guse 70b5d799a2 Share marker works everywhere and share track from TrackDetailActivity. 2019-08-23 07:01:14 +02:00
Dennis Guse 16f18fe16f Removed search suggestion provider as it only provided recent searches. 2019-08-21 22:25:16 +02:00
Dennis Guse 8538c79dd8 Extracted content uri definition. 2019-08-21 22:20:18 +02:00
Dennis Guse f8e3b28e51 Removed landscape mode for TrackDetailActivity as it was broken and portrait mode needs heavy refactoring first. 2019-08-21 07:18:57 +02:00
Dennis Guse 9f665e07fd Show on map: everything is handled by one content provider alone.
To avoid issues forwarding a requests from (temporarily) exported content provider to internal content provider only one content provider is used.
2019-08-20 22:55:32 +02:00
Dennis Guse b7c010c086 Show on map: draft via virtual file (with two content providers).
Exported content provider that temporary needs to grant access to the internal content provider.
2019-08-20 22:49:54 +02:00
Dennis Guse 8296c0194d Show on map: draft via sharing temporary file. 2019-08-20 22:48:05 +02:00
Dennis Guse f537cc7321 Cleanup: automatic code inspection via Android Studio. 2019-08-18 14:46:43 +02:00
Dennis Guse 4fa2401b27 Removed CustomContentProvider.openFile() as it was used for Google Earth integration. 2019-08-18 13:22:41 +02:00
Dennis Guse f30e3cd5cd Removed contentDescription for logo (was still MyTracks). 2019-08-18 13:13:11 +02:00
Dennis Guse b73353120f PreferenceCategory import/export all: now uses Tracks as title. 2019-08-18 11:00:04 +02:00
Dennis Guse 7fa9df1f99 Cleanup. 2019-08-17 00:58:52 +02:00
Dennis Guse ea3de8a987 Renamed SensorManager to RemoteSensorManager. 2019-08-16 21:20:00 +02:00
169 changed files with 1239 additions and 1015 deletions
+116
View File
@@ -0,0 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+1 -1
View File
@@ -4,7 +4,7 @@ dist: trusty
android:
components:
- build-tools-28.0.3
- android-28
- android-29
script:
- ./gradlew build
+12
View File
@@ -0,0 +1,12 @@
# OpenTrack: Developer information
## Create releases for FDroid
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Push commits
7. Tag newly create commit with _version name_
8. Push the tagged to public repository
+8 -7
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
@@ -27,7 +27,7 @@ def getVersionCode = { ->
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1;
return -1
}
}
@@ -41,21 +41,22 @@ def getVersionName = { ->
return stdout.toString().trim()
}
catch (ignored) {
return null;
return null
}
}
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "de.dennisguse.opentracks"
//Useful for debugging.
//versionCode getVersionCode()
//versionName getVersionName()
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -67,7 +68,7 @@ android {
}
release {
minifyEnabled true
minifyEnabled false
}
}
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Jun 19 22:47:37 CEST 2019
#Mon Aug 26 18:59:08 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip
Vendored
+56 -44
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@@ -6,42 +6,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -60,6 +24,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -85,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -150,11 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
Vendored
+4 -10
View File
@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
+15
View File
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
First release of OpenTracks (i.e., hard fork from MyTracks)
Changes:
- Rebranded from MyTracks to OpenTracks
- Added Bluetooth LE heart rate sensor
- Removed all features requiring Internet access
- Removed Google Maps integration
- Removed use of Google Play services
- Removed ANT+ sensors
- Removed Bluetooth classic sensors
- Removed Protobuf
- Removed calorie estimation and activity estimation
- Removed MyTracks Service API (i.e., no integration with other apps)
+17
View File
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Changes:
- Share markers
- Restructured settings
- Removed actionbar back button
- Export: removed photo size settings option
- Export: KML as format is now selectable
Bugfixes:
- export directory is now /OpenTracks/
- landscape mode is not crashing
- fixed KML/KMZ export
Developer:
- Test are executable (but many failing)
- Enabled continuous integration with TravisCI (no tests)
@@ -0,0 +1,12 @@
(tbd): OpenTracks
Changes:
- Added show track in map apps (tested with OsmAnd and Maps.ME)
- Removed landscape mode for track detail view (was buggy)
- Removed search suggestions history
Bugfix:
-
Developer:
-
+1 -1
View File
@@ -1 +1 @@
A sport tracker that completely respects your privacy
A sport tracker that completely respects your privacy.
@@ -45,7 +45,8 @@ public class CustomContentProviderTest {
public void setUp() {
context.deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
customContentProvider = new CustomContentProvider();
customContentProvider = new CustomContentProvider() {
};
}
/**
@@ -32,7 +32,6 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
@@ -455,8 +454,7 @@ public class CustomContentProviderUtilsImplTest {
}
@Override
public String generateTrackDescription(Track aTrack, Vector<Double> distances,
Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
@@ -511,8 +509,7 @@ public class CustomContentProviderUtilsImplTest {
}
@Override
public String generateTrackDescription(Track aTrack, Vector<Double> distances,
Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
@@ -88,7 +88,7 @@ public class DescriptionGeneratorImplTest {
+ "Min grade: 11 %<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, null, null, true));
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
@@ -183,7 +183,7 @@ public class TrackDataHubTest {
// .thenReturn(waypoint2)
// .thenReturn(waypoint1)
// .thenReturn(waypoint2);
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), contentObserverCapture.capture());
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_BASE_URI), contentObserverCapture.capture());
// trackDataListener1.clearWaypoints();
// trackDataListener2.clearWaypoints();
// trackDataListener1.onNewWaypoint(waypoint1);
@@ -417,7 +417,7 @@ public class TrackDataHubTest {
// @Test
// public void testTrackPointsTableUpdate_largeTrackSampling() {
// ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_URI), contentObserverCapture.capture());
// dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_BASE_URI), contentObserverCapture.capture());
//
// when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(200L);
// AndroidMock.expectLastCall().anyTimes();
@@ -565,7 +565,7 @@ public class TrackDataHubTest {
// @Test
// public void testRegisterTracksTableListener() {
// ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), observerCapture.capture());
// dataSource.registerContentObserver(eq(TracksColumns.CONTENT_BASE_URI), observerCapture.capture());
// Track track = TrackStubUtils.createTrack(1);
//
// when(contentProviderUtils.getTrack(capture(new Capture<Long>())).thenReturn(track);
@@ -585,7 +585,7 @@ public class TrackDataHubTest {
// @Test
// public void testRegisterWaypointsTableListener() {
// ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), observerCapture.capture());
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_BASE_URI), observerCapture.capture());
// when(contentProviderUtils.getWaypointCursor(
// capture(new Capture<Long>()), capture(new Capture<Long>()),
// capture(new Capture<Integer>()))).thenReturn(null);
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.services;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -66,7 +67,8 @@ public class TrackRecordingServiceTest {
@Before
public void setUp() throws Exception {
// Set up the mock content resolver
CustomContentProvider customContentProvider = new CustomContentProvider();
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
+8 -12
View File
@@ -17,8 +17,8 @@ limitations under the License.
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="2947"
android:versionName="v3.0.1"> <!-- For FDroid. -->
android:versionCode="2991"
android:versionName="v3.1.0"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -70,7 +70,7 @@ limitations under the License.
android:smallScreens="true"
android:xlargeScreens="true" />
<application
android:name=".InitPreferences"
android:name=".Startup"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo"
@@ -119,7 +119,9 @@ limitations under the License.
android:name="android.app.searchable"
android:resource="@xml/search" />
</activity>
<activity android:name=".TrackDetailActivity" />
<activity
android:name=".TrackDetailActivity"
android:screenOrientation="userPortrait" />
<activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity">
<intent-filter>
@@ -174,16 +176,10 @@ limitations under the License.
</receiver>
<!-- Providers -->
<provider
android:name=".content.CustomContentProvider"
android:authorities="${applicationId}"
android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content"
android:exported="false"
android:grantUriPermissions="true" />
<!-- Search suggestion provider -->
<provider
android:name=".content.SearchEngineProvider"
android:authorities="${applicationId}.search"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -29,15 +29,14 @@ import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.SearchView;
import java.util.ArrayList;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/**
* An abstract class for the following common tasks across
@@ -49,7 +48,7 @@ import java.util.ArrayList;
*
* @author Jimmy Shih
*/
public abstract class AbstractTrackActivity extends AbstractActivity implements ConfirmDeleteCaller {
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
@@ -1,15 +0,0 @@
package de.dennisguse.opentracks;
import android.app.Application;
import de.dennisguse.opentracks.util.PreferencesUtils;
public class InitPreferences extends Application {
@Override
public void onCreate() {
super.onCreate();
//Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
}
}
@@ -31,7 +31,6 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
/**
* An activity to add/edit a marker.
@@ -121,6 +120,7 @@ public class MarkerEditActivity extends AbstractActivity {
} else {
saveMarker();
}
finish();
}
});
@@ -168,8 +168,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointDescription.getText().toString(),
null,
null);
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest);
TrackRecordingServiceConnectionUtils.addMarker(this, trackRecordingServiceConnection, waypointCreationRequest);
}
/**
@@ -49,7 +49,6 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarke
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
/**
* Activity to show a list of markers in a track.
@@ -173,7 +172,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
};
listView.setAdapter(resourceCursorAdapter);
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull
@@ -232,7 +231,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, null);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu);
}
@@ -284,10 +283,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
//TODO Use IntentUtils.newShowOnMapIntent()
startActivity(intent);
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getWaypoint(markerIds[0]));
}
return true;
case R.id.list_context_menu_edit:
@@ -17,12 +17,10 @@
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -38,11 +36,17 @@ import android.widget.ListView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.SearchEngineProvider;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
@@ -57,14 +61,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* An activity to display a list of search results.
@@ -73,7 +69,7 @@ import java.util.SortedSet;
* <p>
* TODO: allow to refine search (present search in context menu)
*/
public class SearchListActivity extends AbstractTrackActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
@@ -98,17 +94,11 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private SearchRecentSuggestions searchRecentSuggestions;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
@@ -136,6 +126,11 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
};
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private final OnSharedPreferenceChangeListener
@@ -171,7 +166,6 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@@ -224,7 +218,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
startActivity(intent);
}
});
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@@ -322,8 +316,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
Log.e(TAG, "Not implemented");
return true;
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newShowOnMapIntent((double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
startActivity(intent);
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
@@ -386,9 +379,6 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
arrayAdapter.addAll(displayResults);
}
});
// Save the query as a suggestion for the future
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
}
/**
@@ -420,10 +410,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
*/
@Deprecated
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
/*
* TODO: It may be more appropriate to obtain the track name as a join in
* the retrieval phase of the search.
*/
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the search.
String trackName = null;
long trackId = waypoint.getTrackId();
if (trackId != -1L) {
@@ -0,0 +1,37 @@
package de.dennisguse.opentracks;
import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import de.dennisguse.opentracks.util.PreferencesUtils;
public class Startup extends Application {
private final static String TAG = Startup.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
//Include version information into stack traces.
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
//Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
//In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
}
}
@@ -17,19 +17,20 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.ShareActionProvider;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
@@ -37,14 +38,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.FileProvider;
import androidx.viewpager.widget.ViewPager;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
@@ -57,7 +52,6 @@ import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.Choos
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils;
@@ -73,7 +67,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackDetailActivity extends AbstractTrackActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
@@ -81,7 +75,6 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key";
private static final String JPEG_EXTENSION = "jpeg";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
@@ -103,6 +96,8 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean showOnLockScreen = PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
@@ -117,6 +112,7 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
long markerId = TrackRecordingServiceConnectionUtils.addMarker(TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest);
if (markerId != -1L) {
//TODO: Make configurable.
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
}
@@ -140,6 +136,11 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key))) {
showOnLockScreen = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key, PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN);
setLockscreenPolicy();
}
if (key == null) return;
runOnUiThread(new Runnable() {
@@ -222,6 +223,22 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
public void onAttachedToWindow() {
setLockscreenPolicy();
super.onAttachedToWindow();
}
private void setLockscreenPolicy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
} else {
if (showOnLockScreen)
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
}
@Override
protected void onStart() {
super.onStart();
@@ -275,14 +292,6 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
return;
}
hasPhoto = resultCode == RESULT_OK;
if (hasPhoto) {
//Register photo in media scanner
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(photoUri);
this.sendBroadcast(mediaScanIntent);
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@@ -308,8 +317,7 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
shareMenuItem = menu.findItem(R.id.track_detail_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
//TODO: Share the actual track when track is finished? How to get the file path or create a new file?
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, trackId, "", TrackFileFormat.KML));
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, new long[]{trackId}));
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
@@ -339,7 +347,10 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
return false;
}
takePicture();
createWaypointWithPicture();
return true;
case R.id.track_detail_menu_show_on_map:
IntentUtils.showTrackOnMap(this, new long[]{trackId});
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
@@ -371,27 +382,13 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
}
}
private void takePicture() {
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
photoUri = FileProvider.getUriForFile(this, FileUtils.FILEPROVIDER, file);
Log.d(TAG, "Taking photo to URI: " + photoUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
takePicture();
createWaypointWithPicture();
}
return;
}
@@ -480,6 +477,12 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
setTitle(title);
}
private void createWaypointWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, trackId);
photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
}
public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
@@ -26,7 +26,6 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.StrictMode;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
@@ -42,6 +41,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.ShareActionProvider;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -73,7 +73,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractTrackActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
@@ -107,16 +107,16 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isSingleSelection = ids.length == 1;
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1;
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
shareActionProvider.setShareIntent(trackIds.length == 0 ? null : IntentUtils.newShareFileIntent(TrackListActivity.this, trackIds));
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(false);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@@ -128,7 +128,7 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
};
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/*
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
@@ -248,18 +248,6 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -316,7 +304,7 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
}
};
listView.setAdapter(resourceCursorAdapter);
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
showStartupDialogs();
@@ -379,7 +367,7 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search);
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, trackController);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
@@ -459,8 +447,7 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(
menu, new int[]{info.position}, new long[]{info.id}, false);
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
@@ -565,9 +552,8 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
*/
private boolean handleContextItem(int itemId, long[] trackIds) {
switch (itemId) {
case R.id.list_context_menu_share:
//TODO
Log.e(TAG, "Not implemented");
case R.id.list_context_menu_show_on_map:
IntentUtils.showTrackOnMap(this, trackIds);
return true;
case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
@@ -0,0 +1,40 @@
package de.dennisguse.opentracks.android;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ContentResolverWrapper implements IContentResolver {
private ContentResolver contentResolver;
public ContentResolverWrapper(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
@Nullable
public Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
return contentResolver.insert(url, values);
}
public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
return contentResolver.bulkInsert(url, values);
}
@Nullable
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
}
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs) {
return contentResolver.update(uri, values, where, selectionArgs);
}
public int delete(@NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs) {
return contentResolver.delete(url, where, selectionArgs);
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.android;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
/**
* Defines the interface actually shared by {@link android.content.ContentProvider} and {@link android.content.ContentResolver}.
* So, both can be used interchangeably.
*/
public interface IContentResolver {
@Nullable
Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values);
int bulkInsert(@RequiresPermission.Write @NonNull Uri url, @NonNull ContentValues[] values);
@Nullable
Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder);
int update(@RequiresPermission.Write @NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs);
int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs);
}
@@ -36,10 +36,15 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
public interface ContentProviderUtils {
/**
* The authority (the first part of the URI) for the app's content
* provider.
* The authority (the first part of the URI) for the app's content provider.
*/
String AUTHORITY = BuildConfig.APPLICATION_ID;
String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
/**
* The base URI for the app's content provider.
*/
String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/**
* The default {@link LocationFactory} which creates a location each time.
*/
@@ -24,18 +24,21 @@ import android.location.Location;
import android.net.Uri;
import android.util.Log;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.android.ContentResolverWrapper;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
* {@link ContentProviderUtils} implementation.
* Allows to use {@link ContentResolver} and {@link android.content.ContentProvider} interchangeably via {@link IContentResolver}.
*
* @author Leif Hendrik Wilden
*/
@@ -45,10 +48,14 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
private static final int MAX_LATITUDE = 90000000;
private final ContentResolver contentResolver;
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
public ContentProviderUtilsImpl(ContentResolver contentResolver) {
this.contentResolver = new ContentResolverWrapper(contentResolver);
}
public ContentProviderUtilsImpl(IContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
@@ -716,8 +723,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
if (maxWaypoints >= 0) {
sortOrder += " LIMIT " + maxWaypoints;
}
return contentResolver.query(
WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
@Override
@@ -1,35 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
/**
* Utilities for serializing primitive types.
*
* @author Rodrigo Damazio
*/
public class ContentTypeIds {
public static final byte BOOLEAN_TYPE_ID = 0;
public static final byte LONG_TYPE_ID = 1;
public static final byte INT_TYPE_ID = 2;
public static final byte FLOAT_TYPE_ID = 3;
public static final byte DOUBLE_TYPE_ID = 4;
public static final byte STRING_TYPE_ID = 5;
public static final byte BLOB_TYPE_ID = 6;
private ContentTypeIds() {
}
}
@@ -20,7 +20,6 @@ import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
@@ -29,25 +28,19 @@ import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.util.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and
* waypoints tables.
*
* @author Leif Hendrik Wilden
*/
public class CustomContentProvider extends ContentProvider {
public abstract class CustomContentProvider extends ContentProvider {
@VisibleForTesting
static final int DATABASE_VERSION = 23;
@@ -59,12 +52,12 @@ public class CustomContentProvider extends ContentProvider {
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
}
@Override
@@ -277,23 +270,6 @@ public class CustomContentProvider extends ContentProvider {
return count;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
try {
File file = new File(uri.getPath());
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException(uri.getPath());
} finally {
File dir = new File(getContext().getCacheDir(), FileUtils.PLAY_TRACKS_DIR);
for (File file : dir.listFiles()) {
file.delete();
}
getContext().revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
/**
* Gets the {@link UrlType} for a url.
*
@@ -23,7 +23,6 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.net.Uri;
import de.dennisguse.opentracks.Constants;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -17,8 +17,6 @@ package de.dennisguse.opentracks.content;
import de.dennisguse.opentracks.stats.TripStatistics;
import java.util.Vector;
/**
* An interface for an object that can generate descriptions of track and
* waypoint.
@@ -31,12 +29,9 @@ public interface DescriptionGenerator {
* Generates a track description.
*
* @param track the track
* @param distances a vector of distances to generate the elevation chart
* @param elevations a vector of elevations to generate the elevation chart
* @param html true to output html, false to output plain text
*/
String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html);
String generateTrackDescription(Track track, boolean html);
/**
* Generate a waypoint description from a trip statistics.
@@ -20,8 +20,6 @@ import android.content.Context;
import androidx.annotation.VisibleForTesting;
import java.util.Vector;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -44,7 +42,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
}
@Override
public String generateTrackDescription(Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track track, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
@@ -20,10 +20,6 @@ import android.location.Location;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@@ -31,6 +27,10 @@ import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Engine for searching for tracks and waypoints by text.
*
@@ -381,6 +381,7 @@ public class SearchEngine {
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
@@ -397,11 +398,13 @@ public class SearchEngine {
public final Track track;
public final Waypoint waypoint;
public final double score;
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
@@ -1,46 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
import de.dennisguse.opentracks.BuildConfig;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
@@ -0,0 +1,210 @@
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
/**
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
* The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}.
* <p>
* Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}.
* <p>
* Explanation:
* Although a request is handled by a {@link android.content.ContentProvider} (with temporarily granted permission), Android's security infrastructure prevents forwarding queries to non-exported {@link android.content.ContentProvider}.
* Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps.
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
*/
public class ShareContentProvider extends CustomContentProvider implements IContentResolver {
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static final String TAG = ShareContentProvider.class.getSimpleName();
private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1;
private static final int URI_KML_WITH_SENSORDATA = 2;
private static final int URI_KMZ_ONLY_TRACK = 3;
private static final int URI_KMZ_WITH_SENSORDATA = 4;
private static final int URI_KMZ_WITH_SENSORDATA_AND_PICTURES = 5;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_";
static {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*", URI_KML_ONLY);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_SENSORDATA.getName() + "/*", URI_KML_WITH_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*", URI_KMZ_ONLY_TRACK);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_SENSORDATA.getName() + "/*", URI_KMZ_WITH_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES.getName() + "/*", URI_KMZ_WITH_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*", URI_GPX);
}
public static Pair<Uri, String> createURI(long[] trackIds, @NonNull TrackFileFormat trackFileFormat) {
if (trackIds.length == 0) {
throw new UnsupportedOperationException();
}
StringBuilder builder = new StringBuilder();
for (long trackId : trackIds) {
builder.append(trackId).append(TRACKID_DELIMITER);
}
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TracksColumns.TABLE_NAME + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
return new Pair<>(uri, mime);
}
private static long[] parseURI(Uri uri) {
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
return new long[]{};
}
String fileExtension = "." + getTrackFileFormat(uri).getExtension();
String[] lastPathSegmentSplit = lastPathSegment.replace(fileExtension, "").split(TRACKID_DELIMITER);
long[] trackIds = new long[lastPathSegmentSplit.length];
for (int i = 0; i < trackIds.length; i++) {
trackIds[i] = Long.valueOf(lastPathSegmentSplit[i]);
}
return trackIds;
}
/**
* Do not allow to be exported via AndroidManifest.
* Check that caller has permissions to access {@link CustomContentProvider}.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new UnsupportedOperationException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
}
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case URI_GPX:
return TrackFileFormat.GPX;
case URI_KML_ONLY:
return TrackFileFormat.KML_ONLY_TRACK;
case URI_KML_WITH_SENSORDATA:
return TrackFileFormat.KML_WITH_SENSORDATA;
case URI_KMZ_ONLY_TRACK:
return TrackFileFormat.KMZ_ONLY_TRACK;
case URI_KMZ_WITH_SENSORDATA:
return TrackFileFormat.KMZ_WITH_SENSORDATA;
case URI_KMZ_WITH_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES;
default: //URI_KML_ONLY
return TrackFileFormat.KMZ_ONLY_TRACK;
}
}
@Nullable
public static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
if (uriMatcher.match(uri) == -1) {
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
// ContentProvider has already checked granted permissions
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = -1;
}
}
cols = Arrays.copyOf(cols, i);
values = Arrays.copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
String mime = getTypeMime(uri);
if (mime != null) {
return mime;
}
return super.getType(uri);
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this);
long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
}
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext(), tracks, null);
PipeDataWriter pipeDataWriter = new PipeDataWriter<String>() {
@Override
public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
trackExporter.writeTrack(fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
}
}
};
return openPipeHelper(uri, getType(uri), null, null, pipeDataWriter);
}
}
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for the track points table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/trackpoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/trackpoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for the tracks table.
*
@@ -33,7 +31,7 @@ public interface TracksColumns extends BaseColumns {
/**
* Tracks provider uri.
*/
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/tracks");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
/**
* Track content type.
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for waypoints table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/waypoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/waypoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -32,13 +32,13 @@ import android.widget.GridView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* A DialogFragment to choose an activity type.
*
@@ -119,11 +119,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment {
return -1;
}
private static void updateWeightContainer(View weightContainer, int position) {
boolean showWeight = position == 0 || position == 1 || position == 2;
weightContainer.setVisibility(showWeight ? View.VISIBLE : View.GONE);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@@ -23,10 +23,10 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import de.dennisguse.opentracks.R;
import java.util.List;
import de.dennisguse.opentracks.R;
/**
* Image adapter for choosing an activity type.
*
@@ -41,8 +41,7 @@ public class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final int padding;
private int selected = -1;
public ChooseActivityTypeImageAdapter(
Context context, List<Integer> imageIds, int width, int height, int padding) {
public ChooseActivityTypeImageAdapter(Context context, List<Integer> imageIds, int width, int height, int padding) {
this.context = context;
this.imageIds = imageIds;
this.width = width;
@@ -63,15 +63,13 @@ public class FileTypeDialogFragment extends AbstractDialogFragment {
protected Dialog createDialog() {
int optionId = getArguments().getInt(KEY_OPTION_ID);
final int titleId = getArguments().getInt(KEY_TITLE_ID);
TrackFileFormat[] trackFileFormats = TrackFileFormat.values();
final TrackFileFormat[] trackFileFormats = {TrackFileFormat.KML_WITH_SENSORDATA, TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES, TrackFileFormat.GPX};
String[] choices = new String[trackFileFormats.length];
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
choices[i] = getString(optionId, trackFileFormat.name(),
FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
choices[i] = getString(optionId, trackFileFormat.getExtension().toUpperCase(), FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
}
return new AlertDialog.Builder(getActivity())
.setCancelable(true)
.setNegativeButton(R.string.generic_cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -82,20 +80,19 @@ public class FileTypeDialogFragment extends AbstractDialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
caller.onFileTypeDone(TrackFileFormat.values()[position]);
caller.onFileTypeDone(trackFileFormats[position]);
}
})
.setSingleChoiceItems(choices, 0, null)
.setTitle(titleId)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
onDismissed();
}
})
.create();
}
@Override
public void onCancel(DialogInterface dialog) {
onDismissed();
}
protected void onDismissed() {
dismiss();
caller.onDismissed();
@@ -39,6 +39,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
@@ -48,7 +49,6 @@ import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.R;
/**
* A fragment to show marker details.
@@ -190,8 +190,7 @@ public class MarkerDetailFragment extends Fragment {
Intent intent;
switch (item.getItemId()) {
case R.id.marker_detail_show_on_map:
intent = IntentUtils.newShowOnMapIntent(waypoint);
startActivity(intent);
IntentUtils.showCoordinateOnMap(getContext(), waypoint);
return true;
case R.id.marker_detail_edit:
intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
@@ -31,6 +31,9 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.EnumSet;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
@@ -40,6 +43,7 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -47,9 +51,6 @@ import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.EnumSet;
/**
* A fragment to display track statistics to the user.
@@ -297,7 +298,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.SensorManager}.
* Tries to fetch most recent {@link SensorDataSet} {@link RemoteSensorManager}.
*/
private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
@@ -7,24 +7,102 @@ import android.os.Parcelable;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.io.file.exporter.TrackExporterListener;
import de.dennisguse.opentracks.io.file.exporter.TrackWriter;
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat implements Parcelable {
KML {
KML_ONLY_TRACK {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new KmlTrackWriter(context, multiple, false);
return new KmlTrackWriter(context, multiple, false, false, false);
}
@Override
public String getMimeType() {
return MIME_KML;
}
public String getExtension() {
return "kml";
}
},
KMZ {
KML_WITH_SENSORDATA {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new KmlTrackWriter(context, multiple, true);
return new KmlTrackWriter(context, multiple, true, true, false);
}
@Override
public String getMimeType() {
return MIME_KML;
}
public String getExtension() {
return "kml";
}
},
KMZ_ONLY_TRACK {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new KmlTrackWriter(context, multiple, false, false, false);
}
@Override
public String getMimeType() {
return MIME_KMZ;
}
public String getExtension() {
return "kmz";
}
},
KMZ_WITH_SENSORDATA {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new KmlTrackWriter(context, multiple, true, true, false);
}
@Override
public String getMimeType() {
return MIME_KMZ;
}
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
return newKmzTrackExporter(context, this.newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener);
}
public String getExtension() {
return "kmz";
}
},
KMZ_WITH_SENSORDATA_AND_PICTURES {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new KmlTrackWriter(context, multiple, true, true, true);
}
@Override
public String getMimeType() {
return MIME_KMZ;
}
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
return newKmzTrackExporter(context, this.newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener);
}
public String getExtension() {
return "kmz";
}
},
GPX {
@@ -32,8 +110,18 @@ public enum TrackFileFormat implements Parcelable {
public TrackWriter newTrackWriter(Context context, boolean multiple) {
return new GpxTrackWriter(context.getString(R.string.app_name));
}
@Override
public String getMimeType() {
return "application/gpx+xml";
}
public String getExtension() {
return "gpx";
}
};
@Deprecated
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
@Override
public TrackFileFormat createFromParcel(final Parcel source) {
@@ -56,25 +144,46 @@ public enum TrackFileFormat implements Parcelable {
dest.writeInt(ordinal());
}
/**
* Creates a new track writer for the format.
*
* @param context the context
* @param multiple true for writing multiple tracks
*/
public abstract TrackWriter newTrackWriter(Context context, boolean multiple);
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
}
/**
* Returns the mime type for each format.
*/
public String getMimeType() {
return "application/" + getExtension() + "+xml";
public abstract String getMimeType();
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks);
}
/**
* Creates a new track writer for the format.
*
* @param context the context
* @param multiple true for writing multiple tracks
*/
public abstract TrackWriter newTrackWriter(Context context, boolean multiple);
/**
* Returns the file extension for each format.
*/
public String getExtension() {
public abstract String getExtension();
/**
* Returns the name of for each format.
*/
public String getName() {
return this.name().toLowerCase(Locale.US);
}
}
@@ -16,13 +16,17 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.Manifest;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
@@ -41,12 +45,14 @@ import de.dennisguse.opentracks.util.FileUtils;
*/
public class ExportActivity extends FragmentActivity implements FileTypeDialogFragment.FileTypeCaller {
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private String directoryDisplayName;
private SaveAsyncTask saveAsyncTask;
private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog;
// the number of tracks successfully saved
@@ -59,6 +65,27 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
fileTypeDialogStart();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
FileTypeDialogFragment
.newInstance(R.string.export_all_title, R.string.export_all_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
@@ -81,14 +108,14 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
//TODO (still needed?): getLastNonConfiguration instance returned SaveAsyncTask before
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof SaveAsyncTask) {
// saveAsyncTask = (SaveAsyncTask) retained;
// saveAsyncTask.setActivity(this);
// if (retained instanceof ExportAsyncTask) {
// exportAsyncTask = (ExportAsyncTask) retained;
// exportAsyncTask.setActivity(this);
// } else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, directory);
saveAsyncTask.execute();
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, directory);
exportAsyncTask.execute();
// }
}
@@ -106,7 +133,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
exportAsyncTask.cancel(true);
dialog.dismiss();
onDismissed();
}
@@ -41,9 +41,10 @@ import de.dennisguse.opentracks.util.SystemUtils;
*
* @author Jimmy Shih
*/
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
//TODO Make independent from ExportActivity?
public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = SaveAsyncTask.class.getSimpleName();
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final File directory;
private final Context context;
@@ -66,11 +67,11 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* Creates an AsyncTask.
*
* @param exportActivity the activity currently associated with this task
* @param exportActivity the activity currently associated with this task
* @param trackFileFormat the track file format
* @param directory the directory to write the file
*/
public SaveAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, File directory) {
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, File directory) {
this.exportActivity = exportActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
@@ -144,7 +145,7 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
/**
* Saves tracks to one file.
* Saves tracks to one file (uses first track to determine the filename).
*
* @param tracks the tracks
*/
@@ -153,23 +154,20 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
return false;
}
Track track = tracks[0];
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, tracks,
trackFileFormat.newTrackWriter(context, tracks.length > 1),
new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
//Update the progress dialog once every 500 points.
if (number % 500 == 0) {
publishProgress(number, max);
}
}
});
boolean useKmz = trackFileFormat == TrackFileFormat.KMZ;
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
TrackExporter trackExporter = useKmz ? new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks) : fileTrackExporter;
TrackExporterListener trackExporterListener = new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
//Update the progress dialog once every 500 points.
if (number % 500 == 0) {
publishProgress(number, max);
}
}
};
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, tracks, trackExporterListener);
Track track = tracks[0];
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension());
File file = new File(directory, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
@@ -20,6 +20,8 @@ import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.OutputStream;
import de.dennisguse.opentracks.Constants;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
@@ -28,8 +30,6 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.util.LocationUtils;
import java.io.OutputStream;
/**
* Track Writer for writing tracks to an {@link OutputStream}.
*
@@ -49,11 +49,11 @@ public class FileTrackExporter implements TrackExporter {
* Constructor.
*
* @param contentProviderUtils the content provider utils
* @param tracks the tracks
* @param trackWriter the track writer
* @param tracks the tracks
* @param trackExporterListener the track export listener
*/
public FileTrackExporter(ContentProviderUtils contentProviderUtils, Track[] tracks, TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
public FileTrackExporter(ContentProviderUtils contentProviderUtils, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener) {
this.contentProviderUtils = contentProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
@@ -18,16 +18,15 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.location.Location;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.StringUtils;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Write track as GPX to a file.
*
@@ -59,11 +58,6 @@ public class GpxTrackWriter implements TrackWriter {
this.creator = creator;
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
@@ -83,11 +77,9 @@ public class GpxTrackWriter implements TrackWriter {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
printWriter.println("version=\"1.1\"");
printWriter.println(
"creator=\"" + creator + "\"");
printWriter.println("creator=\"" + creator + "\"");
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
@@ -20,9 +20,6 @@ import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -36,7 +33,6 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -63,8 +59,10 @@ public class KmlTrackWriter implements TrackWriter {
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean multiple;
private final boolean isKMZ;
private final boolean hasMultipleTracks;
private final boolean exportPictures;
private final boolean exportTrackDetails;
private final boolean exportSensorData;
private final DescriptionGenerator descriptionGenerator;
private final ContentProviderUtils contentProviderUtils;
@@ -76,24 +74,25 @@ public class KmlTrackWriter implements TrackWriter {
private boolean hasCadence;
private boolean hasHeartRate;
public KmlTrackWriter(Context context, boolean multiple, boolean isKMZ) {
this(context, multiple, isKMZ, new DescriptionGeneratorImpl(context));
}
private Location startLocation;
@VisibleForTesting
KmlTrackWriter(Context context, boolean multiple, boolean isKMZ, DescriptionGenerator descriptionGenerator) {
/**
* @param context the context
* @param hasMultipleTracks should encode multiple tracks into one file?
* @param exportPictures should pictures be exported (if true: exports to KMZ)?
* @param exportTrackDetails should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportSensorData should {@link SensorDataSet} be exported?
*/
public KmlTrackWriter(Context context, boolean hasMultipleTracks, boolean exportTrackDetails, boolean exportSensorData, boolean exportPictures) {
this.context = context;
this.multiple = multiple;
this.isKMZ = isKMZ;
this.descriptionGenerator = descriptionGenerator;
this.hasMultipleTracks = hasMultipleTracks;
this.exportTrackDetails = exportTrackDetails;
this.exportSensorData = exportSensorData;
this.exportPictures = exportPictures;
this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.contentProviderUtils = ContentProviderUtils.Factory.get(context);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
@@ -117,8 +116,12 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
if (exportTrackDetails) {
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
}
printWriter.println("<atom:author><atom:name>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
@@ -126,9 +129,12 @@ public class KmlTrackWriter implements TrackWriter {
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
if (exportSensorData) {
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
}
printWriter.println("</Schema>");
}
}
@@ -144,7 +150,10 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeBeginWaypoints(Track track) {
if (printWriter != null) {
printWriter.println("<Folder><name>" + StringUtils.formatCData(context.getString(R.string.track_markers, track.getName())) + "</name>");
printWriter.println("<Folder>");
if (exportTrackDetails) {
printWriter.println("<name>" + StringUtils.formatCData(context.getString(R.string.track_markers, track.getName())) + "</name>");
}
printWriter.println("<open>1</open>");
}
}
@@ -158,7 +167,7 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
if (printWriter != null && exportTrackDetails) {
String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE : WAYPOINT_STYLE;
if (waypoint.hasPhoto()) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
@@ -171,7 +180,7 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeBeginTracks() {
if (printWriter != null && multiple) {
if (printWriter != null && hasMultipleTracks) {
printWriter.println("<Folder id=tour>");
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>");
@@ -180,20 +189,20 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeEndTracks() {
if (printWriter != null && multiple) {
if (printWriter != null && hasMultipleTracks) {
printWriter.println("</Folder>");
}
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
this.startLocation = startLocation;
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, startLocation);
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>");
@@ -208,7 +217,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, null, null, false);
String description = descriptionGenerator.generateTrackDescription(track, false);
writePlacemark(name, "", description, END_STYLE, endLocation);
}
}
@@ -231,14 +240,16 @@ public class KmlTrackWriter implements TrackWriter {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
if (exportSensorData) {
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
@@ -249,9 +260,13 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
if (exportTrackDetails) {
printWriter.println("<when>" + getTime(location) + "</when>");
}
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (location instanceof SensorDataSetLocation) {
if (exportSensorData && location instanceof SensorDataSetLocation) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
@@ -291,13 +306,12 @@ public class KmlTrackWriter implements TrackWriter {
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String category, String description, String styleName, Location location) {
if (location != null) {
private void writePlacemark(String name, String category, String description, String styleName, Location location) {
if (location != null && exportTrackDetails) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println( "<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
@@ -319,10 +333,10 @@ public class KmlTrackWriter implements TrackWriter {
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName, Location location, String photoUrl, float heading) {
if (location != null) {
if (location != null && exportTrackDetails) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println( "<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
@@ -330,15 +344,12 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
if (isKMZ) {
if (exportPictures) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
} else {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment() + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
@@ -355,6 +366,19 @@ public class KmlTrackWriter implements TrackWriter {
}
}
/**
* Returns the formatted time of the location; either absolute or relative depending exportTrackDetails.
*
* @param location the location
*/
private String getTime(Location location) {
if (exportTrackDetails) {
return StringUtils.formatDateTimeIso8601(location.getTime());
} else {
return StringUtils.formatDateTimeIso8601(location.getTime() - startLocation.getTime());
}
}
/**
* Gets the heading to a location.
*
@@ -396,7 +420,7 @@ public class KmlTrackWriter implements TrackWriter {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println( "<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
@@ -38,8 +37,6 @@ import de.dennisguse.opentracks.content.Waypoint;
* @author Jimmy Shih
*/
public class KmzTrackExporter implements TrackExporter {
public static final String KMZ_EXTENSION = "kmz";
public static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml";
@@ -17,11 +17,11 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.location.Location;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import java.io.OutputStream;
/**
* Interface for writing tracks to a file. The expected sequence of calls is:
*
@@ -51,11 +51,6 @@ import java.io.OutputStream;
*/
public interface TrackWriter {
/**
* Gets the file extension (e.g, gpx, kml, ...).
*/
String getExtension();
/**
* Prepares the output stream.
*
@@ -16,14 +16,17 @@
package de.dennisguse.opentracks.io.file.importer;
import android.Manifest;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
@@ -42,7 +45,8 @@ import de.dennisguse.opentracks.util.FileUtils;
*/
public class ImportActivity extends FragmentActivity implements FileTypeDialogFragment.FileTypeCaller {
private static final String TAG = ImportActivity.class.getSimpleName();
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
@@ -62,19 +66,34 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_readable, Toast.LENGTH_LONG).show();
} else {
fileTypeDialogStart();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
FileTypeDialogFragment
.newInstance(R.string.import_selection_title, R.string.import_selection_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
}
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
Intent intent = getIntent();
if (trackFileFormat == null) {
trackFileFormat = TrackFileFormat.GPX;
}
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
@@ -84,14 +103,13 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
String directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
Toast.makeText(this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
//TODO (still needed?): getLastNonConfiguration instance returned SaveAsyncTask before
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ImportAsyncTask) {
// importAsyncTask = (ImportAsyncTask) retained;
@@ -24,7 +24,6 @@ import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +32,6 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
@@ -107,11 +105,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
// Get the wake lock if not recording or paused
boolean isRecording =
PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(importActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
boolean isRecording = PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(importActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
@@ -169,37 +164,28 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param file the file
*/
private boolean importFile(final File file) {
FileInputStream fileInputStream = null;
try {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.KML) {
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
} else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
}
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(context);
} else { //KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
} else {
trackImporter = new GpxFileTrackImporter(context);
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
}
fileInputStream = new FileInputStream(file);
}
try (FileInputStream fileInputStream = new FileInputStream(file)) {
trackId = trackImporter.importFile(fileInputStream);
return trackId != -1L;
} catch (FileNotFoundException e) {
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file input stream", e);
}
}
}
}
@@ -213,22 +199,18 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
File file = new File(path);
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat == TrackFileFormat.KML
&& (TrackFileFormat.KML.getExtension().equals(extension)
|| KmzTrackExporter.KMZ_EXTENSION.equals(extension))) {
files.add(candidate);
} else if (trackFileFormat == TrackFileFormat.GPX && TrackFileFormat.GPX.getExtension().equals(extension)) {
files.add(candidate);
}
if (candidates == null) {
return files;
}
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
return files;
}
}
@@ -58,8 +58,8 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.services.sensors.SensorManager;
import de.dennisguse.opentracks.services.sensors.SensorManagerFactory;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory;
@@ -181,7 +181,7 @@ public class TrackRecordingService extends Service {
};
private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock;
private SensorManager sensorManager;
private RemoteSensorManager remoteSensorManager;
private Location lastLocation;
private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle
@@ -280,9 +280,9 @@ public class TrackRecordingService extends Service {
@Override
public void onDestroy() {
if (sensorManager != null) {
SensorManagerFactory.releaseSystemSensorManager();
sensorManager = null;
if (remoteSensorManager != null) {
RemoteSensorManagerFactory.releaseSystemSensorManager();
remoteSensorManager = null;
}
// Reverse order from onCreate
@@ -617,7 +617,7 @@ public class TrackRecordingService extends Service {
*/
private void startRecording(boolean trackStarted) {
// Update instance variables
sensorManager = SensorManagerFactory.getSystemSensorManager(this);
remoteSensorManager = RemoteSensorManagerFactory.getSystemSensorManager(this);
lastLocation = null;
currentSegmentHasLocation = false;
isIdle = false;
@@ -715,9 +715,9 @@ public class TrackRecordingService extends Service {
splitExecutor.shutdown();
// Update instance variables
if (sensorManager != null) {
SensorManagerFactory.releaseSystemSensorManager();
sensorManager = null;
if (remoteSensorManager != null) {
RemoteSensorManagerFactory.releaseSystemSensorManager();
remoteSensorManager = null;
}
lastLocation = null;
@@ -931,10 +931,10 @@ public class TrackRecordingService extends Service {
}
private SensorDataSet getSensorDataSet() {
if (sensorManager == null || !sensorManager.isEnabled() || !sensorManager.isSensorDataSetValid()) {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
return null;
}
return sensorManager.getSensorDataSet();
return remoteSensorManager.getSensorDataSet();
}
/**
@@ -1104,24 +1104,24 @@ public class TrackRecordingService extends Service {
@Override
public SensorDataSet getSensorData() {
if (trackRecordingService.sensorManager == null) {
Log.d(TAG, "sensorManager is null.");
if (trackRecordingService.remoteSensorManager == null) {
Log.d(TAG, "remoteSensorManager is null.");
return null;
}
if (trackRecordingService.getSensorDataSet() == null) {
Log.d(TAG, "Sensor data set is null.");
return null;
}
return trackRecordingService.sensorManager.getSensorDataSet();
return trackRecordingService.remoteSensorManager.getSensorDataSet();
}
@Override
public SensorState getSensorState() {
if (trackRecordingService.sensorManager == null) {
Log.d(TAG, "sensorManager is null.");
if (trackRecordingService.remoteSensorManager == null) {
Log.d(TAG, "remoteSensorManager is null.");
return SensorState.NONE;
}
return trackRecordingService.sensorManager.getSensorState();
return trackRecordingService.remoteSensorManager.getSensorState();
}
/**
@@ -24,13 +24,13 @@ import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/**
* Bluetooth sensor manager.
@@ -39,7 +39,7 @@ import java.util.ArrayList;
*
* @author Sandor Dornbush
*/
public class BluetoothSensorManager extends SensorManager {
public class BluetoothRemoteSensorManager extends RemoteSensorManager {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
@@ -71,7 +71,7 @@ public class BluetoothSensorManager extends SensorManager {
/**
* @param context the context
*/
public BluetoothSensorManager(Context context) {
public BluetoothRemoteSensorManager(Context context) {
this.context = context;
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler);
}
@@ -18,23 +18,23 @@ package de.dennisguse.opentracks.services.sensors;
import android.util.Log;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import java.util.Timer;
import java.util.TimerTask;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
/**
* Manage the connection to a sensor.
* Manage the connection to a remote sensor.
*
* @author Sandor Dornbush
*/
public abstract class SensorManager {
public abstract class RemoteSensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE = 5000;
public static final long MAX_SENSOR_STATE_AGE = 20000;
private static final String TAG = SensorManager.class.getSimpleName();
private static final String TAG = RemoteSensorManager.class.getSimpleName();
private static final int RETRY_PERIOD = 20000;
private SensorState sensorState = SensorState.NONE;
@@ -88,7 +88,7 @@ public abstract class SensorManager {
}
}
};
timer = new Timer(SensorManager.class.getSimpleName());
timer = new Timer(RemoteSensorManager.class.getSimpleName());
timer.schedule(timerTask, RETRY_PERIOD, RETRY_PERIOD);
}
@@ -19,16 +19,16 @@ package de.dennisguse.opentracks.services.sensors;
import android.content.Context;
/**
* A factory of {@link SensorManager}.
* A factory of {@link RemoteSensorManager}.
*
* @author Sandor Dornbush
*/
public class SensorManagerFactory {
public class RemoteSensorManagerFactory {
private static SensorManager systemSensorManager = null;
private static SensorManager sensorManagerTemporary = null;
private static RemoteSensorManager remoteSensorManagerSystem = null;
private static RemoteSensorManager remoteSensorManagerTemporary = null;
private SensorManagerFactory() {
private RemoteSensorManagerFactory() {
}
/**
@@ -36,49 +36,45 @@ public class SensorManagerFactory {
*
* @param context the context
*/
public static SensorManager getSystemSensorManager(Context context) {
public static RemoteSensorManager getSystemSensorManager(Context context) {
releaseSensorManagerTemporary();
releaseSystemSensorManager();
systemSensorManager = getSensorManager(context);
if (systemSensorManager != null) {
systemSensorManager.startSensor();
}
return systemSensorManager;
remoteSensorManagerSystem = getSensorManager(context);
remoteSensorManagerSystem.startSensor();
return remoteSensorManagerSystem;
}
/**
* Releases the system sensor manager.
*/
public static void releaseSystemSensorManager() {
if (systemSensorManager != null) {
systemSensorManager.stopSensor();
if (remoteSensorManagerSystem != null) {
remoteSensorManagerSystem.stopSensor();
}
systemSensorManager = null;
remoteSensorManagerSystem = null;
}
/**
* Gets the temp sensor manager.
*/
public static SensorManager getSensorManagerTemporary(Context context) {
public static RemoteSensorManager getSensorManagerTemporary(Context context) {
releaseSensorManagerTemporary();
if (systemSensorManager != null) {
if (remoteSensorManagerSystem != null) {
return null;
}
sensorManagerTemporary = getSensorManager(context);
if (sensorManagerTemporary != null) {
sensorManagerTemporary.startSensor();
}
return sensorManagerTemporary;
remoteSensorManagerTemporary = getSensorManager(context);
remoteSensorManagerTemporary.startSensor();
return remoteSensorManagerTemporary;
}
/**
* Releases the temp sensor manager.
*/
public static void releaseSensorManagerTemporary() {
if (sensorManagerTemporary != null) {
sensorManagerTemporary.stopSensor();
if (remoteSensorManagerTemporary != null) {
remoteSensorManagerTemporary.stopSensor();
}
sensorManagerTemporary = null;
remoteSensorManagerTemporary = null;
}
/**
@@ -86,7 +82,7 @@ public class SensorManagerFactory {
*
* @param context the context
*/
private static SensorManager getSensorManager(Context context) {
return new BluetoothSensorManager(context);
private static RemoteSensorManager getSensorManager(Context context) {
return new BluetoothRemoteSensorManager(context);
}
}
@@ -4,7 +4,6 @@ import android.content.Context;
import android.content.DialogInterface;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;
import de.dennisguse.opentracks.R;
@@ -16,11 +15,6 @@ public class ResetDialogPreference extends DialogPreference {
super(context, attrs);
}
@Override
protected View onCreateDialogView() {
return super.onCreateDialogView();
}
@Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
@@ -22,9 +22,8 @@ import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An activity for accessing stats settings.
@@ -77,30 +77,6 @@ public class DoubleBuffer {
return sum / numberOfEntries;
}
/**
* Gets the average and the variance of the buffer.
*
* @return an array of two elements - the first is the average, the second is
* the variance
*/
public double[] getAverageAndVariance() {
int numberOfEntries = isFull ? buffer.length : index;
if (numberOfEntries == 0) {
return new double[]{0, 0};
}
double sum = 0;
double sumSquares = 0;
for (int i = 0; i < numberOfEntries; i++) {
sum += buffer[i];
sumSquares += Math.pow(buffer[i], 2);
}
double average = sum / numberOfEntries;
double variance = sumSquares / numberOfEntries - Math.pow(average, 2);
return new double[]{average, variance};
}
/**
* Adds a double to the buffer. If the buffer is full the oldest element is
* overwritten.
@@ -35,10 +35,7 @@ public class FileUtils {
* Used to transfer picture from the camera.
*/
public static final String FILEPROVIDER = BuildConfig.APPLICATION_ID + ".fileprovider";
/**
* The play tracks directory in the app's cache directory.
*/
public static final String PLAY_TRACKS_DIR = "temp";
/**
* Our external storage directory.
*/
@@ -16,18 +16,28 @@
package de.dennisguse.opentracks.util;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.ShareContentProvider;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.R;
import java.io.File;
/**
* Utilities for creating intents.
@@ -36,6 +46,8 @@ import java.io.File;
*/
public class IntentUtils {
private final static String TAG = IntentUtils.class.getSimpleName();
private IntentUtils() {
}
@@ -50,38 +62,91 @@ public class IntentUtils {
return new Intent(context, cls).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
}
private static final String JPEG_EXTENSION = "jpeg";
/**
* Creates an intent to share a track file with an app.
*
* @param context the context
* @param trackId the track id
* @param filePath the file path
* @param trackFileFormat the track file format
* @param context the context
* @param trackIds the track ids
*/
public static Intent newShareFileIntent(Context context, long trackId, String filePath, TrackFileFormat trackFileFormat) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackId);
String trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, null, null, false);
public static Intent newShareFileIntent(Context context, long[] trackIds) {
String trackDescription = "";
if (trackIds.length == 1) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]);
trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false);
}
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES);
return new Intent(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filePath)))
.putExtra(Intent.EXTRA_STREAM, uriAndMime.first)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription))
.putExtra(context.getString(R.string.track_id_broadcast_extra), trackId)
.setType(trackFileFormat.getMimeType());
.setType(uriAndMime.second)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
public static Intent newShowOnMapIntent(Waypoint waypoint) {
return newShowOnMapIntent(waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName());
public static void showCoordinateOnMap(Context context, Waypoint waypoint) {
showCoordinateOnMap(context, waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName());
}
public static Intent newShowOnMapIntent(double latitude, double longitude, String label) {
public static void showCoordinateOnMap(Context context, double latitude, double longitude, String label) {
//SEE https://developer.android.com/guide/components/intents-common.html#Maps
Intent intent = new Intent(Intent.ACTION_VIEW);
String uri = "geo:0,0?q=" + latitude + "," + longitude;
if (label != null && label.length() > 0) {
uri += "(" + label + ")";
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(uri));
return intent;
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, context.getString(R.string.app_not_installed_show_on_map), Toast.LENGTH_SHORT).show();
}
}
/**
* Send intent to show tracks on a map (needs an another app).
*
* @param context the context
* @param trackIds the track ids
*/
public static void showTrackOnMap(Context context, long[] trackIds) {
if (trackIds.length == 0) {
return;
}
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, TrackFileFormat.KMZ_ONLY_TRACK);
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, context.getString(R.string.app_not_installed_show_on_map), Toast.LENGTH_SHORT).show();
}
}
/**
* Sends a take picture request to the camera app.
* The picture is then stored in the track's folder.
*
* @param context the context
* @param trackId the track id
*/
public static Pair<Intent, Uri> createTakePictureIntent(Context context, long trackId) {
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
Uri photoUri = FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file);
Log.d(TAG, "Taking photo to URI: " + photoUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
return new Pair<>(intent, photoUri);
}
}
@@ -83,6 +83,8 @@ public class PreferencesUtils {
public static final String TRACK_NAME_DEFAULT = "DATE_ISO_8601";
public static final boolean SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN = false;
// Track widget
public static final int TRACK_WIDGET_ITEM1_DEFAULT = 3; // moving time
public static final int TRACK_WIDGET_ITEM2_DEFAULT = 0; // distance
@@ -23,13 +23,13 @@ import android.view.View;
import android.widget.Spinner;
import android.widget.TextView;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.SensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.R;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* Utilities for updating the statistics UI labels and values.
*
@@ -294,7 +294,7 @@ public class StatsUtils {
private static void setHeartRateData(Context context, View viewValue, View viewSensor, SensorDataSet sensorDataSet) {
String heartRate = context.getString(R.string.value_unknown);
String sensor = context.getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.hasHeartRate() && sensorDataSet.isRecent(SensorManager.MAX_SENSOR_DATE_SET_AGE)) {
if (sensorDataSet != null && sensorDataSet.hasHeartRate() && sensorDataSet.isRecent(RemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
sensor = sensorDataSet.getSensorName();
}
@@ -49,7 +49,7 @@ public class SystemUtils {
public static String getAppVersionName(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA);
return pi.versionName;
return pi.versionName + "/" + BuildConfig.VERSION_NAME_FULL;
} catch (NameNotFoundException e) {
Log.w(TAG, "Failed to get version info.", e);
return "";
@@ -17,19 +17,14 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R;
/**
* Utilities for track name.
*
-170
View File
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Distance, calorie, total time, and moving time -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/stats_distance"
layout="@layout/stats_large_item" />
<include
android:id="@+id/stats_activity_type"
layout="@layout/stats_activity_type" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:visibility="invisible" />
<include
android:id="@+id/stats_total_time"
layout="@layout/stats_large_item" />
<include
android:id="@+id/stats_moving_time"
layout="@layout/stats_large_item" />
</LinearLayout>
<!-- Horizontal line -->
<View style="@style/StatsHorizontalLine" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<!-- Speed and average speed -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/stats_speed"
layout="@layout/stats_small_item" />
<include
android:id="@+id/stats_average_speed"
layout="@layout/stats_small_item" />
</LinearLayout>
<!-- Max speed and average moving speed -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/stats_max_speed"
layout="@layout/stats_small_item" />
<include
android:id="@+id/stats_average_moving_speed"
layout="@layout/stats_small_item" />
</LinearLayout>
<!-- Latitude and longitude -->
<LinearLayout
android:id="@+id/stats_coordinate_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/stats_latitude"
layout="@layout/stats_small_item" />
<include
android:id="@+id/stats_longitude"
layout="@layout/stats_small_item" />
</LinearLayout>
</TableLayout>
<!-- Vertical line -->
<View
android:id="@+id/stats_elevation_separator"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/stats_separator" />
<LinearLayout
android:id="@+id/stats_elevation_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout style="@style/StatsSmallGroupContainer">
<!-- This is a placeholder -->
</LinearLayout>
<!-- Elevation -->
<LinearLayout style="@style/StatsSmallGroupContainer">
<!-- Elevation label -->
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_elevation" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<!-- Elevation -->
<include
android:id="@+id/stats_elevation"
layout="@layout/stats_small_group_item" />
<!-- TODO DISABLED AS computation from GPS is not precise enough, go for getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure) -->
<!-- Elevation gain -->
<!--<include-->
<!--android:id="@+id/stats_elevation_gain"-->
<!--layout="@layout/stats_small_group_item" />-->
<!-- Elevation min -->
<include
android:id="@+id/stats_elevation_min"
layout="@layout/stats_small_group_item" />
<!-- Elevation max -->
<include
android:id="@+id/stats_elevation_max"
layout="@layout/stats_small_group_item" />
</TableLayout>
</LinearLayout>
</LinearLayout>
<!-- Sensor data: heart rate-->
<View
android:id="@+id/stats_sensor_separator"
style="@style/StatsHorizontalLine" />
<LinearLayout
android:id="@+id/stats_sensor_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/stats_sensor_heart_rate_value"
layout="@layout/stats_large_item" />
<LinearLayout style="@style/StatsSmallGroupContainer">
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<include
android:id="@+id/stats_sensor_heart_rate_sensor"
layout="@layout/stats_tiny_item" />
</TableLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</merge>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<include layout="@layout/track_controller" />
<TabHost
android:id="@android:id/tabhost"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<include layout="@layout/track_details_tab" />
</TabHost>
</LinearLayout>
-1
View File
@@ -8,7 +8,6 @@
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="239dp"
android:contentDescription="@string/image_my_tracks_logo"
android:scaleType="fitCenter"
android:src="@drawable/ic_logo" />
+1 -1
View File
@@ -25,5 +25,5 @@ limitations under the License.
<TextView
android:id="@+id/marker_list_empty"
style="@style/ListEmptyTextView"
android:text="@string/marker_list_empty_message"></TextView>
android:text="@string/marker_list_empty_message" />
</merge>
+1 -1
View File
@@ -25,5 +25,5 @@ limitations under the License.
<TextView
android:id="@+id/search_list_empty"
style="@style/ListEmptyTextView"
android:text="@string/search_list_empty_message"></TextView>
android:text="@string/search_list_empty_message" />
</merge>
+1 -1
View File
@@ -96,7 +96,7 @@ limitations under the License.
android:id="@+id/stats_elevation"
layout="@layout/stats_small_group_item" />
<!-- Elevation gain -->
<!-- TODO DISABLED AS computation from GPS is not precise enough, go for getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure) -->
<!-- TODO DISABLED AS computation from GPS is not precise enough, go for getAltitude(RemoteSensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure) -->
<!--<include-->
<!--android:id="@+id/stats_elevation_gain"-->
<!--layout="@layout/stats_small_group_item" />-->
+1 -1
View File
@@ -73,7 +73,7 @@ limitations under the License.
android:layout_weight="22"
android:gravity="left"
android:text="@string/track_list_empty_message"
android:textColor="@color/list_empty_text"></TextView>
android:textColor="@color/list_empty_text" />
<View
android:layout_width="0dp"
+1 -3
View File
@@ -119,7 +119,5 @@ limitations under the License.
android:layout_weight="1" />
</LinearLayout>
<ImageView
style="@style/TrackWidgetLogo"
android:contentDescription="@string/image_my_tracks_logo" />
<ImageView style="@style/TrackWidgetLogo" />
</RelativeLayout>
+1 -3
View File
@@ -192,7 +192,5 @@ limitations under the License.
</LinearLayout>
</LinearLayout>
<ImageView
style="@style/TrackWidgetLogo"
android:contentDescription="@string/image_my_tracks_logo" />
<ImageView style="@style/TrackWidgetLogo" />
</RelativeLayout>
+1 -3
View File
@@ -192,7 +192,5 @@ limitations under the License.
</LinearLayout>
</LinearLayout>
<ImageView
style="@style/TrackWidgetLogo"
android:contentDescription="@string/image_my_tracks_logo" />
<ImageView style="@style/TrackWidgetLogo" />
</RelativeLayout>
+1 -3
View File
@@ -199,7 +199,5 @@ limitations under the License.
</LinearLayout>
</LinearLayout>
<ImageView
style="@style/TrackWidgetLogo4x4"
android:contentDescription="@string/image_my_tracks_logo" />
<ImageView style="@style/TrackWidgetLogo4x4" />
</RelativeLayout>
+5
View File
@@ -26,6 +26,11 @@ limitations under the License.
android:orderInCategory="2"
android:showAsAction="ifRoom"
android:title="@string/menu_insert_photo" />
<item
android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_menu_show_on_map"
android:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" />
<item
android:id="@+id/track_detail_share"
android:actionProviderClass="android.widget.ShareActionProvider"
-1
View File
@@ -27,7 +27,6 @@ limitations under the License.
<item
android:id="@+id/track_list_markers"
android:icon="@drawable/ic_menu_insert_marker"
android:orderInCategory="5"
android:showAsAction="ifRoom"
android:title="@string/menu_markers" />
<item
-1
View File
@@ -152,7 +152,6 @@
<string name="image_marker">‏‮Marker‬‏</string>
<string name="image_marker_count">‏‮Marker‬‏ ‏‮count‬‏</string>
<string name="image_my_location">‏‮My‬‏ ‏‮Location‬‏</string>
<string name="image_my_tracks_logo">‏‮My‬‏ ‏‮Tracks‬‏ ‏‮logo‬‏</string>
<string name="image_pause">‏‮Pause‬‏</string>
<string name="image_photo_marker">‏‮Photo‬‏ ‏‮marker‬‏</string>
<string name="image_record">‏‮Record‬‏</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">علامة</string>
<string name="image_marker_count">عدد العلامات</string>
<string name="image_my_location">موقعي</string>
<string name="image_my_tracks_logo">شعار مساراتي</string>
<string name="image_pause">إيقاف مؤقت</string>
<string name="image_photo_marker">علامة الصورة</string>
<string name="image_record">تسجيل</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Маркер</string>
<string name="image_marker_count">Брой маркери</string>
<string name="image_my_location">Моето местоположение</string>
<string name="image_my_tracks_logo">Лого на Моите маршрути</string>
<string name="image_pause">Пауза</string>
<string name="image_photo_marker">Маркер за снимки</string>
<string name="image_record">Запис</string>
-1
View File
@@ -152,7 +152,6 @@
<string name="image_marker">Marker</string>
<string name="image_marker_count">Marker count</string>
<string name="image_my_location">My Location</string>
<string name="image_my_tracks_logo">OpenTracks logo</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Photo marker</string>
<string name="image_record">Record</string>
-1
View File
@@ -152,7 +152,6 @@ es vegi el cel. Torneu-ho a provar més tard.</string>
<string name="image_marker">Marcador</string>
<string name="image_marker_count">Recompte de marcadors</string>
<string name="image_my_location">La meva ubicació</string>
<string name="image_my_tracks_logo">Logotip de OpenTracks</string>
<string name="image_pause">Atura</string>
<string name="image_photo_marker">Marcador de fotos</string>
<string name="image_record">Enregistra</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Značka</string>
<string name="image_marker_count">Počet značek</string>
<string name="image_my_location">Moje poloha</string>
<string name="image_my_tracks_logo">Logo aplikace Moje trasy</string>
<string name="image_pause">Pozastavit</string>
<string name="image_photo_marker">Fotografická značka</string>
<string name="image_record">Zaznamenat</string>
-1
View File
@@ -152,7 +152,6 @@
<string name="image_marker">Markør</string>
<string name="image_marker_count">Antal markører</string>
<string name="image_my_location">Min placering</string>
<string name="image_my_tracks_logo">Logoet for OpenTracks</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Billedmarkør</string>
<string name="image_record">Registrer</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Markierung</string>
<string name="image_marker_count">Anzahl der Markierungen</string>
<string name="image_my_location">Mein Standort</string>
<string name="image_my_tracks_logo">Logo von \"Meine Tracks\"</string>
<string name="image_pause">Pausieren</string>
<string name="image_photo_marker">Fotomarkierung</string>
<string name="image_record">Aufzeichnen</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Markierung</string>
<string name="image_marker_count">Anzahl der Markierungen</string>
<string name="image_my_location">Mein Standort</string>
<string name="image_my_tracks_logo">Logo von \"Meine Tracks\"</string>
<string name="image_pause">Pausieren</string>
<string name="image_photo_marker">Fotomarkierung</string>
<string name="image_record">Aufzeichnen</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Markierung</string>
<string name="image_marker_count">Anzahl der Markierungen</string>
<string name="image_my_location">Mein Standort</string>
<string name="image_my_tracks_logo">Logo von \"Meine Tracks\"</string>
<string name="image_pause">Pausieren</string>
<string name="image_photo_marker">Fotomarkierung</string>
<string name="image_record">Aufzeichnen</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Δείκτης</string>
<string name="image_marker_count">Αριθμός δείκτη</string>
<string name="image_my_location">Η τοποθεσία μου</string>
<string name="image_my_tracks_logo">Λογότυπο εφαρμογής \"Οι Διαδρομές μου\"</string>
<string name="image_pause">Παύση</string>
<string name="image_photo_marker">Δείκτης φωτογραφίας</string>
<string name="image_record">Εγγραφή</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Marker</string>
<string name="image_marker_count">Marker count</string>
<string name="image_my_location">My Location</string>
<string name="image_my_tracks_logo">OpenTracks logo</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Photo marker</string>
<string name="image_record">Record</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Marker</string>
<string name="image_marker_count">Marker count</string>
<string name="image_my_location">My Location</string>
<string name="image_my_tracks_logo">OpenTracks logo</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Photo marker</string>
<string name="image_record">Record</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Marker</string>
<string name="image_marker_count">Marker count</string>
<string name="image_my_location">My Location</string>
<string name="image_my_tracks_logo">OpenTracks logo</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Photo marker</string>
<string name="image_record">Record</string>
-1
View File
@@ -150,7 +150,6 @@
<string name="image_marker">Marker</string>
<string name="image_marker_count">Marker count</string>
<string name="image_my_location">My Location</string>
<string name="image_my_tracks_logo">OpenTracks logo</string>
<string name="image_pause">Pause</string>
<string name="image_photo_marker">Photo marker</string>
<string name="image_record">Record</string>
-1
View File
@@ -152,7 +152,6 @@
<string name="image_marker">[Måŕķéŕ one]</string>
<string name="image_marker_count">[Måŕķéŕ çöûñţ one two]</string>
<string name="image_my_location">[Mý Ļöçåţîöñ one two]</string>
<string name="image_my_tracks_logo">[Mý Ţŕåçķš ļöĝö one two]</string>
<string name="image_pause">[Þåûšé one]</string>
<string name="image_photo_marker">[Þĥöţö måŕķéŕ one two]</string>
<string name="image_record">[Ŕéçöŕð one]</string>
-1
View File
@@ -152,7 +152,6 @@
<string name="image_marker">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‏‎‎‎‏‏‎‏‎‎‏‏‎‎‎‏‎‏‏‎‎‎‏‎‏‏‎Marker‎‏‎‎‏‎</string>
<string name="image_marker_count">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‏‏‏‎‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‎‎‎‎‎‎‎‎‏‏‎‏‎‎‏‏‏‎‏‎‎‏‏‏‎‏‎‏‎Marker count‎‏‎‎‏‎</string>
<string name="image_my_location">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‏‎‎‏‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‎‎‎‏‏‎‏‎‏‎‎‎‏‏‎‎‏‎‎‎‎My Location‎‏‎‎‏‎</string>
<string name="image_my_tracks_logo">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‏‎‏‏‏‎‏‏‎‎‎‏‎‏‎‎‎‎‎‎‏‏‎‎‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‎OpenTracks logo‎‏‎‎‏‎</string>
<string name="image_pause">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‏‎‎‎‎‎‎‎‏‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‎‎Pause‎‏‎‎‏‎</string>
<string name="image_photo_marker">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‎‎‏‏‎‏‎‏‏‏‎‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‏‏‏‎Photo marker‎‏‎‎‏‎</string>
<string name="image_record">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‏‎‎‏‎‏‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‏‎‏‏‏‏‎‏‏‏‏‎‎‎‎‎‏‎‎‎‎‎‎‏‎‏‏‎‎Record‎‏‎‎‏‎</string>

Some files were not shown because too many files have changed in this diff Show More