LocalDateTime.now() падает на Sony Bravia

Я использую ThreeTen Android Backport в приложении для AndroidTV.

Хотя на Nexus Player и на всех протестированных устройствах Amazon Fire TV все работает идеально, LocalDateTime.now() постоянно вылетает приложение на 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)

Что происходит и что я могу с этим сделать?

2 ответа

Решение

Причина вашего исключения довольно ясна и задокументирована в вашем сообщении об исключении:

Идентификатор разбитой зоны ("-01:00GMT-02:00,J086/02:00,J176/02:00").

Также ясно, что Threeten-ABP (и Java-8 тоже) вообще не позволяет создавать недопустимый идентификатор зоны, см. Следующий пример, в котором используется синтаксически правильный формат:

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

Это отличается от старого JDK-класса java.util.TimeZone где вы можете установить любой произвольный идентификатор. Поэтому возникает вопрос, что делать с таким идентификатором зоны. Он настолько ужасен, что вы даже не можете догадаться, какой реальный идентификатор часового пояса имел в виду.

Единственное разумное - использовать часовой пояс базовой платформы, который все еще доступен выражением TimeZone.getDefault() хотя его идентификатор зоны не используется. Обратите внимание, что идентификатор поврежденной зоны не позволяет использовать данные tz Threeten-ABP или любого другого хранилища tz, кроме данных платформы.

Лучший обходной путь / взлом на основе данных о часовом поясе платформы выглядит следующим образом (по-прежнему используется только ThreetenABP):

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));
}

Как я упоминал в своем комментарии, я принял во внимание это странное поведение некоторых устройств Android, чтобы стабилизировать свою временную библиотеку Time4A, поэтому я считаю полезным упомянуть следующую более чистую и безопасную альтернативу, начиная с версии v3.16-2016a:

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

Это чище, потому что это не зависит от какой-либо уродливой обработки исключений, даже внутри. Если идентификатор зоны базового системного часового пояса не может быть разрешен, тогда Time4A автоматически переключается на оболочку вокруг системного часового пояса вместо использования собственного репозитория tz. Никаких действий пользователя не требуется.

Обратите внимание, что Time4A имеет единый фасад для часовых поясов, основанный на собственных данных tz, а также на основе данных tz платформы Android. Вы даже можете использовать обе данные tz параллельно (Timezone.of("Europe/Berlin") использует данные Time4A (в актуальном состоянии), в то время как Timezone.of("java.util.TimeZone~Europe/Berlin") использует данные платформы, которые могут быть устаревшими). Эта функция очень полезна для разрешения локального времени пользовательского ввода, отображаемого на устройстве Android.

Это также безопаснее, потому что временные метки экзотических устройств корректно проверены в отличие от ThreetenABP, см. Также некоторые другие SO-сообщения, подобные этой и той.

Мост от Time4A до ThreetenABP может выглядеть так:

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

Тем не менее, я не рекомендую это, потому что

а) предел dex может быть скоро достигнут (используя две библиотеки одновременно),

b) Time4A имеет так много функций и предлагает намного лучший опыт работы с i18n и превосходный механизм форматирования и анализа, что он может полностью заменить ThreetenABP.

Единственная проблема Time4A заключается в следующем: он не очень известен.

Ага, чем проклятый Sony Bravia 4K 2015

Не только LocalDate.now() может генерировать, фактически любой метод, который зависит от "ZoneId.systemDefault()". Так что завертывание в try-catch каждый раз может привести к... неприятному опыту кодирования.

LocalDate.now() неявно называет ZoneId.systemDefault(),

Итак, в качестве обходного пути я создаю надежную защиту ZoneId и кормить его LocalDate.now() и тому подобное.

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;
    }
}

Использование:

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

или же

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

Очевидно, вы должны помнить, чтобы использовать его всегда и везде. Блин, Sony.
Методы расширения Kotlin пригодятся здесь. Что-то вроде ZoneId.systemDefaultAndNowSeriously() или же LocalDate.nowLikeAGoodGirl(),
Но все же, черт.

PS:
FWIW
Ну, я считаю, что у Bravia не совсем неправильный offsetId, поэтому я просто анализирую его:

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;
}

НО, я не уверен насчет последнего, под рукой нет устройства. Возможно, обновит квитанцию, когда узнает больше.

Другие вопросы по тегам