From 96a2995df4d411fa91c996f4e6c23af06d53faa0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 13 Aug 2026 15:58:52 +0200 Subject: [PATCH] build: add lib-recur to :app, and a dmfs v23 fixture for the import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The own store expands recurrences itself, so lib-recur is a direct dependency now rather than something :provider drags in. Still pinned at 0.12.2 — 0.16.0 removed RecurrenceSet. scripts/make_import_fixture.py writes the tasks.db the one-shot import will be tested against: the provider's DATABASE_VERSION 23 schema, seeded with the cases the import has to get right (a task with no UID, a deleted row, a recurring series, an all-day task, a subtask, an alarm property, and a list under a real CalDAV account). The provider is being deleted, so a fixture is the only way to keep testing against the schema it wrote. --- app/build.gradle.kts | 6 + app/src/androidTest/assets/tasks-v23.db | Bin 0 -> 20480 bytes scripts/make_import_fixture.py | 142 ++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 app/src/androidTest/assets/tasks-v23.db create mode 100644 scripts/make_import_fixture.py diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 488e8ca..6f0249f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -169,6 +169,12 @@ dependencies { implementation(libs.androidx.navigation.compose) ksp(libs.hilt.compiler) + // RFC 5545 recurrence expansion, in-process. Pulled in directly rather than + // through :provider: the own store expands occurrences itself. Pinned at + // 0.12.2 — 0.16.0 removed RecurrenceSet. rfc5545-datetime comes with it and + // is part of its API surface, so it isn't declared separately. + implementation(libs.dmfs.lib.recur) + implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) diff --git a/app/src/androidTest/assets/tasks-v23.db b/app/src/androidTest/assets/tasks-v23.db new file mode 100644 index 0000000000000000000000000000000000000000..e305e90abadb448158ee637c3791555232f6f13d GIT binary patch literal 20480 zcmeI4&1)M+6u@UCEB3ar)f9|qgJIGT>JUYiV&}sR36X8Z4v|0N$W09evDqElgIBxq z?8=E<3JxtLmqJgy_|zWTOMB@hP!cFTwEsa4J@nX9q4eC&tafK-W!cD=Ldjdu>g~+z zdvAX8=863n-M?3*KEZ1)YZyKrM{giaL$@$S2xY<#{|dG=-}i;G_|o_%ryO?^GpMk; zHOOoF`uEV#)Zm}`w_Jb?2mk>f00e-*D^B3k(Z2rNnKRnc)4uW0CQHn166RCl#ZqQ^ zxl*oGaIHL5t>CyEkG_V(t=T?pr%o+=XQ5V^tt{iEFD2EPQl~dT-@P5jjFicONTDok3oF$s4=C!}BpeglH4R@xOFJyqiQASW3X6CqqfMnv zi`ijom#rHP-6bqMZZhgJ>hBy3y9*LIn#!=`O`o^t3mnl|x?oD*?Z%&4kPy}DuB zmJyr4d;(_88K9EG4GCF zZ@3m+qumsAaoyhy!C5V35?d{%4hiE^H5@J3Bl0*d#EiA%a5fDlSj34Xk3E-XC}+K6 zST_to(qIkAC67g!2;+82i+kFnj?=A2e5}{0W7u`-a5j!f>SBfnz*qZA|L zFuO=iqAbc>LXL8ZkfU@WNX*7-vt%8n5a;@Ui&okZE^*^l`bY(M}A00AHX1b_e#00KY&2mk>f00e-5N?dIB+F1D{D73p<+b1(W8nb zely?Y_t^OPyB}`0um4}7?BCfxvM=L)fCL1901yBIKmZ5;0U!VbfB+Bx0zd!={2vGm zrPF7$CHeAAT2G~OTJUn1u4`PZiWfD~;`(2I{Y#YnJbQWQ#n9Z~*MsLyet+@`FM6RRn_vB?58J)`vSk~wZoTrBeK z;)K{;6FYGjZj(C$!o91~G76qO3ipzmK7eBPQsUwRQwJM(BYun-c_%#W!SprO&rRzW z6gv?arHG6Cd1!2UTC63o#mn3N24A^wOq*9fIw5SHQw(sMnM_7MFdB-@8ZjE!U0d_` zEB4y{kAHoKRI}RtPj8<@J#_fWld~CNy(1R5b@Z;_eO-E$fc=0k@nAtLG;PE2y`*gc zyV4TZhGSEN+0@bYU%aKE+sl=EHy%_fcdM({r&edmt2gEs^R>AiyuAFVFM%i{mXUKj zM@Pgm5o={Ew_ykmo;~|v&q}F|_F#B|rH_mUJ;IZqGi&f!!uT{X315hmfYXD`@%K|5 zBccg2-DYQGoqtdBMJB<3@CW!lzxEzgd#dgKTKHveOA~rWl#`;^PI652WvaD<8`R!R zs{47s>18^V+Ry)s;{E^aY*DccA`k!qKmZ5;0U!VbfB+Bx0zd!=0D)JSz)X4s>GSCH zAqch|t6(_6i~a?(#b2~%1%CUdF~iuA+O1plv5 enwTh-f?IvmFWuC*KeOmm7IoEhq@wX+sq`;1!{ None: + lists = db.executemany( + "INSERT INTO Lists (_id, account_name, account_type, list_name, list_color," + " visible, sync_enabled, list_owner) VALUES (?,?,?,?,?,?,?,?)", + [ + (1, *LOCAL, "Personal", 0xFF7A5C6B, 1, 1, None), + (2, *LOCAL, "Hidden list", 0xFF445566, 0, 1, None), + # An external account inside our own authority: only reachable if the + # user pointed DAVx5 at us. Imported as a local list, UID preserved. + (3, *CALDAV, "Work", 0xFF2244AA, 1, 1, "Me"), + ], + ) + del lists + + def task(**kw): + cols = ", ".join(kw) + marks = ", ".join("?" * len(kw)) + db.execute(f"INSERT INTO Tasks ({cols}) VALUES ({marks})", tuple(kw.values())) + + task(_id=1, list_id=1, title="Buy milk", due=T0 + DAY, status=0, + _uid="a1b2c3d4-0000-4000-8000-000000000001", created=T0, last_modified=T0) + # No UID: the import mints one. + task(_id=2, list_id=1, title="Call the dentist", due=T0 + 2 * DAY, status=1, + percent_complete=40, created=T0, last_modified=T0) + task(_id=3, list_id=1, title="Gather receipts", parent_id=1, status=0, + _uid="a1b2c3d4-0000-4000-8000-000000000003", created=T0, last_modified=T0) + task(_id=4, list_id=1, title="Renew domain", status=2, percent_complete=100, + completed=T0 - DAY, is_closed=1, + _uid="a1b2c3d4-0000-4000-8000-000000000004", created=T0, last_modified=T0) + task(_id=5, list_id=1, title="Water the plants", dtstart=T0, due=T0 + 3600_000, + rrule="FREQ=WEEKLY;BYDAY=MO,TH", tz="Europe/Berlin", + _uid="a1b2c3d4-0000-4000-8000-000000000005", created=T0, last_modified=T0) + task(_id=6, list_id=1, title="Team offsite", dtstart=T0 - T0 % DAY, + due=T0 - T0 % DAY + DAY, is_allday=1, + _uid="a1b2c3d4-0000-4000-8000-000000000006", created=T0, last_modified=T0) + # Deleted-but-unsynced: gone as far as the user is concerned, so not imported. + task(_id=7, list_id=1, title="Cancelled thing", _deleted=1, + _uid="a1b2c3d4-0000-4000-8000-000000000007", created=T0, last_modified=T0) + task(_id=8, list_id=2, title="Task in a hidden list", status=0, + _uid="a1b2c3d4-0000-4000-8000-000000000008", created=T0, last_modified=T0) + task(_id=9, list_id=3, title="Ship the release", due=T0 + 5 * DAY, status=0, + _uid="a1b2c3d4-0000-4000-8000-000000000009", created=T0, last_modified=T0, + _sync_id="https://dav.example.org/tasks/9.ics") + + db.executemany( + "INSERT INTO Properties (property_id, task_id, mimetype, data0, data1, data2, data3)" + " VALUES (?,?,?,?,?,?,?)", + [ + # data0 minutes before, data1 reference (1 = DUE), data3 alarm type. + (1, 1, ALARM_MIMETYPE, "30", "1", None, "1"), + (2, 9, ALARM_MIMETYPE, "1440", "1", "Ship it", "1"), + # A non-alarm property the import must skip. + (3, 1, "vnd.android.cursor.item/category", "Errands", None, None, None), + ], + ) + + +def main() -> int: + os.makedirs(os.path.dirname(OUT), exist_ok=True) + if os.path.exists(OUT): + os.remove(OUT) + db = sqlite3.connect(OUT) + try: + for statement in DDL: + db.execute(statement) + db.execute("PRAGMA user_version = 23") + seed(db) + db.commit() + finally: + db.close() + print(f"wrote {OUT}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())