LocalDateTime.now() crashes on Sony Bravia

1.2k views Asked by At

I am using the ThreeTen Android Backport in an app for AndroidTV.

While everything works perfectly on the Nexus Player and on all tested Amazon Fire TV devices, the call to LocalDateTime.now() consistently crashes the app on a Sony Bravia 4K 2015 (KD-55x8509C).

Caused by: org.threeten.bp.DateTimeException: Invalid ID for ZoneOffset, invalid format: -01:00GMT-02:00,J086/02:00,J176/02:00
at org.threeten.bp.ZoneOffset.of(ZoneOffset.java:221)
at org.threeten.bp.ZoneId.of(ZoneId.java:344)
at org.threeten.bp.ZoneId.of(ZoneId.java:285)
at org.threeten.bp.ZoneId.systemDefault(ZoneId.java:244)
at org.threeten.bp.Clock.systemDefaultZone(Clock.java:137)
at org.threeten.bp.LocalDateTime.now(LocalDateTime.java:152)

What's going on and what can I do about it?

2

There are 2 answers

2
Meno Hochschild On BEST ANSWER

The reason of your exception is quite clear and documented in your exception message:

Broken zone id ("-01:00GMT-02:00,J086/02:00,J176/02:00").

It is also clear that Threeten-ABP (and Java-8, too) does not allow to construct an invalid zone id at all, see following example which tries a syntactically valid format:

String unsupported = "System/Unknown";
ZoneId zid = ZoneId.of(unsupported);
// org.threeten.bp.zone.ZoneRulesException: Unknown time-zone ID: System/Unknown

This is different than in old JDK-class java.util.TimeZone where you can set any arbitrary ID. So the question arises what to do with such a zone-id. It is so horribly broken that you cannot even guess which real timezone identifier was meant.

The only reasonable thing is to use the underlying platform timezone which is still available by the expression TimeZone.getDefault() although its zone-id is unuseable. Note that the broken zone id prevents you from using the tz data of Threeten-ABP or any other tz repository but the platform data.

The best workaround / hack based on the platform timezone data is as follows (still using ThreetenABP only):

LocalDateTime ldt;

try {
    ldt = LocalDateTime.now();
} catch (DateTimeException ex) {
    long now = System.currentTimeMillis();
    int offsetInMillis = TimeZone.getDefault().getOffset(now);
    ldt = 
        LocalDateTime.ofEpochSecond(
            now / 1000, 
            (int) (now % 1000) * 1_000_000, 
            ZoneOffset.ofTotalSeconds(offsetInMillis / 1000));
}

As I mentioned in my comment, I have taken this strange behaviour of some Android devices into account to stabilize my time library Time4A so I feel it is good to mention following cleaner and safer alternative starting with version v3.16-2016a:

PlainTimestamp tsp = SystemClock.inLocalView().now();

It is cleaner because it does not depend on any ugly exception handling, not even internally. If the zone id of the underlying system timezone cannot be resolved then Time4A automatically switches to a wrapper around the system timezone instead of using the own tz repository. No user action required.

Note that Time4A has a uniform facade for timezones based on the own tz data as well as based on the Android platform tz data. You can even use both tz data in parallel (Timezone.of("Europe/Berlin") uses the Time4A-data (up-to-date) while Timezone.of("java.util.TimeZone~Europe/Berlin") uses the platform data which might be old). This feature is very useful to resolve local times of user-input as displayed on Android device.

It is also safer because exotic device timestamps are correctly validated in contrast to ThreetenABP, see also some other SO-posts like this and that.

A bridge from Time4A to ThreetenABP might look like:

LocalDateTime threeten = 
    LocalDateTime.of(
       tsp.getYear(), tsp.getMonth(), tsp.getDayOfMonth(), 
       tsp.getHour(), tsp.getMinute(), tsp.getSecond(), 
       tsp.getInt(PlainTime.NANO_OF_SECOND));

However, I don't recommend it because

a) the dex limit can be reached soon (using two libraries simultaneously),

b) Time4A has so many features and offers much better i18n-experience and a superior format and parse engine that it can completely replace ThreetenABP.

The only problem of Time4A is just this: It is not well known.

0
vigilancer On

yeah, than damned Sony Bravia 4K 2015

Not only LocalDate.now() can throw, in fact any method that depends on "ZoneId.systemDefault()". So wrapping in try-catch every time can lead to... unpleasant coding experience.

LocalDate.now() implicitly calls ZoneId.systemDefault().

So, As workaround I'm constructing foolproof ZoneId and feeding it to LocalDate.now() and such.

public final class Hack {

    private static @NonNull String fix_TimeZone_getDefault_getID(String offsetId) {
        /* todo */
        return fixed_offset_id;
    }

    public static @NonNull ZoneId ZoneId() {
        String mayBeWeirdZoneId = TimeZone.getDefault().getID();
        ZoneId id;
        try {
            id = ZoneId.of(mayBeWeirdZoneId, ZoneId.SHORT_IDS);
        } catch (DateTimeException ignore) {
            id = ZoneId.of(fix_TimeZone_getDefault_getID(mayBeWeirdZoneId));
        }
        return id;
    }
}

Use:

LocalDateTime.now(Hack.ZoneId() /* instead of ZoneId.systemDefault() */ );  

or

ZoneId systemZone = Hack.ZoneId(); // my timezone, instead of ZoneId.systemDefault()

Obviously, you have to remember to use it everytime everywhere. Damn, Sony.
Kotlin's extension methods will come in handy here. Something like ZoneId.systemDefaultAndNowSeriously() or LocalDate.nowLikeAGoodGirl().
But still, damn.

PS:
FWIW
Well, I believe Bravia have not completely wrong offsetId, so I just parsing it:

private static @NonNull String fix_TimeZone_getDefault_getID(String offsetId) {
    if (offsetId == null) return ZoneOffset.UTC.getId();

    int gmt_pos = offsetId.indexOf("GMT");
    String off_fix = ZoneOffset.UTC.getId();
    if (gmt_pos > 0) {
        off_fix = offsetId.substring(0, gmt_pos);
    }
    return off_fix;
}

BUT, I'm not sure about last one, don't have device at hands. Probably, will update receipt when will know more.