diff --git a/doc/activities.txt b/doc/activities.txt
index da8c59beb..47b00f8c1 100644
--- a/doc/activities.txt
+++ b/doc/activities.txt
@@ -47,6 +47,7 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ExportActivity
-SettingsActivity --> ImportActivity
+SettingsActivity --> DirectoryChooserActivity
+DirectoryChooserActivity --> ImportActivity
@enduml
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index f8b5ee154..35321db8f 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -44,6 +44,8 @@ limitations under the License.
android:name=".HelpActivity"
android:label="@string/menu_help" />
+
+
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
index e07905d3c..46f82ee34 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
@@ -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;
@@ -83,6 +85,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String speed;
protected String waypointType;
protected String photoUrl;
+ protected String uuid;
// The current track data
private TrackData trackData;
@@ -91,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;
@@ -244,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);
}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java
new file mode 100644
index 000000000..20261a8fc
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java
@@ -0,0 +1,41 @@
+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.util.IntentUtils;
+
+public 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();
+
+ Intent intent = IntentUtils.newIntent(this, ImportActivity.class);
+ intent.putExtra(ImportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
+ startActivity(intent);
+ }
+ finish();
+ }
+ }
+}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportActivity.java
index b09a20e88..f3bb09834 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportActivity.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportActivity.java
@@ -16,24 +16,24 @@
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 android.util.Log;
-import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
+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.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
-import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An activity to import files from the external storage.
@@ -42,14 +42,18 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/
public class ImportActivity extends FragmentActivity {
- private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
+ private static final String TAG = ImportActivity.class.getSimpleName();
+
+ 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;
+ private DocumentFile pickedDirectory;
private String directoryDisplayName;
- private ImportAsyncTask importAsyncTask;
+
+ private Thread importTask = new ImportThread();
private ProgressDialog progressDialog;
private int importedTrackCount;
@@ -59,29 +63,22 @@ public class ImportActivity extends FragmentActivity {
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);
+ Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
+ pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
+ directoryDisplayName = FileUtils.getPath(pickedDirectory);
}
@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);
+ protected void onStart() {
+ super.onStart();
+ showDialog(DIALOG_PROGRESS_ID);
+ importTask.start();
+ }
- directoryDisplayName = FileUtils.getPath(pickedDirectory);
- TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(this);
-
- importAsyncTask = new ImportAsyncTask(this, trackFileFormat, pickedDirectory);
- importAsyncTask.execute();
- } else {
- finish();
- }
- }
+ @Override
+ protected void onPause() {
+ super.onPause();
+ importTask.interrupt();
}
@Override
@@ -89,13 +86,10 @@ public class ImportActivity extends FragmentActivity {
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();
- }
+ this, R.string.import_progress_message, dialog -> {
+ importTask.interrupt();
+ dialog.dismiss();
+ finish();
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
@@ -120,18 +114,12 @@ public class ImportActivity extends FragmentActivity {
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();
- }
+ .setMessage(message).setOnCancelListener(dialogInterface -> {
+ dialogInterface.dismiss();
+ finish();
+ }).setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
+ dialogInterface.dismiss();
+ finish();
}).setTitle(titleId).create();
default:
return null;
@@ -145,17 +133,12 @@ public class ImportActivity extends FragmentActivity {
* @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);
- }
-
- /**
- * Shows the progress dialog.
- */
- public void showProgressDialog() {
- showDialog(DIALOG_PROGRESS_ID);
+ runOnUiThread(() -> {
+ importedTrackCount = aSuccessCount;
+ totalTrackCount = aTotalCount;
+ removeDialog(DIALOG_PROGRESS_ID);
+ showDialog(DIALOG_RESULT_ID);
+ });
}
/**
@@ -171,4 +154,54 @@ public class ImportActivity extends FragmentActivity {
progressDialog.setProgress(Math.min(number, max));
}
}
+
+ public class ImportThread extends Thread {
+
+ @Override
+ public void run() {
+ List files = FileUtils.getFiles(pickedDirectory);
+ int totalTrackCount = files.size();
+ int importedTrackCount = 0;
+
+ for (int i = 0; i < totalTrackCount; i++) {
+ if (Thread.interrupted()) {
+ return;
+ }
+ if (importFile(files.get(i))) {
+ importedTrackCount++;
+ }
+ setProgressDialogValue(i + 1, totalTrackCount);
+ }
+
+ onAsyncTaskCompleted(importedTrackCount, totalTrackCount);
+ }
+
+ /**
+ * Imports a file.
+ *
+ * @param file the file
+ */
+ private boolean importFile(final DocumentFile file) {
+ TrackImporter trackImporter;
+ String fileExtension = FileUtils.getExtension(file);
+
+ if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
+ trackImporter = new GpxFileTrackImporter(ImportActivity.this);
+ } else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
+ trackImporter = new KmlFileTrackImporter(ImportActivity.this, -1L);
+ } else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
+ trackImporter = new KmzTrackImporter(ImportActivity.this, -1L, file.getUri());
+ } else {
+ Log.i(TAG, "Unsupported file format.");
+ return false;
+ }
+
+ try (InputStream inputStream = ImportActivity.this.getContentResolver().openInputStream(file.getUri())) {
+ return trackImporter.importFile(inputStream) != -1L;
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to import file", e);
+ return false;
+ }
+ }
+ }
}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java
deleted file mode 100644
index 6c2837e4d..000000000
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java
+++ /dev/null
@@ -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 {
-
- 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 getFiles(DocumentFile file, TrackFileFormat trackFileFormat) {
- List 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 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;
- }
- }
-}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java
index 13e9b68ad..bcc557c07 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java
@@ -72,7 +72,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* Constructor.
*
* @param context the context
- * @param importTrackId track id to import to. -1L to import to a new track.
*/
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, new ContentProviderUtils(context));
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
index ac166dd49..dfae0a27f 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
@@ -56,8 +56,6 @@ 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.
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java
index 0b75dcd5c..2d51d154f 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java
@@ -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);
}
diff --git a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
index ea50a8648..6936adca1 100644
--- a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
@@ -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,18 @@ public class FileUtils {
file.delete();
}
}
+
+ public static ArrayList getFiles(DocumentFile file) {
+ ArrayList files = new ArrayList<>();
+
+ for (DocumentFile candidate : file.listFiles()) {
+ if (!candidate.isDirectory()) {
+ files.add(candidate);
+ } else {
+ files.addAll(getFiles(candidate));
+ }
+ }
+
+ return files;
+ }
}
diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml
index 051073bab..18d472198 100644
--- a/src/main/res/xml/settings.xml
+++ b/src/main/res/xml/settings.xml
@@ -135,7 +135,7 @@ limitations under the License.