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.