actually use index added/lastUpdated dates in UTC

The date/time written to index.xml and index-v1.json should always be in
UTC format.  These formats are often in the form of just a date, e.g.
2019-04-28.  Those are then converted to UNIX seconds, which includes the
time.  In the date only case, the time is assumed to be 00:00, which will
be different per time zone.

index-v1.json is better since it mostly uses Java-style UNIX time in millis
but the dates/times are parsed then stored in the local database in the old
format yyyy-MM-dd_HH:mm:ss which will result in different UNIX times when
the device is in different time zones.

fdroid/fdroidclient#1757
This commit is contained in:
Hans-Christoph Steiner 2019-05-10 12:00:20 +02:00
parent c0c5721f6a
commit 1d359f82ce
7 changed files with 106 additions and 15 deletions

View File

@ -79,6 +79,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -96,6 +97,8 @@ public final class Utils {
private static final SimpleDateFormat TIME_FORMAT = private static final SimpleDateFormat TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.ENGLISH); new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.ENGLISH);
private static final TimeZone UTC = TimeZone.getTimeZone("Etc/GMT");
private static final String[] FRIENDLY_SIZE_FORMAT = { private static final String[] FRIENDLY_SIZE_FORMAT = {
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB", "%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB",
}; };
@ -583,6 +586,7 @@ public final class Utils {
} }
Date result; Date result;
try { try {
format.setTimeZone(UTC);
result = format.parse(str); result = format.parse(str);
} catch (ArrayIndexOutOfBoundsException | NumberFormatException | ParseException e) { } catch (ArrayIndexOutOfBoundsException | NumberFormatException | ParseException e) {
e.printStackTrace(); e.printStackTrace();
@ -595,21 +599,34 @@ public final class Utils {
if (date == null) { if (date == null) {
return fallback; return fallback;
} }
format.setTimeZone(UTC);
return format.format(date); return format.format(date);
} }
/**
* Parses a date string into UTC time
*/
public static Date parseDate(String str, Date fallback) { public static Date parseDate(String str, Date fallback) {
return parseDateFormat(DATE_FORMAT, str, fallback); return parseDateFormat(DATE_FORMAT, str, fallback);
} }
/**
* Formats UTC time into a date string
*/
public static String formatDate(Date date, String fallback) { public static String formatDate(Date date, String fallback) {
return formatDateFormat(DATE_FORMAT, date, fallback); return formatDateFormat(DATE_FORMAT, date, fallback);
} }
/**
* Parses a date/time string into UTC time
*/
public static Date parseTime(String str, Date fallback) { public static Date parseTime(String str, Date fallback) {
return parseDateFormat(TIME_FORMAT, str, fallback); return parseDateFormat(TIME_FORMAT, str, fallback);
} }
/**
* Formats UTC time into a date/time string
*/
public static String formatTime(Date date, String fallback) { public static String formatTime(Date date, String fallback) {
return formatDateFormat(TIME_FORMAT, date, fallback); return formatDateFormat(TIME_FORMAT, date, fallback);
} }

View File

@ -10,6 +10,8 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import java.io.File; import java.io.File;
import java.util.Date;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -77,9 +79,9 @@ public class UtilsTest {
assertEquals("three", tripleValue[2]); assertEquals("three", tripleValue[2]);
assertNull(Utils.serializeCommaSeparatedString(null)); assertNull(Utils.serializeCommaSeparatedString(null));
assertNull(Utils.serializeCommaSeparatedString(new String[] {})); assertNull(Utils.serializeCommaSeparatedString(new String[]{}));
assertEquals("Single", Utils.serializeCommaSeparatedString(new String[] {"Single"})); assertEquals("Single", Utils.serializeCommaSeparatedString(new String[]{"Single"}));
assertEquals("One,TWO,three", Utils.serializeCommaSeparatedString(new String[] {"One", "TWO", "three"})); assertEquals("One,TWO,three", Utils.serializeCommaSeparatedString(new String[]{"One", "TWO", "three"}));
} }
@Test @Test
@ -192,4 +194,25 @@ public class UtilsTest {
} }
// TODO write tests that work with a Certificate // TODO write tests that work with a Certificate
@Test
public void testIndexDatesWithTimeZones() {
for (int h = 0; h < 12; h++) {
for (int m = 0; m < 60; m = m + 15) {
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT+%d%02d", h, m)));
String timeString = "2017-11-27_20:13:24";
Date time = Utils.parseTime(timeString, null);
assertEquals("The String representation must match", timeString, Utils.formatTime(time, null));
assertEquals(timeString + " failed to parse", 1511813604000L, time.getTime());
assertEquals("time zones should match", -((h * 60) + m), time.getTimezoneOffset());
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT+%d%02d", h, m)));
String dateString = "2017-11-27";
Date date = Utils.parseDate(dateString, null);
assertEquals("The String representation must match", dateString, Utils.formatDate(date, null));
assertEquals(dateString + " failed to parse", 1511740800000L, date.getTime());
assertEquals("time zones should match", -((h * 60) + m), date.getTimezoneOffset());
}
}
}
} }

View File

@ -7,10 +7,12 @@ import android.net.Uri;
import org.fdroid.fdroid.Assert; import org.fdroid.fdroid.Assert;
import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import org.fdroid.fdroid.data.Schema.RepoTable; import org.fdroid.fdroid.data.Schema.RepoTable;
import org.fdroid.fdroid.mock.MockApk; import org.fdroid.fdroid.mock.MockApk;
import org.fdroid.fdroid.mock.MockRepo; import org.fdroid.fdroid.mock.MockRepo;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -18,6 +20,7 @@ import org.robolectric.annotation.Config;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.TimeZone;
import static org.fdroid.fdroid.Assert.assertCantDelete; import static org.fdroid.fdroid.Assert.assertCantDelete;
import static org.fdroid.fdroid.Assert.assertResultCount; import static org.fdroid.fdroid.Assert.assertResultCount;
@ -34,6 +37,13 @@ public class ApkProviderTest extends FDroidProviderTest {
private static final String[] PROJ = Cols.ALL; private static final String[] PROJ = Cols.ALL;
@BeforeClass
public static void setRandomTimeZone() {
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT-%d:%02d",
System.currentTimeMillis() % 12, System.currentTimeMillis() % 60)));
System.out.println("TIME ZONE for this test: " + TimeZone.getDefault());
}
@Test @Test
public void testAppApks() { public void testAppApks() {
App fdroidApp = insertApp(context, "org.fdroid.fdroid", "F-Droid"); App fdroidApp = insertApp(context, "org.fdroid.fdroid", "F-Droid");
@ -153,7 +163,7 @@ public class ApkProviderTest extends FDroidProviderTest {
@Test @Test
public void testCount() { public void testCount() {
String[] projectionCount = new String[] {Cols._COUNT}; String[] projectionCount = new String[]{Cols._COUNT};
for (int i = 0; i < 13; i++) { for (int i = 0; i < 13; i++) {
Assert.insertApk(context, "com.example", i); Assert.insertApk(context, "com.example", i);
@ -315,12 +325,13 @@ public class ApkProviderTest extends FDroidProviderTest {
assertNull(apk.added); assertNull(apk.added);
assertNull(apk.hashType); assertNull(apk.hashType);
apk.antiFeatures = new String[] {"KnownVuln", "Other anti feature"}; apk.antiFeatures = new String[]{"KnownVuln", "Other anti feature"};
apk.features = new String[] {"one", "two", "three" }; apk.features = new String[]{"one", "two", "three"};
long dateTimestamp = System.currentTimeMillis();
apk.added = new Date(dateTimestamp);
apk.hashType = "i'm a hash type"; apk.hashType = "i'm a hash type";
Date testTime = Utils.parseDate(Utils.formatTime(new Date(System.currentTimeMillis()), null), null);
apk.added = testTime;
ApkProvider.Helper.update(context, apk); ApkProvider.Helper.update(context, apk);
// Should not have inserted anything else, just updated the already existing apk. // Should not have inserted anything else, just updated the already existing apk.
@ -340,9 +351,10 @@ public class ApkProviderTest extends FDroidProviderTest {
assertArrayEquals(new String[]{"KnownVuln", "Other anti feature"}, updatedApk.antiFeatures); assertArrayEquals(new String[]{"KnownVuln", "Other anti feature"}, updatedApk.antiFeatures);
assertArrayEquals(new String[]{"one", "two", "three"}, updatedApk.features); assertArrayEquals(new String[]{"one", "two", "three"}, updatedApk.features);
assertEquals(new Date(dateTimestamp).getYear(), updatedApk.added.getYear()); assertEquals(testTime.getYear(), updatedApk.added.getYear());
assertEquals(new Date(dateTimestamp).getMonth(), updatedApk.added.getMonth()); assertEquals(testTime.getYear(), updatedApk.added.getYear());
assertEquals(new Date(dateTimestamp).getDay(), updatedApk.added.getDay()); assertEquals(testTime.getMonth(), updatedApk.added.getMonth());
assertEquals(testTime.getDay(), updatedApk.added.getDay());
assertEquals("i'm a hash type", updatedApk.hashType); assertEquals("i'm a hash type", updatedApk.hashType);
} }

View File

@ -11,6 +11,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -19,6 +20,7 @@ import org.robolectric.shadows.ShadowContentResolver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.TimeZone;
import static org.fdroid.fdroid.Assert.assertContainsOnly; import static org.fdroid.fdroid.Assert.assertContainsOnly;
import static org.fdroid.fdroid.Assert.assertResultCount; import static org.fdroid.fdroid.Assert.assertResultCount;
@ -36,6 +38,13 @@ public class AppProviderTest extends FDroidProviderTest {
private static final String[] PROJ = Cols.ALL; private static final String[] PROJ = Cols.ALL;
@BeforeClass
public static void setRandomTimeZone() {
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT-%d:%02d",
System.currentTimeMillis() % 12, System.currentTimeMillis() % 60)));
System.out.println("TIME ZONE for this test: " + TimeZone.getDefault());
}
@Before @Before
public void setup() { public void setup() {
TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class);

View File

@ -30,6 +30,7 @@ import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.RepoTable; import org.fdroid.fdroid.data.Schema.RepoTable;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -37,6 +38,7 @@ import org.robolectric.annotation.Config;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -48,6 +50,16 @@ public class RepoProviderTest extends FDroidProviderTest {
private static final String[] COLS = RepoTable.Cols.ALL; private static final String[] COLS = RepoTable.Cols.ALL;
/**
* Set to random time zone to make sure that the dates are properly parsed.
*/
@BeforeClass
public static void setRandomTimeZone() {
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT-%d:%02d",
System.currentTimeMillis() % 12, System.currentTimeMillis() % 60)));
System.out.println("TIME ZONE for this test: " + TimeZone.getDefault());
}
@Test @Test
public void countEnabledRepos() { public void countEnabledRepos() {

View File

@ -29,6 +29,7 @@ import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.mock.MockRepo; import org.fdroid.fdroid.mock.MockRepo;
import org.fdroid.fdroid.mock.RepoDetails; import org.fdroid.fdroid.mock.RepoDetails;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -48,6 +49,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -63,6 +65,16 @@ public class RepoXMLHandlerTest {
private static final String FAKE_SIGNING_CERT = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"; // NOCHECKSTYLE LineLength private static final String FAKE_SIGNING_CERT = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"; // NOCHECKSTYLE LineLength
/**
* Set to random time zone to make sure that the dates are properly parsed.
*/
@BeforeClass
public static void setRandomTimeZone() {
TimeZone.setDefault(TimeZone.getTimeZone(String.format("GMT-%d:%02d",
System.currentTimeMillis() % 12, System.currentTimeMillis() % 60)));
System.out.println("TIME ZONE for this test: " + TimeZone.getDefault());
}
@Test @Test
public void testExtendedPerms() throws IOException { public void testExtendedPerms() throws IOException {
Repo expectedRepo = new Repo(); Repo expectedRepo = new Repo();
@ -129,6 +141,12 @@ public class RepoXMLHandlerTest {
"org.gege.caldavsyncadapter", "org.gege.caldavsyncadapter",
"info.guardianproject.checkey", "info.guardianproject.checkey",
}); });
for (App app : actualDetails.apps) {
if ("org.mozilla.firefox".equals(app.packageName)) {
assertEquals(1411776000000L, app.added.getTime());
assertEquals(1411862400000L, app.lastUpdated.getTime());
}
}
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -897,7 +915,7 @@ public class RepoXMLHandlerTest {
List<App> apps = actualDetails.apps; List<App> apps = actualDetails.apps;
assertNotNull(apps); assertNotNull(apps);
assertEquals(apps.size(), appCount); assertEquals(apps.size(), appCount);
for (App app: apps) { for (App app : apps) {
assertTrue("Added should have been set", app.added.getTime() > 0); assertTrue("Added should have been set", app.added.getTime() > 0);
assertTrue("Last Updated should have been set", app.lastUpdated.getTime() > 0); assertTrue("Last Updated should have been set", app.lastUpdated.getTime() > 0);
} }

File diff suppressed because one or more lines are too long