Configurable filename for export: used for all automatic exports after recording, exporting all, and sharing tracks.

Fixes #512.
This commit is contained in:
Dennis Guse
2022-05-03 20:06:29 +02:00
committed by GitHub
parent 8a2d1c42a8
commit 0d3cf4ce0d
11 changed files with 253 additions and 9 deletions
-1
View File
@@ -60,7 +60,6 @@ captures/
.idea/encodings.xml
.idea/jsLibraryMappings.xml
.idea/libraries/
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.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);
}
}
@@ -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 androidx.documentfile.provider.DocumentFile;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@@ -12,16 +13,20 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.util.IntentUtils;
public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = ImportExportSettingsFragment.class.getSimpleName();
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_import_export);
setExportTrackFileFormatOptions();
setExportDirectorySummary();
setFilenameTemplate();
}
@Override
@@ -77,4 +82,19 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
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.Speed;
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.Layout;
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();
}
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() {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
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.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ShareUtils {
@@ -54,8 +54,9 @@ public class ShareUtils {
continue;
}
String trackName = FileUtils.sanitizeFileName(track.getName());
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat());
TrackFileFormat format = PreferencesUtils.getExportTrackFileFormat();
String trackName = PreferencesUtils.getTrackFileformatGenerator().format(track, format);
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, format);
uris.add(uriAndMime.first);
mime = uriAndMime.second;
@@ -108,7 +108,7 @@ public class ExportUtils {
}
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);
if (exportDocumentFileUri == null) {
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
@@ -119,10 +119,6 @@ public class ExportUtils {
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) {
final ContentResolver resolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
+3
View File
@@ -48,6 +48,9 @@
<string name="post_workout_export_enabled_key" translatable="false">instantExportEnabled</string>
<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_export" translatable="false">settingsExport</string>
+1
View File
@@ -371,6 +371,7 @@ limitations under the License.
<!-- Settings Advanced -->
<string name="settings_advanced">Advanced</string>
<string name="settings_default_trackfileformat">Export/sharing file format</string>
<string name="settings_export_filename_title">Format of the filename</string>
<!-- Settings Chart -->
<string name="settings_chart_by_distance">By distance</string>
<string name="settings_chart_by_time">By time</string>
@@ -43,6 +43,10 @@
android:key="@string/export_trackfileformat_key"
android:title="@string/settings_default_trackfileformat"
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>
</PreferenceScreen>