Proper handling of timezone data on UI. Fixes #301.

Details:
- Date and time handled by OffsetDateTime Java class.
- Track default name with offset (for date local and ISO 8601 options).
- TrackListActivity: shows relative today's date (like Today, Yesterday, 22 Dec, 22 Dec 2021, etc).
- StatisticsRecordedFragment: always shows the original date and time. For example, if you record an activity at 20h then it always will show 20h regardless the zone are where you are.
- Added new tests.
This commit is contained in:
Román Ginés
2021-12-29 18:33:35 +01:00
committed by Román Martínez
parent 1631836327
commit 5cd6992c90
14 changed files with 196 additions and 97 deletions
@@ -27,6 +27,8 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
@@ -91,7 +93,7 @@ public class DescriptionGeneratorTest {
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -127,7 +129,7 @@ public class DescriptionGeneratorTest {
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -27,9 +29,23 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.TextStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
@@ -184,4 +200,82 @@ public class StringUtilsTest {
assertEquals("12.0 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
assertEquals("7.5 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
}
@Test
public void testFormatDateTime() {
LocalDateTime localDateTime = LocalDateTime.parse("2022-01-02T10:15:30");
setLocale("en", "US");
testFormatDateTime(localDateTime, localDateTime.getMonth().getDisplayName(TextStyle.SHORT, Locale.getDefault()) + " " + localDateTime.getDayOfMonth() + ", " + localDateTime.getYear() + " 10:15:30");
setLocale("es", "ES");
testFormatDateTime(localDateTime, localDateTime.getDayOfMonth() + " " + localDateTime.getMonth().getDisplayName(TextStyle.SHORT, Locale.getDefault()) + " " + localDateTime.getYear() + " 10:15:30");
setLocale("de", "DE");
testFormatDateTime(localDateTime, String.format("%02d", localDateTime.getDayOfMonth()) + "." + String.format("%02d", localDateTime.getMonthValue()) + "." + String.format("%04d", localDateTime.getYear()) + " 10:15:30");
}
private void testFormatDateTime(LocalDateTime localDateTime, String expectedFormat) {
// given
int offsetFromLocale = OffsetDateTime.now().getOffset().getTotalSeconds() - 28800; // -08:00
OffsetDateTime localOffsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.systemDefault().getRules().getOffset(Instant.now()));
OffsetDateTime outOffsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.ofTotalSeconds(offsetFromLocale));
// when
String formatDate = StringUtils.formatDateTime(outOffsetDateTime);
// then
assertEquals(localOffsetDateTime.getOffset().getTotalSeconds(), outOffsetDateTime.getOffset().getTotalSeconds() + 28800);
assertEquals(formatDate, expectedFormat);
}
@Test
public void testFormatDateTodayRelative() {
// given
ArrayList<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
LocalDate yesterday = today.minus(1, ChronoUnit.DAYS);
LocalDate dayName = today.minus(2, ChronoUnit.DAYS);
LocalDate thisYear = today.minus(15, ChronoUnit.DAYS);
LocalDate aYearAgo = today.minus(400, ChronoUnit.DAYS);
int offsetFromLocale = OffsetDateTime.now().getOffset().getTotalSeconds() - 28800; // -08:00
OffsetDateTime todayRecordedOdt = OffsetDateTime.of(LocalDateTime.of(today.getYear(), today.getMonth(), today.getDayOfMonth(), 20, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime yesterdayRecordedOdt = OffsetDateTime.of(LocalDateTime.of(yesterday.getYear(), yesterday.getMonth(), yesterday.getDayOfMonth(), 1, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime dayNameRecordedOdt = OffsetDateTime.of(LocalDateTime.of(dayName.getYear(), dayName.getMonth(), dayName.getDayOfMonth(), 7, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime thisYearRecordedOdt = OffsetDateTime.of(LocalDateTime.of(thisYear.getYear(), thisYear.getMonth(), thisYear.getDayOfMonth(), 12, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime aYearAgoRecordedOdt = OffsetDateTime.of(LocalDateTime.of(aYearAgo.getYear(), aYearAgo.getMonth(), aYearAgo.getDayOfMonth(), 17, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
// when
String formatToday = StringUtils.formatDateTodayRelative(context, todayRecordedOdt);
String formatYesterday = StringUtils.formatDateTodayRelative(context, yesterdayRecordedOdt);
String formatDayName = StringUtils.formatDateTodayRelative(context, dayNameRecordedOdt);
String formatThisYear = StringUtils.formatDateTodayRelative(context, thisYearRecordedOdt);
String formatAYearAgo = StringUtils.formatDateTodayRelative(context, aYearAgoRecordedOdt);
// then
assertEquals("Today", formatToday);
assertEquals("Yesterday", formatYesterday);
assertTrue(shortDays.contains(formatDayName)); // Something like Friday
if (today.getYear() != thisYear.getYear()) {
assertTrue(shortMonths.stream().anyMatch(fty -> formatThisYear.matches("\\d+ " + fty + " \\d{4}"))); // Something like 14 Dec 2021
} else {
assertTrue(shortMonths.stream().anyMatch(fty -> formatThisYear.matches("\\d+ " + fty))); // Something like 14 Dec
}
assertTrue(shortMonths.stream().anyMatch(fty -> formatAYearAgo.matches("\\d+ " + fty + " \\d{4}"))); // Something like 14 Dec 2021
}
private void setLocale(String language, String country) {
Locale locale = new Locale(language, country);
// here we update locale for date formatters
Locale.setDefault(locale);
// here we update locale for app resources
Resources res = context.getResources();
Configuration config = res.getConfiguration();
config.locale = locale;
res.updateConfiguration(config, res.getDisplayMetrics());
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
@@ -24,16 +26,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Locale;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link TrackNameUtils}.
*
@@ -43,7 +42,7 @@ import static org.junit.Assert.assertEquals;
public class TrackNameUtilsTest {
private static final Track.Id TRACK_ID = new Track.Id(1L);
private static final Instant START_TIME = Instant.ofEpochMilli(1288213406000L);
private static final OffsetDateTime START_TIME = OffsetDateTime.parse("2022-01-02T10:15:30+01:00");
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
@@ -53,7 +52,7 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_date_local() {
PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals(StringUtils.formatDateTimeWithOffset(START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -62,8 +61,7 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals(StringUtils.formatDateTimeIso8601(START_TIME.toInstant(), START_TIME.getOffset()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**