forked from upstream-mirrors/OpenTracks
Configurable filename for export: used for all automatic exports after recording, exporting all, and sharing tracks.
Fixes #512.
This commit is contained in:
@@ -60,7 +60,6 @@ captures/
|
|||||||
.idea/encodings.xml
|
.idea/encodings.xml
|
||||||
.idea/jsLibraryMappings.xml
|
.idea/jsLibraryMappings.xml
|
||||||
.idea/libraries/
|
.idea/libraries/
|
||||||
.idea/misc.xml
|
|
||||||
.idea/modules.xml
|
.idea/modules.xml
|
||||||
.idea/scopes/scope_settings.xml
|
.idea/scopes/scope_settings.xml
|
||||||
.idea/shelf/
|
.idea/shelf/
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.TimezoneRule;
|
||||||
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class TrackFilenameGeneratorTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||||
|
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<String[]> data() {
|
||||||
|
return Arrays.asList(new String[][]{
|
||||||
|
{"{uuid}_{name}", "0000fee0_Best Track.gpx" },
|
||||||
|
{"{name}_{uuid}", "Best Track_0000fee0.gpx" },
|
||||||
|
{"{time}_{name}", "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||||
|
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TrackFilenameGenerator subject;
|
||||||
|
private final String expected;
|
||||||
|
|
||||||
|
public TrackFilenameGeneratorTest(String template, String expected) {
|
||||||
|
this.subject = new TrackFilenameGenerator(template);
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilenameTemplate() {
|
||||||
|
// given
|
||||||
|
Track track = new Track();
|
||||||
|
track.setName("Best Track");
|
||||||
|
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
|
||||||
|
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
|
||||||
|
|
||||||
|
// when
|
||||||
|
String filename = subject.format(track, TrackFileFormat.GPX);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expected, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
|
||||||
|
//TODO Merge with TrackFilenameGeneratorTest whenever Junit5 gets available.
|
||||||
|
//https://github.com/android/android-test/issues/224
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class TrackFilenameGeneratorTest2 {
|
||||||
|
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<String> data() {
|
||||||
|
return Arrays.asList(
|
||||||
|
"{name}_{starime}",
|
||||||
|
"{name",
|
||||||
|
"name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TrackFilenameGenerator subject;
|
||||||
|
|
||||||
|
public TrackFilenameGeneratorTest2(String template) {
|
||||||
|
this.subject = new TrackFilenameGenerator(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TrackFilenameGenerator.TemplateInvalidException.class)
|
||||||
|
public void testFilenameTemplate() {
|
||||||
|
// given
|
||||||
|
Track track = new Track();
|
||||||
|
track.setName("Best Track");
|
||||||
|
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
|
||||||
|
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
|
||||||
|
|
||||||
|
// when
|
||||||
|
String filename = subject.format(track, TrackFileFormat.GPX);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
|
||||||
|
public class TrackFilenameGenerator {
|
||||||
|
|
||||||
|
public static final String UUID_KEY = "{uuid}";
|
||||||
|
public static final String TRACKNAME_KEY = "{name}";
|
||||||
|
public static final String CATEGORY_KEY = "{category}";
|
||||||
|
public static final String STARTTIME_KEY = "{time}";
|
||||||
|
|
||||||
|
public static String getAllOptions() {
|
||||||
|
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_KEY)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String template;
|
||||||
|
|
||||||
|
public TrackFilenameGenerator(@NonNull String template) {
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String format(@NonNull Track track, @NonNull TrackFileFormat trackFileFormat) {
|
||||||
|
Map<String, String> values = new HashMap<>();
|
||||||
|
|
||||||
|
values.put(UUID_KEY, track.getUuid().toString().substring(0, 8));
|
||||||
|
values.put(TRACKNAME_KEY, track.getName());
|
||||||
|
values.put(CATEGORY_KEY, track.getCategory());
|
||||||
|
values.put(STARTTIME_KEY, track.getStartTime().toString());
|
||||||
|
|
||||||
|
return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(String template, Map<String, String> values) {
|
||||||
|
StringBuilder templateCompiler = new StringBuilder(template);
|
||||||
|
List<String> valueList = new ArrayList<>();
|
||||||
|
|
||||||
|
Matcher keyMatcher = Pattern
|
||||||
|
.compile("\\{(\\w+)\\}")
|
||||||
|
.matcher(template);
|
||||||
|
|
||||||
|
while (keyMatcher.find()) {
|
||||||
|
String key = keyMatcher.group();
|
||||||
|
|
||||||
|
if (!values.containsKey(key)) {
|
||||||
|
throw new TemplateInvalidException(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = templateCompiler.indexOf(key);
|
||||||
|
if (index != -1) {
|
||||||
|
templateCompiler.replace(index, index + key.length(), "%s");
|
||||||
|
valueList.add(values.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String templateCompiled = templateCompiler.toString();
|
||||||
|
if (templateCompiled.contains("{") || templateCompiled.contains("}")) {
|
||||||
|
throw new TemplateInvalidException(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.format(templateCompiled, valueList.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTemplate() {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
try {
|
||||||
|
getExample();
|
||||||
|
return !template.isEmpty();
|
||||||
|
} catch (TemplateInvalidException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExample() {
|
||||||
|
Track track = new Track();
|
||||||
|
track.setName("Berlin");
|
||||||
|
track.setUuid(UUID.fromString("fefefefefe-0000-1000-8000-00805f9b34fb"));
|
||||||
|
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
|
||||||
|
|
||||||
|
return format(track, TrackFileFormat.GPX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TemplateInvalidException extends RuntimeException {
|
||||||
|
public TemplateInvalidException(String invalidTemplate) {
|
||||||
|
super(invalidTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.preference.EditTextPreference;
|
||||||
import androidx.preference.ListPreference;
|
import androidx.preference.ListPreference;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
@@ -12,16 +13,20 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
|
|
||||||
public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
private static final String TAG = ImportExportSettingsFragment.class.getSimpleName();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
addPreferencesFromResource(R.xml.settings_import_export);
|
addPreferencesFromResource(R.xml.settings_import_export);
|
||||||
|
|
||||||
setExportTrackFileFormatOptions();
|
setExportTrackFileFormatOptions();
|
||||||
setExportDirectorySummary();
|
setExportDirectorySummary();
|
||||||
|
setFilenameTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -77,4 +82,19 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
|||||||
return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
|
return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setFilenameTemplate() {
|
||||||
|
EditTextPreference preference = findPreference(getString(R.string.export_filename_format_key));
|
||||||
|
preference.setOnBindEditTextListener(t -> {
|
||||||
|
t.setHint(getString(R.string.export_filename_format_default));
|
||||||
|
});
|
||||||
|
preference.setDialogMessage(TrackFilenameGenerator.getAllOptions());
|
||||||
|
|
||||||
|
preference.setOnPreferenceChangeListener((p, newValue) -> new TrackFilenameGenerator(newValue.toString()).isValid());
|
||||||
|
|
||||||
|
preference.setSummaryProvider(p ->
|
||||||
|
PreferencesUtils.getTrackFileformatGenerator()
|
||||||
|
.getExample()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import de.dennisguse.opentracks.data.models.Distance;
|
|||||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||||
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
|
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
|
||||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
@@ -573,6 +574,16 @@ public class PreferencesUtils {
|
|||||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TrackFilenameGenerator getTrackFileformatGenerator() {
|
||||||
|
String DEFAULT = getString(R.string.export_filename_format_default, null);
|
||||||
|
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
|
||||||
|
if (generator.isValid()) {
|
||||||
|
return generator;
|
||||||
|
} else {
|
||||||
|
return new TrackFilenameGenerator(DEFAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static TrackFileFormat getExportTrackFileFormat() {
|
public static TrackFileFormat getExportTrackFileFormat() {
|
||||||
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
|
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
|
||||||
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
|
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
|||||||
import de.dennisguse.opentracks.data.ShareContentProvider;
|
import de.dennisguse.opentracks.data.ShareContentProvider;
|
||||||
import de.dennisguse.opentracks.data.models.Marker;
|
import de.dennisguse.opentracks.data.models.Marker;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
|
||||||
|
|
||||||
public class ShareUtils {
|
public class ShareUtils {
|
||||||
|
|
||||||
@@ -54,8 +54,9 @@ public class ShareUtils {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String trackName = FileUtils.sanitizeFileName(track.getName());
|
TrackFileFormat format = PreferencesUtils.getExportTrackFileFormat();
|
||||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat());
|
String trackName = PreferencesUtils.getTrackFileformatGenerator().format(track, format);
|
||||||
|
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, format);
|
||||||
|
|
||||||
uris.add(uriAndMime.first);
|
uris.add(uriAndMime.first);
|
||||||
mime = uriAndMime.second;
|
mime = uriAndMime.second;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public class ExportUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
|
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
|
||||||
String exportFileName = getExportFileNameForTrack(track, trackFileFormat.getExtension());
|
String exportFileName = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
|
||||||
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
|
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
|
||||||
if (exportDocumentFileUri == null) {
|
if (exportDocumentFileUri == null) {
|
||||||
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
|
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
|
||||||
@@ -119,10 +119,6 @@ public class ExportUtils {
|
|||||||
return exportDocumentFileUri;
|
return exportDocumentFileUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getExportFileNameForTrack(Track track, String trackFileFormatExtension) {
|
|
||||||
return track.getUuid().toString().substring(0, 8) + "_" + FileUtils.sanitizeFileName(track.getName()) + "." + trackFileFormatExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
|
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
|
||||||
final ContentResolver resolver = context.getContentResolver();
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
|
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
<string name="post_workout_export_enabled_key" translatable="false">instantExportEnabled</string>
|
<string name="post_workout_export_enabled_key" translatable="false">instantExportEnabled</string>
|
||||||
<bool name="post_workout_export_enabled_default" translatable="false">false</bool>
|
<bool name="post_workout_export_enabled_default" translatable="false">false</bool>
|
||||||
|
|
||||||
|
<string name="export_filename_format_key" translatable="false">instantExportFilename</string>
|
||||||
|
<string name="export_filename_format_default" translatable="false">{time}_{name}</string>
|
||||||
|
|
||||||
<string name="settings_import" translatable="false">settingsImport</string>
|
<string name="settings_import" translatable="false">settingsImport</string>
|
||||||
<string name="settings_export" translatable="false">settingsExport</string>
|
<string name="settings_export" translatable="false">settingsExport</string>
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ limitations under the License.
|
|||||||
<!-- Settings Advanced -->
|
<!-- Settings Advanced -->
|
||||||
<string name="settings_advanced">Advanced</string>
|
<string name="settings_advanced">Advanced</string>
|
||||||
<string name="settings_default_trackfileformat">Export/sharing file format</string>
|
<string name="settings_default_trackfileformat">Export/sharing file format</string>
|
||||||
|
<string name="settings_export_filename_title">Format of the filename</string>
|
||||||
<!-- Settings Chart -->
|
<!-- Settings Chart -->
|
||||||
<string name="settings_chart_by_distance">By distance</string>
|
<string name="settings_chart_by_distance">By distance</string>
|
||||||
<string name="settings_chart_by_time">By time</string>
|
<string name="settings_chart_by_time">By time</string>
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
android:key="@string/export_trackfileformat_key"
|
android:key="@string/export_trackfileformat_key"
|
||||||
android:title="@string/settings_default_trackfileformat"
|
android:title="@string/settings_default_trackfileformat"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="@string/export_filename_format_key"
|
||||||
|
android:title="@string/settings_export_filename_title"
|
||||||
|
android:defaultValue="@string/export_filename_format_default" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
Reference in New Issue
Block a user