Compare commits

..

22 Commits

Author SHA1 Message Date
Dennis Guse a2b53c75f4 Release v3.7.6 2020-07-13 18:47:50 +02:00
Dennis Guse 7a5f69d3a5 Settings: default export format is now KMZ incl. data and pictures. 2020-07-13 18:19:34 +02:00
Dennis Guse 7fd9893560 TrackDeleteActivity: keep a wakelock while deletion is ongoing. 2020-07-13 18:19:34 +02:00
Román f209031df0 Pass extra TrackFileFormat enum like a Serializable instead of String (with String resulted in Null after get extra). 2020-07-13 18:19:34 +02:00
Dennis Guse 2dbab929ac Export: replaced async task with thread.
Almost identical to the import.
2020-07-13 18:19:34 +02:00
Dennis Guse e9eb764d12 Import: split ImportActivity into fragments.
The fragements keep the state while the activity is re-created (e.g., orientation change).
2020-07-13 18:19:34 +02:00
Dennis Guse 41a4a5527a Import: replaced async task with thread.
Extracted directory chooser into a separate activity.
2020-07-13 18:19:34 +02:00
Dennis Guse 4ded3c6bc8 TrackDelete: use a Thread and always require TrackIds. 2020-07-08 08:25:36 +02:00
Dennis Guse 51e573e475 TrackDelete: hide progress bar before finishing and add custom animations. 2020-07-08 08:25:36 +02:00
Dennis Guse 98c3169929 Track deletion UI: code cleanup. 2020-07-08 08:25:36 +02:00
Dennis Guse ff65a4d04f Track deletion shows an infinite progressbar.
Fixes #242.
2020-07-08 08:25:36 +02:00
Dennis Guse 278f49dec8 TrackRecordingActivity: removed start/resume toast message. 2020-07-07 18:13:37 +02:00
Dennis Guse d444e1ca3c TrackRecordingActivity: recreate activity properly. 2020-07-07 18:13:37 +02:00
Dennis Guse 13e07696b9 Re-implemented landscape mode.
Fixes #32.
2020-07-07 18:13:37 +02:00
Román Martínez 23c3a4d651 Import/Export speed in GPX and KML files. (#306)
Import/Export speed in GPX and KML files. Fixes #300.
2020-06-30 14:39:44 +02:00
Dennis Guse c9869bab0e Generate UUID for each track (necessary for synchronization).
Fixes #294.
2020-06-30 12:39:30 +02:00
Dennis Guse ecdb509371 Export/import test now verifies the TrackPoints. 2020-06-22 21:56:22 +02:00
transifex-integration[bot] de376a84f8 Apply translations in fr
translation completed for the source file '/fastlane/metadata/android/en-US/short_description.txt'
on the 'fr' language.
2020-06-21 18:19:11 +02:00
transifex-integration[bot] 9abd8bcaa6 Apply translations in de
translation completed for the source file '/fastlane/metadata/android/en-US/short_description.txt'
on the 'de' language.
2020-06-21 18:18:58 +02:00
Román Martínez 8ddadb8e6d Aggregated statistics: updated PlantUML and handle empty list. (#296) 2020-06-20 19:02:02 +02:00
transifex-integration[bot] 924ef3313e Translate /src/main/res/values/strings.xml in nl
translation completed for the source file '/src/main/res/values/strings.xml'
on the 'nl' language.
2020-06-16 19:33:52 +02:00
Dennis Guse 8cb68a6e64 Migrate to default branch main. 2020-06-15 12:10:26 +02:00
51 changed files with 1461 additions and 1033 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## Craft new releases
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
1. Get next _version code_ (main): `git rev-list HEAD --count main`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `build.gradle`
4. Create changelog (`_version code_.txt`)
+2 -2
View File
@@ -64,9 +64,9 @@ android {
applicationName: "@string/app_name"
]
versionCode 3427
versionCode 3449
//versionCode getVersionCode() //Useful for debugging.
versionName "v3.7.5"
versionName "v3.7.6"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
+5 -1
View File
@@ -12,6 +12,7 @@ endlegend
TrackListActivity --> TrackRecordingActivity : Start recording
TrackListActivity --> SearchListActivity
TrackListActivity --> MarkerListActivity : Show all markers
TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> TrackDeleteActivity
@@ -46,6 +47,9 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ExportActivity
SettingsActivity --> ImportActivity
SettingsActivity --> DirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
@enduml
@@ -1 +1 @@
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,11 @@
v3.7.6: OpenTracks
Changes:
- Landscape mode
- Track deletion UI is now only a spinner
- Speed is exported and imported (GPX/KML/KMZ)
- Default export format is KMZ incl. all data and pictures
Developer:
- Replaced AsyncTasks with Threads
- Synchronization: UUID is generated per track (not yet exported)
@@ -1 +1 @@
OpenTracks - Suivi d'activités sportives respectueux de votre vie privée.
OpenTracks - Suivi d'activités sportives respectueux de votre vie privée.
@@ -39,7 +39,7 @@ public class TestDataUtil {
TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; i++) {
trackPoints[i] = (createTrackPoint(i));
trackPoints[i] = createTrackPoint(i);
}
return new Pair<>(track, trackPoints);
@@ -56,17 +56,22 @@ public class TestDataUtil {
/**
* Creates a location.
*
* @param i the index to set the value of location.
* @return created location
* @param i the index for the TrackPoint.
*/
public static TrackPoint createTrackPoint(int i) {
Location location = new Location("test");
location.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
location.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
location.setAccuracy((float) i / 100.0f);
location.setAltitude(i * ALTITUDE_INTERVAL);
location.setTime(i + 1);
return new TrackPoint(location);
TrackPoint trackPoint = new TrackPoint();
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(i + 1);
trackPoint.setSpeed(5f + (i / 10));
trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(200f + i);
trackPoint.setCyclingCadence_rpm(300f + i);
trackPoint.setPower(400f + i);
return trackPoint;
}
/**
@@ -37,6 +37,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
@@ -47,6 +48,7 @@ import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
import static org.mockito.Mockito.when;
@@ -171,22 +173,26 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testCreateTrack() {
int startColumnIndex = 1;
int columnIndex = startColumnIndex;
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex++);
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex++);
columnIndex = startColumnIndex;
// Id
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
// Name
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
long trackId = System.currentTimeMillis();
columnIndex = startColumnIndex;
int columnIndex = 1;
// Id
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getLong(columnIndex)).thenReturn(trackId);
//Uuid
columnIndex++;
when(cursorMock.getColumnIndexOrThrow(TracksColumns.UUID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getBlob(columnIndex)).thenReturn(UUIDUtils.toBytes(UUID.randomUUID()));
// Name
columnIndex++;
String name = NAME_PREFIX + trackId;
when(cursorMock.getString(columnIndex++)).thenReturn(name);
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getString(columnIndex)).thenReturn(name);
Track track = contentProviderUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId());
@@ -5,6 +5,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.VisibleForTesting;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -36,7 +37,8 @@ public class CustomSQLiteOpenHelperTest {
*
* @return Map(TableName, SQL)
*/
private static Map<String, String> getTableSQL(SQLiteDatabase db) {
@VisibleForTesting
public static Map<String, String> getTableSQL(SQLiteDatabase db) {
HashMap<String, String> tableSQL = new HashMap<>();
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + WaypointsColumns.TABLE_NAME + "')", null, null, null, "name")) {
if (cursor != null) {
@@ -53,6 +55,7 @@ public class CustomSQLiteOpenHelperTest {
*
* @return Map(tableName, SQL)
*/
@VisibleForTesting
public static Map<String, String> getIndexSQL(SQLiteDatabase db) {
HashMap<String, String> indexSQL = new HashMap<>();
try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) {
@@ -104,24 +107,7 @@ public class CustomSQLiteOpenHelperTest {
@Test
public void onUpgrade_FromVersion23() {
// Manually create database schema with version 23 (base version)
{
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}.getWritableDatabase();
dbBase.execSQL(TRACKS_CREATE_TABLE_V23);
dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23);
dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23);
dbBase.close();
}
createVersion23();
// Open database with SQL upgrade
Map<String, String> tableByUpgrade;
@@ -130,10 +116,9 @@ public class CustomSQLiteOpenHelperTest {
tableByUpgrade = getTableSQL(dbUpgraded);
indicesByUpgrade = getIndexSQL(dbUpgraded);
}
// Open database via
context.deleteDatabase(DATABASE_NAME);
// Open database via creation script
Map<String, String> tablesByCreate;
Map<String, String> indicesByCreate;
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
@@ -150,7 +135,8 @@ public class CustomSQLiteOpenHelperTest {
Assert.assertEquals(tablesByCreate.get(WaypointsColumns.TABLE_NAME), tableByUpgrade.get(WaypointsColumns.TABLE_NAME));
// then - verify custom indices
Assert.assertEquals(2, indicesByCreate.size());
Assert.assertEquals(3, indicesByCreate.size());
Assert.assertEquals(indicesByUpgrade.get(TracksColumns.TABLE_NAME), indicesByCreate.get(TracksColumns.TABLE_NAME));
Assert.assertEquals(indicesByUpgrade.get(TrackPointsColumns.TABLE_NAME), indicesByCreate.get(TrackPointsColumns.TABLE_NAME));
Assert.assertEquals(indicesByUpgrade.get(WaypointsColumns.TABLE_NAME), indicesByCreate.get(WaypointsColumns.TABLE_NAME));
}
@@ -176,4 +162,23 @@ public class CustomSQLiteOpenHelperTest {
// then - verify custom indices
Assert.assertEquals(0, indicesByDowngrade.size());
}
private void createVersion23() {
// Manually create database schema with version 23 (base version)
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}.getWritableDatabase();
dbBase.execSQL(TRACKS_CREATE_TABLE_V23);
dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23);
dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23);
dbBase.close();
}
}
@@ -17,6 +17,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -48,7 +49,10 @@ public class ExportImportTest {
private static final String TRACK_ICON = "the track icon";
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Waypoint> waypoints = new ArrayList<>();
private final List<TrackPoint> trackPoints = new ArrayList<>();
private long importTrackId;
private final long trackId = System.currentTimeMillis();
@@ -61,6 +65,9 @@ public class ExportImportTest {
contentProviderUtils.insertTrack(track.first);
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
trackPoints.clear();
trackPoints.addAll(Arrays.asList(track.second));
for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint(track.second[i].getLocation());
waypoint.setName("the waypoint " + i);
@@ -104,7 +111,40 @@ public class ExportImportTest {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(context, outputStream);
System.out.println(outputStream.toString());
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context, -1L);
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. waypoints
assertWaypoints();
// 3. trackpoints
assertTrackpoints(false, false, false);
}
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track});
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(context, outputStream);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
@@ -115,7 +155,6 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
//TODO assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -124,14 +163,8 @@ public class ExportImportTest {
// 2. waypoints
assertWaypoints();
//TODO Check absolute time of trackpoints
}
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() {
// TODO
Log.e(TAG, "Test not implemented.");
// 3. trackpoints
assertTrackpoints(true, true, true);
}
@LargeTest
@@ -185,7 +218,6 @@ public class ExportImportTest {
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
//TODO assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
@@ -196,7 +228,8 @@ public class ExportImportTest {
// 2. waypoints
assertWaypoints();
//TODO Check absolute time of trackpoints
// 3. trackpoints
assertTrackpoints(false, false, false);
}
private void assertWaypoints() {
@@ -217,4 +250,33 @@ public class ExportImportTest {
assertEquals(waypoint.getLocation().getAltitude(), importedWaypoint.getLocation().getAltitude(), 0.001);
}
}
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
TrackPoint importedTrackPoint = importedTrackPoints.get(i);
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01);
// TODO Not exported for GPX/KML
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
if (verifyHeartrate) {
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
}
if (verifyCadence) {
assertEquals(trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
}
if (verifyPower) {
assertEquals(trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
}
}
}
}
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.util;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.UUID;
@RunWith(AndroidJUnit4.class)
public class UUIDUtilsTest {
@Test
public void test() {
UUID uuid = UUID.randomUUID();
byte[] bytes = UUIDUtils.toBytes(uuid);
UUID output = UUIDUtils.fromBytes(bytes);
Assert.assertEquals(uuid, output);
}
}
+10 -13
View File
@@ -38,15 +38,17 @@ limitations under the License.
<activity
android:name=".TrackDeleteActivity"
android:theme="@style/ThemeCustomTransparent" />
android:theme="@style/TrackDeleteActivity" />
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent">
android:theme="@style/ThemeCustomTransparentActivity">
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<action android:name="android.intent.action.VIEW" />
@@ -85,13 +87,9 @@ limitations under the License.
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".TrackRecordedActivity"
android:screenOrientation="userPortrait" />
<activity android:name=".TrackRecordedActivity" />
<activity
android:name=".TrackRecordingActivity"
android:screenOrientation="userPortrait" />
<activity android:name=".TrackRecordingActivity" />
<activity android:name=".TrackEditActivity" />
@@ -105,14 +103,13 @@ limitations under the License.
android:value=".SearchListActivity" />
</activity>
<activity
android:name=".AggregatedStatisticsActivity"
android:screenOrientation="userPortrait"
android:label="@string/menu_aggregated_statistics"/>
<activity android:name=".AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
<activity
android:name=".io.file.exporter.ExportActivity"
android:theme="@style/ThemeCustomTransparent" />
android:theme="@style/ThemeCustomTransparentActivity" />
<activity android:name=".settings.SettingsActivity" />
@@ -62,23 +62,22 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
@Override
public void onConfirmDeleteDone(long[] trackIds) {
boolean stopRecording = false;
if (trackIds.length == 1 && trackIds[0] == -1L) {
stopRecording = true;
} else {
long recordingTrackId = PreferencesUtils.getRecordingTrackId(this);
for (long trackId : trackIds) {
if (trackId == recordingTrackId) {
stopRecording = true;
break;
}
long recordingTrackId = PreferencesUtils.getRecordingTrackId(this);
for (long trackId : trackIds) {
if (trackId == recordingTrackId) {
stopRecording = true;
break;
}
}
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this, false);
}
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
/**
@@ -20,6 +20,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
super.onCreate(savedInstanceState);
listView = findViewById(R.id.aggregated_stats_list);
listView.setEmptyView(findViewById(R.id.aggregated_stats_empty_view));
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats().observe(this, new Observer<AggregatedStatistics>() {
@@ -1,94 +0,0 @@
package de.dennisguse.opentracks;
/*
* 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.
*/
import android.content.Context;
import android.os.AsyncTask;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Async Task to delete tracks.
*
* @author Jimmy Shih
*/
class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final long[] trackIds;
private final Context context;
private TrackDeleteActivity deleteActivity;
// true if the AsyncTask has completed
private boolean completed;
/**
* Creates an AsyncTask.
*
* @param deleteActivity the activity currently associated with this task
* @param trackIds the track ids to delete. To delete all, set to size 1 with
* trackIds[0] == -1L
*/
public DeleteAsyncTask(TrackDeleteActivity deleteActivity, long[] trackIds) {
this.deleteActivity = deleteActivity;
this.trackIds = trackIds;
context = deleteActivity.getApplicationContext();
completed = false;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param deleteActivity the current activity, can be null
*/
public void setActivity(TrackDeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
if (completed && deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
}
}
@Override
protected void onPreExecute() {
if (deleteActivity != null) {
deleteActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context);
} else {
for (long id : trackIds) {
if (isCancelled()) {
return false;
}
contentProviderUtils.deleteTrack(context, id);
}
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
}
}
}
@@ -51,7 +51,6 @@ public class TrackController {
private boolean isPaused;
private long totalTime = 0;
// the timestamp for the toal time
private long totalTimeTimestamp = 0;
private boolean isResumed = false;
@@ -16,26 +16,26 @@
package de.dennisguse.opentracks;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.View;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* An activity for delete tracks.
* An activity for deleting tracks.
*
* @author Jimmy Shih
*/
public class TrackDeleteActivity extends Activity {
public class TrackDeleteActivity extends AbstractActivity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private static final int DIALOG_PROGRESS_ID = 0;
private long[] trackIds;
private DeleteAsyncTask deleteAsyncTask;
private Thread deleteThread;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -44,53 +44,48 @@ public class TrackDeleteActivity extends Activity {
setResult(RESULT_CANCELED);
Intent intent = getIntent();
long[] trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
deleteThread = new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof DeleteAsyncTask) {
deleteAsyncTask = (DeleteAsyncTask) retained;
deleteAsyncTask.setActivity(this);
} else {
deleteAsyncTask = new DeleteAsyncTask(this, trackIds);
deleteAsyncTask.execute();
}
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
for (long id : trackIds) {
if (Thread.interrupted()) {
break;
}
contentProviderUtils.deleteTrack(TrackDeleteActivity.this, id);
}
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (Thread.interrupted()) {
return;
}
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
});
}
@Override
public Object onRetainNonConfigurationInstance() {
deleteAsyncTask.setActivity(null);
return deleteAsyncTask;
protected void onStart() {
super.onStart();
deleteThread.start();
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(
this, R.string.track_delete_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
deleteAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
});
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected int getLayoutResId() {
return R.layout.track_delete;
}
/**
* Invokes when the associated AsyncTask completes.
*/
public void onAsyncTaskCompleted() {
removeDialog(DIALOG_PROGRESS_ID);
findViewById(R.id.progressbar).setVisibility(View.INVISIBLE);
setResult(RESULT_OK);
finish();
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -495,9 +495,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
if (trackIds.length > 1 && trackIds.length == listView.getCount()) {
trackIds = new long[]{-1L};
}
deleteTracks(trackIds);
return true;
case R.id.list_context_menu_select_all:
@@ -11,10 +11,10 @@ import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -41,6 +41,8 @@ import de.dennisguse.opentracks.util.TrackUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -57,11 +59,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private ViewPager pager;
private TrackController trackController;
// From intent
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
private long trackId;
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused;
private final Runnable bindChangedCallback = new Runnable() {
@@ -70,29 +71,22 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// After binding changes (is available), update the total time in trackController.
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
if (recordingTrackId == -1L) {
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
return;
}
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
return;
}
if (!service.isRecording()) {
// Starts or resumes a track.
int msg;
if (trackId == -1L) {
// trackId isn't initialized -> leads a new recording.
trackId = service.startNewTrack();
recordingTrackId = trackId;
msg = R.string.track_detail_record_success;
} else {
// trackId is initialized -> resumes the track.
recordingTrackId = trackId;
service.resumeTrack(trackId);
msg = R.string.track_detail_resume_success;
}
// A recording track is on.
Toast.makeText(TrackRecordingActivity.this, msg, Toast.LENGTH_SHORT).show();
trackDataHub.loadTrack(trackId);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
@@ -103,12 +97,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackRecordingActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackRecordingActivity.this);
setLockscreenPolicy();
@@ -168,52 +156,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent());
trackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
if (savedInstanceState != null) {
//Activity was recreated.
trackId = savedInstanceState.getLong(EXTRA_TRACK_ID, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
} else {
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT && contentProviderUtils.getTrack(trackId) == null) {
finish();
}
}
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackDataHub = new TrackDataHub(this);
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@Override
public int getCount() {
return 3;
}
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatisticsRecordingFragment();
case 1:
return ChartFragment.newInstance(false);
case 2:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.settings_chart_by_time);
case 2:
return getString(R.string.settings_chart_by_distance);
}
return "Unknown Tab";
}
};
pager = findViewById(R.id.track_detail_activity_view_pager);
pager.setAdapter(adapter);
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), 1));
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
tabs.setupWithViewPager(pager);
if (savedInstanceState != null) {
@@ -277,7 +240,20 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
trackController.onResume(true, recordingTrackPaused);
}
startRecording();
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
outState.putLong(EXTRA_TRACK_ID, trackId);
}
@Override
@@ -294,24 +270,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
trackDataHub.stop();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
}
@Override
protected int getLayoutResId() {
return R.layout.track_record;
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_record, menu);
@@ -368,22 +331,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
return trackDataHub;
}
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
return;
}
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
track = contentProviderUtils.getLastTrack();
if (track != null) {
trackId = track.getId();
return;
}
finish();
}
}
/**
* Updates the menu items.
*/
@@ -404,14 +351,44 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
private void startRecording() {
trackRecordingServiceConnection.startAndBind(this);
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
super(fm, behavior);
}
@Override
public int getCount() {
return 3;
}
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatisticsRecordingFragment();
case 1:
return ChartFragment.newInstance(false);
case 2:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.settings_chart_by_time);
case 2:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.content.data;
import java.util.UUID;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
@@ -27,6 +29,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
public class Track {
private long id = -1L;
private UUID uuid = UUID.randomUUID();
private String name = "";
private String description = "";
private String category = "";
@@ -46,6 +50,14 @@ public class Track {
this.id = id;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public String getName() {
return name;
}
@@ -35,6 +35,7 @@ public interface TracksColumns extends BaseColumns {
String DEFAULT_SORT_ORDER = "_id";
// Columns
String UUID = "uuid";
String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
@@ -71,5 +72,9 @@ public interface TracksColumns extends BaseColumns {
+ MINELEVATION + " FLOAT, " // min elevation
+ MAXELEVATION + " FLOAT, " // max elevation
+ ELEVATIONGAIN + " FLOAT, " // elevation gain
+ ICON + " TEXT)"; // icon
+ ICON + " TEXT, " // icon
+ UUID + " BLOB)"; // UUID
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
}
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
/**
* {@link ContentProviderUtils} implementation.
@@ -99,6 +100,7 @@ public class ContentProviderUtils {
*/
public Track createTrack(Cursor cursor) {
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID);
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
@@ -118,6 +120,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(idIndex)) {
track.setId(cursor.getLong(idIndex));
}
if (!cursor.isNull(uuidIndex)) {
track.setUuid(UUIDUtils.fromBytes(cursor.getBlob(uuidIndex)));
}
if (!cursor.isNull(nameIndex)) {
track.setName(cursor.getString(nameIndex));
}
@@ -160,9 +165,7 @@ public class ContentProviderUtils {
return track;
}
/**
* Deletes all tracks (including waypoints and trackPoints).
*/
@VisibleForTesting
public void deleteAllTracks(Context context) {
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
@@ -287,6 +290,7 @@ public class ContentProviderUtils {
if (track.getId() >= 0) {
values.put(TracksColumns._ID, track.getId());
}
values.put(TracksColumns.UUID, UUIDUtils.toBytes(track.getUuid()));
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
@@ -1,15 +1,20 @@
package de.dennisguse.opentracks.content.provider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.UUID;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.util.UUIDUtils;
/**
* Database helper for creating and upgrading the database.
@@ -17,13 +22,11 @@ import de.dennisguse.opentracks.content.data.WaypointsColumns;
@VisibleForTesting
class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
@VisibleForTesting
private static final int DATABASE_VERSION = 25;
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
@VisibleForTesting
private static final String DATABASE_NAME = "database.db";
private static final int DATABASE_VERSION = 26;
private static final String DATABASE_NAME = "database.db";
public CustomSQLiteOpenHelper(Context context) {
this(context, DATABASE_NAME);
@@ -45,6 +48,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL(TrackPointsColumns.CREATE_TABLE_INDEX);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE_INDEX);
db.execSQL(WaypointsColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE_INDEX);
@@ -61,6 +65,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 25:
upgradeFrom24to25(db);
break;
case 26:
upgradeFrom25to26(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -79,6 +86,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 24:
downgradeFrom25to24(db);
break;
case 25:
downgradeFrom26to25(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
@@ -107,7 +117,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
}
private void downgradeFrom24to23(SQLiteDatabase db) {
//Not needed as the delete columns did not contain any data
//Not needed as the deleted columns did not contain any data
db.beginTransaction();
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)");
@@ -142,4 +152,40 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom25to26(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE tracks ADD COLUMN uuid BLOB");
try (Cursor cursor = db.query("tracks", new String[]{"_id"}, null, null, null, null, null)) {
if (cursor.moveToFirst()) {
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
do {
long trackId = cursor.getLong(trackIdIndex);
ContentValues cv = new ContentValues();
cv.put("uuid", UUIDUtils.toBytes(UUID.randomUUID()));
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId)});
} while (cursor.moveToNext());
}
}
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom26to25(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("DROP INDEX tracks_uuid_index");
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)");
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, 0, 0, icon FROM tracks_old");
db.execSQL("DROP TABLE tracks_old");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
@@ -43,7 +42,7 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
/**
* Create a new instance.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L
* @param trackIds list of track ids to delete.
*/
public static void showDialog(FragmentManager fragmentManager, long[] trackIds) {
Bundle bundle = new Bundle();
@@ -69,17 +68,14 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId;
int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
int titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
int messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmDeleteDone(trackIds);
}
});
getActivity(),
titleId,
getString(messageId),
(dialog, which) -> caller.onConfirmDeleteDone(trackIds)
);
}
/**
@@ -89,12 +85,6 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
*/
public interface ConfirmDeleteCaller {
/**
* Called when confirm delete is done.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
void onConfirmDeleteDone(long[] trackIds);
}
}
@@ -0,0 +1,105 @@
/*
* 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.fragments;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
public class ExportFinishedDialogFragment extends DialogFragment {
private static final String TAG = ExportFinishedDialogFragment.class.getSimpleName();
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
private static final String EXTRA_EXPORTED_TRACK_COUNT = "exported_track_count";
private static final String EXTRA_TOTAL_TRACK_COUNT = "";
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
private DismissCallback caller;
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
Bundle bundle = new Bundle();
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
bundle.putInt(EXTRA_EXPORTED_TRACK_COUNT, successCount);
bundle.putInt(EXTRA_TOTAL_TRACK_COUNT, fileCount);
ExportFinishedDialogFragment dialogFragment = new ExportFinishedDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
int exportedTracksCount = getArguments().getInt(EXTRA_EXPORTED_TRACK_COUNT);
int totalTracksCount = getArguments().getInt(EXTRA_TOTAL_TRACK_COUNT);
int iconId;
int titleId;
String message;
String totalTracks = getResources().getQuantityString(R.plurals.tracks, totalTracksCount, totalTracksCount);
if (exportedTracksCount == totalTracksCount) {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_success, totalTracks, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.export_error, exportedTracksCount, totalTracks, directoryDisplayName);
}
Dialog alertDialog = new AlertDialog.Builder(getContext()).setCancelable(true)
.setIcon(iconId).setMessage(message)
.setPositiveButton(R.string.generic_ok, (dialog, arg1) -> {
dialog.dismiss();
caller.onDismissed();
}).setTitle(titleId)
.create();
alertDialog.setCanceledOnTouchOutside(false);
return alertDialog;
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
public interface DismissCallback {
void onDismissed();
}
}
@@ -0,0 +1,245 @@
/*
* 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.fragments;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ExportProgressDialogFragment extends DialogFragment {
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
private static final String TAG = ExportProgressDialogFragment.class.getSimpleName();
public static final String EXPORT_DIALOG_TAG = "export_dialog_tag";
private DismissCallback caller;
private ExportThread exportThread;
private String directoryDisplayName;
private int trackExportSuccessCount;
private int trackCount;
/**
* Create a new instance.
*/
public static void showDialog(FragmentManager fragmentManager, TrackFileFormat trackFileFormat, Uri documentFileUri) {
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
bundle.putSerializable(EXTRA_TRACKFILEFORMAT_KEY, trackFileFormat);
ExportProgressDialogFragment dialogFragment = new ExportProgressDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.setRetainInstance(true);
dialogFragment.show(fragmentManager, EXPORT_DIALOG_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
setRetainInstance(true);
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
TrackFileFormat trackFileFormat = (TrackFileFormat) getArguments().getSerializable(EXTRA_TRACKFILEFORMAT_KEY);
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
directoryDisplayName = FileUtils.getPath(documentFile);
exportThread = new ExportThread(trackFileFormat, documentFile);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(getContext(),
R.string.export_progress_message,
dialog -> {
exportThread.interrupt();
dialog.dismiss();
caller.onExportCanceled(directoryDisplayName, trackExportSuccessCount, trackCount);
}, directoryDisplayName);
progressDialog.setIndeterminate(false);
progressDialog.setMax(trackCount);
progressDialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
return progressDialog;
}
@Override
public void onStart() {
super.onStart();
setProgress();
if (!exportThread.isAlive()) {
exportThread.start();
}
}
@Override
public void onPause() {
super.onPause();
if (!exportThread.isAlive()) {
exportThread.interrupt();
}
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
private void onProgress(int trackImportSuccessCount, int fileCount) {
this.trackExportSuccessCount = trackImportSuccessCount;
this.trackCount = fileCount;
setProgress();
}
private void setProgress() {
ProgressDialog dialog = (ProgressDialog) getDialog();
if (dialog != null) {
dialog.setIndeterminate(false);
dialog.setMax(trackCount);
dialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
}
}
private void onExportCompleted(int successCount, int fileCount) {
if (caller != null && !isDetached()) {
caller.onExportFinished(directoryDisplayName, successCount, fileCount);
}
dismissAllowingStateLoss();
}
private class ExportThread extends Thread {
private TrackFileFormat trackFileFormat;
private DocumentFile directory;
ExportThread(TrackFileFormat trackFileFormat, DocumentFile directory) {
this.directory = directory;
this.trackFileFormat = trackFileFormat;
}
@Override
public void run() {
Context context = ExportProgressDialogFragment.this.getContext();
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor == null) {
onExportCompleted(0, 0);
return;
}
trackCount = cursor.getCount();
for (int i = 0; i < trackCount; i++) {
onProgress(i, trackCount);
if (Thread.interrupted()) {
return;
}
cursor.moveToPosition(i);
Track track = contentProviderUtils.createTrack(cursor);
if (track != null && exportTrack(context, track)) {
trackExportSuccessCount++;
}
}
onExportCompleted(trackExportSuccessCount, trackCount);
}
}
private Boolean exportTrack(Context context, Track track) {
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track});
//TODO Move to helper function
String fileName = track.getId() + "." + trackFileFormat.getExtension();
// Overwrite a file if it exists; DocumentFile.createFile() creates a new file appending a suffix if the displayname already exists.
DocumentFile file = directory.findFile(fileName);
if (file == null) {
file = directory.createFile(trackFileFormat.getMimeType(), fileName);
}
try (OutputStream outputStream = context.getContentResolver().openOutputStream(file.getUri())) {
if (trackExporter.writeTrack(context, outputStream)) {
return true;
} else {
if (!file.delete()) {
Log.e(TAG, "Unable to delete file");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
return false;
}
}
}
public interface DismissCallback {
void onExportCanceled(String directoryDisplayName, int successCount, int fileCount);
void onExportFinished(String directoryDisplayName, int successCount, int fileCount);
}
}
@@ -0,0 +1,111 @@
/*
* 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.fragments;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
public class ImportFinishedDialogFragment extends DialogFragment {
private static final String TAG = ImportFinishedDialogFragment.class.getSimpleName();
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
private static final String EXTRA_IMPORTED_TRACK_COUNT = "imported_track_count";
private static final String EXTRA_FILE_COUNT = "file_count";
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
private DismissCallback caller;
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
Bundle bundle = new Bundle();
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
bundle.putInt(EXTRA_IMPORTED_TRACK_COUNT, successCount);
bundle.putInt(EXTRA_FILE_COUNT, fileCount);
ImportFinishedDialogFragment dialogFragment = new ImportFinishedDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
int importedTrackCount = getArguments().getInt(EXTRA_IMPORTED_TRACK_COUNT);
int totalTrackCount = getArguments().getInt(EXTRA_FILE_COUNT);
int iconId;
int titleId;
String message;
String totalFiles = getResources().getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
if (importedTrackCount == totalTrackCount) {
if (totalTrackCount == 0) {
iconId = R.drawable.ic_dialog_info_24dp;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
Dialog dialog = new AlertDialog.Builder(getContext())
.setCancelable(true).setIcon(iconId)
.setMessage(message)
.setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
dialogInterface.dismiss();
caller.onDismissed();
})
.setTitle(titleId)
.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
public interface DismissCallback {
void onDismissed();
}
}
@@ -0,0 +1,226 @@
/*
* 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.fragments;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.importer.GpxFileTrackImporter;
import de.dennisguse.opentracks.io.file.importer.KmlFileTrackImporter;
import de.dennisguse.opentracks.io.file.importer.KmzTrackImporter;
import de.dennisguse.opentracks.io.file.importer.TrackImporter;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ImportProgressDialogFragment extends DialogFragment {
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
public static final String IMPORT_DIALOG_TAG = "import_dialog_tag";
private DismissCallback caller;
private ImportThread importThread;
private String directoryDisplayName;
private int trackImportSuccessCount;
private int fileCount;
/**
* Create a new instance.
*/
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri) {
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.setRetainInstance(true);
dialogFragment.show(fragmentManager, IMPORT_DIALOG_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
setRetainInstance(true);
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
directoryDisplayName = FileUtils.getPath(documentFile);
importThread = new ImportThread(documentFile);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(
getContext(), R.string.import_progress_message,
dialogInterface -> {
importThread.interrupt();
dialogInterface.dismiss();
caller.onImportCanceled(directoryDisplayName, trackImportSuccessCount, fileCount);
}, directoryDisplayName);
progressDialog.setIndeterminate(false);
progressDialog.setMax(fileCount);
progressDialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
return progressDialog;
}
@Override
public void onStart() {
super.onStart();
setProgress();
if (!importThread.isAlive()) {
importThread.start();
}
}
@Override
public void onPause() {
super.onPause();
if (!importThread.isAlive()) {
importThread.interrupt();
}
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
private void onProgress(int trackImportSuccessCount, int fileCount) {
this.trackImportSuccessCount = trackImportSuccessCount;
this.fileCount = fileCount;
setProgress();
}
private void setProgress() {
ProgressDialog dialog = (ProgressDialog) getDialog();
if (dialog != null) {
dialog.setIndeterminate(false);
dialog.setMax(fileCount);
dialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
}
}
private void onImportCompleted(int successCount, int fileCount) {
if (caller != null && !isDetached()) {
caller.onImportFinished(directoryDisplayName, successCount, fileCount);
}
dismissAllowingStateLoss();
}
private class ImportThread extends Thread {
private DocumentFile file;
ImportThread(DocumentFile file) {
this.file = file;
}
@Override
public void run() {
Context context = ImportProgressDialogFragment.this.getContext();
List<DocumentFile> files = FileUtils.getFiles(file);
int importedTrackCount = 0;
for (int i = 0; i < files.size(); i++) {
onProgress(i, files.size());
if (Thread.interrupted()) {
return;
}
if (importFile(context, files.get(i))) {
importedTrackCount++;
}
}
onImportCompleted(importedTrackCount, files.size());
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final Context context, final DocumentFile file) {
TrackImporter trackImporter;
String fileExtension = FileUtils.getExtension(file);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(context);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext(), -1L);
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(context, -1L, file.getUri());
} else {
Log.i(TAG, "Unsupported file format.");
return false;
}
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
return trackImporter.importFile(inputStream) != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
}
public interface DismissCallback {
void onImportCanceled(String directoryDisplayName, int successCount, int fileCount);
void onImportFinished(String directoryDisplayName, int successCount, int fileCount);
}
}
@@ -16,160 +16,60 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ExportFinishedDialogFragment;
import de.dennisguse.opentracks.fragments.ExportProgressDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An activity for saving tracks to the external storage.
*
* @author Rodrigo Damazio
*/
public class ExportActivity extends FragmentActivity {
public class ExportActivity extends FragmentActivity implements ExportProgressDialogFragment.DismissCallback, ExportFinishedDialogFragment.DismissCallback {
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
private String directoryDisplayName;
private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog;
private int processedTrackCount;
private int totalTrackCount;
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
Uri directoryUri = resultData.getData();
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
DocumentFile exportDirectory = pickedDirectory.findFile(FileUtils.EXPORT_DIR);
if (exportDirectory == null) {
exportDirectory = pickedDirectory.createDirectory(FileUtils.EXPORT_DIR);
}
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(this);
directoryDisplayName = FileUtils.getPath(exportDirectory);
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, exportDirectory);
exportAsyncTask.execute();
} else {
finish();
}
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
TrackFileFormat trackfileformat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
ExportProgressDialogFragment.showDialog(getSupportFragmentManager(), trackfileformat, directoryUri);
}
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.export_progress_message,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
exportAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalTrackCount, totalTrackCount);
if (processedTrackCount == totalTrackCount) {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_success, totalTracks, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.export_error, processedTrackCount, totalTracks, directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
}).setTitle(titleId);
return builder.create();
default:
return null;
}
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of tracks successfully saved
* @param aTotalCount the number of tracks to save
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) {
processedTrackCount = aSuccessCount;
totalTrackCount = aTotalCount;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
@Override
public void onExportCanceled(String directoryDisplayName, int successCount, int fileCount) {
onExportFinished(directoryDisplayName, successCount, fileCount);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
@Override
public void onExportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
runOnUiThread(() -> {
ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount);
});
}
/**
* Sets the progress dialog value.
*
* @param number the number of points saved
* @param max the maximum number of points
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
@Override
public void onDismissed() {
finish();
}
}
@@ -1,168 +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.io.file.exporter;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* Async Task to save tracks to the external storage.
*
* @author Jimmy Shih
*/
//TODO Make independent from ExportActivity?
class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final DocumentFile directory;
private final ContentProviderUtils contentProviderUtils;
private ExportActivity exportActivity;
private WakeLock wakeLock;
private int processedTrackCount = 0;
private int totalTrackCount = 0;
/**
* Creates an AsyncTask.
*
* @param exportActivity the activity currently associated with this task
* @param trackFileFormat the track file format
* @param directory the directory to write the file
*/
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.exportActivity = exportActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
contentProviderUtils = new ContentProviderUtils(exportActivity);
}
@Override
protected void onPreExecute() {
if (exportActivity != null) {
exportActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
return exportAllTracks();
} finally {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (exportActivity != null) {
exportActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
if (exportActivity != null) {
exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
}
}
@Override
protected void onCancelled() {
if (exportActivity != null) {
exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
}
}
/**
* Export the track to one file.
*
* @param track the track
*/
private Boolean exportTrack(Track track) {
TrackExporter trackExporter = trackFileFormat.newTrackExporter(exportActivity, new Track[]{track});
String fileName = track.getId() + "." + trackFileFormat.getExtension();
// Overwrite a file if it exists; DocumentFile.createFile() creates a new file appending a suffix if the displayname already exists.
DocumentFile file = directory.findFile(fileName);
if (file == null) {
file = directory.createFile(trackFileFormat.getMimeType(), fileName);
}
try (OutputStream outputStream = exportActivity.getContentResolver().openOutputStream(file.getUri())) {
if (trackExporter.writeTrack(exportActivity, outputStream)) {
return true;
} else {
if (!file.delete()) {
Log.d(TAG, "Unable to delete file");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
return false;
}
}
/**
* Saves all the tracks.
*/
private Boolean exportAllTracks() {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor == null) {
return false;
}
totalTrackCount = cursor.getCount();
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
Track track = contentProviderUtils.createTrack(cursor);
if (track != null && exportTrack(track)) {
processedTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
}
}
}
@@ -176,6 +176,7 @@ public class GpxTrackWriter implements TrackWriter {
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("<speed>" + trackPoint.getSpeed() + "</speed>");
printWriter.println("</trkpt>");
}
}
@@ -46,9 +46,10 @@ public class KmlTrackWriter implements TrackWriter {
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
public static final String SENSOR_TYPE_CADENCE = "cadence";
public static final String SENSOR_TYPE_HEART_RATE = "heart_rate";
public static final String SENSOR_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEART_RATE = "heart_rate";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
@@ -64,6 +65,7 @@ public class KmlTrackWriter implements TrackWriter {
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
private final List<Float> speedList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
@@ -123,10 +125,12 @@ public class KmlTrackWriter implements TrackWriter {
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms));
if (exportSensorData) {
writeSensorStyle(SENSOR_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(SENSOR_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(SENSOR_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
}
printWriter.println("</Schema>");
}
@@ -229,6 +233,7 @@ public class KmlTrackWriter implements TrackWriter {
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
speedList.clear();
powerList.clear();
cadenceList.clear();
heartRateList.clear();
@@ -240,15 +245,18 @@ public class KmlTrackWriter implements TrackWriter {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (speedList.size() > 0) {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
}
if (exportSensorData) {
if (powerList.size() > 0) {
writeSensorData(powerList, SENSOR_TYPE_POWER);
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
}
if (cadenceList.size() > 0) {
writeSensorData(cadenceList, SENSOR_TYPE_CADENCE);
writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
}
if (heartRateList.size() > 0) {
writeSensorData(heartRateList, SENSOR_TYPE_HEART_RATE);
writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
}
}
printWriter.println("</SchemaData>");
@@ -266,6 +274,10 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
if (trackPoint.hasSpeed()) {
speedList.add(trackPoint.getSpeed());
}
if (exportSensorData) {
if (trackPoint.hasHeartRate()) {
heartRateList.add(trackPoint.getHeartRate_bpm());
@@ -281,12 +293,12 @@ public class KmlTrackWriter implements TrackWriter {
}
/**
* Writes the sensor data.
* Writes the simple array data.
*
* @param list a list of sensor data
* @param name the name of the sensor data
* @param list a list of simple array data
* @param name the name of the simple array data
*/
private void writeSensorData(List<Float> list, String name) {
private void writeSimpleArrayData(List<Float> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
@@ -446,14 +458,14 @@ public class KmlTrackWriter implements TrackWriter {
}
/**
* Writes a sensor style.
* Writes a simple array style.
*
* @param name the name of the sesnor
* @param sensorType the sensor display name
* @param name the name of the simple array.
* @param extendedDataType the extended data display name
*/
private void writeSensorStyle(String name, String sensorType) {
private void writeSimpleArrayStyle(String name, String extendedDataType) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<displayName>" + StringUtils.formatCData(sensorType) + "</displayName>");
printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
}
@@ -31,6 +31,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
@@ -72,6 +73,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The current element content
//TODO Should be made private and getter be used by child classes.
protected String content;
protected String icon;
protected String name;
protected String description;
@@ -80,8 +82,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String longitude;
protected String altitude;
protected String time;
protected String speed;
protected String waypointType;
protected String photoUrl;
protected String uuid;
// The current track data
private TrackData trackData;
@@ -90,11 +94,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private Locator locator;
/**
* Constructor.
*
* @param context the context
* @param importTrackId the track id to import to. -1L to import to a new track.
*/
@Deprecated
// Remove importTrackId
AbstractFileTrackImporter(Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
this.context = context;
this.importTrackId = importTrackId;
@@ -243,6 +247,16 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (name != null) {
trackData.track.setName(name);
}
UUID uuidParsed;
try {
uuidParsed = UUID.fromString(uuid);
} catch (IllegalArgumentException | NullPointerException e) {
Log.w(TAG, "could not parse Track UUID, generating a new one.");
uuidParsed = UUID.randomUUID();
}
trackData.track.setUuid(uuidParsed);
if (description != null) {
trackData.track.setDescription(description);
}
@@ -319,20 +333,22 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime();
if (!trackPoint.hasSpeed()) {
long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime();
// Check for negative time change
if (timeDifference <= 0) {
Log.w(TAG, "Time difference not postive.");
} else {
// Check for negative time change
if (timeDifference <= 0) {
Log.w(TAG, "Time difference not positive.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the last two points.
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed);
/*
* We don't have a speed and bearing in GPX, make something up from the last two points.
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed);
}
}
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
}
@@ -389,9 +405,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
else {
return null;
}
return null;
}
/**
@@ -428,7 +443,20 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
}
return new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue);
TrackPoint trackPoint = new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue);
float speedValue;
if (speed != null) {
try {
speedValue = Float.valueOf(speed);
trackPoint.setSpeed(speedValue);
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
return trackPoint;
}
/**
@@ -0,0 +1,66 @@
package de.dennisguse.opentracks.io.file.importer;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
public abstract class DirectoryChooserActivity extends AppCompatActivity {
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
Uri directoryUri = resultData.getData();
startActivity(createIntent(directoryUri));
}
finish();
}
}
protected abstract Intent createIntent(Uri directoryUri);
public static class ImportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override
protected Intent createIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ImportActivity.class);
intent.putExtra(ImportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
return intent;
}
}
public static class ExportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override
protected Intent createIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this));
return intent;
}
}
}
@@ -39,6 +39,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_SPEED = "speed";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
@@ -115,6 +116,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
time = content.trim();
}
break;
case TAG_SPEED:
if (content != null) {
speed = content.trim();
}
break;
case TAG_ELEVATION:
if (content != null) {
altitude = content.trim();
@@ -149,6 +155,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
speed = null;
}
/**
@@ -16,159 +16,58 @@
package de.dennisguse.opentracks.io.file.importer;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.fragments.ImportFinishedDialogFragment;
import de.dennisguse.opentracks.fragments.ImportProgressDialogFragment;
/**
* An activity to import files from the external storage.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends FragmentActivity {
public class ImportActivity extends FragmentActivity implements ImportProgressDialogFragment.DismissCallback, ImportFinishedDialogFragment.DismissCallback {
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
private static final String TAG = ImportActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private String directoryDisplayName;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
private int importedTrackCount;
private int totalTrackCount;
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
Uri directoryUri = resultData.getData();
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
directoryDisplayName = FileUtils.getPath(pickedDirectory);
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(this);
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, pickedDirectory);
importAsyncTask.execute();
} else {
finish();
}
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), directoryUri);
}
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
if (importedTrackCount == totalTrackCount) {
if (totalTrackCount == 0) {
iconId = R.drawable.ic_dialog_info_24dp;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
dialogInterface.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
dialogInterface.dismiss();
finish();
}
}).setTitle(titleId).create();
default:
return null;
}
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of files successfully imported
* @param aTotalCount the number of files to import
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) {
importedTrackCount = aSuccessCount;
totalTrackCount = aTotalCount;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
@Override
public void onImportCanceled(String directoryDisplayName, int successCount, int fileCount) {
onImportFinished(directoryDisplayName, successCount, fileCount);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
@Override
public void onImportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
runOnUiThread(() -> {
ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount);
});
}
/**
* Sets the progress dialog value.
*
* @param number the number of files imported
* @param max the maximum number of files
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
@Override
public void onDismissed() {
finish();
}
}
@@ -1,179 +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.io.file.importer;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* AsyncTask to import files from the external storage.
*
* @author Jimmy Shih
*/
class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final DocumentFile directory;
// TODO Use weak reference
private ImportActivity importActivity;
private WakeLock wakeLock;
private int importTrackCount;
private int totalTrackCount;
private long lastSuccessfulTrackId;
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.importActivity = importActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
importTrackCount = 0;
totalTrackCount = 0;
lastSuccessfulTrackId = -1L;
}
@Override
protected void onPreExecute() {
if (importActivity != null) {
importActivity.showProgressDialog();
}
}
/**
* Gets a list of files.
*/
private static List<DocumentFile> getFiles(DocumentFile file, TrackFileFormat trackFileFormat) {
List<DocumentFile> files = new ArrayList<>();
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
if (extension != null && trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
} else {
files.addAll(getFiles(candidate, trackFileFormat));
}
}
return files;
}
@Override
protected void onProgressUpdate(Integer... values) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
}
}
@Override
protected void onCancelled() {
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<DocumentFile> files = getFiles(directory, trackFileFormat);
totalTrackCount = files.size();
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final DocumentFile file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(importActivity);
} else {
//KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
} else {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(importActivity);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(importActivity, newId, file.getUri());
}
}
try (InputStream inputStream = importActivity.getContentResolver().openInputStream(file.getUri())) {
lastSuccessfulTrackId = trackImporter.importFile(inputStream);
return lastSuccessfulTrackId != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
}
@@ -61,18 +61,18 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false;
private String sensorType;
private String extendedDataType;
private ArrayList<TrackPoint> trackPoints;
private ArrayList<Float> speedList;
private ArrayList<Float> cadenceList;
private ArrayList<Float> heartRateList;
private ArrayList<Float> powerList;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. -1L to import to a new track.
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
*/
@Deprecated //TODO Do not pass importTrackId here; get it from somewhere else
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, new ContentProviderUtils(context));
}
@@ -102,7 +102,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
onTrackSegmentStart();
break;
case TAG_GX_SIMPLE_ARRAY_DATA:
onSensorDataStart(attributes);
onExtendedDataStart(attributes);
break;
}
}
@@ -124,7 +124,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd();
onExtendedDataValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
@@ -209,6 +209,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
protected void onTrackSegmentStart() {
super.onTrackSegmentStart();
trackPoints = new ArrayList<>();
speedList = new ArrayList<>();
heartRateList = new ArrayList<>();
cadenceList = new ArrayList<>();
powerList = new ArrayList<>();
@@ -222,6 +223,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
}
@@ -229,7 +233,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
}
if (i < powerList.size()) {
trackPoint.setPower(cadenceList.get(i));
trackPoint.setPower(powerList.get(i));
}
insertTrackPoint(trackPoint);
@@ -261,16 +265,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
/**
* On sensor data start. gx:SimpleArrayData start tag.
* On extended data start. gx:SimpleArrayData start tag.
*/
private void onSensorDataStart(Attributes attributes) {
sensorType = attributes.getValue(ATTRIBUTE_NAME);
private void onExtendedDataStart(Attributes attributes) {
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
}
/**
* On sensor value end. gx:value end tag.
* On extended data value end. gx:value end tag.
*/
private void onSensorValueEnd() throws SAXException {
private void onExtendedDataValueEnd() throws SAXException {
if (content == null) {
return;
}
@@ -284,18 +288,21 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
switch (sensorType) {
case KmlTrackWriter.SENSOR_TYPE_POWER:
switch (extendedDataType) {
case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED:
speedList.add(value);
break;
case KmlTrackWriter.EXTENDED_DATA_TYPE_POWER:
powerList.add(value);
break;
case KmlTrackWriter.SENSOR_TYPE_HEART_RATE:
case KmlTrackWriter.EXTENDED_DATA_TYPE_HEART_RATE:
heartRateList.add(value);
break;
case KmlTrackWriter.SENSOR_TYPE_CADENCE:
case KmlTrackWriter.EXTENDED_DATA_TYPE_CADENCE:
cadenceList.add(value);
break;
default:
Log.w(TAG, "Data from sensor " + sensorType + " is not (yet) supported.");
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
}
}
}
@@ -56,13 +56,12 @@ public class KmzTrackImporter implements TrackImporter {
private Uri uriKmzFile;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
* @param uriFile URI of the kmz file.
*/
KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
@Deprecated //TODO Do not pass importTrackId here; get it from somewhere else
public KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
this.context = context;
this.importTrackId = importTrackId;
this.uriKmzFile = uriFile;
@@ -22,7 +22,7 @@ import java.io.InputStream;
*
* @author Jimmy Shih
*/
interface TrackImporter {
public interface TrackImporter {
/**
* Import a file.
@@ -30,5 +30,6 @@ interface TrackImporter {
* @param inputStream the file's input stream
* @return the imported track id or RECORDING_TRACK_ID_DEFAULT.
*/
//TODO Figure out how can make the import an atomic operation (incl. database transaction rollback).
long importFile(InputStream inputStream);
}
@@ -52,17 +52,6 @@ public class DialogUtils {
.setTitle(titleId).create();
}
/**
* Creates a spinner progress dialog.
*
* @param context the context
* @param messageId the progress message id
* @param onCancelListener the cancel listener
*/
public static ProgressDialog createSpinnerProgressDialog(Context context, int messageId, DialogInterface.OnCancelListener onCancelListener) {
return createProgressDialog(true, context, messageId, onCancelListener);
}
/**
* Creates a horizontal progress dialog.
*
@@ -72,27 +61,26 @@ public class DialogUtils {
* @param formatArgs the format arguments for the messageId
*/
public static ProgressDialog createHorizontalProgressDialog(Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
return createProgressDialog(false, context, messageId, onCancelListener, formatArgs);
return createProgressDialog(context, messageId, onCancelListener, formatArgs);
}
/**
* Creates a progress dialog.
*
* @param spinner true to use the spinner style
* @param context the context
* @param messageId the progress message id
* @param onCancelListener the cancel listener
* @param formatArgs the format arguments for the message id
*/
private static ProgressDialog createProgressDialog(boolean spinner, final Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
final ProgressDialog progressDialog = new ProgressDialog(context, R.style.ThemeCustomNotTransparent);
private static ProgressDialog createProgressDialog(final Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
final ProgressDialog progressDialog = new ProgressDialog(context, R.style.ThemeCustomNotTransparentDialog);
progressDialog.setCancelable(true);
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setIcon(R.drawable.ic_dialog_info_24dp);
progressDialog.setIndeterminate(true);
progressDialog.setMessage(context.getString(messageId, formatArgs));
progressDialog.setOnCancelListener(onCancelListener);
progressDialog.setProgressStyle(spinner ? ProgressDialog.STYLE_SPINNER : ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setTitle(R.string.generic_progress_title);
return progressDialog;
}
@@ -29,6 +29,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import de.dennisguse.opentracks.BuildConfig;
@@ -111,6 +112,10 @@ public class FileUtils {
return fileName.substring(index + 1);
}
public static String getExtension(DocumentFile file) {
return getExtension(file.getName());
}
/**
* Builds a filename with the given base and the given extension, possibly adding a suffix to ensure the file doesn't exist.
*
@@ -229,13 +234,13 @@ public class FileUtils {
return file.getAbsolutePath();
}
/**
/**
* Copy a File (src) to a File (dst).
*
* @param src source file.
* @param dst destination file.
*/
public static void copy(FileDescriptor src, File dst) {
*/
public static void copy(FileDescriptor src, File dst) {
try (FileChannel in = new FileInputStream(src).getChannel();
FileChannel out = new FileOutputStream(dst).getChannel()) {
in.transferTo(0, in.size(), out);
@@ -262,7 +267,7 @@ public class FileUtils {
* @param context the Context.
* @param trackId the id of the Track.
* @param uri the uri to check.
* @return File object or null.
* @return File object or null.
*/
public static File getPhotoFileIfExists(Context context, long trackId, Uri uri) {
if (uri == null) {
@@ -300,4 +305,20 @@ public class FileUtils {
file.delete();
}
}
public static ArrayList<DocumentFile> getFiles(DocumentFile file) {
ArrayList<DocumentFile> files = new ArrayList<>();
if (!file.isDirectory()) return files;
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
files.add(candidate);
} else {
files.addAll(getFiles(candidate));
}
}
return files;
}
}
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.util;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.util.UUID;
public class UUIDUtils {
public static UUID fromBytes(byte[] bytes) {
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
long mostSignificant = byteBuffer.getLong();
long lestSignificant = byteBuffer.getLong();
return new UUID(mostSignificant, lestSignificant);
}
public static byte[] toBytes(@NonNull UUID uuid) {
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
return byteBuffer.array();
}
}
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<include layout="@layout/track_controller" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_detail_activity_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="@color/holo_orange_dark" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/track_detail_activity_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
+28
View File
@@ -11,4 +11,32 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0" />
<RelativeLayout
android:id="@+id/aggregated_stats_empty_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
style="@style/TextLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="4"
android:gravity="center_horizontal|center_vertical"
android:text="@string/aggregated_stats_empty_message"
android:textColor="@color/list_empty_text" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</RelativeLayout>
</LinearLayout>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="100dp"
android:minWidth="100dp" />
</LinearLayout>
+8 -2
View File
@@ -36,12 +36,12 @@ limitations under the License.
Gebruik het kleine kaarticoontje.
a)
(live updates) Installeer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>.
Het biedt online of offline kaarten via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.\n
Dit biedt online of offline kaarten via <a href="https://www.openstreetmap.org">OpenStreetmap</a>.\n
Eigenlijk is OSMDashboard een aparte app die alleen (tijdelijk) toegang heeft tot de gegevens van track(s) die getoond gaan worden.
\n
b)
Via een normale kaarttoepassing (bijv. <a href="http://osmand.net/">OsmAnd</a> en <a href="https://maps.me/">MAPS.ME</a>).
Via een reguliere kaarttoepassing (zoals <a href="http://osmand.net/">OsmAnd</a> en <a href="https://maps.me/">MAPS.ME</a>).
Deze app moet het KMZ-bestandsformaat ondersteunen (alleen locaties en tijdstempels).
In feite moet een app <i>ACTION_VIEW</i> ondersteunen met MIME <i>application/vnd.google-earth.kmz</i>.</string>
@@ -88,6 +88,11 @@ e) koop in <a href="https://play.google.com/store/apps/details?id=de.dennisguse.
f) doneert u, en
g) doe wat je denkt dat OpenTracks helpt.</string>
<string name="open_track_using_dashboard">Open via OpenTracks-Dashboard</string>
<string name="open_track_as_KMZ">Open als KMZ</string>
<string name="open_track_as_GPX">Open als GPX</string>
<string name="open_track_as_trackfileformat">Open als %1$s</string>
<!-- Activity Type -->
<string name="activity_type_airplane">vliegtuig</string>
<string name="activity_type_atv">quad</string>
@@ -326,6 +331,7 @@ g) doe wat je denkt dat OpenTracks helpt.</string>
<string name="settings_show">Weergeven</string>
<!-- Settings Advanced -->
<string name="settings_advanced">Geavanceerd</string>
<string name="settings_default_trackfileformat">Bestandsformaat voor exporteren/delen</string>
<!-- Settings Chart -->
<string name="settings_chart_by_distance">Op afstand</string>
<string name="settings_chart_by_time">Op tijd</string>
+1 -1
View File
@@ -222,7 +222,7 @@
<string name="export_trackfileformat_key" translatable="false">exportTrackFileFormat</string>
<!-- See TrackFileFormat -->
<string name="export_trackfileformat_default" translatable="false">KMZ_WITH_TRACKDETAIL_AND_SENSORDATA</string>
<string name="export_trackfileformat_default" translatable="false">KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES</string>
<string name="settings_recording_key" translatable="false">settingsRecordingKey</string>
</resources>
+3
View File
@@ -193,6 +193,7 @@ limitations under the License.
<string name="description_pace_imperial">Pace (min/mi)</string>
<string name="description_pace_metric">Pace (min/km)</string>
<string name="description_recorded_time">Recorded: %1$s</string>
<string name="description_speed_ms">Speed (m/s)</string>
<string name="description_sensor_cadence">Cadence (rpm)</string>
<string name="description_sensor_heart_rate">Heart rate (bpm)</string>
<string name="description_sensor_power">Power (W)</string>
@@ -430,6 +431,8 @@ limitations under the License.
<string name="track_edit_new_track_title">New Track</string>
<!-- Track List -->
<string name="track_list_empty_message">Start recording your next adventure here</string>
<!-- Aggregated Statistics -->
<string name="aggregated_stats_empty_message">Record your first track to see aggregated statistics</string>
<!-- Track Widget -->
<!-- Tracks -->
<string name="tracks_few">%1$d tracks</string>
+10 -3
View File
@@ -35,8 +35,8 @@ limitations under the License.
<item name="android:colorAccent">@color/holo_orange_dark</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="ThemeCustomNotTransparent" parent="ThemeCustom">
<style name="ThemeCustomNotTransparentDialog" parent="ThemeCustom">
<!-- TODO Remove when moving to settings to fragments. -->
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
@@ -44,7 +44,7 @@ limitations under the License.
<item name="android:backgroundDimEnabled">true</item>
</style>
<style name="ThemeCustomTransparent" parent="ThemeCustom">
<style name="ThemeCustomTransparentActivity" parent="ThemeCustom">
<!-- TODO Remove when moving to settings to fragments. -->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
@@ -53,4 +53,11 @@ limitations under the License.
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="TrackDeleteActivity" parent="ThemeCustom">
<!-- TODO Remove when moving to TrackDeleteActivity to fragments. -->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>
+2 -2
View File
@@ -135,12 +135,12 @@ limitations under the License.
<PreferenceCategory android:title="@string/generic_tracks">
<Preference android:title="@string/menu_import_all">
<intent
android:targetClass="de.dennisguse.opentracks.io.file.importer.ImportActivity"
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity"
android:targetPackage="@string/applicationId" />
</Preference>
<Preference android:title="@string/menu_export_all">
<intent
android:targetClass="de.dennisguse.opentracks.io.file.exporter.ExportActivity"
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity"
android:targetPackage="@string/applicationId" />
</Preference>
<ListPreference