From 424c4c914fd3bb70d142f930ab344e428f17a6dc Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 28 Jul 2026 20:37:25 +0200 Subject: [PATCH 1/2] Open every format the core renders, not just half of them odrcore v6 keeps reference html output for doc, ppt, xls and pptx next to the ones the app already claimed, so the "TODO: enable pptx too" that sat in CoreLoader was the only thing keeping presentations out - and the legacy binary formats reached the core only by accident, through the doc mime type that was listed before ppt and xls ever were. What the loader claims is now one list: opendocument, the whole ooxml family, the three legacy microsoft formats and pdf. Prefixes, so the templates and the macro enabled variants come along - the latter are spelled vnd.ms-word/vnd.ms-excel/vnd.ms-powerpoint rather than under the openxmlformats prefix, which is why those appear without their subtypes. Text, csv and images stay out: the core renders them too, but RawLoader is what gives them their player, and it only gets a turn when the core loader says no. The manifest filter and SupportedDocumentTypes follow, so the system offers us pptx/ppt/xls and the folder browser lists them. Two things fall out of it. The doOoxml flag is gone - it has been passed true since ooxml stopped being experimental and now gates nothing. And the ".doc-files are not real documents" special case becomes a check for all three legacy types: the core does parse them now, but declares them neither editable nor savable, so keeping one open only buys a second parse. pdf changes which viewer a failed upload falls back to, so that stays explicit: google docs still gets the pdf, the microsoft viewer keeps everything else the core knows. Verified on device - .pptx, .ppt, .doc and .xls all render through the core, with CoreTest covering each of them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp --- CLAUDE.md | 18 ++-- app/src/androidTest/assets/11KB.doc | Bin 0 -> 28160 bytes .../assets/file_example_XLS_10.xls | Bin 0 -> 8704 bytes .../androidTest/assets/style-various-1.ppt | Bin 0 -> 708096 bytes .../androidTest/assets/style-various-1.pptx | Bin 0 -> 57211 bytes .../app/opendocument/droid/test/CoreTest.kt | 74 +++++++++++++- app/src/main/AndroidManifest.xml | 47 ++++++++- .../droid/background/CoreLoader.kt | 62 ++++++++---- .../droid/background/LoaderService.kt | 2 +- .../droid/background/OnlineLoader.kt | 8 +- .../background/SupportedDocumentTypes.kt | 15 ++- .../droid/background/CoreLoaderTest.kt | 92 ++++++++++++++++++ .../droid/background/OnlineLoaderTest.kt | 4 +- .../droid/background/RawLoaderTest.kt | 2 +- .../background/SupportedDocumentTypesTest.kt | 13 ++- 15 files changed, 295 insertions(+), 42 deletions(-) create mode 100644 app/src/androidTest/assets/11KB.doc create mode 100644 app/src/androidTest/assets/file_example_XLS_10.xls create mode 100644 app/src/androidTest/assets/style-various-1.ppt create mode 100644 app/src/androidTest/assets/style-various-1.pptx create mode 100644 app/src/test/java/app/opendocument/droid/background/CoreLoaderTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 95859fd9d9f2..2502fda528fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,12 +145,18 @@ the `.pro` suffix) differ on purpose - do not "fix" the mismatch: `AndroidFileCache`, the SharedPreferences name in `MainActivity`) follows `applicationId` and must stay that way, or existing users lose their saved prefs. -### Supported file types are declared twice - -`SupportedDocumentTypes` (mime prefixes plus an extension fallback) and the `STRICT_CATCH` -`activity-alias` in `AndroidManifest.xml` describe the same set from two directions - the manifest -is what the system offers the app for, `SupportedDocumentTypes` is what the folder browser on the -landing screen offers itself for. **Nothing keeps them in step, so change both.** +### Supported file types are declared three times + +`CoreLoader.MIME_PREFIXES`, `SupportedDocumentTypes` (mime prefixes plus an extension fallback) and +the `STRICT_CATCH` `activity-alias` in `AndroidManifest.xml` describe the same set from three +directions - the manifest is what the system offers the app for, `SupportedDocumentTypes` is what +the folder browser on the landing screen offers itself for, and the loader is what the core is +expected to render. **Nothing keeps them in step, so change all three.** + +The set follows odrcore: whatever it keeps reference html output for in `test/data/reference-output` +is what the app claims - odf, ooxml, the legacy binary doc/ppt/xls and pdf. Text, csv and images are +the core's too but belong to `RawLoader`, which only gets its turn when `CoreLoader.isSupported()` +says no. The duplication is deliberate: `MetadataLoader` decides what is really supported by running libmagic over the file *after* copying it into the cache, and `CoreLoader.isSupported()` / diff --git a/app/src/androidTest/assets/11KB.doc b/app/src/androidTest/assets/11KB.doc new file mode 100644 index 0000000000000000000000000000000000000000..3940e5d26a6503f0bf792e0b2fbff9358beff623 GIT binary patch literal 28160 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 E2ktrrk^lez literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/file_example_XLS_10.xls b/app/src/androidTest/assets/file_example_XLS_10.xls new file mode 100644 index 0000000000000000000000000000000000000000..1464ee150a463d887ac5197242c2dc26d95e1eee GIT binary patch literal 8704 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 z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eQa|di1Ee4nAVhqD4r& zef#!@4jsC3+r7 z$J;x1?x^|sPe1+i;>C;W*RQu}(?-pTt@8h5$&#&FwF-Ir-Me>d)vASciWe`Ax9avd zJ3Av})~s1ajvPUVRsN5HJ(kw5UmrO?HpwdgpD+azHW91*zjf=@SYh$v#p(){`9D7H^UpuW(%*mo{cUv2 zprr_b9XfQd4CFiw&;Sk4K>Q7$IE}`6b$#U9fBf->+Ma{fcm$#~2H9+#I&~})L`yyb zt@3|V4J_ADTaAVc8KMSa{iI2gzFom8|3|+7RzORXx`Ji?kG=^kLL~(y|F_XG^XkEuj8?>mPi5^)rGFwdO2sm}>6cSmpW)0ppY}n8;!AX-QA;%G0M$$2w>%QX^vi%l968c*!#Ga^G(ZD15Pt(0#*;jG za^$hSD22M-=JZ<(!^ zB1MW=ekkW@fCgxQ2I6S|t?QXGWkSwcs8Asc;|vZCwu%`4{QvCPvzBrF{r!>bph1Jw zK+Jz!{vQ?=hPdeJy?y(3xW$CQKM1#Kn6lge&eH%5&;Sj*!vIFkiJv=-R9lg?T)5pxW-+qfgv_x43 za-IfgfCgwFo(53Cjw*!u8|Fk5R5hVLNf+#P{yqLn86d|B(pp|3V_@u~jo? z+!26Ifu~QOUbt`pd3BN`Nid_fELryd1_cG77>^Zx{`qID@cQ*@%l=<%!-^FvuoNRm z=gpgk)Nb9nrAE)3IWykI^P6%t8lV9hpaClbX#aO~bVLUGc0&F?cI;SnwfN`%XhC|L z0%qLZ-I1{)k!8!4S%w!ZSP+4h`9E$Puw3EHnKM@TKh_^TdbERsgXL;ywmNa*gu0RZ zO9M1O12ph)22ds+HEI;vd@v4w*sx)iE%{fkUPaXacL7+YeD2&i1n%FzUky~pi=x<% z0sMY`ei%54!9Q=G|8V*8WwkFD88~Ke4;NzX+qVz%=g*(VJQ{Y26)T3k9i7DpeE9HT z?ED|A;l{L~Lx*A`F{TJN4!=!b-3I=p0UDqI8u;`E)chY0?Rm#XBYU^(|Fv549pnG4 z*5q3npaB}7fxnM|kI4TgPMlb#OquoT*Q?)x`TyT%Z*og$fCgwF{s!Wc|D)>`9nEjk zv~2%JAR3g)mMv@Z{@=GzI70(8Km#=JcQb%Nm>B1ekw5R=Fv}KuJpUO{-ah{s5it_! z?eiSal!T@8=g)t4f?P@iG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D z12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMb zG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1Km#;D12jMbG(ZD1 zKm#;D12jMbG(ZD1Km#;D12jMbH1O9Nc>VhIqeqV-A|n3U&ETig01eOp4Okk8ii#RH zZd{%`c~YfHl_^uE5hF%m>Bfy4%b2F)$B$dCynOlcqD70QPMtb^`t<*6?|wq!IKw!O zzpHMGrPS6o6%54S#Tu&SpGobhVudsahDwuw2}!kT6B-q(h=)L%kP5+qQ1oIXmmpY( zA&7?}k{kjFB&4?_HHQ*%NUh}1{8K^*{ms5+7-q(C)$!2gJB!OZ@B8?^?|gQBcAcG> zJ$Ue-nr_^Pft%bH8rVElUgz}Gq-Hna{Bb?XV0GbB7>`Sb#>~tv$HeU zL=6H6Ab>zD0s{jBHb2|@Q>RX;+p}lS+SN;!F1e#zxpKuDxp(hgH7V`e%5&$=xsfUi z4GkreNpHMcE-o&FKeNJ9qBXJM#+{E+{fI|BsK4+sLe}tk%}nt5>hyzI|K&JM7r8!`7FUmTIR^ z#Qe$s5jZpBe6lh0~+hJ(_Z*Fe3+56$chi+sr|382Je8Ywfipxbts;a7NCxe57-X>p}{OQaYVE?>UvjdaU87RdVw;?{c$9^!>VZ>mue){*S<+2>}EUh(#dw z{J(GCzUa(vTMG*dBj!*3kHDb`0R#|;MIiS4-`?JCGyK@t*q>obPEL*jbRJp6{K@|j zI5Z)E00OZH#Ge00M@Mb`@9XOe&QQny*jAkv5Nvg8$p0<{1q2X4V2uP~&;L)JJjusFPu z*t5WV$A7+f@j^{HVqcLudOsC7uS!iOc>Jd-`}_NClHasx)3s~Y?9rY&YpkWE#n$Uw zPhXSCq_) zii&hC2wS6zKg`a~`ev>RzG+bR8lmKWpAV}DAb>!O0;|mbCr+HuWJ2@5>h%&ZJUr}P z{L^NBo%Y+-)~0g;f?J`(ffS?v7wlPM(rSHuy&|>$$2O@_Gg&PE+evC~(#@MU&!0a(GBPqXHTCrA)5RuVSy^dc zBXoU0JNKUf6Ao-v7JXrM*(MwY9wecR?s1fB*t(B%tqM>Fw>+mo0kx*E@fkfxX}V zrH^#=C12~;ulGhq@BjBkreB_$o3q~(!~6gA(TGF<0R&bi;QM4&U-hV~_iMRx=Z>#K zU*xS@w``=2^Y=xrW>w!8rgH?2A3wgDPWW8}5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5J2ER zCtzN~B~RP@t?AUQ-F$C8HD$VWr*5UD#kA?xV!ks?rqq0`n6i{mf$kHpq1FF!lfO(o z#+_!PsWz>8ggZ>nvNrQWlQggX(4FVRyFdT<%~0*{@BQ?*`PKYZ{8w0KnX3FO!?q+$ zm1b~E&%RpIV2*}`N7m(<4Bd=z`gGGGCFf3^MTj_){qurOei z>5_SeX5i>=G+*hjuu1FbF8LZiOTkI~q&vRj_3^?0d(08jsF9snG->SRE{^|lx8IrX z?E7}4J$m%JQ(Nqmjw~pAu5zR1bZo(8@mYPO@!wGMoBBz2``=OQ(O;ZL!EvigjdM)- z&3W#Bp&2x5^bU>fWV|CPx27V)@0{_MyZwKttG5%`sScCIzpcxg&nGJw-}zhl!EV0! E7v$+qeES)goJc=ccX}OcSv_gcS$4N-6|zr(%mIpg8%Oi-7|acx%Zw~ zYp-XmXU*KcaEbcncX55LvySsXQ3etU3k(hp4otOHR1xfVzYu{x8`!_LbYNxz{azL; zCj%*j6m)!#-Lud#tBw|vaVQqBB)W$R$2N<4@FhSt{`QzKEE_Q&&izXoC+Z04;t|%n zt&F@c+}tf@+7>i71Y7JpclNHj%d>hNdzrf-+Vrneo08q*!fN8^mLahOls=9d7pQWI zB!*~9JoO#xSd*-@n%9ZWLYikzHX%+=@cOFhaBX;tk55JmqY9N3IL&aO?O33^N<&^; zLwZ+;m*kLJY>b!`Sl{tvpohi7z3+ihd0a-{QCZrz+lkV+-Nf-MOMLKi15ynHVgg>~ zyn%2D3QpFjH-w@LEF3Zn-^x7ji3K6Rz!d-ROvr%$VQcHi>|kkT{94w?&Bn>m;rCy# zvN5?@Sso@W%g?i5eLlXzO03XG&oiS@Q~CH%C1t8g&+D_#B`5w;r(S1{?>;IcKzRpu z4$W4G8Kal~!E<=qpw!W`wVb|oZ7yCpkBb+MWzDa@6e}FdK;Ls0`59|sScJOrH!PdR zuHHD8R~)Qr&f=W*X6TY|Y&o<{rE28DNc669L%yoIotjKea*Vut(EaRfFuToUN@x^$ z1^Jzao)^hC(hfa=`8eZ?)H%fDWM{3$-{Xu2@oMqVhv=Ej!<19f;=8Xi_4Qa8BflJi z$2rl9#kX#0_m7tZc3JDKt{8~s5XPkP6;!6~mE`aF7Wlo~daX7W$&5qV$M6yF|w=O6EcNar{n&>7W#2Vp0 z?ZYCS!*YUdOrKX1j(?113@7>75(@r}gXs(8vdD{z8tQp$K-j`wIp-oao?!AXH6 zv#>%xph-he+bC_Yc&VX-?`T2{dX&&$E4z9);WEMfR{7yzD%NORb603-N#;>)#yxX6 zy%q&3`fWgrB%&xJP6B*%ApaFco^gU0$xxlQo@YVwTGoJbS9o1$HVthThBDs_{t1ua z5SFHz0C5UAHum$~ty1hFg%u~(nZI02aPUS<{>_xWRPFjOVlKUICN{3!kSybQK{x8v zvUn2iji8fU{#a`_;nlVIiIMdi{pp5Uws{{P;k&10w)lP-=xbVo*cWd;+ap(f<`RQs zSPYts;s>vbHP*#a$tfCb9BDZ2Z;0|JheRGd(xEuwqbM+gS~ozNr&}4Ih#C~#(=obt zklEb8&cgWe)F##TbHrY82#3Og7CDBFR{F4-{&&>=>j2TJE2M%VkA*pOTWwqu#ymJZ zJk;D#t*>n)ej6^@&bXA6izw-o`kl_z38gUw2n1K?ffx+pt!*yp5;Diu>3XyII@u4Q zp#m;4!J>qr?hvy2V7IlH4NHUs4yUOEBxy-gJC@5_7p?L{il~S5NCMVRmN(c3uE47Z zOl&@Bd0dTnPQSf>(UYFej#Y@OE#LflDCk0{sJ~aJI6p7Ko3nZgC80Cjh}$2cr_AUb zks0K;s28|k6q?q@>$<}el&eMpieA3!Ol~JHwBsDEEct4PZ810gC=sI2tKwvR`7nJS zdtSn%i2xEYDNEF&F1{fK|$WjQwo!Zhx!? zp}cLmQpL>DX7xI9z!j6{1dh(4Mq2#?5i?50hK(T`z2_lRLK|-|LX&l>S8~dRcK|^Y zZvQsRUC^D=2`uQgPW9QU>I1hmANtR3E9t*@(YYQUbag$OITkFh<%-Jh@)-ewv}CG_ z8l1)2W7z_oTrlHQ^{?>}?k-yD;~*IKN41MRXi&S_`T5(g7BK1k&z#h9uxARQ$$Czt z0??nxZUrX4xi&eio9gM)q_fwS?qX|Qg~>t{KvG?fzrkMY%NG+QmCZ77H7eV~eEHQ+ zV=`Ux6LjfNUU^Yqab}$EghzI{bgZaSl4>nl<5ES`Yy~@~uBJKZq6oz;E(UiRc7%7L zr53mb$yn=oo{198S+sIG|7r3OeO+lX~?xOS@VFjfI;hvBvN6UiMxFLYB2KxWCINt}ol>=Nb__s`Q-6fx;BpSIS5x zieJh|h!pBjLW=E7F_$XUffJ)drtGtMUU1O^cL8xpjJViQvW2)T5%`{!hf&oW49o^C zx8T-0h1~lKSq)SNVsNoBo*u)kNRQZiwf6duz}L>E8I0vwiz7G&4_{bFJm8z?QsdjK zg=$RN$yE!L%vF^?cl5Jv<9+HRFf@XN4x;ZrLc+kvv%7`xIUw;1IWl0}E7zI%g7*Zc z-x{UBgZPjr-dzpqOLb@M!z5#?D!?t@y=s*H??0Dn2ft_JS}uY<)(B)F62tke;I#W>#;RC z>mWTRpRVSoGVWY4A+Nk)9ekwH%IrKt$8{+Wq$Th-|`SMe-v} zb;jRNE)FCBhrTA*Q$(_!fDlpp4G}FGhpfLL;_w?H zjsGhmEy|CGlmduc0EjdIh`iz4Cd*ptFPTLKArj;oApsyl`-q6y|3st!gvdT(8I^gd z#3Lg6RsV>{5_Av<5gY&!{~=wCt4Bnr07S|j5vlrxNJZx2NFoRkir*uG`C-C39xo~W z#HRX7llU_hSk13OzN^aBI{pDrX6fxhBm8I; z#Q(quI=~3@-x%5PDQAxS8zW~*TROq%UB73f2E>T;FGfZ*)Q7|W&WP}Dj1+?yIs3)P zKK%b=qyb(iL=G)}`<)4%9qD=f6b;F;Z^2-3-ck{+yA26-AysraWS-bm?5TF#Mx$WGyN4AN#*YJ~ug55;RR3#Y$S54?ydcE-y;M z8*~c6hQYcpLezHO3LU5>)%dv|jdSEPF{6ftxX#+H zFXED`X~RU~?NzJi&tvxp#TT*OBM3*-*v4B@I^-22biYdTRaf3T9DlewW2*clyZtHyLp*p7E4ZWHY}uG# zvreKEwMNEH{q1+Hk%8@Hcn4PVA}2e`COu+UiQALQxsw571JL&^OLra03>*y174omV zZO?waZS(f?Bxqj88Z5jsLa~HXCz``~KyI1xS>*+>4|?^aWG9&Ofv4A#=ru083E8xi z^Y?F~4QKfdGWkf}DR9a~PV1&ghavS!po{CWq-eZMSuMHQUP2>(RorLB2tD#G5#N)? zjZK|gW&4REy#@|=01J8Q>bN!s*~iLQ&Pf6(ApT=3b53@vGE0^3A`5bINafH_B9}o~ zuTMFwC2R#D$}r}6z=;aLisFcUx|ZlyM>d2;8?rz(ACOLIiC^m?Z!LJKji>dMoGJ|` zjK-~}E;d0TJomnNZCwCd>QWHzGQAt`Lxsi%*kpt-Emf!24eh)U2kJss8*9Ck>>ll1 zeQd=uh7l)FbNwbxQBYQ$EESeR%Q>jZnk%qUAv;t{Q$O-euDwG4PC6iKsJBTbgk_{N$JTOUOv9KDPzZ4Y%lr3>Wxtd|Bo_j#XugL%7G(d}C8#96s$ zIEGwgm>*CxQhJ#N=T=``T{IU75j!#<0G2QdQP&!}YH1t6pStGfZM&`c2I8;vz-&gn=L%n`Z&NcEkCBGu++nTuZ7{VGMOV zD_36p24(!3<}LpFkH_Pfy~CNxK|Sn*&eBpjm}mnQbd(_?oC^KxS7bhC-4fSaA%f+Y3D@XE=Ddi9vR+5MoQSI-WT`kJjAP z-GcJbR0|d9nDuL#?hr((aAgL2PmcT1T_a-#Z0NGQ749W_e+7w(AalbHYkt<717tpE zCV~#Fwd%-l&n){b%%k$wAf~sJXFw!Kz6VH{!3Ibm=U{}kzAPy`&`8B+VyHtqB1OB$ zLgUU#is*>5RD)Cp8*4qyHqoMUzz4#7XNe4u2|IXvG+q=$r?dgW{JE)JZI*`T_ZKAn zM3O_9v6^xh7t&5(DnhI^)pUo`g~2h4bVZqMF-bKQNs7n2Fysma`u=>M$%|;xRG~3y zVR~w7vArWP&eahRDM;Zx=0aVbn}6VTA+O?}G0*wz9+28=?`BW$-=PN? zGm|8V;a6`n+D;FYF#f%%lGxQ#&W?-+8{i=RhpK`4f5gYkL5ZeGKK~E8{0sC z1KA$vZ>+tCY;I?H-dNAXPcZ>y%xPm><&?>G4IwZ^ZNxjq=QA4xuBFH2acqy6bu7l}(5l($muo5>`@*Xew16Luwn-#hu99uTGzIF*r0B-#T2Zw~R9k_);H}Nd5|-kEHh;RAI&8&K0EczX79ZS3CiX4NQ&^f71@95^AS z)}0Z)zb#W<4`Y3hQinDrf^_f+B*elUrfgxAl#KqoiwIWj4@cJQU1xcBG&j4_O*T8! z#r}nVzs$8)iBrZOVv**Es=SZzxTn1LQo4$)OKIeY`C=OUXT{aHhTvH*=alL>v$@d=k_V z$JRnoZ*E1O6{Nm}?)-wOF{u}%>&QfH`}FOI-QM7H1pNW|gCl?u-+`Zulw+=LW{I#l zcz%%4<~X%ffZM*En@^S6j|t9z$lzLu->~8ORD4b?9=dGoRXCifGH$(Aijf{G>cukP zz(biVGV^YBeVfYXYSm2+jhI#F>Zx3G`$=qTj5Uzl}HMl$%HX^xbe`W0lm+QA?KyKNMww=e?TmnNbkBhHR4XA%bVe>fV z4PG_M1nm=uhK1F^&EUY47g|fB>c@N#2%B5Aopo0x@*%G{Y>M#Bd$2#!S>f-k&Kyaa z?UE~U#zs^h__3?fENrG-ZQu_QYGk^bUNK_a5n<@5`JFJQ9^$V^QzQ;b?wK1csL0%G zU>lGpPswY%?Q@(^LeC{`;?NeY=Y5eypY?#+zY~#Xbo{jR{iTr&OsAP)a#?hDgB@># zr-k9g`Q=PmETg!Rga?>LR8nQKC8JE9DP+`0Hhe>?DE`~?XFFd$+Y>Q3HP)M0sXK=@ zQ*S)*mNHL^V?m-TsJEjMJ)^lm=QT7Fzb<_J;D6@j!VfUIQB( z4`b#Bl!qf|QXju@I5m8%5JeMdcpy{)ntrtJ>vV?~k3 zuIr_Xd^BTwaSy{GZe;mp1rFuZ7GL`WOqjkcO1+eb2^Cu-+wX$mTLQMK^JzL=UbEfh zTa(y&c7^tXZ23*|YKkWtntBcFZ{`k5lFIxl6z@O6{iHv|~ z2%sC-VXa*EevIG84oQ>(=mx@g+!DS3S{;WU%KY>@p}8(kwjKC?tkEDbO~3Y4*GbokjjU+V5&_)pNCU8%T_Z9QKsxHkBiS1}qXT35vs?^c=dh;a4R3guqnYJ{~ zy-JmF6le!hFx&rX2Rhp*D^_*(wG?TLqOXx!QsO^(icuR7m9~KMolkmHf|j9iy*ILX zEWIUBQ_qY;ot#hfqF3%mJDyqm8~O6Yt_>kUKC42V(t1M3Q%wS46I2bL4XB#rGSd$G zU1T2%vKjwM&RODSj!Yf7#%ta3VSV<-^5MoT5d3ktlXu`%!fR0u40qZ-7b^)p{XrKV zVS(jmkF7qGoo&lF+x^>c#{)ef^hjf9pRe8;cE8a)dSRq%asYVP-7>N>J|;jOl2}TV z_jznx_~qfl)`1(viyEzw_o)N5Kt&@%j71($3#`2NYHogBk6txj5 z5FfCX15Ql#3yjh}=b0&XZDCkDo75SI_6_DIOtk73DW7WHFe~PNl4U>n=mNa#&vQ~A z0(?TB1mQ!W@H1&QyyCikTgY|!8Xua|klyjl`{5@mKr3+hw4^0h#PzPQGKIE%lAF=o zqn=)M|8_1dRjQ|NA}<~GPBK*AA8UL3hYv<3HQ)9y3awHDrVGAuPBv^dL0miniXMM( z?=q>+-D)FRz#WMxF|waCHQY!pCNyjT$()3cCp$uH<4m~1o38h?rpLM2XE;P~c3}7m zd_uNhaBE&@6gF$VwmB}aQ-MIlX+ zFGwmpHLSR_cfK-h`@(b?Df+@XWAsDku9n~9<8OuF*>cO9Z1~&bPu$q^Xy4k!UszeKEe*P8MqGT+klsq0C*3sIK&RXPV z6ukmp{2fM!7h*^`UDvh5P(89?9ZBLK9O&^Ek=osPs{i=-KSb;n_>+k*J6P0)n3~zo zY4AJ{Mjt@s8QGWtkH57AjE`A%N?mj}@k;5@F&HfR zRZ0mNT4^G=;ZlZeA>|f&i$oKn?V`KDKAV<6u^c2xLI^4ag62xhx;a8@wo?KE-RBB5 zW4rCuLp|K)KAg48ED;B!y#2)q$9yl#Qre8)TnsUD)f+hZU<>Yb;8iigo6qH2|P59e@eq*Q$Sg1cTQlTtWVNf@eN zzJ0E{H-=i3fErU#{&JTQp)V!!T~tZeA;#t6JBW8L=Z8C8I3;@!YS&OS)!m=wC-Ye% zVeeL~up)YzK_?LM^&(j|v^}|hN^g0jK;kdQaeaE-&)S%n_ggkrZ9dNq`VgKxQ|Eu`$>}QRf z^pEi&{eW_m)t!NH*T`Z~dYfDS&tq;YTBnj@W&$zR^tGahw^)yUB)zRkn z*?OWGeT0s4quqMi-g-T``wA$Mxq%}2>5{GgyIvb{bJGvsBdj;Z$)eFrB-vUk)sf+! zS$0{NqX0$nX9OVqUsW0b+C?0apdz_Q!dx3+;fuz*l;6fhUSzk=$f{|iKMP3KmkQB` znCiPJ)zBm~Nzuw5?@mzy@u8cDMT=8ewBMpUfuT*C;BXq7_#srXnq0XC7#FE{)u|GK z;zMbv^l$N@B3j0a|1K)#N~t27{`0G^vd_!xfz+SyNq*V2PJGUlvgWtmclb{4xm_5m zc&5xhP`O8>W~Dtiaf}sx$J-azlNSr>#l_%6J0?-DhP2#YR1V%*!ypEC@Z$1Z&KL3^aXMEvgvWIc` zR(MMm!Ku@LJpbIp${(RV`dc$w$vv5HCPNv+fXVx8U{v%(F@H+8_Gf_Lf1mo+-zVis zdC`m8#vNVx)t$ST)yX6)tX98_uNgtjtq@6p>9>gni-bho^9G$JCfA}#wdM!2en-OPi7wGa53L{tKKs@+ z?pq|$?VPv%dKL&0qgH`!Q1<^9$N#@doQE=4;-Z*TrSy$5O}J!Q;a zAqS0ABLqP6qFL^rBh@cHQQx|?IY>TMLRS=V$A%%|4ua-I8hq3#1)PF<2^66@OvR?8 zwtNPJtOjP*jg^zaO?Q&#q!g?ph@U$EMtta_P4(TCYiQz|=xODNPt>3RMiz*kX-%n$ z_TvML@ahm8UShjE1X9eB7Jns;R8uuzRzu{%W@n=x=(hIsz@3fsTdd(ceR+AOYW(eE zUwE-b^t%&W=Xkk6BzSG1wdzXBA0Zg$S_p__q_%>X9;Hf2sSZy%U%w$+>#QA{?vPVjPI}Efc7X= z02b{LLXvZ*5pFWSXg%G?B6&1pXl`O~q24M#c~l_e>x|WaIkUkBl*kQ9;&otl<8o`| z648t?&MJH8AQr{A5Q#3qPyMe77w`aoRJ)vFB>Z4MfFaS$-TZ?UOi4LEh!aC~K z`AWN7uVhQ5Q`JJ`WQR$E z7h;l}Z!L0AyL-KX87eFs8gbAp z2$H`xFM5=befnLV1TAv+VV4uvfr+Qcz27!T_~nZY;5$Ac4B zqeniPh_6GhwQ^3;6(UnGpk0pO>2y=Y+16LFX)J4l};sCWnf-zW z&v(MugOEHUW*|@6a?kbXEPkKzn$f%g_0VYJ5>H-%vEvmMJG%~Ufhz)RKTeKl>~j`7 ztruMexh~Q?id3cAUS?cZF$?;*ZKDosVuvHt-m}BVA7m7!V-4+5M+*q$1`my13KPXb z8asJ&@+_$pG!X_r9Yr}rK&oH7qvqD%d=advYx1tF=3?f}+3ao41QMpbYV>gqJ@*a{ z$Np#7_b?fX4nTR4C)$rJ?aP((5(3&U<>d^H6!d_r8@S*RNlws1+05YykGCfy6H{yp z<<~@REU!`P1w87cN9eZ6(kPZUQUW!t?4q4Kp(L9Hx5~idrAU zLb{J0rlW-(5KwA5w+jk#61FQv7C6tBE@XMad9C7CUy1iW1Sz#jdauQu%e~$E#LYTj z8o!-8OymDx%Jt=;P$`E~_ALFy+e7ecf`=y9zSTs^6P_rCMdE|=$DHXwZyzZQxakFd zEoXWx2@d~_ni}C(O}+pBbxqv{xm!&FQcUJGbzXR0#Qsh(X+TC+IFWv$5X&et`azVP zxwR-|dtjju>$Af7K)RXP#$@I*t!W@e)`L5yM!opx3RfxcLuK@E`| z8;h+!e8Jk&?J*vtVW~2(oHFfUI|jJI>GXdV3xZ;NzfRQv?fnkpTnPd3=-MvVnti*R zQe8tka!L|a>E3Rlqe2{SGbOI%HH#aTqQd+x&?xQo;l&P)o?V|Z_2!$}ystY3|1rw* z$>->^SKMP9Py=O5X+K<@`O~}~YPx+6C^kNrDqps%Wpd2u@e3qu5x;z*{kLc5)P;X;+mm-s6&c!0#jI{7xrTO zEEP{E7P*R$;c_VuFo}igg{AR5KD`drCI6Jl28(()F;!QkW{UYsTq9(H|AgDu;9KD} z6_7D;BG0o^$@wEZPqM9f%$W3<%F?$kgcE@Inzt_0I3#7QWj4< zlavxjna=S*DHCk`H{M-t7iY=P=8VSqXbfmRu+8K=Rok6YZr%qJ@CDS~!0a|uzsoTa zQy0H)ygW*_h6rn3SRW;FNkTo^t{rg*QvHVc5qNA?eL)J!MNBAIACSWG^s$OY5%0R; zM;KoI2)QcSWuYus&%B!FZcuTX`ol1&0~{18a;7&voB#m*wUYX0PwF4%a=5}jLq$Kk ztQ0VpV|9KdgC~WyGvh)zOG-g8RGZQ-<5sEBQj)uvOPqnZmQ*}2q3(csoKRE$X+TC-Zh<>Q41~i;*O5LS3_qM*hx{%GKbFpPaqC&-(GxM;n zTH>bKc*d2-KwK`~n;(Br^WN!5ajrXTf16FSu>!-mx%aO? z!?-+W+IETw9Ai#m<94SML{~_@3r-{6nPxW2Hw--f2W%6IadhRkP7bFh#82Axxph53!nFEGKgh4)i+V*R_=Z}2O(3oD z9$MxD#B;WuH_bUtG9%W3mF356`zekXZrU`x!2y8iMv*}3M^4?ZSG#EiJ=TzrTG|a6{k?;9ewo?PB3I>SkyUEyZYAEeT71QUO zAOa4jfynKdzWWz8gXS1X2OV z{9<2QW~co_k}t`GQT6lq1tsgPDsM1!s|DBL{vz8G5# zHIE1x7%1FZay)H;J*6O>%eXR0=dJS)<~iZQENbggKb8 zeO@z_Up{}77=7IoQ7-{LA{8Jr`eXUzzjp|aS(J+ows-XuD1DNMH6ONrtzy!ufymQ6dR2z4in?Z#A$iPan@ zz!NUAwD<#LxCM+OKGjpKV6Gy(zH9hv)Om%t7}v{FvoYXhWMC9uRm+R0G8CIk=2mQO zw-H8W#O@r>t)HFudQ5G@RmxwL{>!KF-#dn9&p|_@|N3M2DZu-`V|YyV{_Bt78OQ%Y z0W9DC+$H?K+wdq;|L0@)PsySG%|P)7S;K!@y!;Q(ApgC~!1@PB{I^|(Kd7nx+m7M? z>T4ic{z2;WpAsehy-v$xzx=;GRQ|zL_>XxO{=K{K2TAmQ3K0KZqu<;NExL)A)Qo(1ip-N5L^!%kTm8nADwjg%A4)n0{d zjGEBAmaErEy}!*k45;s zD-+?F-CQtks=pfL;n{Z0-oVDZ|6(r1N>K3qeS~@=a@R22N$Ff(OkuQ2s-_Fu^Y1ovdiiF2VN9?1Fs{%sCfI@1lprf?+t(PqugNaTDj)Lm z?N5Ug;I|?;<+xsnU#NaeeREWb9$LmAzfH&d5Nzxzoby!zMrD%E>JW~L5vc}8Ie;lL zSZRmhKp#!$)PwOwL~N`z>@Wti;fq=6X-O66sNfJ`cpUO2ey-u%e3ypd@`G_k#UCF( zX0_ytGmNG6qmtrUDM2SB$tT$C>8yE0r!{jL`L zbxtc7i9|@L(f7GaSAP#(l-P9~&yn<0ep45vbOmZHpR*iZ;@+q4YFp?ElHzi*ItUbc zH`z)1eEWEat(%l+@UWfJp$YCCUA9GnP@*Sej{%dGk%A*Kdt`%XiHA{svqKniq|fdw zMJ8pwal?KxVzo`PFcTr|iYC%}`A;g9o5mnBDKt_QW@Pqun88XhtMX2!*WJ$>{dA69 z0;xgPH0OXd4LZ*W)GuqAW8`1fH0%FhO=A#1AdlPM3s}?a|FWjBj3i{$1LmH2e`y`% z=fl0JHUu(KYCoohT2z^pU&{yfsCw$H1*>3WZo$GBdWBz0MaQ$xPP~gfEHG=y`8s08 z{S`~>Err1NmxGtflC|s8@MQ?;nUuJ8pB!l?3pyK0M*QK$hgH;+i?Nx%`M&Uc=FQ2b zLOp2VKC1H$Ot^`b(e|({1!GNf2!C(5b`|01ahJ-JGq3iXC>vj@nxbDyB(OW9!!5j{ zYQb)^IM~quqRIw*OpOxvN6~COcqz_D`=>&Bj1eA^w1j*;7Em>Fy8~Iz79ABo?s!;7 z;-E9i$?r?|c0k5v_^=-^269X;Sq*`)AY%a+_F9AimE4*kjMdm@UumTD6-9OtNJgKyvKbu=jkd?rZ^Pd{cmyih7xRC-1} z($!^r#*UY9L_g1)o(tnDatG1VN7NmczI56qQQBt0yjXL)rRJ^feaq1ukxqg2VMisZ zwJ!Z)n_MY3?~OSr?YcoNLz5USNrOolEOday&=G+$UJ?9vKX5nWu~^)tf+G+7)S213 zqzyZia*-%4;(mPU9?wZu7TIwloG?$(CAU}waVZJG)J?5a2}FA+*6ADia(t`+2%TYG z6!+#9=XQ=mxtG%AGJVGaJEv$2Gr%tThQ=ltuovmcTc0JB~)Z?(RGAjIh@)Ac>bqkY9|3y)e|; zeI5VuypmNluLICjT#qBry>O30pKGkTY&RJn>7$Q}ZD*&QT~7Wfr`H6@=?!;`wIAj5 z6Tjs2DLB`*4>z^Z>%vrrbAX)w$1gcOVHFZuG;qI%{!|+FuioOvPz0bZ^D-sn6|$G+o^qv~^RDMmVYin?rSLe%X>66xP6ysjtQHPy|}9`H~{% zWDtFb+T8QWo{GiT@p0;GYl^3@XDQ8!6dMOQkNY_jUzGD+fPBI(`6!w{06t;t^BkQC zk_<^PlT+P|+li+q^zGs-B^l-4qjw8($;1WhfK3rgH)nng{u|=hv8Pf!OKh4VhwH;g zJ`#n#r)17;}LlrM5bs}=2oKedOm<^xJN`tmV2(pAQt=&SWzpuO+O{v@D z)in2hH8j9*2;rpAci&{7O)=0pWoLK_F`A#zH1)8P95=H8Hrrk*{w8?f`N)qk$kX>C zzDC>it8&A+EKo0Lzbu8-+#Y4!ls??L1aWg)(8!K85ID)CUi_dIe%+=lBlYx0`Rjhh z0blMeWf8i^{HYlZ`i7)&=^3XkCk-xieR|-%$QwuReNVs3=&!N+c&EV% z&6be%kOt9`4~k5HSya!}=K{{6mvuqwz{-eNeEX7{UTC@E%P_r9pl4Z)FH=+_T_Hin ze3uXGd5q?`?S&nOt7g~K75*e{U{!`MIagngmFXvPxUzFP?JD<1UR7ORgz1{j8Fj_k zeuvWNz0b3EDN=J(8?FPAJdyn~%T?h5AUTQ)SWBcSaBctEmf+9UJV?CzJI@fSzj=mC zt5AbHLrf_Q!CIU<+7Tt~0E5Vaq+;Sc=wTvfR0&t-dJFv8&Cdvwi0PRWxOV-jv>VNx zGaARRm=UG9)p?Pzt(_j$C$rx_$tqZ>=#RI}aY12?%bWRHY@f%Y4Vo`0d*rYuRjSiY zD8q`>Q02d0_Gz|~Or+p8D~@g)oEhn#3DRLAbW%KeMX|+A@d3EHHrVsEo!C-HHW0;S zf%OfP`MI#3WrpHfYE760b5UWY7m@g7qJ}bY>+Rzw$I-?T6pSA7uNXNv@%g~yk%vk} z$rt!*gJb3`xcKUB!<0rxEsc`bxCtn9o>Ruw28PnF;yqxrDUJKG8cprd_cXoRxa{7^ z*H7l&d&_?_;cOI5!>1+T*Uu-I1mcESz%t}ca(MJ1s_(JjdmXw|8xlSL{x#pX21 zwNQ%xDP;zjS>hWrBD4*`yhx`fx^;c9(9hG|&PHh$^|hb8=c9->Zt+_64>f}u^Lfmk=ejCob0nEc5go5j!gGt+b*W^8`+j>NpvA#!WM*TspX2rYLOA}}}fTwfrv@P*RkGkZ|% zmPbLmPJE{UDnakKDev&*Pw4JLmBux0iZ!EM+Q| zdk_Bk+0R{$ub^EHEK{xEY3W4~H#p9j-rg%BVx6by6(tyDjn4zIo<=EVu+x9*x%ev2lzyD-jT-CXmwpv_a|c8LSqccD462}vR+Ys+9hxQ zV5p79T|+Ia&Vj9T8f8I=ID#W_=k+PWn^fvjlFnK774}qv%oKz?^pI?LbtV2Xk+^fN z;nhJ#YwANQ8aMB=)0cbL8?Sf+2w%ssO8BWqH!@lFFXuvm3{NHD2u42=SxnY#Er`8z zDxMOJyg-{0kht*Tm@eugbdn40hXnRwnHkmRKl<=e!S;P$HLvZ++}(Rvd?Tn_JIvDP zwhlPt#8)uBG)gAKdlOzlytV;Au#C?waIww3zEom^&b4m;Hx^XYo zGQ<}3@una?N^yp^5DC?b0s`Lt*MYjxMnfrVAOUaR-#0k`4WeBiEohSiFg$(SFZ3Ot zL41y`|M1k(B#7BjXiz)z5Wz@q*<*suj2F20u_E$P`rWkh`P;aZ{D**1<=>3ID0m=q zTMJbDxE(Lz`5vqxxKJxB{4Pl`YYoPrc8ilXXrAO9>KDefHm%GuNumPYc*Nzv>eH(w zj}`>|&p*8mlC=B&W{x*(R@F5Q0P^RH9Ay8SlH+&cwx8k!kZsQIg28K!zZ*zB27{jl zQg8omAk{jFI$qsE;EW2p0myT$K(;wDf3wZeW^e%Hxo)-*F;%KqwK0C%u7k+@8SN=W zRc2v`%?b43V8Y4IhU{LK;bFei{G{_}oAWY%lCh3uQbyU-9dXo3B$j4CQpUo!glb=UxT?jd@d73@!W?%Yp#?#`n;H<7Qf zDiM(9;(>wWxlStc{~*sb{GB{k&KJt6%T8@Rs6!0?mgJx1xqEM|ZyNx4ZnLc|AxNIv z{7>>+<%VDK+}{5o&)qxizbU;v_XvQWrSEeZOF96&qb}A7mCWYYI4o$^4UAQ6BVP)l zm;vj60f_z9z&c=&-FIP#rX+MpM;o<^v;*hLK|~?@4wwnjyALiKkvT?*Ffc(%yt2A+ zSA8!+tJm50;6maULfNu}oCN_ihF^!L$GjR!!9*pTxCpEwty4ImOerU6abpfI8`SN@ zzcy<}Snr>1GxB&fhOb&F-EHUt^QlM>;Kk=PcCa=0WsXZtC0M zfIwL5qL!ORT-}k?t-D?4fjTR!^iH7zCK0`5utDMBwoMHem*8*bNtG#pR}P02kGnH{ zfRop;*GS$>AExwEkXH_dv>n5JxZ;}2Z0M^u3beZ{cFOS5Qo+5sN~ftHvz$Yf)9V^I zV9Ti-u3Cf)w><}L+$A&i2c4@CcJ$&*VgRbcw~HV31s#veOfba8`LzH8HGfH;`?It5 zKN$EY6E#pm^uoEl9WnNQHBoa4oLxfJFU6f(RJtVTehiqx?I#s?rUPItgh9c#A+*4ks24nOJ;x$WbS;SQ=?5 zxxkNBt#9R9pD)<&7Wq4(`X+Dt6y=~rc&mb(qaa_pVhdW)*0#ye67hDIK(h^e=q7xo z)}Sa_f^S`m0nQk8xgYlK2NcCDWbv#6LP>Fx{w|TBIgJS^-D>@N-iQTtk*mpH9%?i! zX4Ahs)YPIV%DPydl&{&0!BP@_^T!f(j9W3kSg<{ku=lb)msJ>osuOs3ZpFtTxk1z< z_Hn~k1i$`GB48RNOXTlAc)DusSW7GG#fgfOjcLV-B)xPCvctg-)~o@UMxCDayQ;qh z%GTrq%8im8Wt3KqM-R1KLX`=?4o6Il+m;B@etlY0QLs_*V2-TV2XBO#E82qyIrO1y zuzquxv*yAAr~jV7GMu1#DaM81VnV3r(L;^-A3fCYcmbU{{{C>&hZthxueMvC9zE3b z*5{f6;w{sn=UX*yrahQekiV-M(a@~G;-i)@5R%ufwe`ntGG z!8-crp=JVArtR+hg8eS&HxIQ~f=3We815qh5Ozb~03h@jrJ}sZrx=5W+vG?X59yiw zyNB8iF^13&1GFv@KN!>>;VZ921)Yg?#yxZJb*^SCPBB510TIdCWD@|wpL?kN)?u}_ zaeVC{XXN1c+TMZrr{4K@fAt6Bd8rZ-GC2?Q_Zr2hXyaDemTx66_<0B$TFLy3%}g3lj=@(;DV1i4^gK) zg5+sN;@3`)5-0Z?cX_RN5Vl6L#tFHH9aq;D@b!|i@`&&oaXoR6)cWjxuvn6}Xo##n zMRpiW4b*Vb5bIUCD@jqBW;#?(JBW=jBuzjG1}O< zq7c1xk)@c`m|`P0b38u`#T=Mfp9s~oA$N;4tj}z>F%?J2#kl;4fG;I^QYniIU}7#E z`Dy$3wa2V}A7;gml*+T4!1Fzb^A!&Q&dP(gw$$0j^{ZQFb?aP>s3@cpExD_?Vzg;f zH5Xv`OD{M4v68m%EbGSuFC}MW>+9n7vWPFKrMZdLFPxVgb|HMlfs1abEQf^&Q1JT_-wUny>N?P8MO{=Pr>P;KFo_Y2PTkY8z{_w3Z$V!8 zV)XeThX?vVcV(_&NDez|P3tH9s2pCrOC$;-F5WMeqLnb_`QyjMro68rJf%V}BZ)@@ zXcq&U(b8h~Fvr8IXcDRHAzVm8yfy}^88hBzJhOw@%EPF12~BiHV_MR;P@29MB#a#z z&;X&bxKJ*f*8U?9IPyU6_47kFHCp`qauey`yp_VU9h0G-yMd+!Oo2FMSU^k zLbXA+cGUNV>d&Rr63#t4aVqL@DTRSEA>0>^Vtk+W{8Dij#k8B96?+?-HVz{9S(aLAOLe#Te^I zC#JTjPA!iwr|rn|t5&kY(0+e6fi7vZK;XCM` zc0O1O8gHL}z9Yl?a{S>Gd!m9q%;->G?qWYft+68_v$VIz%3b#IeXsB_Loda^Vaz>j zh`m?xv2*+nvMLv^|rJUZ3gOuZe))pl>)6EjfO;NAm8t6%AIa#H&AP20E7G%oG`ydk zRUHb7xl`jybQRevUsO!tCKzuUE`<%=lH{*$FSn)AsqHc+-ITZc)2Ywe!lpc7e#8z9 z1<$}{n?fT@Q;r_Si{!-wZm&`M3XhN_MGo$C(4ar33s-$HOXDF{r8+i`q8K)D{i;o3$s z2`@W{wE=m=z?dQlbz@INDh}d#whBZ_M{_BtiM%LtMRs2B{q)_)gzi<-`hW^CYn-7{ zc@BrBo6QGyC(BQiPOq#Q5lW}jm5J+}Q=TVU`{_B;231N@Vk4a2Y-N7GUb4XOcqiIW zX82vHrVgn*_v7gii4(oiLNP@=vZpf8^0U5AHk&O`&gERdV?=ol#e64x<4tibOg76G zDP`UpU0SB{N z_7y^`D$dK&{g5u7517IX$Cf7P%D}G;UQ{KvhJ^JgKxeUp+CKYe*J2z}h|UDt=!2e# zV#iAi6->}efP6!-btGn)6w6jxoph2Fbn)LqV2$ro68&s{(TN#=7NO=b|%;gzxgak?+W? zW`8)D2ai%Dkla6c(~JSi3-i=Ls5_(2rLkRK12>*+teoK-Q*l17&N#S2JFK{p+i4mZ z9uMcJ`8rta!dfhqC`>|4_!Hh)9kR9%;aD;hH@HO)vbGpOp(T!p5cNB6L(mRCRg$me zA?mS6?eNo{oT~R9C8g5IR&<2V%?0?|&dWao!p6kd;&WKu#=y*kk-KUkVDskK=I(yTD zd-9eWuey7yV+S8=b9Wc*K$HN%`{mIt9Z#qI*XFLZ*u=h%bs&FD06gsY#aq@lEAs6X z${JU+4`x`xV*R~Xv)ne!oUd4Ad^a@ee9-f|+ut-yf$_{Ug0UR(jeuT5v_ZfwL^GZAuapyo3{PgxaUOu zRzt_wh?YdZDY#>Z=;uKYgu90l^-&=C5eb|N=VZDYw`2}jllPt*=VZb(#c#uLFZqXj zfe66`F$IS_Bk097&Itr^mCu2LiXdZ}76y>{Hf4+ls6A$F3m`g`A zB?8j^bByjLHkXcSjTPK=ymy_XCLP2jK6M=>CmqN|Y0LJ+;E{Vyh!^N07w19V507d| zDA-Fvo*wh4u%}bmlpgggGtl#JYr`Wa8sThP6TFfBXfXnUSh$6H%O zEG*zKhXk@~Y zDp9ZMd9X51*X=!ZD2F8QttajD(3OUc34OzmMG$?9VA-FjDJR>=46$y%vbAYk8f z4+We{^AFUQ&?~KZt3-a6S&fbtnsc@Odag@>GI21X+)Tsn<|Jbi!%4d&S2EULQy!U~{VKJ1#W6hxDlnvQ@Z-cRbLw&8$Aca7 zYCTZHM4J0{b=f_s7cq#jm^(5|l$fzrVz>CnN4-s2n#@j9YcsnaHYw@X^Tw#9Vz4=Y z`AoKrU$d^J%pL>P9j2)6&y1T3lCU;Ik6JP9+P6IT-OWMsy*oM~MlyNQ(9Ek(_s=Vo zycvCBiSB!5<7!@(jvZM-*(X?rAB-1RIADJl!}?4n=IiLiJeREug4ur`fc_()pTg40}_|b)AIIp zbdLC!Ldo#+952Z#10%i@F12D7T`d+5BKG)G-5RI{>82U2nwE|>+s<-XZ4`<&_tB0%sIfSVH?1LfMdHrE7eWeb|E_B{j;8R8*%5{ z=gbz}wU8%W$bS<1*^wn>Amy6tjadufk78OSQFx5&XlnkZ7q~hjAwV%iiI@o^Pj6DQ8yL5u{xsA)ts)oP zNvG;Awi;R8oMC5;P7?9bt__e>K`})pzl|%-RLgFYayxtn$PvQxCX`Y%C0$J=G6M1p zxtBFrv|4q(7K}c5Y2z#6Krl<8}sqX9n(+eo=QIx6HGhU*XoyJq0QCHv7G)Kzzd3K#hIg%$@4_oOtYylXN^EX*$W_@-vbzQ-_7LTRKU?h>rvZJKK zGQeL~8Ld_=dA`kuNmBFK8*^d9w>oTXV2)r$Y*`O%ju(+vX9X}IjY4V~9#a*;*Yv?~ zG##+KwChrYKfR!sHcgzl&@nLPahOfo>#Q4io#!d&>+l_T3qUFfTcR5(VqeH`y+ju? z-4&kADwK^~;9|!6^?e0DLf7CtYeoA0%A?It#s-0Lha&u)`}C$te3N68T4aCSjLTA$ zqd@<GdCF`oL%-#fL)!aZK&3- zqpRr*C{?;!%}d;2oL|>(A3ubQnd)D%xlQVsW9iaAbUo-XRsfltKHO`+L(-Vq?gxO{ z)+I9IbAWZXrQ{k#2KM`|WG3vj)3E!yFjx62$5ZrO&e;vH5LD`3ztbG#%N{#){Y*A5 zr#lZjK52w+)Qvp&LiuOJlii9|6p*X!7Z7~PEpA4m1( z8Mx>oBD@+%jz(K8g%p_5!%<(~!ncFjeSktTrDa~bjq(BESj!);Pb=4S!Z?JDTSC(p zA+yD_s^6AtL)rHP0UP%r0-`kf_HZ|i4a#S#b`|;mlRjHVk;r?2@0y=sWB}xE&I^)5#eU2lpb_k!UVW z#`DbrW^M0o>AjP0|jTZ=+MQS>M`k^A)ixQmD&64GPC z<6J|vV!nf%Gzral8eOozcwO^g>rQ>QqP@Xl2BajOCh@$ik!0$LpW+;i;ZbSqEBO-0 ztf#R#0Q;0n{!F*eaT2kJQ#3~2m?Hi%dS`>Me%d7+pktOk+K~-G@F9}ZXU|1A)cDk!#Z;?(NVMydeny8D59I?AW_%!p@;%L6#k>-K)w=GjJ;%AZd%Pg*@-_qYq3X~^?*5{$*S+WHRXcB6Ohh?ZJaX=%lHYu?H= zZkD*}QJaXLqV&HqldA_rYIC%vBElB`F+LLaJc>hiB{uY2c2PgB?$_c^-A?QkYmi(* z)4|OS!sPrE(e{F)Dz-tkK5*^wGH-R9RWFN>H-QNHGz@x8fEDO>Qxp6Elw z73=NBOAnOj{=hJ1YXxpn!ZgI}unM4}V3OAoy!=-vL(N;j;mkDcSe!>d9!2^?C@}#rqOJGZ-6wQhuiGm3O)kAQWj+72kTL z-ihd-Gpa^veWIt-srrvplS1}4#_x?T7qK9a8d`wqVQ~41JeZVa)sK6vK;P$g0|L5G z+_PsDqE<`po@snem1v<;?!J)Q#gm?SuKAZrpdPS)$sbIS&aA=0U{EvU@iM!?73tzM zrdMVx60l}>%Jn(Hb7%w^TVX{RZ8E&NCq9z1#@W&z_0^2*LUD!MKYe?5K-9koHugkX zWf9jr={30JshLj~-hx?75aRe{MdbiLnF;4S+Vmol;+8qi%fVs;-aMlTNIn(j zg8;JQmy*hxOIg?bo`BIWR5WGsY~H(*S_7s{Azn678*19It?oDd_sU44%^f&W?N^Y3MUZtT z2}dDN*Fw;pOjRndOT31<4hzdesm2}y$ttVyjO`pr-H()XuXl~{b7m$REUHx4-g`ih zWQg^!ZFcMKJg{|(pZ=Y4fOoMbf{eali=}NDH*Smgqno2gFB6Qmy18OzlDEU#N( zVe57eVyiNBo?LPz4m^!ly^8MQh<)$*_=rlXT?}mie%1P@<0J1fA7WdvtenLy_t0Uk zEED+&U0a5sVd|3Q7iO@XwgQjP#wI1EMiwfgaSF4Ju)pk%srWqYV%09p6}fNFr_f3B zg>o{*26h^3F=0lLrtAAcRzyTc10VZT5d^q73W@8*3}d>#Z@M zy=(%%clB|sPYV{`vD3OrUMI6h^hcUKSARUiRoOm&Z$W|A!m;A;$9B+2Mt_Rx!n)~V zL56{W<;C}EZ^C6nWqC-n3b(h7dXFnDw%Pia)~iM9>2sj}dFUM=W0n`D+!J;Y^66VT zln@P<$5utZTbS;;7cnn|f4CL%d={k|f2NZ1D^*v7n%=t{=?w(U`+4il!f6*~l>UdW zCjBA}xJeYL_S3@_s?M0?{C&!&YIdHgg1g<+?zq{GgF`SV?`r4t#Qn$+m#h@5eAr_w zw>P;D^n^2nS)|`jdA2u5*oY)m! zpz!qjZy~YNsUBc~QarO5`xggh&u20wBaD1f*R!qnF?+-B6wiXnV)<&Rq?23OAi;O+ zVd&|yvkO3inlnF!xTVX|3r;XT`08TWfo#dW!*LiRGJNiW#v{Rdb%H4im6X1)D)LU$ z3~W@jLUk^VX|G*}E=9y)f4D0dzb>w&qfr}6X#RLU?B1WB<+S6G7=+<~0!4O(>aij! zb!P|NJuw)VKH6mL(s3BwKL4vL9%j9D-;WFcF!TBP&)Pv$|LCy(fzXtGdrtosJn_#b z|C-*ZjA4ZSlurtF^A%}XFF-|%Ez%LTw~u`R7WA|Uv_W!IxZA|82?PZPAM(0fcZ;Sj z8{(+p0%Mwx{OJJ(Q|TI6n;9hKqi&l^g+7Q0j;a*oxf@o9D~0re@h%H7!xI(Wa%&zx z+s06q@?#3J`cB1#LRF&)aXTJxKVmp4GRWTAQU88+m)?4@NpyGPcr}#mD<`2j11wn% z6s{;_Mt9<{`Yi~^b?4AYHc!QOadHvF{)+JsUg&+LdJ#fXUwXaYjpv%g)Uxi+MOXbT z%=pj7^S^J)zuy1ZkbO0cPgXp_`z0mB$7%u!eV75K_%s_VLW5`~j$e_vR*f(cN$33? zE?xLJ3G56l1yXN}=w3QIb2P9bUY6Y{RAnn$AquDLD&(g}gGPd3*6wP$T&2<{CoL~0 z5run*>ywjqv@Q=D)bJ@qi2mE8>QjIF)1>NCe|zQAe#eRVxBj-uq(ts9nE7eAvBb=8 zld7nX6V8vn+_x2dB82%ugeOc%ckxNI=6g`ZS*p29H4;YF3KLxMuKwt6r`j-Uh*9P3 zeX}B!noJ@aaw%NjBpi+sxBRqgvn)%d{>?`llpZZ>;L^jx>ou&EP-!VvU{7;03%f5= zMP^+fMxh6Pt;75D)R}_BfYj^{Fmi-PSdaQP=*h=G0zVAM)Qukr*Tk%|+4+hn4{YNE zBh%vK=EyzTj8@Aqj+busy;|<2esCz88D>~PuqvF)*iUwY^w{Ug}i?sqw_Q-d6anR@G9!`I68m#5pyPcRrD!i`B(w=|_ra ze{OJxx&|jTv77xv*b_EFud03K?iefJQ&OH^Cf!ga$Ci9z3I&?8sgdb)nks=~+!+@k zQK*^oPkZ2p(#ev%3%{*A7cPI`9j>6;UO#xE!i%w~eI!+ppJB~{>Px9XdR3S-U!&-q zCCV}6Ll|70>nUIZ@I@|yEZRA-5xyGyG5UFR3p*nYy3tE-z&>fHlL*6>nys){VM+bk<@~h zQ{pY3;pGp+CZ3y2d;G~xu!Q-$WP!4RWX-L_M^nL2BWhhGw>((28Z`?oC3vZJ zlET7h08Ocx$vVW+Qu&rz_RHG881`F7j0mrI2)~VZCetdddL;jNF{t*nbV@yMf$Gx! zHsbjRI&Sm-WY|0Y_G0jG#ZcpYLyq|X6d&I}R+l;NU%3OAD7yZt-`Gd+=4E8MV z03Gh5D^-I;vV_~78Wa$n3`&$JqP})x)hE02& zdXySs?Z(GG$C)wqMZj(HO<&BGT1pl?K+;j zka~xc5Q^mO0F)@xsT9-CuhlHse;ZGcdr#s!Zx#&{KFvu5}wy;vA9fxlM|nho>q3P}AU9 zIZ~4yhw0KnWi~gn>U${dCYVQ1R56ZXfqfYHFdW?|_p7>D?!&>E?Pu2gq&qiOGGbUr!DIJ86>KBN_PppYdbLZvmVt`+$9p|i&ejQ5t{(TxIKxq(79pNGkUN+S|>GHO26s z<|mge{fFP(evh;WZ&j>CptU?~j6pb59KgDeuh@gONv?kLRA2E@)O>-+i6rT#Nssvk z6|CiZi{|?W35WN`1OC^f)+gHq#IQ9YrwZ-IxzLE%6^H*EP04Pxl~4KO zW)d?yu6CGj!gp#NsC!IJXtE|#1`H^HGy@8ozbTPLW9JT9VU65KP}*}?8li?Nd_AUE zuDbwHNGnUycw~CejPKkE-9(My|MgjMdwzvvxFQqXHmTg!5M+NF((I|%(szg3722Uykqufzl7&N= zmUs_EG&kGFTCv7>A;JynB&2x^MSKgFY|T5{7U{?b=tSqlVh!^ZNBfIfnyIIz2l5$4 zQxN~%XVxF-f^k(Fr(H6Hj}5)t?e;*D)$rXg@A!kX^a5Nmih!~kbKOon$*LxlQ zI>Vu^w#8HFb4wGy7MA)llR9h<(W;Cs!|s^OPrDJ)2);i!l4;QQv22cWKL|KRRn4a zkzCTan7m}wPZJ--VSk4b+6p2(&suzzpLYsYLlkO2P?E$VXAJ_MvcVMGu=N!c-F=y?fr{5 z0rqf{usx{T$9H(Q zy2w&c30SrvCe_8QK`t3mFOB{}CJM4rkwvBEcLf!$>UKZG8TzkDY;!Zt$cr*)>p|je zic^>+%oN`onqA8d=tPlX1@rpA<-^oRboG9=7JW5j^(mlRMgT08eTR1$hQQ)b_Zoi! zED*WkN7xik#qrJ(B7RrQQ=q`yN;Hpo-mrXv*2XVAFhGk2g&Q3>XDNpa9_Q*0Glyrn z#$b0^gJhEilUUVwH?1(1*RS&=`NsY3mUTxy@FdjP#uB?mGX$e27Csw?B9B(68zVT=f4R6|0l_X ze^6n!Bu;>Xj90^aSmH~3)W-Iw#C0>E?T3mApK!07-O*vh*1-VZ3U&?noynt+qb2(iGEYR1+MlcfrFe^!B$B~N|&111N1)+y!+bnaw44UsSn@SrbQ6lStOlr9JrPKg{?kNUYJp&(qNL zzZnPrL4xO>Z213^;^6=96!0(M^UBy>%YFs~;mc>JTrW~CfBEop?HT@@)}TA!poSz7 z1czM?(0Nro( z-f(!%#;tb)!S+xBJ!7{JaYO zx{aN!;je@`SY7^6Z2dizsReAn!`Tlr*wwGoxSvfP%^Pzg)B7K0fJT%KfhqIivZi=M z&uy53j&*a4m=TIP1%%pc<506!V0ZP>u2VoW0?j?LkK)-(NOo1NLW(Zn@qoTVl?pQ+Faa#m&Om)bw`gjIRI@G2q;m3+4D*k+Ye0O9RoN_7> zg{mDrVi#Ah%XV(2C&pp=b&g7!Tc6g^lA%Ucx=M?S->7R;Ru<;^r>Z`!qw)ByB)~M- z$XHRK*l)+R4*s%^hPcD}r+esSn$15uRttSCO5)E~|L9nCO**ghBj!JT62yr#voa48 zqd(w(Zow3o+DTLtxz@At^svN?DT;4LGxBf;RvJQUe5+=jOu8i~T^F-LJD8r3zXfUT zc8x|0Lz(~-4PU5PPtoRJnVp!kT%^4CDP?c3p;IMV95S*{>@Wv_eI&2X5is;=5Uu+4 zDwLGD-PG)~TQ75R!PgsmJ1PSdrOsR^s)KZ5#X)TDyvD?`%SCtbJ1Z;Be$N++HLPF4 zD`6GFN+I}NG9b8!`Ddow(#vbc&*KCjZ}ddA_1DHJtiyYO*97=t@H2p)KG0%SR{ul9 zXj!7tu7>)Zpdg}L!lj5nLTDvt`!qU^e3D!jkUB(|)|@KEsdQHbRH5T)u55N* z(BeC?1Z-RCOFy^{Uf?BC7k~F#f%clzDpVJ0QYAmCmQ@R8N;5~HL-&0f60b>I43WY$ zXo+%_@*(|vUkw3AKVGUB7nT1B4R;GEv1beimwOR90j&{8+_&!uU&9nypm$ zrG;{Q^bxkf3EpcJuF>LJOw}k8N1EF(U){I&E`K!lpXE7{#b3M6s7_p&GGW1fRhiKE zd6fz6Qdg9Xp%vAHwB@XWl`rYWlA`3<1a8SHt1UhY4>yzPvSj|G? zNst<0+Zg9kptR6KSSspwDuy35j4SAED286>bA$_ESu^YA;r40f$^OM+9tZL zn3DeJO-^p#IFDyb9Y#BmLMl)>1rCT9F-}B}VRTB*$A=dhfvlO7!xTtarG{Ln&a|&? z8geM3JnCcfAjRgTEmIRg~22}`keOP$=eUqeJ;4|UR3J4MJP{}xrr>BP% z)ubv3Ru#QdLL@hwBzY8)g1!g^rGBHa;v+Prnn{f-X(&IUpUUFIbA@xkg_UU&^}c&i zSt)2XF)VGWalD|!nMW58O)jek@9QXK1De664@;_^M6Tnd36}iK*gvQ`X^vB5-YH+K z%UWc2JZa{!!lKqg-3*7$ie{&e_eD4JRO#S1J|$C*s4n={*GvNJ!f<#w(@K~wpb zw5k+6VDTMoVJCC8X~NO=3jhljhl3{?@rDGFw0VJK; z=w~o1PRZxR9wPd}D^d%Kc~y8s!_ujes#zr|)4Abcvbv1h&rd{fcnQ*R5knGc zOxFyUk$K5+%hZ@=trZ`ovRlEoKUx@=3bzuuLmnxO=NRe$Im%pVcFLx3Rm;(^Lk?-{ zz>UWvG^7%=jTxM$^pjnohtr;4D-YaJoR?!*7q{9o-#$=zrN_|WJ3fYyq(ls;6+$xo z2f~%^7B77pzGP{f{oo;|bbWO|z|%R)F8}3FdszUW#rRsdrAX-C;@heW5psR3xcwHU zoMKldT_aTc>*~;7@fY&HSe0Q2Yo`;H7dvH=wwzt+Ksi}w0Rq%_e_tf9IO#wmTds@7;0+&%Rbj<`5+emRzfrg_gxsW566yz^b@hrx;MLcc;-o z{JF#{pcaiXF;|QCpKvp;EY0)FD%GLBgvxLMM(B z0Wo_SsVAGaRToAY4=8Cf1-NU7iAK6_5Q)aJPTIO70Z_9^5{X7O;g;VNgY{s|Clr>B zg(-%ld&+zrq<;xc$3aClI@}?IegP9U#U#FKC;7gn?|~rF3QT)gLBUQ3+R4+Afk-Nq zdMrhB3k8LGlUOFm6oTgLx(ijs3ygH(x3`zl<5;1KHW=JXBh3K0D_yPQZf{)_F70kP zTbDv5X(LO6ugpl9HHV=b&HC!}TmT?7{>6`fYfYM6*bb)5mTTT{Sy?;Z$KHs^iwg^{ ziYd`($>mH{<>=0jNg-~V@c+i?^_*T>aP#pKGqe7pP~xwXYRKd^ZVaQ z1eBRwnkgE`$epz15ik}MOjMV>X4s59(y+YKRq%!chs1{jSrKikIP5weCOqWtI zbIz1Ffk88-8=v5B2;hxK4^G0I0dL?>ARO3}{NN@TK!Rf%sS20}!;HfhO zgFNe=k8v>XHv*qOlx87oWDm$9%h4$F6;Fp5^%z<8>Dl`$3-Qy@y7hc~kmdLo3f)1( z8g+iG=ca|_!`xi;JWC^^KSG{E=ehOc3i1r^(W@fV%XH47M}8qkR9?%1B1VXhkX$2> zRRL8(<(XRrqdLVy*kX1%^H8mFFk%55L}Og_YqqHoNyB*u$c$ zj&V-(XjjA6`G&-}`Z?R3svOf{T(zv7F`oGvtxu!eAxkqpz`6R6v@8J2lU!Rl0GHR( zE{VktxQ9k1c@y6J@8hGC&r{{c*7Qa37Ni!9GnV_(n$Pe)KQpE23+AQDlV?x%eXqw8 zo7&7>rcVsz(DC_Bo7%G^D9%Bu(uGQbm%Z^oRGUL5f^b=*Gmf zkCH)G*jNT(^MJdDGRRXpI=;Ihv=)6=z=paup7CVKg{cgR20*bauU|VaJmPbQ`GE8bQw4wAX*SAHl?(?=|xkwuz z;-!I4;b{6~dH@@8H(stoKArSgnxyT%G5yB(ta2Be-md%K2Vsa0OUp{fWh|w#0UP2Ae%8e~D^sjpvePfy z(YR@QoDfbq5j>C*v{``cR_O%F!Ki^Sz$o`&PeIuRKfftGRdaz)Q zIDK=*QT=w(?S~>pfa@h;h}$TK?F9G_fRFbd0H4cK)q;h0&3A0g$^wQ*K}qArUrJA| z%Zk9L6IVIHO*`x1i%!;?^PHg)ap0Zkaq)p9Jw1j!WP#LXyy_GE(l(o#^(;9e@AL!r zLl3<~fN+ixDCa#~WaMD)O~kb;Q2|?LC^*s!#T(|0R@QFnVvtkOu7pMn@Wh-8H<@BI zrOTW=R-3+Vd{&#@*n|v^BK(Bk3HN?#?O3Zv3+tc*E4g7Gx}bmmX6WEqzrh>pNks(vho7zF~y(0q6__rWqv$h+B^wygF!LCEz z>Skti(ynz4tY=|AV)#bE7o<;MU1;j_@jD#9Pi*IxsPhS|!=7;_UKjaObk)sSCb;~m zWq8_3-!9m7E{*7W(2tTY6TUY#^bf$t&oktPH)2&09ni{Jtl4(h23>lQ68#0NSIwK# z&(G8_IDF$_h;C8_n%8*R_Kz?Z*tyXE4XzJDkifFDtD)m3+dKW@8JbmR>Pg@I&aHi* z%H4ad;E8EAOskCr_1w7)Ym(E^7%;MtD`WfC2Tku0_VIj&SxMogsnw~#iiE3ktDhW> zWz$-QUs4#&M_)26lt+|gTY`a!b;M=E!nr6q2yd7Fws_;2mU%aAt(sJy8(kE?Jg;)y zeV*TiX6z09^9s+UqAf8?ON6Hz7{fg_|Eg;KiXuqSTcAAtBOR|r%5nC@-{_(NyUdXD4+SMv?}_hmsdfXq$u zH?HvC#@PR~)zAM2p{2%Mt~QfJD*ubhV&!asMS0o-I6{P#G7OA(+S%eI0GIAe!%36u zO8=XF)b7RZ1sDFoo%MRC@!%&mh(^Kn2OA_!b^~qeY{mJ54U&FhgFrSZB4+jM#h#WT zsebhhN{TrvoIHa|Gij^?c$DKL6)Gbv1HE%=+cPoX05%{Kv(6FNeWaG}_fl_-21ycP zI3S)7105j3DyYS0oq41G#0Jx>+F>cNdHSq4SVzi6a24ho@w+(uP(QL+T)V>c_dc(kxzIbpWo^>%vJLfj{i!|0DcmiSnh;&n1ULj?n zn-w~3_;{({tK{R3v=4U!y3ueZ1m7SsTkjyZjB}ILq4|UcJIK;VF>9i<>KAz9nnvLXXPFZ>&7FNl?7c7lO0MR9LD z>t3vNYR(EL7OJIOhJzv6AzSl%6aw3G44koh^Y&TG{SsXY&{$BP&Q8@;1W=-B63<*< zlSgvI6~9*%+rG;tV%vQpgGcDu1+a}QnOk2BrXhuAG<%2nGX)qUZwr-B-5BFTQ9_~U z`AK(VA07lJSnmVo@50-RML^^;U%*|AL7;IuI!(L*XNZIeLis*{!L^BPf_L4V-@qX1 zCoot)f93V$U62M&X$#FmgWNn_L4<-f2}_>0J~d#oHs(OCexOqORbbrMLj2^Ve%PJ+Sksgx0|)=`m)1eH9VW-KZn4Rk zBVyY@a{7TBvAe|V*2k}ic0E_2;=?{h;xHq0dApVdkbRNLt&JG%dOIn&$2_23AJVtu zOu`vQ-%Nk=+|>$scnbe{i`JmLgDxnC+e$*|=1{=5IEY$;yAO$uz)#Gb7lRd!{l_x= z@2Kj(Ws*bwj4Fq}E$II#V)KV4_FqM8{_UfGA$NldqrZFCpi@3wg64Xsu>%g_=hl*h zQz{aC{7bvwjiiK>h0m{0f`s1~8W5&_=vz3usCLo8Z{7-b^*m1>g@;Ed0ktunm%N65 z&Ctz;@+?u?=aQxxK;(;9h<6u#4TmFKGW+4p+*9FgN^{gQi^*P&prttZ*NLR!(x&pg~7b(|h~GuW1otU*Jy31`?Y%II=}U`}})9{-Y&)?89VLBhcUYLFm|fjJ=q zw!dz7*L*}-|04^T-d0DGC{wH{4dCq3C(WiVdiGRl81BBzGejZSeE{C`L88N`!&nI7 zmie#?Q(uv{kvXMhR)f71p}6X>{`WR>Pu2yrufnL(GsPQGr*bs~kQR@^_zpMe6l1&B zj(Y$hUvT#dbwD%9x8emDTX^cUMiE7NL~NSvMH@dUG+!0bJ>a0Fq~8w`DmN7+?59`@ zQED00(H2?<4ns*Uqs5<7n1GT|Q<`u|Hx(x&rbrtnH1J4znGgSRs~nfdw!!De_9r+8 z*=a_tpz3ske}PWB+WM>YWRuzwA?1o=+_FUyAtZvHx|kR~Jbd?M42sK( z>Abw1L{J4M_VcvEkyns`?t(S%O}DlV4r&)Usv&7!Mr>?1*^IB#!>olDk;OBlgh&uL zVG6eZakpX_4m{#A`a>REq$`;hy9F!yKAX{^U5Cc4x64S4XU7d2Tst6J( zf>1M^)S)eM2)ya@DQ{!fdH(8bp#*avi7M+OGLj0OpS((=UefGRqp@^4<*A!$;f4;j z={$QkZH0#zf@|3ruuU_4Sfs`84-ke;3@H{eR*h`^x0Cz~+_{BaBM*`Yd9+R5^eYlv zXlA;Pvo(6jM%(kG%7K@RbACr(?SZFQR~_p27aXwG;NW7O$2X>iEy>cxJFh=Qi7fYo z2&?XWYhDv75<$b-o4=$R8NrR0nr4h;SIcN>ozi+}r0kWN z5J2XWECim#vcudG!EyoRaYW~@5d_%esEXfbUHzE-@e>_{TJLrGMv)5I1tC7}3`w!v ztwY;t52dOH{{3eGp%#Y_r_l+_fc>Dsi6N;pc|^Ak1#fwKlr>3w;s6E9=Zv4EVbDMgmQKJl;062x-~geH0yS6M6X zbIOa@I@PSQP_wzKgwIAjSEO1~B4J9R5y_O@FFnUqyb7>y-eR6x;m&fUaFrrUdAF=^@)y^hU5krUE%LnkM*Q64{v*2J{WLK_#FY~>RMNV#j0D>)xt7~h~jChW78JlIkOJdh40GRa-C#ypq~ zhcm%P>VX8S#kyLb>_>9(EBZAaO6V*;b+zpYUex<2AHNM{-0f1}7Kp?h#-wvh<6YNA z*fnkplJQtO0;E6G90OV}^qYd$c)+LQ_rQ5B^#WQ?_oIQ=^y9g<;B!HEEm3d-M7?Ys z0kl+WJM*^*=(SNC^A4=h#OFM&f1Kg}eh9B!;huZ`y_TJsrSlpJ~D;z2_40G{Cq^N z9sqS)y`DGvp%%B0KwUww{u=Oe2n4ZSL*S@7fJQQO9was)HOEC)`JSwhpbfd2CKRPH zrBsP%ge^fWY*JY@k6{|f*G)&@ErYHht80*x9XVkqRC?WZ<@VUe z2GYJ(-VYe$7ugz^HX^?s*fLB%!k$Kg=Ei1pTX0s{2F2gi|I9SGzdK-F_vgH!W_b1$p zyI970cP#{aRH6DJT4(e-1LLA_p?1Q+Z&N60fK_}PT`^b0Ve%~RA?ObNux*t| zZ{5kzwuQ-U%729*vNDYzy)mU>6{&Jg!w3wQr$0q!2c7cy1Bk;j(l zT_;BiM}4dL;9}`)e_xEvq)k1p5&d8Z%cAl{5dF+zSAK8tE}W|?8;i!LDooAwh$pgiHf2T38h{|*=op= z?=v&+@8@~O%)H}!UBBO79oIG2xLnW2eeQG4ea?CAbM8C9r!?!rq3DtHx$^DH$OA$F z6}^!TuVK;^#x#y;lg_c$`Tj-|QfMngQU18L^YB=>;>pQP*S~Noyo*v&{c@vUZMLlc zoL8&c3eAuE7x&)0*LvvovQaTpSE1F{r@BM5OM8ot@-+TvR7MW80oIMzqctvayr}6(b2{1b1j4^7bB*rlF>|xkX+)~V}6EJA(L-Rnu_x*b{ zj0+VLC9(>U2L_y;7!OKohs1B*BS+!->^zM)xH+xZ)!f1{P+MVy7G&fVLUR+rC z`KX)mQ(kF_H2Qr#SC4S<>xyTng{c+vsj$R^0$sWik-GK^CL{3~*~$reX5E`K;15|- z?PBSLGW4FX&`Gh)G`c%d2bn!*I@}`gGArr21w6p|Ma%ir7nhT^Yi^gj)TdHesxLSo zavkw{$7*u3>0jZ_+FZkb4SYmwe$T%`sk+!UvFk^EEn5>j&Yjl6b{hGnCSdXSnbxR; z?(+7#zpaaEMq&&uQSRd_m#Mhl$M-me=Wf3FEtQbJXW;KTL;5tfj=w3|0(bf-c$4m@ zyd@344pj_-$VCAZvQE@ zdHZp*27ePxTCqN*^i2(sTK&&;+kI!_dWORjuiP?AchB{9bj`S5Zj+Zm6&$C2;j?B- zZ6@1L1VU_8%)VSid($YQUn=|RIWBbpXJ^Vit(q@aX7sIjXxRiT^c|DLUh^bg>~*4_ z8#2p$QFfs&9X3GYFwl5VlF9S07@u)caEM}<~g@N;~Wddn+@wsUl6rna&fqxX(y z9bty!H;T?3s#XZCi~ss7NN>=!HX%Do(7c4lj;rr1rOpE#$r9H$95y))pXHpqPs{ID znf=Wtcb^F@kz=OGCRf9*sZ^LzZCi*&p++v67Ssqjpq8&t7^>Bqzc$up)?THM$%Bt` zR6Kanb)xi(&d^;K9<7td65P+meiu7@N8F{4q1@2n@ey>tv)7zr(l40i+m-uEH1;q< zGgFwO#){7~RVq{7BMG-@T`axLddnuHHPT(>EE62OLMK_+Vac**zDmcP345Pbf0j?r zCFtw*WWU|_p6#4rMcbHp>aJen?HnF^Wzm&2c;4@;rT#>&M#!1Ma;NAojP?P?@25ID zz=ao;I)uwV_RM|Atm2WDd{HwzbVovV=ESctkyw^M&qf8a=|@?vK0nMGG?8bON?}Jb ziZvGWBY#Fbr7DKW@+2$Tup7QUK{Fc^MD0S)I3>cIbmhWP)6&RxuG;9*z$DvGBj2p- z;%6%>)3TCf=8bLlmj0r@Empd}$kW#*T(YjhbXusw*g*1=m6ha@Y2u>B^w=s}Q0Hgt zwy+lQTu=!xKStc7Ug8rlKiD&=-~O1NKj!C;`T1jh{@=|HZaeqK{QNOLf6UJx^Yede zeprB);&vXkMqVDC-Y{DaFMCXw?I2?Vj}|xsaD2h!L23c@K@PPZlAJSVtNdA zF0Tc}tyY7ba(M^4-K&Q?E3-e2BCN)GI5JT;xf2T>Yj4&}(aq?K68(G~p}Hr&MKCdS zi{==`jhM=k+$zhIZ8uIiHRo>Oqu%}TL6ty-W>uy!3}r++Ovk|X$o|XL%e&8uIEOE@ zi{uE|qN*s^b#ro(tz9iNysnv4Bv`EQa!s{j)QR~3FYV^-)Re)lD1O*b$E7^DA3{7ew|7ZDIT(RY`ZLzO{%AaUbW|FYLcNHgsDwr>ZR7oFCmO5noB^3a7d(>2E+2-@-6J<$<(p5=1kH^cY(Q(+y9&q=hv}I-1_` zt}1Epos$T5tt+pGx%0FKKIYUenDGB8>yrO0rN@FRA5}Bsfq8HGp!J^lS3nB^fUbz^ z35cdcVWIBf?gPY-w=(hbwD&%RQPp##UVH|e`N`Ch5Hm_xw$KfE=h|d${4qX}^8t57 z)Am+bTU1vLte~aKis620Y?%-Cwjh>QN4Y1tt$pWB#+n5Lv*?tWoTRpG8j04HY(z$? z@F~X^jt+frKu?RR(Nx1wbDnN7fre2>-Wb7o>FD0wFxz?@Zp+knMLT8LMudL`p}0GG zWJ@h~x>t;+_!6yNb%(+9myUy|x;Kgp%DpSP#ePoQZ`&5Z{yZD_~nGz)@G$+pLdA3(#)1T zWMw_I;Exv4af*83_K_1UdQek_=>w|goZ%!TbEkcZs_-=f8-qQ?s)MVy0}{1jn^!rn zG^*1|YdmF-XE}5cuxjdAvv^^E`_uw04UA5Zl9X*v0+SGQW*l zRilyqJmB=P8Q^C5iyX_9bLbxSSOypHneR&!Ff^z!Gi%|7F7ewybocfB>Y3iYx2qS&!c zCF%lhfj$dMo-BkwU~iksvvHr0JI*smUMV$mTF&??=kX&~A9q&3yPV%gTBpUjHOon@&u-uc3l`-;gpDbxEj)&IB%sOH~Tv~75L!O13pUfK37?t|F&Zv-=sRc z3q>778==#?#LO@k*T{A`BVGo%Jf%r5_xy?WuImi`bhpGImVbt$?BU4S0`@^eCy=DcC7!X=y1;sty}5Zc!S+T(VLH{tjHn z#Vfc=x7e8c_z$LRGfe3P9%^Ewpx6U>j2#$j|LZ^1xAF5h@AHrU6!(h+cH4~4>wD^H zavI>>1d~`WoP260u?YB5URI7ScTI$zgolUB|!cFxAE2 zpyJU2=ORdnCjrOU5`12^<;TrVfT9q<$qJ}=qJTn4@vy6YeB7srkJfd7c$iE>Q1QmL z{R@tucs?r1F(`o7xvsk3ft3I$@gxY!2cu^i>HCKVt?qW<#uh1_93kzAg&Q4p0&tj= ztI+DoagoM}6Kvg)mv7@{0bU1CIVf+Ihcr(DdpQ@sIXDG!Q1Jk-6G+wot*+Frf8i0T zuFXT0zcJcdKh{_QIw|dm6KoA;zKFLN-uligv-e+k1e@dcsR@P*fLFM#x|aJ%^9Y)P zyf-k2&A7Rt`xJ;DG`m;Or^Z{1LR##K~Hcs5V=V_k^3`K^ZA*b>!?YmKqd>(%h8!}^h-?sjMwT2jE|cg6 z(N1#YpmCOdgJ@5Ave4LpvO)9(1#-~1Jlh*Y2P=|=CT}D{dq}tvS!nDmy+Nt3DwBmK z-mk%<94hrS6|&I82P5$zlcq`z8n>$2phB|M$U%#fXPlr4DLh0L`oFQLrRwCMiH{g? zD?;a&^26kiv6J8iHC?Si4jDHe5=0XhxasZ*h9tRIz}3Yn>g@!;C$au@iw0z z%_F#L1*cAiiuacpX&i2%#}^NrffmXWvLwyJ^+kLhIH4=lmPlCPc%Zzkkf_tRv(v;U z1aQJfs3Ik;36MemhTs+loQn|SxnC=8b~qe<=0W_bf)nmxLPxKa-3~YcZY7S-Avk=a zL4muW;sl??!SFNo;fn)~aSiN(KqWK)q@QE|7VaCL3yuN|ttK*XJr^%tHa-j-DdK@njQ>k$O^WZ*b${A;xUzUf?nAV9k@ z;&VGV_8F9R_$o0P@sUDYHsu&%G~%lRa3n6MY;v*0XvAkg;r1R=<{r-pBy zz*mT%CuOoba}K9f6=xCQdk6zm7n#7GSq2#-qJfP)!8<+xS%gdlQ-0o&~ - + @@ -259,7 +259,10 @@ + + + @@ -331,6 +334,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -419,6 +443,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt index 887f2c1bb97a..173bad10ee9a 100644 --- a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt @@ -43,8 +43,7 @@ import java.io.IOException */ // the context is nullable like FileLoader's own field, which close() clears and the unit // tests never set - isSupported() is pure and does not need one -class CoreLoader(context: Context?, private val doOoxml: Boolean) : - FileLoader(context, LoaderType.CORE) { +class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { private var server: HttpServer? = null private var httpThread: Thread? = null @@ -131,22 +130,25 @@ class CoreLoader(context: Context?, private val doOoxml: Boolean) : else FALLBACK_SERVER_PORT } + /** + * What the core renders itself: opendocument, ooxml, the three legacy binary microsoft formats + * and pdf. odrcore keeps reference html output for every one of them, doc, ppt and xls + * included, so there is nothing left here to hold back. + * + * Text, csv and images are left out although the core takes those too - [RawLoader] is what + * gives them their player or their viewer, and this answer is what lets it have its turn. The + * rest of it decides whether a failed load is worth reporting and which viewer [OnlineLoader] + * falls back to. + * + * Matching by prefix carries the variants along: the ooxml prefix covers the templates, and the + * macro enabled ones are spelled `vnd.ms-word.document.macroEnabled.12` and so on, which is why + * the legacy powerpoint and excel types appear here without their subtypes and why + * `vnd.ms-word` is listed next to `msword`. + */ override fun isSupported(options: Options): Boolean { val fileType = options.fileType ?: return false - return fileType.startsWith("application/vnd.oasis.opendocument") || - fileType.startsWith("application/x-vnd.oasis.opendocument") || - fileType.startsWith("application/vnd.oasis.opendocument.text-master") || - fileType.startsWith("application/msword") || - (doOoxml && - (fileType.startsWith( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ) || - fileType.startsWith( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - // TODO: enable pptx too - // fileType.startsWith("application/vnd.openxmlformats-officedocument.presentationml.presentation") - )) + + return MIME_PREFIXES.any { fileType.startsWith(it) } } override fun loadSync(options: Options) { @@ -232,8 +234,10 @@ class CoreLoader(context: Context?, private val doOoxml: Boolean) : if (keepDocument) { closeDocument() - // .doc-files are not real documents in core - if (file.isDocumentFile && file.fileType() != FileType.LEGACY_WORD_DOCUMENT) { + // only what [edit] can do something with: the core parses the legacy binary formats + // but declares them neither editable nor savable, so holding one open would buy a + // second parse and nothing else + if (file.isDocumentFile && !isLegacy(file.fileType())) { // TODO this will cause a second load document = file.asDocumentFile().document() } @@ -326,12 +330,34 @@ class CoreLoader(context: Context?, private val doOoxml: Boolean) : document = null } + private fun isLegacy(fileType: FileType): Boolean = + fileType == FileType.LEGACY_WORD_DOCUMENT || + fileType == FileType.LEGACY_POWERPOINT_PRESENTATION || + fileType == FileType.LEGACY_EXCEL_WORKSHEETS + /** A translated view of a document, ready to be opened in the WebView. */ data class HostedView(val name: String, val url: String) companion object { private const val TAG = "CoreLoader" + /** See [isSupported]. Mirrored by [SupportedDocumentTypes] and the STRICT_CATCH filter. */ + private val MIME_PREFIXES = + listOf( + // odf, including the master documents and the x- spelling some providers use + "application/vnd.oasis.opendocument", + "application/x-vnd.oasis.opendocument", + // ooxml: word, excel and powerpoint, their templates and their macro variants + "application/vnd.openxmlformats-officedocument", + "application/vnd.ms-word", + // legacy binary microsoft: doc, xls and ppt + "application/msword", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + // not a document, but the core renders it just the same + "application/pdf", + ) + /** The loopback address the server binds to. */ private const val SERVER_BIND_ADDRESS = "127.0.0.1" diff --git a/app/src/main/java/app/opendocument/droid/background/LoaderService.kt b/app/src/main/java/app/opendocument/droid/background/LoaderService.kt index f04a8ef8de31..cb2088a971a7 100644 --- a/app/src/main/java/app/opendocument/droid/background/LoaderService.kt +++ b/app/src/main/java/app/opendocument/droid/background/LoaderService.kt @@ -60,7 +60,7 @@ class LoaderService : Service(), FileLoader.FileLoaderListener { crashManager, ) - coreLoader = CoreLoader(this, true) + coreLoader = CoreLoader(this) coreLoader.initialize(this, mainHandler, backgroundHandler, analyticsManager, crashManager) rawLoader = RawLoader(this, coreLoader) diff --git a/app/src/main/java/app/opendocument/droid/background/OnlineLoader.kt b/app/src/main/java/app/opendocument/droid/background/OnlineLoader.kt index 3e534f1a6797..6863e2308b56 100644 --- a/app/src/main/java/app/opendocument/droid/background/OnlineLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/OnlineLoader.kt @@ -133,8 +133,12 @@ class OnlineLoader(context: Context?, private val coreLoader: CoreLoader) : } private fun buildViewerUri(options: Options, downloadUrl: String): Uri { - if (coreLoader.isSupported(options)) { - // ODF does not seem to be supported by google docs viewer + // ODF does not seem to be supported by google docs viewer, but pdf is the one thing the + // microsoft viewer will not take - and the core opens both, so what it supports no longer + // tells the two apart on its own + val isPdf = options.fileType?.startsWith("application/pdf") == true + + if (coreLoader.isSupported(options) && !isPdf) { return Uri.parse(MICROSOFT_VIEWER_URL + downloadUrl) } diff --git a/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt b/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt index 20ae54b4b992..bd8d797b9c43 100644 --- a/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt +++ b/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt @@ -18,13 +18,15 @@ object SupportedDocumentTypes { /** Mime types and prefixes taken from the STRICT_CATCH filter. */ private val MIME_PREFIXES = listOf( + // everything the core opens as a document - the same set as CoreLoader.isSupported "application/vnd.oasis.opendocument", - // the two ooxml types the filter spells out, rather than the family they belong to: - // pptx shares that prefix and CoreLoader does not open it yet, so a folder would - // offer a presentation and then drop it into the upload flow - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/x-vnd.oasis.opendocument", + "application/vnd.openxmlformats-officedocument", + "application/vnd.ms-word", "application/msword", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + // and what the core or RawLoader shows without being a document "application/pdf", "application/zip", "text/plain", @@ -45,7 +47,10 @@ object SupportedDocumentTypes { "otg", "doc", "docx", + "xls", "xlsx", + "ppt", + "pptx", "pdf", "txt", "csv", diff --git a/app/src/test/java/app/opendocument/droid/background/CoreLoaderTest.kt b/app/src/test/java/app/opendocument/droid/background/CoreLoaderTest.kt new file mode 100644 index 000000000000..2b1ef296cdae --- /dev/null +++ b/app/src/test/java/app/opendocument/droid/background/CoreLoaderTest.kt @@ -0,0 +1,92 @@ +package app.opendocument.droid.background + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * What the core is expected to render. odrcore v6 keeps reference html output for every format + * asserted here, so these are the types a load is expected to succeed for. + */ +class CoreLoaderTest { + + private lateinit var coreLoader: CoreLoader + + @Before + fun setUp() { + // no context: isSupported() is pure, and constructing a loader has no side effects + coreLoader = CoreLoader(null) + } + + private fun isSupported(fileType: String?): Boolean { + val options = FileLoader.Options() + options.fileType = fileType + + return coreLoader.isSupported(options) + } + + @Test + fun opendocumentIsSupported() { + assertTrue(isSupported("application/vnd.oasis.opendocument.text")) + assertTrue(isSupported("application/vnd.oasis.opendocument.spreadsheet")) + assertTrue(isSupported("application/vnd.oasis.opendocument.presentation")) + assertTrue(isSupported("application/vnd.oasis.opendocument.graphics")) + assertTrue(isSupported("application/vnd.oasis.opendocument.text-master")) + // the spelling some providers use + assertTrue(isSupported("application/x-vnd.oasis.opendocument.text")) + } + + @Test + fun ooxmlIsSupported() { + assertTrue( + isSupported("application/vnd.openxmlformats-officedocument.wordprocessingml.document") + ) + assertTrue(isSupported("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) + assertTrue( + isSupported("application/vnd.openxmlformats-officedocument.presentationml.presentation") + ) + assertTrue( + isSupported("application/vnd.openxmlformats-officedocument.wordprocessingml.template") + ) + } + + @Test + fun theMacroEnabledOoxmlVariantsAreSupported() { + // spelled outside the openxmlformats family, which is why the legacy types below are + // matched by their prefix and vnd.ms-word is listed at all + assertTrue(isSupported("application/vnd.ms-word.document.macroEnabled.12")) + assertTrue(isSupported("application/vnd.ms-excel.sheet.macroEnabled.12")) + assertTrue(isSupported("application/vnd.ms-powerpoint.presentation.macroEnabled.12")) + } + + @Test + fun theLegacyBinaryFormatsAreSupported() { + assertTrue(isSupported("application/msword")) + assertTrue(isSupported("application/vnd.ms-excel")) + assertTrue(isSupported("application/vnd.ms-powerpoint")) + } + + @Test + fun pdfIsSupported() { + assertTrue(isSupported("application/pdf")) + } + + @Test + fun whatRawLoaderShowsIsNotClaimed() { + // the core does render these, but RawLoader is what gives them their player or viewer + assertFalse(isSupported("text/plain")) + assertFalse(isSupported("text/csv")) + assertFalse(isSupported("image/png")) + assertFalse(isSupported("application/zip")) + } + + @Test + fun whatTheCoreCannotOpenIsNotClaimed() { + assertFalse(isSupported("application/vnd.wordperfect")) + assertFalse(isSupported("text/rtf")) + assertFalse(isSupported("application/vnd.apple.pages")) + assertFalse(isSupported("application/octet-stream")) + assertFalse(isSupported(null)) + } +} diff --git a/app/src/test/java/app/opendocument/droid/background/OnlineLoaderTest.kt b/app/src/test/java/app/opendocument/droid/background/OnlineLoaderTest.kt index 20ce05bb644d..230764ee872f 100644 --- a/app/src/test/java/app/opendocument/droid/background/OnlineLoaderTest.kt +++ b/app/src/test/java/app/opendocument/droid/background/OnlineLoaderTest.kt @@ -10,7 +10,7 @@ class OnlineLoaderTest { @Before fun setUp() { - onlineLoader = OnlineLoader(null, CoreLoader(null, true)) + onlineLoader = OnlineLoader(null, CoreLoader(null)) } private fun options(fileType: String): FileLoader.Options { @@ -84,6 +84,6 @@ class OnlineLoaderTest { Assert.assertFalse(isConvertible("text/plain")) Assert.assertFalse(isConvertible("image/png")) Assert.assertFalse(isConvertible("application/zip")) - Assert.assertFalse(isConvertible("application/vnd.ms-excel.sheet.macroEnabled.12")) + Assert.assertFalse(isConvertible("application/vnd.apple.pages")) } } diff --git a/app/src/test/java/app/opendocument/droid/background/RawLoaderTest.kt b/app/src/test/java/app/opendocument/droid/background/RawLoaderTest.kt index c682c7a8ad86..e49e0244174b 100644 --- a/app/src/test/java/app/opendocument/droid/background/RawLoaderTest.kt +++ b/app/src/test/java/app/opendocument/droid/background/RawLoaderTest.kt @@ -10,7 +10,7 @@ class RawLoaderTest { @Before fun setUp() { - rawLoader = RawLoader(null, CoreLoader(null, true)) + rawLoader = RawLoader(null, CoreLoader(null)) } private fun isSupported(fileType: String): Boolean { diff --git a/app/src/test/java/app/opendocument/droid/background/SupportedDocumentTypesTest.kt b/app/src/test/java/app/opendocument/droid/background/SupportedDocumentTypesTest.kt index 5d089641280e..224856f7480f 100644 --- a/app/src/test/java/app/opendocument/droid/background/SupportedDocumentTypesTest.kt +++ b/app/src/test/java/app/opendocument/droid/background/SupportedDocumentTypesTest.kt @@ -31,16 +31,19 @@ class SupportedDocumentTypesTest { } @Test - fun presentationsTheCoreCannotOpenAreNotSupported() { - // pptx sits in the same ooxml family as docx and xlsx, but neither the STRICT_CATCH filter - // nor CoreLoader takes it - listing one would promise an open that ends in the upload flow - assertFalse( + fun presentationsAndTheLegacyBinaryFormatsAreSupported() { + assertTrue( supported( "application/vnd.openxmlformats-officedocument.presentationml.presentation", "deck.pptx", ) ) - assertFalse(supported("application/vnd.ms-powerpoint", "deck.ppt")) + assertTrue(supported("application/vnd.ms-powerpoint", "deck.ppt")) + assertTrue(supported("application/vnd.ms-excel", "budget.xls")) + // the octet-stream a provider volunteers for them, caught by the extension instead + assertTrue(supported("application/octet-stream", "deck.pptx")) + assertTrue(supported("application/octet-stream", "deck.ppt")) + assertTrue(supported("application/octet-stream", "budget.xls")) } @Test From 888bd6df56f61a451516ec4e2ecd894272a4f766 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 28 Jul 2026 21:21:46 +0200 Subject: [PATCH 2/2] Ask the core what can be edited instead of guessing by mime type Codex was right on #546: doc, ppt and xls now reach CoreLoader, and prepareActions offered Edit for every CORE result except ODS and PDF - so the legacy formats got a button that could only ever end in a failed save, because host() would not keep a document open for them. The list of exceptions was never ours to keep. odrcore answers this itself: Document.isEditable()/isSavable() is false for the three legacy binary formats, for ooxml spreadsheets and presentations, and for every spreadsheet including odf - the last being the core's own TODO, the same gap as #442 that the UI was spelling out by mime prefix. host() now keeps the document only when the core says yes, which makes "we have a document" the answer, and it travels to the fragment on Result. While in there, two more copies of the same idea: - CoreLoader.MIME_PREFIXES was a second SupportedDocumentTypes. It is gone; the loader defers to the table. What stays separate is the manifest, which XML keeps out of reach - so SupportedFormatsTest walks odrcore's FileType values, asks Odr.mimetypeByFileType for each, and asserts the table and the package manager agree. Adding a format to one side and forgetting the other is now a red test. The core cannot replace the table itself: its lookups are native, and know one canonical mime per format - no templates, no macroEnabled, no x-vnd.oasis, which is exactly what providers hand out. - toggleDarkMode's isDarkModeSupported was dead. Nothing in the body read it while DocumentFragment worked out that a pdf is not worth inverting. Renamed to disableDarkening(), which is what it does. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp --- CLAUDE.md | 40 +++-- .../app/opendocument/droid/test/CoreTest.kt | 36 ++++ .../droid/test/SupportedFormatsTest.kt | 163 ++++++++++++++++++ .../droid/background/CoreLoader.kt | 70 ++++---- .../droid/background/FileLoader.kt | 9 + .../background/SupportedDocumentTypes.kt | 80 ++++++--- .../droid/ui/activity/DocumentFragment.kt | 32 +--- .../droid/ui/activity/MainActivity.kt | 2 +- .../opendocument/droid/ui/widget/PageView.kt | 8 +- 9 files changed, 337 insertions(+), 103 deletions(-) create mode 100644 app/src/androidTest/java/app/opendocument/droid/test/SupportedFormatsTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 2502fda528fd..79bc80de16f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,23 +145,43 @@ the `.pro` suffix) differ on purpose - do not "fix" the mismatch: `AndroidFileCache`, the SharedPreferences name in `MainActivity`) follows `applicationId` and must stay that way, or existing users lose their saved prefs. -### Supported file types are declared three times +### Supported file types are declared twice, and a test keeps them in step -`CoreLoader.MIME_PREFIXES`, `SupportedDocumentTypes` (mime prefixes plus an extension fallback) and -the `STRICT_CATCH` `activity-alias` in `AndroidManifest.xml` describe the same set from three -directions - the manifest is what the system offers the app for, `SupportedDocumentTypes` is what -the folder browser on the landing screen offers itself for, and the loader is what the core is -expected to render. **Nothing keeps them in step, so change all three.** +`SupportedDocumentTypes` is the single kotlin table: mime prefixes for what the core renders, mime +prefixes for what `RawLoader` shows, plus an extension fallback for the `application/octet-stream` +a provider volunteers when it knows nothing better. `CoreLoader.isSupported()` defers to it - it +used to keep a second copy of the same prefixes. + +The `STRICT_CATCH` `activity-alias` in `AndroidManifest.xml` is the second declaration and cannot be +collapsed into the first, because XML cannot read a kotlin list. It is covered instead: +`SupportedFormatsTest` (instrumented) walks odrcore's own `FileType` values, asks +`Odr.mimetypeByFileType` for each, and asserts that `SupportedDocumentTypes` and the package +manager give the same answer - so a format added on one side and forgotten on the other fails CI +rather than shipping. The set follows odrcore: whatever it keeps reference html output for in `test/data/reference-output` is what the app claims - odf, ooxml, the legacy binary doc/ppt/xls and pdf. Text, csv and images are the core's too but belong to `RawLoader`, which only gets its turn when `CoreLoader.isSupported()` says no. -The duplication is deliberate: `MetadataLoader` decides what is really supported by running -libmagic over the file *after* copying it into the cache, and `CoreLoader.isSupported()` / -`RawLoader.isSupported()` both take a filled-in `FileLoader.Options`. A folder listing only has a -name and whatever mime type the provider volunteered, so it cannot ask them and has to guess. +Why the app has a table at all when odrcore has `Odr.fileTypeByMimetype` / +`Odr.fileTypeByFileExtension`: both are native calls into `libodr_jni`, and both know exactly one +canonical mime type per format - no templates, no macro-enabled variants, no +`application/x-vnd.oasis...`, which are the spellings content providers actually hand out. The +guessing stage needs to be broader than the core's own lookup. What the core *does* decide is +everything after the file is in the cache: `MetadataLoader` runs libmagic over the copy, and +`CoreLoader.isDocumentEditable` asks the opened document. + +### Editability comes from the core, never from a mime type + +`Document.isEditable()`/`isSavable()` is what decides whether `DocumentFragment` puts the Edit +button up, carried across on `FileLoader.Result.isEditable`. `CoreLoader.host()` only holds a +document open when the core says yes, so "we have a document" *is* the answer. + +Do not reintroduce a list of editable formats in the UI. The core already says no to the legacy +binary doc/ppt/xls, to ooxml spreadsheets and presentations, and to every spreadsheet including odf +(its own TODO, the same gap as issue #442) - all of which the app used to spell out by mime prefix +and got wrong the moment a new format started going through `CoreLoader`. ### Storage access diff --git a/app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt b/app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt index ae4b3b021ec0..f829aa4553a6 100644 --- a/app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt +++ b/app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt @@ -181,6 +181,42 @@ class CoreTest { Assert.assertFalse("hosting the XLS file should produce a view", views.isEmpty()) } + /** + * Which of the formats the core renders it can also write back again - the answer + * `DocumentFragment` puts the Edit button up by. Offering it for a document the core will not + * save only gets as far as a failed save, which is what happened to the legacy binary formats + * when they started going through the core. + */ + @Test + fun testEditableFormats() { + assertEditable("odt-editable", requireFile(testFile), true) + assertEditable("docx-editable", requireFile(docxTestFile), true) + + // the core declares these read only: the three legacy binary formats, ooxml presentations + // and every spreadsheet - the last being issue #442, which the core has its own TODO for + assertEditable("doc-editable", requireFile(docTestFile), false) + assertEditable("ppt-editable", requireFile(pptTestFile), false) + assertEditable("xls-editable", requireFile(xlsTestFile), false) + assertEditable("pptx-editable", requireFile(pptxTestFile), false) + assertEditable("ods-editable", requireFile(spreadsheetTestFile), false) + } + + private fun assertEditable(prefix: String, file: File, expected: Boolean) { + coreLoader.host( + prefix = prefix, + inputPath = file.absolutePath, + cachePath = File(cacheDir(), prefix).path, + editable = true, + keepDocument = true, + ) + + Assert.assertEquals( + "the core should report ${file.name} as ${if (expected) "editable" else "read only"}", + expected, + coreLoader.isDocumentEditable, + ) + } + @Test fun testPasswordProtectedDocumentWithoutPassword() { Assert.assertThrows(OdrException.FileEncrypted::class.java) { diff --git a/app/src/androidTest/java/app/opendocument/droid/test/SupportedFormatsTest.kt b/app/src/androidTest/java/app/opendocument/droid/test/SupportedFormatsTest.kt new file mode 100644 index 000000000000..3541394ccc3f --- /dev/null +++ b/app/src/androidTest/java/app/opendocument/droid/test/SupportedFormatsTest.kt @@ -0,0 +1,163 @@ +package app.opendocument.droid.test + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import app.opendocument.core.FileType +import app.opendocument.core.Odr +import app.opendocument.droid.background.CatchAllSetting +import app.opendocument.droid.background.CoreLoader +import app.opendocument.droid.background.SupportedDocumentTypes +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Keeps the two remaining declarations of what the app opens in step with each other, so that + * changing one and forgetting the other is a failing test rather than a bug report. + * + * [SupportedDocumentTypes] is one of them. The other is the STRICT_CATCH `activity-alias` in + * AndroidManifest.xml, which is XML and cannot read a kotlin list - but it can be *asked*: an + * intent-filter is only worth anything if the package manager resolves a real intent through it, + * which is what [resolvesToUs] does here. There used to be a third copy in `CoreLoader`; that one + * is gone, it defers to [SupportedDocumentTypes] now. + * + * The mime side needs no list of its own at all: [FileType] is the core's complete set of formats + * and [Odr.mimetypeByFileType] names each one, so the test walks what odrcore knows and demands + * that the app and the manifest give the same answer for every entry. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class SupportedFormatsTest { + + /** + * For every format odrcore can name a mime type for, what the app offers itself for and what + * the manifest offers it for have to be the same answer. + * + * This is the assertion that catches a format being added to the loader without being added to + * the manifest, which is the failure the user sees as "the app is not in the share sheet". + */ + @Test + fun theManifestClaimsExactlyWhatTheAppClaims() { + var checked = 0 + + for (fileType in FileType.values()) { + val mimeType = mimeTypeOrNull(fileType) ?: continue + checked++ + + val claimedByApp = SupportedDocumentTypes.isSupported(mimeType, null) + val claimedByManifest = resolvesToUs(mimeType, "document") + + Assert.assertEquals( + "$fileType ($mimeType): the app says $claimedByApp, the manifest says" + + " $claimedByManifest - AndroidManifest.xml and SupportedDocumentTypes have" + + " drifted apart", + claimedByApp, + claimedByManifest, + ) + } + + // a guard on the guard: if the core ever stops naming mime types the loop above would + // silently assert nothing at all + Assert.assertTrue("no file type was checked", checked > 10) + } + + /** Every extension the folder browser offers for has to be one odrcore actually recognises. */ + @Test + fun theExtensionFallbackNamesFormatsTheCoreKnows() { + for (extension in SupportedDocumentTypes.EXTENSIONS) { + Assert.assertNotEquals( + "odrcore does not know the extension .$extension", + FileType.UNKNOWN, + Odr.fileTypeByFileExtension(extension), + ) + } + } + + /** + * The case the extension fallback exists for: a provider that volunteers nothing better than + * `application/octet-stream`. The app goes by the name, and the manifest has to as well - it + * claims `application/octet-stream` outright, so this is really a check that the claim stays. + */ + @Test + fun anOctetStreamWithAKnownExtensionReachesUs() { + for (extension in SupportedDocumentTypes.EXTENSIONS) { + Assert.assertTrue( + "the app does not offer for document.$extension", + SupportedDocumentTypes.isSupported( + "application/octet-stream", + "document.$extension", + ), + ) + Assert.assertTrue( + "the manifest does not offer for document.$extension", + resolvesToUs("application/octet-stream", "document.$extension"), + ) + } + } + + /** What the app deliberately stays out of - the reason the catch-all filter defaults to off. */ + @Test + fun unrelatedTypesReachNeither() { + for (mimeType in listOf("text/vcard", "text/calendar", "audio/mpeg", "video/mp4")) { + Assert.assertFalse( + "the app should not offer for $mimeType", + SupportedDocumentTypes.isSupported(mimeType, null), + ) + Assert.assertFalse( + "the manifest should not offer for $mimeType", + resolvesToUs(mimeType, "document"), + ) + } + } + + /** + * Whether an `ACTION_VIEW` for this mime type and filename lands on our own activity - the only + * thing an intent-filter is actually good for, and the one question a hand written XML list can + * still be asked. + */ + private fun resolvesToUs(mimeType: String, filename: String): Boolean { + val context = InstrumentationRegistry.getInstrumentation().targetContext + + val intent = + Intent(Intent.ACTION_VIEW).apply { + addCategory(Intent.CATEGORY_DEFAULT) + setDataAndType(Uri.parse("content://app.opendocument.test/$filename"), mimeType) + } + + return context.packageManager + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + .any { it.activityInfo?.packageName == context.packageName } + } + + /** Null for the types odrcore has no mime type for - word perfect, rtf, cfb and the like. */ + private fun mimeTypeOrNull(fileType: FileType): String? = + try { + Odr.mimetypeByFileType(fileType) + } catch (e: RuntimeException) { + null + } + + companion object { + + // @JvmStatic because junit requires @BeforeClass to be static + @JvmStatic + @BeforeClass + fun prepare() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + + // Odr's tables live in libodr_jni; nothing here opens a file, so the data paths that + // initializeCore also sets up are along for the ride rather than needed + CoreLoader.initializeCore(context) + + // STRICT_CATCH is what this test is about, and the component states survive a test run + // - a previous one that flipped the switch would otherwise leave CATCH_ALL answering + // for everything. False is the shipped default, so nothing has to be put back. + CatchAllSetting.setEnabled(context, false) + } + } +} diff --git a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt index 173bad10ee9a..f062007df1d3 100644 --- a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt @@ -9,7 +9,6 @@ import app.opendocument.core.DecodePreference import app.opendocument.core.DecodedFile import app.opendocument.core.Document import app.opendocument.core.DocumentType -import app.opendocument.core.FileType import app.opendocument.core.GlobalParams import app.opendocument.core.Html import app.opendocument.core.HtmlConfig @@ -52,6 +51,18 @@ class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { private var lastInputPath: String? = null private var lastCachePath: String? = null + /** + * Whether the document [host] last opened is one [edit] can do something with. + * + * This is the core's own answer, not a list of formats kept here: [host] only holds a document + * open when it reports itself editable and savable, so having one is the answer. The core says + * no to the legacy binary formats, to ooxml spreadsheets and presentations and to opendocument + * spreadsheets - the last of those being the same gap as issue #442, which the app used to + * check for by mime type on its own. + */ + val isDocumentEditable: Boolean + get() = document != null + // the port the server actually got, which is not always the preferred one. see bind() private var serverPort = PREFERRED_SERVER_PORT @@ -131,25 +142,16 @@ class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { } /** - * What the core renders itself: opendocument, ooxml, the three legacy binary microsoft formats - * and pdf. odrcore keeps reference html output for every one of them, doc, ppt and xls - * included, so there is nothing left here to hold back. + * What the core renders itself, out of [SupportedDocumentTypes] so the folder browser and this + * cannot disagree about it. * * Text, csv and images are left out although the core takes those too - [RawLoader] is what * gives them their player or their viewer, and this answer is what lets it have its turn. The * rest of it decides whether a failed load is worth reporting and which viewer [OnlineLoader] * falls back to. - * - * Matching by prefix carries the variants along: the ooxml prefix covers the templates, and the - * macro enabled ones are spelled `vnd.ms-word.document.macroEnabled.12` and so on, which is why - * the legacy powerpoint and excel types appear here without their subtypes and why - * `vnd.ms-word` is listed next to `msword`. */ - override fun isSupported(options: Options): Boolean { - val fileType = options.fileType ?: return false - - return MIME_PREFIXES.any { fileType.startsWith(it) } - } + override fun isSupported(options: Options): Boolean = + SupportedDocumentTypes.isRenderedByCore(options.fileType) override fun loadSync(options: Options) { val result = Result(type, options) @@ -193,6 +195,8 @@ class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { keepDocument = true, ) + result.isEditable = isDocumentEditable + for (view in views) { result.partTitles.add(view.name) result.partUris.add(Uri.parse(view.url)) @@ -234,12 +238,18 @@ class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { if (keepDocument) { closeDocument() - // only what [edit] can do something with: the core parses the legacy binary formats - // but declares them neither editable nor savable, so holding one open would buy a - // second parse and nothing else - if (file.isDocumentFile && !isLegacy(file.fileType())) { + if (file.isDocumentFile) { // TODO this will cause a second load - document = file.asDocumentFile().document() + val document = file.asDocumentFile().document() + + // keep only what [edit] can do something with, and let the core be the one to say + // so - see [isDocumentEditable]. Holding a read only document open buys a second + // parse and nothing else. + if (document.isEditable && document.isSavable) { + this.document = document + } else { + document.close() + } } } @@ -330,34 +340,12 @@ class CoreLoader(context: Context?) : FileLoader(context, LoaderType.CORE) { document = null } - private fun isLegacy(fileType: FileType): Boolean = - fileType == FileType.LEGACY_WORD_DOCUMENT || - fileType == FileType.LEGACY_POWERPOINT_PRESENTATION || - fileType == FileType.LEGACY_EXCEL_WORKSHEETS - /** A translated view of a document, ready to be opened in the WebView. */ data class HostedView(val name: String, val url: String) companion object { private const val TAG = "CoreLoader" - /** See [isSupported]. Mirrored by [SupportedDocumentTypes] and the STRICT_CATCH filter. */ - private val MIME_PREFIXES = - listOf( - // odf, including the master documents and the x- spelling some providers use - "application/vnd.oasis.opendocument", - "application/x-vnd.oasis.opendocument", - // ooxml: word, excel and powerpoint, their templates and their macro variants - "application/vnd.openxmlformats-officedocument", - "application/vnd.ms-word", - // legacy binary microsoft: doc, xls and ppt - "application/msword", - "application/vnd.ms-excel", - "application/vnd.ms-powerpoint", - // not a document, but the core renders it just the same - "application/pdf", - ) - /** The loopback address the server binds to. */ private const val SERVER_BIND_ADDRESS = "127.0.0.1" diff --git a/app/src/main/java/app/opendocument/droid/background/FileLoader.kt b/app/src/main/java/app/opendocument/droid/background/FileLoader.kt index c9eaf94b6e6f..8785706eefd8 100644 --- a/app/src/main/java/app/opendocument/droid/background/FileLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/FileLoader.kt @@ -200,6 +200,13 @@ abstract class FileLoader(context: Context?, protected val type: LoaderType) { val partTitles: MutableList = LinkedList() val partUris: MutableList = LinkedList() + /** + * Whether the loaded document can be edited and saved again, as the core reports it - see + * [CoreLoader.isDocumentEditable]. False for every other loader, which have nothing to + * edit. + */ + var isEditable: Boolean = false + override fun describeContents(): Int = 0 override fun writeToParcel(parcel: Parcel, flags: Int) { @@ -207,6 +214,7 @@ abstract class FileLoader(context: Context?, protected val type: LoaderType) { parcel.writeParcelable(options, 0) parcel.writeList(partTitles) parcel.writeList(partUris) + ParcelUtil.writeBoolean(parcel, isEditable) } companion object { @@ -225,6 +233,7 @@ abstract class FileLoader(context: Context?, protected val type: LoaderType) { return Result(loaderType, options).also { parcel.readList(it.partTitles, classLoader) parcel.readList(it.partUris, classLoader) + it.isEditable = ParcelUtil.readBoolean(parcel) } } diff --git a/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt b/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt index bd8d797b9c43..bb07ebc63301 100644 --- a/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt +++ b/app/src/main/java/app/opendocument/droid/background/SupportedDocumentTypes.kt @@ -1,41 +1,72 @@ package app.opendocument.droid.background /** - * Whether a document is worth offering in the folder browser. + * The one list of what the app claims to open, for both of the places that have to guess. * - * Deliberately free of Android dependencies so it can be covered by plain JVM unit tests. + * There used to be two copies of this - one here for the folder browser, one in [CoreLoader] for + * the loader - which is one copy too many for a set that only ever changes as a whole. + * [isRenderedByCore] is what [CoreLoader.isSupported] answers with, [isSupported] adds what + * [RawLoader] shows and the extension fallback on top, and the STRICT_CATCH `activity-alias` in + * AndroidManifest.xml is the third and last declaration, which XML keeps out of reach. + * `SupportedFormatsTest` walks this table and asks the package manager whether the manifest still + * agrees, so the two cannot drift silently any more. * - * This is a guess, not the real answer. [MetadataLoader] decides what is supported by running - * libmagic over the file once it has been copied into the cache, and neither [CoreLoader] nor - * [RawLoader] can answer before that - so a folder listing, which only has a name and whatever mime - * type the provider volunteered, has to go on appearances. + * Deliberately free of Android dependencies, and of the core's own tables - odrcore does know + * [app.opendocument.core.Odr.fileTypeByMimetype] and `fileTypeByFileExtension`, but both are native + * calls into libodr_jni, and both only know one canonical mime type per format: no templates, no + * macro enabled variants, no `application/x-vnd.oasis...`, which are exactly the spellings a + * content provider hands out. What the core does decide is everything *after* the file is in the + * cache - [MetadataLoader] runs its libmagic over it, and [CoreLoader.isDocumentEditable] asks the + * opened document itself. * - * Mirrors the STRICT_CATCH intent filter in AndroidManifest.xml. **Keep the two in step**: that - * filter is what the system offers us for, this is what we offer ourselves for. + * So this is a guess, not the real answer: a folder listing only has a name and whatever mime type + * the provider volunteered, and it has to go on appearances. */ object SupportedDocumentTypes { - /** Mime types and prefixes taken from the STRICT_CATCH filter. */ - private val MIME_PREFIXES = + /** + * What the core renders itself: opendocument, ooxml, the three legacy binary microsoft formats + * and pdf. odrcore keeps reference html output for every one of them, doc, ppt and xls + * included. + * + * Matching by prefix carries the variants along: the ooxml prefix covers the templates, and the + * macro enabled ones are spelled `vnd.ms-word.document.macroEnabled.12` and so on, which is why + * the legacy powerpoint and excel types appear here without their subtypes and why + * `vnd.ms-word` is listed next to `msword`. + */ + private val CORE_MIME_PREFIXES = listOf( - // everything the core opens as a document - the same set as CoreLoader.isSupported + // odf, including the master documents and the x- spelling some providers use "application/vnd.oasis.opendocument", "application/x-vnd.oasis.opendocument", + // ooxml: word, excel and powerpoint, their templates and their macro variants "application/vnd.openxmlformats-officedocument", "application/vnd.ms-word", + // legacy binary microsoft: doc, xls and ppt "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", - // and what the core or RawLoader shows without being a document + // not a document, but the core renders it just the same "application/pdf", - "application/zip", - "text/plain", - "text/csv", - "image/", ) - /** The extensions the STRICT_CATCH pathPatterns spell out. */ - private val EXTENSIONS = + /** + * What [RawLoader] shows instead - text, csv, images and zip - which the core would take too + * but which get their own player or viewer here. + * + * Narrower than [RawLoader.isSupported], which also takes audio and video: those are worth + * playing once someone hands us one, but not worth offering the app for. + */ + private val RAW_MIME_PREFIXES = listOf("text/plain", "text/csv", "image/", "application/zip") + + /** + * The extensions to fall back on, spelling out the same set once more because providers + * regularly volunteer nothing better than `application/octet-stream`. + * + * Public so `SupportedFormatsTest` can walk it: it holds every one of these against the core's + * own extension table and against the manifest. + */ + val EXTENSIONS: Set = setOf( "odt", "ods", @@ -57,20 +88,27 @@ object SupportedDocumentTypes { "zip", ) + /** Whether [CoreLoader] is expected to render this - see [CORE_MIME_PREFIXES]. */ + fun isRenderedByCore(mimeType: String?): Boolean = matches(mimeType, CORE_MIME_PREFIXES) + + /** Whether the folder browser should offer this at all. */ fun isSupported(mimeType: String?, filename: String?): Boolean { - // providers regularly hand out application/octet-stream for anything they do not know, - // so a recognised extension has to be able to win on its own + // a recognised extension has to be able to win on its own, or every octet-stream is lost val extension = MimeTypeResolver.parseExtension(filename)?.lowercase() if (extension != null && extension in EXTENSIONS) { return true } + return isRenderedByCore(mimeType) || matches(mimeType, RAW_MIME_PREFIXES) + } + + private fun matches(mimeType: String?, prefixes: List): Boolean { if (mimeType == null) { return false } val normalized = mimeType.lowercase() - return MIME_PREFIXES.any { normalized.startsWith(it) } + return prefixes.any { normalized.startsWith(it) } } } diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt index ed774bbe0b88..bac352217085 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt @@ -381,33 +381,11 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener { * that finished loading is not one of those things. */ private fun prepareActions(result: FileLoader.Result) { - val loaderType = result.loaderType - val fileType = result.options.fileType - - var isEditEnabled = false - var isDarkModeSupported = true - - if (loaderType == FileLoader.LoaderType.CORE) { - isEditEnabled = true - - // Edit is currently broken for ODS spreadsheets - // See: https://github.com/opendocument-app/OpenDocument.droid/issues/442 - if ( - fileType != null && - fileType.startsWith("application/vnd.oasis.opendocument.spreadsheet") - ) { - isEditEnabled = false - } - - // Edit is not supported for PDF documents - if (fileType != null && fileType.startsWith("application/pdf")) { - isEditEnabled = false - isDarkModeSupported = false - } - } - + // whether editing is on offer is the core's answer, not a list of formats kept here: it + // knows which of the documents it renders it can also write back, which is why neither the + // legacy binary formats nor the spreadsheets of issue #442 need naming val unfolding = ArrayList() - if (isEditEnabled) { + if (result.isEditable) { unfolding.add( DocumentActions.Action( DocumentActions.ACTION_EDIT, @@ -468,7 +446,7 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener { unfolding, ) - pageView?.toggleDarkMode(isDarkModeSupported) + pageView?.disableDarkening() } /** Takes the buttons away while the document has the screen to itself. */ diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt index d30765784db4..74ab4ea391e6 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt @@ -504,7 +504,7 @@ class MainActivity : AppCompatActivity() { analyticsManager.report("menu_print") documentFragment?.pageView?.let { pageView -> - pageView.toggleDarkMode(false) + pageView.disableDarkening() printingManager.print(this, pageView) } diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt index d39cd49ebbd4..1453e9bc0502 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt @@ -160,11 +160,13 @@ constructor(context: Context, attributeSet: AttributeSet?) : * moved to Material3.DayNight - at targetSdk 33 and up the webview only darkens when the app * theme reports itself as dark, so under the old AppCompat.Light theme none of this ever fired. * - * [isDarkModeSupported] is kept as the hook a user facing "dark documents" setting would use; - * DocumentFragment already passes false for pdfs. + * There is no parameter to this any more. It used to take an `isDarkModeSupported` that nothing + * in here ever read - both callers reached the same three lines - while [DocumentFragment] went + * to the trouble of working out that a pdf is not worth inverting. A user facing "dark + * documents" setting can grow the argument back when there is something on the other end of it. */ @Suppress("DEPRECATION") // setForceDarkAllowed and setForceDark are the pre-webkit-1.6 api - fun toggleDarkMode(isDarkModeSupported: Boolean) { + fun disableDarkening() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { isForceDarkAllowed = false }