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.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@ -96,6 +97,8 @@ public final class Utils {
private static final SimpleDateFormat TIME_FORMAT =
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 = {
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB",
};
@ -583,6 +586,7 @@ public final class Utils {
}
Date result;
try {
format.setTimeZone(UTC);
result = format.parse(str);
} catch (ArrayIndexOutOfBoundsException | NumberFormatException | ParseException e) {
e.printStackTrace();
@ -595,21 +599,34 @@ public final class Utils {
if (date == null) {
return fallback;
}
format.setTimeZone(UTC);
return format.format(date);
}
/**
* Parses a date string into UTC time
*/
public static Date parseDate(String str, Date fallback) {
return parseDateFormat(DATE_FORMAT, str, fallback);
}
/**
* Formats UTC time into a date string
*/
public static String formatDate(Date date, String fallback) {
return formatDateFormat(DATE_FORMAT, date, fallback);
}
/**
* Parses a date/time string into UTC time
*/
public static Date parseTime(String str, Date fallback) {
return parseDateFormat(TIME_FORMAT, str, fallback);
}
/**
* Formats UTC time into a date/time string
*/
public static String formatTime(Date date, String fallback) {
return formatDateFormat(TIME_FORMAT, date, fallback);
}

View File

@ -10,6 +10,8 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.io.File;
import java.util.Date;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -77,9 +79,9 @@ public class UtilsTest {
assertEquals("three", tripleValue[2]);
assertNull(Utils.serializeCommaSeparatedString(null));
assertNull(Utils.serializeCommaSeparatedString(new String[] {}));
assertEquals("Single", Utils.serializeCommaSeparatedString(new String[] {"Single"}));
assertEquals("One,TWO,three", Utils.serializeCommaSeparatedString(new String[] {"One", "TWO", "three"}));
assertNull(Utils.serializeCommaSeparatedString(new String[]{}));
assertEquals("Single", Utils.serializeCommaSeparatedString(new String[]{"Single"}));
assertEquals("One,TWO,three", Utils.serializeCommaSeparatedString(new String[]{"One", "TWO", "three"}));
}
@Test
@ -192,4 +194,25 @@ public class UtilsTest {
}
// 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.BuildConfig;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import org.fdroid.fdroid.data.Schema.RepoTable;
import org.fdroid.fdroid.mock.MockApk;
import org.fdroid.fdroid.mock.MockRepo;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -18,6 +20,7 @@ import org.robolectric.annotation.Config;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import static org.fdroid.fdroid.Assert.assertCantDelete;
import static org.fdroid.fdroid.Assert.assertResultCount;
@ -34,6 +37,13 @@ public class ApkProviderTest extends FDroidProviderTest {
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
public void testAppApks() {
App fdroidApp = insertApp(context, "org.fdroid.fdroid", "F-Droid");
@ -153,7 +163,7 @@ public class ApkProviderTest extends FDroidProviderTest {
@Test
public void testCount() {
String[] projectionCount = new String[] {Cols._COUNT};
String[] projectionCount = new String[]{Cols._COUNT};
for (int i = 0; i < 13; i++) {
Assert.insertApk(context, "com.example", i);
@ -315,12 +325,13 @@ public class ApkProviderTest extends FDroidProviderTest {
assertNull(apk.added);
assertNull(apk.hashType);
apk.antiFeatures = new String[] {"KnownVuln", "Other anti feature"};
apk.features = new String[] {"one", "two", "three" };
long dateTimestamp = System.currentTimeMillis();
apk.added = new Date(dateTimestamp);
apk.antiFeatures = new String[]{"KnownVuln", "Other anti feature"};
apk.features = new String[]{"one", "two", "three"};
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);
// 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[]{"one", "two", "three"}, updatedApk.features);
assertEquals(new Date(dateTimestamp).getYear(), updatedApk.added.getYear());
assertEquals(new Date(dateTimestamp).getMonth(), updatedApk.added.getMonth());
assertEquals(new Date(dateTimestamp).getDay(), updatedApk.added.getDay());
assertEquals(testTime.getYear(), updatedApk.added.getYear());
assertEquals(testTime.getYear(), updatedApk.added.getYear());
assertEquals(testTime.getMonth(), updatedApk.added.getMonth());
assertEquals(testTime.getDay(), updatedApk.added.getDay());
assertEquals("i'm a hash type", updatedApk.hashType);
}
@ -381,8 +393,8 @@ public class ApkProviderTest extends FDroidProviderTest {
assertEquals("a hash type", apk.hashType);
String[] projection = {
Cols.Package.PACKAGE_NAME,
Cols.HASH,
Cols.Package.PACKAGE_NAME,
Cols.HASH,
};
Apk apkLessFields = ApkProvider.Helper.findApkFromAnyRepo(context, "com.example", 11, null, projection);

View File

@ -11,6 +11,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -19,6 +20,7 @@ import org.robolectric.shadows.ShadowContentResolver;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import static org.fdroid.fdroid.Assert.assertContainsOnly;
import static org.fdroid.fdroid.Assert.assertResultCount;
@ -36,6 +38,13 @@ public class AppProviderTest extends FDroidProviderTest {
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
public void setup() {
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.Utils;
import org.fdroid.fdroid.data.Schema.RepoTable;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -37,6 +38,7 @@ import org.robolectric.annotation.Config;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -48,6 +50,16 @@ public class RepoProviderTest extends FDroidProviderTest {
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
public void countEnabledRepos() {

View File

@ -29,6 +29,7 @@ import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.mock.MockRepo;
import org.fdroid.fdroid.mock.RepoDetails;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -48,6 +49,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -63,6 +65,16 @@ public class RepoXMLHandlerTest {
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
public void testExtendedPerms() throws IOException {
Repo expectedRepo = new Repo();
@ -129,6 +141,12 @@ public class RepoXMLHandlerTest {
"org.gege.caldavsyncadapter",
"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)
@ -897,7 +915,7 @@ public class RepoXMLHandlerTest {
List<App> apps = actualDetails.apps;
assertNotNull(apps);
assertEquals(apps.size(), appCount);
for (App app: apps) {
for (App app : apps) {
assertTrue("Added should have been set", app.added.getTime() > 0);
assertTrue("Last Updated should have been set", app.lastUpdated.getTime() > 0);
}

File diff suppressed because one or more lines are too long