解决小米日历 (.ics) 导入失败问题

开篇废话

这篇文章更多的是介绍 debug 思路过程,并且错误好像跟 MIUI 没啥关系

大一开学前随便写了个教务系统课表转 .ics 的小玩具,后面简易封装成 api 给同学们用了,然后就发现 MIUI 无法正常导入我的 .ics 文件,并且没有任何日志给我(或者说我没有 MIUI 的手机,无法 adb logcat 之类的)
到了大二才修,拖延症效果拉满

查找问题(debug)

首先赞扬下 MIUI ,MIUI 是国产 rom 中少数能系统级别导入 .ics 文件的,还算良心,并且还有些 api 之类的可以直接从教务系统拉课表出来,不过我对这个系统以及学校没兴趣就没安排了。

由于没有日志文件输出,所以我们这个时候只能采取一行一行删除方法来测试到底是哪一(几)行格式错误而导致的导入失败。

以下是最简 ics 示例:

BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-PUBLISHED-TTL:PT1H
REFRESH-INTERVAL;VALUE=DURATION:P1H
PRODID:-//huggy//Powered by huggy//ZH
X-WR-CALNAME:课表
X-WR-CALDESC:课表
X-WR-TIMEZONE:Asia/Shanghai
BEGIN:VEVENT
UID:114514
DTSTART;TZID=Asia/Shanghai:20211117T143000
DTEND;TZID=Asia/Shanghai:20211117T161000
TRANSP:OPAQUE
SUMMARY:高等数学
LOCATION:西栋203
SEQUENCE:1
BEGIN:VALARM
X-WR-ALARMUID:20211117T143000
UID:20211117T143000
TRIGGER:-PT10M
DESCRIPTION:上课提醒
ACTION:DISPLAY
END:VALARM
END:VEVENT
END:VCALENDAR

其实如果找规律的话,已经大概可以知道是哪个地方炸了

最后我删到 REFRESH-INTERVAL;VALUE=DURATION:P1H 行的时候,最后就导入成功了。

很明显就是我带的 REFRESH-INTERVAL 格式有误导致的,当时应该是抄这篇博客,没经过测试就合并到了我的代码里面了。
vialect / FireFox History / vscode
通过浏览器记录可以知道,我是2021年5月,也就是当时封装 web 的时候把这个带进去的,没想到是个大坑。

解决

查资料

我以关键词 “REFRESH-INTERVAL;VALUE=DURATION” 来搜索,相关资料很少(所以才直接抄了上面博客的内容),不过可以找到定义这个参数的是 rfc7986
不过没有具体说明 DURATION 的格式,继续以 DURATION format 作为关键词,似乎也没找到什么有用的资料。

反编译

既然找不到资料,那我觉得直接看源码可能会快很多。 然后我在酷安评论区找到了提取版的小米日历,然后直接用 mt管理器 反编译,万幸的是,MIUI 似乎没有上什么混淆加固之类的,至少 dex 可以直接读取。

MIUI calendar dex search .ics

在 .dex 搜索 .ics 搜索出的结果就可以发现,小米日历使用了 ical4j 作为 .ics 文件的解析库,那么我们直接来看 ical4j 源码,看看 DURATION 要跟什么格式才 ok

经过一番搜索,可以找到 model/property/Duration.java 文件
最后可以找到 model/TemporalAmountAdapter.java 的 parse 函数

可以大概猜得到 是 java.time.Duration.parse 最终函数,我们可以简单写个 parser valider 来校验 Duration 格式是否 ok

valider

很明显,正确的 Duration 格式为PT1H 而不是 P1H 上面的那篇博客提供的结果是错误的。

具体测试代码可以在这个项目找到(顺便学了下 Java)

番外篇

前几天修复就发现了是 REFRESH-INTERVAL 问题
我当时是判断机型直接把这行删除了,而不是寻找源头(当时我认为 debug 无从下手),然后今天突然想到,小米日历大概也是使用现成的库来 parser ics 的,应该还是有办法 debug 的才继续深入研究并且解决了。

当时代码:

if(ctx.get('User-Agent').toLowerCase().includes('mi')){
    ics = ics.replace('REFRESH-INTERVAL;VALUE=DURATION:P1H','');
}

然后把这几行删了,加个T解决问题

后记

具体思路大概就是 确认错误是在哪一行(REFRESH-INTERVAL) -> 查找相关解决方案(然后找不到能用的资料) -> 反编译找到具体的库(ical4j) -> 使用库的代码来确认值到底填什么(改成 PT1H 然后解决问题)

虽然 RFC 给了足够的格式规范,不过更具体的内容有些还没完善,又或者藏在别的地方了,比如说本文中有关的 rfc7986 给了我不少误导,RFC 上定义的例子为 P1W,在 java.time.Duration.parse 定义中是无效的,但是我看到 RFC 的时候看到 P1W (1 Week) 然后认为 P1H 也是正常的,最后就去看别的帖子了然后抄到了 P1H 的错误用法。

以及网上抄来的东西不一定是对的,有条件还是全平台测试一下,不然大锅就来了。