Skip to content

Commit

Permalink
Configurable filename for export: used for all automatic exports aft…
Browse files Browse the repository at this point in the history
…er recording, exporting all, and sharing tracks.

Fixes #512.
  • Loading branch information
dennisguse committed May 3, 2022
1 parent 8a2d1c4 commit 0d3cf4c
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 9 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -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/
Expand Down
@@ -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);
}
}
}
Expand Up @@ -4,6 +4,7 @@
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;
Expand All @@ -12,16 +13,20 @@

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
Expand Down Expand Up @@ -77,4 +82,19 @@ private void setExportDirectorySummary() {
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()
);
}
}
Expand Up @@ -44,6 +44,7 @@
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;
Expand Down Expand Up @@ -573,6 +574,16 @@ public static boolean shouldInstantExportAfterWorkout() {
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);
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/de/dennisguse/opentracks/share/ShareUtils.java
Expand Up @@ -15,8 +15,8 @@
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 {

Expand Down Expand Up @@ -54,8 +54,9 @@ public static Intent newShareFileIntent(Context context, Track.Id... trackIds) {
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;
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/de/dennisguse/opentracks/util/ExportUtils.java
Expand Up @@ -108,7 +108,7 @@ public static List<String> getAllFiles(Context context, Uri directoryUri) {
}

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);
Expand All @@ -119,10 +119,6 @@ private static Uri getExportDocumentFileUri(Context context, Track track, TrackF
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));
Expand Down
3 changes: 3 additions & 0 deletions src/main/res/values/settings.xml
Expand Up @@ -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>

Expand Down
1 change: 1 addition & 0 deletions src/main/res/values/strings.xml
Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions src/main/res/xml/settings_import_export.xml
Expand Up @@ -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>

0 comments on commit 0d3cf4c

Please sign in to comment.