From 6656786ecff40b1ad28e0b24dc99906a4e90c85e Mon Sep 17 00:00:00 2001 From: REAndroid Date: Wed, 7 Dec 2022 11:27:26 -0500 Subject: [PATCH] V1.0.3 --- README.md | 70 +-- libs/ArchiveUtil.jar | Bin 28513 -> 28768 bytes .../java/com/reandroid/lib/apk/ApkEntry.java | 10 - .../com/reandroid/lib/apk/ApkJsonDecoder.java | 171 +++++++ .../com/reandroid/lib/apk/ApkJsonEncoder.java | 110 ++++ .../java/com/reandroid/lib/apk/ApkModule.java | 72 +-- .../java/com/reandroid/lib/apk/ApkUtil.java | 101 +++- .../reandroid/lib/apk/BlockInputSource.java | 25 +- .../lib/apk/JsonManifestInputSource.java | 21 + .../reandroid/lib/apk/JsonXmlInputSource.java | 47 ++ .../com/reandroid/lib/apk/ResourceIds.java | 472 ++++++++++++++++++ .../lib/apk/SingleJsonTableInputSource.java | 53 ++ .../lib/apk/SplitJsonTableInputSource.java | 38 ++ .../reandroid/lib/apk/StringPoolBuilder.java | 135 +++++ .../com/reandroid/lib/apk/TableBlockJson.java | 62 +++ .../lib/apk/TableBlockJsonBuilder.java | 63 +++ .../reandroid/lib/apk/UncompressedFiles.java | 81 +++ .../lib/arsc/array/EntryBlockArray.java | 2 +- .../lib/arsc/array/SpecTypePairArray.java | 51 ++ .../reandroid/lib/arsc/array/StringArray.java | 35 +- .../lib/arsc/array/TypeBlockArray.java | 12 + .../reandroid/lib/arsc/base/BlockArray.java | 17 +- .../lib/arsc/chunk/BaseTypeBlock.java | 7 + .../lib/arsc/chunk/PackageBlock.java | 122 +++-- .../reandroid/lib/arsc/chunk/TableBlock.java | 6 +- .../lib/arsc/chunk/xml/BaseXmlChunk.java | 25 +- .../lib/arsc/chunk/xml/ResXmlBlock.java | 2 +- .../lib/arsc/chunk/xml/ResXmlElement.java | 32 ++ .../lib/arsc/chunk/xml/ResXmlText.java | 3 +- .../reandroid/lib/arsc/item/IntegerArray.java | 8 - .../reandroid/lib/arsc/item/SpecString.java | 1 + .../reandroid/lib/arsc/item/StringItem.java | 6 +- .../reandroid/lib/arsc/item/StyleItem.java | 27 +- .../reandroid/lib/arsc/item/TypeString.java | 1 + .../lib/arsc/model/StyleSpanInfo.java | 6 +- .../lib/arsc/pool/BaseStringPool.java | 201 ++++++-- .../lib/arsc/pool/SpecStringPool.java | 13 +- .../lib/arsc/pool/TypeStringPool.java | 18 +- .../lib/arsc/pool/builder/SpannedText.java | 101 +++- .../lib/arsc/value/BaseResValue.java | 3 + .../lib/arsc/value/BaseResValueItem.java | 3 + .../reandroid/lib/arsc/value/EntryBlock.java | 100 ++-- .../reandroid/lib/arsc/value/ResConfig.java | 13 +- .../reandroid/lib/arsc/value/ResValueBag.java | 9 +- .../lib/arsc/value/ResValueBagItem.java | 17 + .../reandroid/lib/arsc/value/ResValueInt.java | 14 + .../lib/arsc/value/ResValueItem.java | 1 + .../lib/arsc/value/ResValueReference.java | 15 +- .../com/reandroid/lib/common/ROArrayList.java | 21 - .../reandroid/lib/common/ROSingleList.java | 25 - .../com/reandroid/lib/json/JSONArray.java | 11 + .../com/reandroid/lib/json/JSONObject.java | 13 +- .../resources/fwk/android_resources_30.arsc | Bin 915764 -> 915764 bytes 53 files changed, 2107 insertions(+), 365 deletions(-) delete mode 100644 src/main/java/com/reandroid/lib/apk/ApkEntry.java create mode 100644 src/main/java/com/reandroid/lib/apk/ApkJsonDecoder.java create mode 100644 src/main/java/com/reandroid/lib/apk/ApkJsonEncoder.java create mode 100644 src/main/java/com/reandroid/lib/apk/JsonManifestInputSource.java create mode 100644 src/main/java/com/reandroid/lib/apk/JsonXmlInputSource.java create mode 100644 src/main/java/com/reandroid/lib/apk/ResourceIds.java create mode 100644 src/main/java/com/reandroid/lib/apk/SingleJsonTableInputSource.java create mode 100644 src/main/java/com/reandroid/lib/apk/SplitJsonTableInputSource.java create mode 100644 src/main/java/com/reandroid/lib/apk/StringPoolBuilder.java create mode 100644 src/main/java/com/reandroid/lib/apk/TableBlockJson.java create mode 100644 src/main/java/com/reandroid/lib/apk/TableBlockJsonBuilder.java create mode 100644 src/main/java/com/reandroid/lib/apk/UncompressedFiles.java delete mode 100755 src/main/java/com/reandroid/lib/common/ROArrayList.java delete mode 100755 src/main/java/com/reandroid/lib/common/ROSingleList.java mode change 100755 => 100644 src/main/resources/fwk/android_resources_30.arsc diff --git a/README.md b/README.md index a917514..74bfcde 100755 --- a/README.md +++ b/README.md @@ -5,59 +5,27 @@ import com.reandroid.lib.arsc.chunk.TableBlock; import com.reandroid.lib.arsc.io.BlockReader; - public static void example() throws IOException { - File inFile=new File("resources.arsc"); - - TableBlock tableBlock=new TableBlock(); - tableBlock.readBytes(inFile); - - //edit tableBlock as desired, for example to change the package: - PackageBlock packageBlock=tableBlock.getPackageArray().get(0); - packageBlock.setPackageName("com.new.package.name"); - - //refresh to recalculate offsets - tableBlock.refresh(); - - //convert to json object - JSONObject jsonObject=tableBlock.toJson(); - System.out.println(jsonObject.toString(4)); - - //save the edited table - File outFile=new File("resources_out.arsc"); - tableBlock.writeBytes(outFile); - } - - public static void exampleManifest() throws IOException { - File inFile=new File("AndroidManifest.xml"); - - AndroidManifestBlock manifestBlock=new AndroidManifestBlock(); - manifestBlock.readBytes(file); - - List permissionNames = manifestBlock.getUsesPermissions(); - for(String perm:permissionNames){ - System.out.println(perm); - } - - //edit AndroidManifest as desired, for example to change the package: - - manifestBlock.setPackageName("com.new.package.name"); - - // add some permission - - manifestBlock.addUsesPermission("android.permission.WRITE_EXTERNAL_STORAGE"); - - //refresh to recalculate offsets - manifestBlock.refresh(); - - //save the edited xml - File outFile=new File("AndroidManifest_out.xml"); - manifestBlock.writeBytes(outFile); - } - public static void convertToJson() throws IOException{ + public static void example() throws IOException{ File inFile=new File("test.apk"); File outDir=new File("test_out"); + ApkModule apkModule=ApkModule.loadApkFile(inFile); - apkModule.convertToJson(outDir); - } + + ApkJsonDecoder decoder=new ApkJsonDecoder(apkModule); + outDir=decoder.writeToDirectory(outDir); + System.out.println("Decoded to: "+outDir); + + // You can do any logical modification on any json files + + // To convert back json to apk + + ApkJsonEncoder encoder=new ApkJsonEncoder(); + ApkModule encodedModule=encoder.scanDirectory(outDir); + + File outApk=new File("test_out_re-encoded.apk"); + encodedModule.writeApk(outApk); + + System.out.println("Created apk: "+outApk); + } ``` diff --git a/libs/ArchiveUtil.jar b/libs/ArchiveUtil.jar index 44b6cd7b56aafa85396ce88fe7bda2512b289aa8..3cf1d7193443ce57fc6f7d2f27f14728b96cea8e 100644 GIT binary patch delta 10303 zcmZ8{byyt1lQt6Eg1ftGaCdhI?ydoX%LZq0Slr!1aQ8)mTX2HAJ4=FmN$z{@es}Z7 zba%}=)g$#(SJzwH8_-)@&}iyPu5ax(QuVlsZVl zR<*s06RoGTO(Loxr7%*ikke=D;3@({3aDL21CW|B93~U75kt6u-|8xJzM&a$s_O;~ zi7)i&Si2BHhWlCt9RzDRm1`8#B4{J*s{|7Sj&$BM(`!-E;>-1BDZ(mz)=nM5^MDOn(l2wb)TbdQMH;Ag}j-&ut!UG zg*2D!kq$3pmDO8BBDfbRlNsqEP#w(I0QhJTD{U0%{wke#zvdkA(ks;`cRMw-NR~?t zFUmVuid$BeVBv6Gc2p*+`{(MNcLaO*u3^2D*ZUGsQQk&p2mO6E2cs2luwAksP~@7I zsNnz_p@;Z0qZ`S$LSVkz_`4C66Ucqg9P;CyGiF#gLT7D!2ND7rHcTr)e4+s+Kwf<- zXiL2?JbsDC%}^p1|02dO@~)6#?eNhVTcx?Sy}^b1NElI?AhG##)Zx`CgTiLxgqmQPFXRM7#dOjdxkRetfvYaXdp!HG*9ZLpg za3mCnCWATo4Rvev6?M!{xly(;6%fxEF*fF57prSKN?;3t#a>unA-khzf71*nj!wek z9Mc>X&l(hzjpYjCR2iB;U7nnM)yJXATl`Y)DUQx;V++ca+#x>_oO)3idAw7oXqAVK zcA@VHA2RBR93@LmFy9*i_;HNpj*%R1+jS+9DK61xmF|rIFOZl5C4M^ytkYQ6MRv=b2 z-lxAzUYE@V?PLqdk7RV!nV6loLq;)2Xs6SJSkzkxqD zOh6U|26JlDI+bz616bWQRLv1Kl^_x%Ocs^E{iLuFRG^N_C+|3OCes(Gz*;8f^wY62 zhg6hK)OsNsEz67wcd0dp);xy?>ibGnJ}sTb!jsOb$svKZ*!Jl*aGkny2J6tGcCmeS zS7n9QE+L?nF*P?5*OZ03ga_Q*e()aDcUfwaaX$1mF1W7;xYRMePv~$UVfX;UgFXhk z%!gWizBtkHb7stOFtO$uR?2gYH`rxw6QfFg#m=P060o|yz_dsSwt|{cvCcg;XutZ> z;L>7z$vOX9*0=q4QK3E?ADY~NODjt`k*9ecoh^%@M^1Dtcb#CS08y#pA^V4HO1?Iz z=6E7E<0A`KK&vrkl#(p#wA2df&nUmU1*H!=@h5-~{qPye@`Tf{ z@yD?F8*jmmVwLBSewB?Bn)aIgcL;WpkG9p+rTc1`_Z>8;1yd7MJ)~ACFN)VXu`!foXk+2S-g(zY))Y7RLl< zXbQz`;*MjTD%9wN_zGF~&7Y8N$rNG8x1=OHlpB;g0UqHc)ieoa{g(PAI(6#>#z%uO zBLu#?jGq;_?o4^MlOW>PU&4S62rg-(tcA?D0HtC%g207fc^^6uC3$Il0+#47{aqft zfUJLiC<}a|+7X+q|HHfbmmSQV+YxyuV`fqbKiun2S+7L4!qG*pbA{jGaU_|xwdTm^ z(`(Iv&*!B&zht@a;;dZ6Y4W#!2_>=Q6Dn{Kjndw@28q*%4(TVY_Z>=Td;_81Qe`TV z0w`Yuu}1gR6-reVdXH?`9yv{qEc0;4gDH-(_tJepY8*Xk&Z)jJKn7X1Fv?w}Djdmr z#3sXFZhr|KuAx@`k0SkhqH*9Kr{DSaD)~?fVXP5vnZH?rlH9Ib2l;6#cdCP@A4APS zKw3jqjmomej`>7NHuZzulzX22?s4i}a>waMert<3uGuxjO&`R~SB$?-SVlwg^5l*>pJwS2MkQZe01nVm9umsh_e*4!=kv0Kw1&oL0yf^K=G?F0DUBDN3f)v4XKD! z`HdS&IQf$!C0&losw3nEz?BEM3r*V6@Tme*iV?wW;~3VT5&9+X;pChmP&sEitTea{ zmwdrD!ZS8ta5Qbm_+IQ{0T%(xZYwP|FA76IUDHAX`Voz6Jmq9aYfM}KniC3JgfG5j zOzNv3G@cN))a-8>8!U?bLe9eI3?)L-jW3H6Ro0bFQ}iOEJFngYbccta=K7YqAszha zm3mUF4S_iiAH|LFqg^OvhDrxRRw5Gbsc$?}ee2doC$S{y^hFD=Y$S?hSP$r`_ zsMWp5Om#;y+?V&)hsP#Pwp=bnj~)sAlFF%BJ4Q6{IgP{NKf7tmeg)#fKtXN&S%ChA zuQ*VMh=@>B>|u?>%w4Fke>(rnh{72c5T~{F$BuAvJkR(&*LmzU4H(iRG*4(sgF+)= zuq3r5qs1$9XvkXQ74T`j_V{u0;jo&p#R7HeL@tFQ<=>`A~U% z&FYdKaC8@j%R{i({&=5-n<^Sy2RLUfX2hZjr`3;x_VCwn3;mU!Fr}_55v892Ip#~F zEy+&bLxcUXFJ98Xg{nDQlbpW&=)p z@qr8iykkepdcoE^eX_6#@|WbIG!HYwtWMl#FH2U0S>tYm%7Heo=T4G5GPX}D73=~` zQvFHY1{ZA2es*)#EVG`a3CwB=NfK^^yxkue{SnaM493qxnd`g`iJRB~DGn7goka$; zMeo8DmNkcTeiS}_+u*wh-A%Mzmfjfj>bDsJRJ#WgMbKR zrjOEiAdrRx%V6*~VBrIN)$=vNIV22%$AeGX#?k01DndkQqjP93l9rA|C`e)`vd?n! z4okI8KVyq}kk97rUMul#dbFRGp0~QT(^_j%_#>KQ?w%7m4m5pQp_>-s783HWc%ID{ zyd}MS?3(ruxD-02eiM3x`tkvifM5N|kx_StmdWHqhSKHU-5roEY@!>HzWcy+*2lbe zt%&61odLVew&#scx9~>64mx|4TRCEvhO8^o{!Mnb56VW>jx*Km1J*k2 z?d}KdH#W?GR^?6VPnY*Qfj+(Xg(SD15Y#b}aJy9ZN`CtSDu%TH3bD zI@`Rz^lM72YBiwkBVGPY5!&PEQ4pren4RJfA+9ZMN}QJ*R(&Go<-!P#_5;Zx{pU9y zDx3$8LSqSgfE_~-{jg<32y15N=~sKb1k|VBKuki1OC)Kz7fz{!cb2p2Gv7vFz=cTQ zUREyNyrYEp5g1v21c{iXNUB2V$dIfumhee`T%V322`NBZ7?tcMR5>A~h?| zP8{QM!ICm_Ehca96GN@pHLdm~Mx3XC)X6;&o8c+fYOHfHNFJv z3ud(J4ijLCSua(_f{19cjRYC!j~rU`ks5kM^=iE$M1U|#$k;zw3dr!WC8PYK)#=o= zvqy_e9;XN=%j`5J?dJ)CI8}l&W*gppXzWYSr+iTd{<+-kx)9$BhLKoG%eh`9HJVtx zK|!7>dKfUg`KiaA-&fYErQsUt-0eiNY5J@Xt3d$EPvphDVu7y({#0eSxU*5~E5)5E zeFY)IW%jZ&)d-;v&StACnz}|mEDJdudB&~Z;qSw*1kd>7KDNKJ4v(sCOj}fI{SrlqHRRX&P;1Ap>^|Y4ndW7@+qWEj*gAM>hCTG4=8;=ZuFV%2SB%TI3FveJ48XQmUm~V= zLL#QOZ;|nglM*8)kLIr^dI}23`RpK6WOIP4I*I@!e4L3MZELKdpF};eay$Vth(Wjs zRgkQRZ@a{?5x+B{g0zBnmabTZWo=!eWwG5B@9IscIZ4olnPaZcu>$14-q#{%oA_)A zyPRlzQXj8j(fD>EnSx5TN7@Q@=o?w9HZ=~ESHWfUw#Fo)HsaUGY<{Z9Ag&wd=4WG2`!@L)KtO89kcn| zaB!Z26jtmumNF8@`O|9iL(M!_DKpp=4|5fldw8H@J+Y&#wzf-~xON>=pGq-SoEZee18(sGBb zaZx{`-mxrR{vB~^EHUP>aJqegfKPX2izAe#rQF`*I?AnSlcSgCr$IYSHnyLS zQO4BN@`PP@o)6FPZcKKAz#%7~C{dwkhh6jjwa`oMaBQ1AC* zdLmN7^t4u1EKUn!a}hFp1Ml|p7S0-&&Qu9h;%nN)eqrxDO@#eU(F92Fc;|e?(HwX? zC@stj_svF7B_=xrEt4^>PlLMG;~ZY-G%c7*n5nw7rX)7xD$&_6v|vvN4y(Q5iiUG&|q z=azvaC&U(6rd*}{{y?F>R)Wwbqk~0waxKl{!B1(z=HxdRkIvX3Yn0OGKTo%iF>=C@FBV74K^#}Xr^CAU8h~SoQp+7@azN0AYFHmP z*6Lvt5fXD%wS3KUP!x$m=5m*eEr#x7gK^_iuXo~TuOg{b)RjQPwCH;xf>GG90g~QF zN*U{P*b=TnSSwR+tgUG7VE_{T-qX*f>m;BJm?||rnEieGWD5;YVBfM_|@D` zvuSSsnc-U_905pk>w{;ikYx+YT4_LyI&?>$fjZ-7Zdv(RFgXH z$@XrkR?=7~3B{}4eIvFLIUL z#`%!R-P%h<2bP0-u=%#1c>U4Bbzw{`lMd61RbCMf-Cb(y>^UnFon24C44SG>8DlYq zY649s%0g@UM2okw&IR+e`y#7H(T_%QFGeIhxgRB#^hX(_6$^}{zmBm42NbT-=w|B= zR_E0s4cSxyoV*suhJtmK#e?wrKQV#Hs_7Pc4lXO|FKp zup4KJOPRGqV-t6|0SA@yWw3L!cYX&4E=EH)bs2C}GB)VArCfFWg$rK{-;W<*Heh+< z>nyNTd7PCcar{gkixxVw=#BaqVDfFx*3_`tQ-9T9w5#+ztms9v&1QDD6}PzUIrX6g z-A<4@KzM7N7)B5ke1-7FGKFB{&-944=SVI>0^-^LmK0Hy6g zvRu@uL92V97Go*no|5=Har_8*p#TLL!dYsRYoa z(Sl+ucd%-s%dP8cqKX}Nh;)Z$vHd)+_hQQX#KD87h(lON&&$%-OTUk=B7)g72>O^U zo{ID~xUOj7s$7>y;a)tJ&Q2e~Y=4hqqqrUY8V4csta+SA3Em&qm5KXm1Z(7bJn#F+ z1eq+gH3{03u)XvQ-M$H7HwG0GvcH?GJ4mdlKf9vP+mn-3 z7>(kcI{996Wn3~4FxF1sEQ-${mS^;N>ARgSNela+m$T=FOGy{9#+qccuLMAPR@~2= zPu_lL&oJG4!j8@!45Gox=`Ws7^V4K{PME(9GI@^d8% zdH6CkNhPz-??_rjjplQ`?|l_E(|6-M3p0@szN-MN)A~rP5I6jml<)4Gp%J^T zpZx8p8y<~}yiHtJO@8Qb+u{Mf2{ik--(j6HFB{|?GwYM_3gGFU+0j;3OOntiSUTyc zHka9s#M4N*D2HD-tA9l@&x)}*^TXy_A0E70IW9+k-`3WKCp4=+V#0N0Z9-F7Pxx`K zr?vQGIJ`Za{!?E6CbH+KbI#T7r^x}6v&Jg|c(j0_fsfZ8Q2aUU%zNJhdcTC?$@Tl9 zlylf_GsGK01YL+A5&QvKT|!pdZm5R*^i2^QHD(aY&i=kumWkb_@J52xE9ZcbK5y0l z41`et6XFg7`njy;n?^$XCH@sGir8QR&e=p%R5&>bn;3LwRjUvNF&))6+tm;2jOR74 zFXZDd!p1S^jRNxP#HbJewtg{9wUW+GLvC}@xK`1dw{@zpQu;UBec_|?VR_m9CR@3h z;ao%6V4OKQwe+Y)iCEIsjVqGb_jEzg-tsa6!X)!q>v9nK`U|k#X&f0LCV!q zgz0{Ljwh4D$*gpt6#o4WS;~*o(hotEW}9U_r=}RXxj$I8IV_g|I}}FTSIk8daQzOA z{i2!of%GZqZt`b8RV^50H5uqLd8UC6v=M|(m$p7GU$?C)Os9?>{3*+vrwz9#a(J#B z)23nFv^XGd_dm>oSjhL~G!Ca0_q0?$m#(i^{&7h^S9;wy*&l=IsIHM}sfCRTg+~Gl zZYWa}UCl|K6g^1+4yC{3>`rII64Q!}yrp(mQE-LKiZDc^{SbvTk}hfBs|aBCKwE?T zk!lvg_dJx-Uv!^fW7nMZjFFYFh81x%3Bu{}72^_0dG5!kPdRuMGAk|7_R*>QvZL78 zSxLk)6=*h9_L7vT5E_7coj}w%o9V!)GZmn4cqrqdkzxrTLHe$@n4(1}^^cEdm{3|K zUGJAB;=#xE1{s+mzt~q+BW};1*SIv)xQv<)xQDUGU4q@!viRbid$9H7e6b7+n!_#l zv{Uyv7NJX<3weY?!92Npjbkr}06E(FFf)^r>{KRprq!L-{Z>FMj&qDwAT`8eyN*|P(V&h^^y?d5se}G_`W;>iM*3a$!w-M&HyuJp^O39H z=XMy1r}RKCD4DX#!6WpsI7y=q&=6++oZc*qI-m^TtG4&m9m}%`0Eqj-Jq}Ho(zA zasXC5DNrpS!C3AeJDbezfxw0wt4b8kl>}!3V)d<=klw*+#J(RNue$6%G zqBHKj;EM)>#amk5VXzeKd0F6dcjsG89iZJ?d&~ia#4#|n(DN=}CVqpz#Vs&();~_w zNw!utUWvsv^0L-lw}wN8tuY{Op?-Ciu3WQ?<(0X_t5bI4+Zrd{XbK5e;FYbGeJPE?vtVR5@FCK2X{JQZ9_3N5$+Hw zto%F;4RY}N5M0=(A;b)qrETPoHV3MQCb6r5n~$ukfz3)zD%J0sRXWO?SIc|v(_a|_ zlUpvm54ccJ$L4MG1jfC4_FvL&f8L$|TzcPaVLppQt4_!z_JyE&rGVQRPU;z?6eF5u zxC8+?3{>C84ssjDMl+d1>A{+?_r^cm6nHPOYQ0Z=)NoXMT+zy|dx$2sUbq6pM-cWQ zQtl7tgcZlskct^my0rv3OfbC_q$FNVW;Q}?{D8aRS51v5ef9MC2};ASqImXWNJB){ zT}oq$l7VH*seJ{pkZm5Hf10q+0(FWgS`vWKU)0i1Nk9nzcJ!7G65Xh0Wi255I8Z``W{ zf8Dnwsya9$kz=0(cQlren%+pa%T?Q>35`LNrWuG;_5bXqg9(d)tXg@QMkIWVK|V#) z_{qA9jk&SayUaD%(*l`O(RgT&U&8@ycsf@#J1902O=FdUs+pNjWJImpw^l%0&y;>F zZ^tu>FFa>!(l{RoR5N#3Q=1$2*j83<2+r9?i7-lY#;iUE=!`5K#hvcysZ*vhp;2G^ ze|Lsy;Wt!SNs3MHtT6xW7k3g?*GgBZpc6IyTQ7k77oRSHs^LZm_sCwtiBCL$!^wNh z!yS)t^7bgMM}p!vYDpzv&w6bBmCb z^k)T=%)uHwJcs?3P|;Z@)vZXtW*^3tR2l?3ez)CZY9gK);^IsoRyC{xD>kgA_pa1W zW=RcgqR13n+gYZ_53t$tHR1+bd2m8djF##i^R7R9BAO=ffll{))?|4Z>s!0>g5~?9 zHFY%DP~cLT1IO8Eu_d$4^@-RU^_i%b^9sv^_sUTJ1)aU|7vJnHLAu32e7=A!t$ul2 zbv=QIN!wUW@eTTrFdkBIUMjafto6ywlWh!8DSwsA)fZ$lHRkigR1pLC*7@7~6b83b zQL^XVu}lj?tE6dhhdS4On>Kb{qh^{>l|z-yH2<;oRykMh0ll`Gy}<_~gA$ErGC+au zdBI8K+T20Whcm0>GCpU26r=XMOiVijCLNzrwq{qfC}fNBi@EWV#m*NT(t6q*^Nb8~fYV z8aYxLRJ=oVCDIV}hhN~qf&{cftePR$8s^9#d;WB7lA9!t7iW$jC!LXux9~Y%-XYDU zYq|Clz-(Qw-O^N!Hy-O|fas>&1dY^q`BL={p#%OHr5XfF9#B2N9n3iI$!YEt+KGtXe<@?pxKbpfmJ<*Nq3B*gFc^uV(P zM|e*A-_lb=d^rI<`Q&gvP7=B3nK^O#%xlr|-J_s%3-1`AUPQlO|G?g=p%<*yea}J^ z&*V-z5TG2>pC)YBUvNRpk~KR%Aw0YQ1s}-$R-l`EyPG?X80Xy`vRZ2$_N^cbB$`Xn z?6-qX)%cS+iVU63qTl+5Fjn&D)=Jv3OT1yZq2v-W1WOu#$U`;w=tr9Mv=*W3Bqv0^ z|8rRa-w^0Y?{=mMK{!G>$G;Mr+I-I$L6At_hMd?-3O3Sla~T2=n9p?DZzFC<*A}N# zMm!K@dCFR)bW)TiXvA;o)eb6U^C)~&_8l#J;H?kNTbTBmZH_o);(-BFk8P4MZHzxR zF-8HqK0yO4PBucubMyM##uPu%-}Wz1?N!jdDO)kP!9ovT!dKN&6b&rqS-vlbaE!ZcSFbvq65HG()C8 zR`}9|wFl*?TGfE8xl1Xm;#Z+=_S8t0r@)k@aW9w^Dp+PJ7aC7T%Kb){VPV4K8x2|D ztBi%(p2Dwp^mC3Alc&Z={o&IF#nDwe40d!7rf|jDj2_j;(#to~*yNjC>5lzr%mRbk zJ68jEWWxTyqdDOh*l*W;W2nUp)c>;n;ZFizab>PycYcKLSuHP&A7gHL<66ir``ni^Hv;=Gs5s2eONq)kcA(>U12n%O;q{`_Q&IwOJ!nL8{hFPPkjAmD2(j&N zKH+WChpxPN`)>np{}`54D$S)a1SqKV_kWjuXc%Ou|1K}nf%;?X{ z680eUKXqn0|6Nsr2->l?h53dDGIyp2)#4$7{2Z8I5DEVb2|zs#loWr#Z8g=u6#vYV zjR6Hk_g{+t|CyWk9}wS>gyK)cq4Pg9^Zy0TQvCxuIts$D(f^xk$`Oa+4-KvJpA;kW z|5EPB`0pMRMqCiP(|?hTqE5sxyUhQ@aTWk delta 10063 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 T7))XGk7OYyN`&uLf4}?}%RS+M diff --git a/src/main/java/com/reandroid/lib/apk/ApkEntry.java b/src/main/java/com/reandroid/lib/apk/ApkEntry.java deleted file mode 100644 index 7054a3e..0000000 --- a/src/main/java/com/reandroid/lib/apk/ApkEntry.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.reandroid.lib.apk; - -import com.reandroid.archive.InputSource; - -public class ApkEntry { - private InputSource mInputSource; - public ApkEntry(InputSource inputSource){ - this.mInputSource=inputSource; - } -} diff --git a/src/main/java/com/reandroid/lib/apk/ApkJsonDecoder.java b/src/main/java/com/reandroid/lib/apk/ApkJsonDecoder.java new file mode 100644 index 0000000..d00bf49 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/ApkJsonDecoder.java @@ -0,0 +1,171 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock; +import com.reandroid.lib.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +public class ApkJsonDecoder { + private final ApkModule apkModule; + private final Set decodedPaths; + private final boolean splitTypes; + public ApkJsonDecoder(ApkModule apkModule, boolean splitTypes){ + this.apkModule = apkModule; + this.splitTypes = splitTypes; + this.decodedPaths = new HashSet<>(); + } + public ApkJsonDecoder(ApkModule apkModule){ + this(apkModule, false); + } + public File writeToDirectory(File dir) throws IOException { + this.decodedPaths.clear(); + writeUncompressed(dir); + writeManifest(dir); + writeTable(dir); + writeResourceIds(dir); + writeResources(dir); + writeRootFiles(dir); + return new File(dir, apkModule.getModuleName()); + } + private void writeUncompressed(File dir) throws IOException { + File file=toUncompressedJsonFile(dir); + UncompressedFiles uncompressedFiles=new UncompressedFiles(); + uncompressedFiles.add(apkModule.getApkArchive()); + uncompressedFiles.toJson().write(file); + } + private void writeResources(File dir) throws IOException { + for(ResFile resFile:apkModule.listResFiles()){ + writeResource(dir, resFile); + } + } + private void writeResource(File dir, ResFile resFile) throws IOException { + if(resFile.isBinaryXml()){ + writeResourceJson(dir, resFile); + } + } + private void writeResourceJson(File dir, ResFile resFile) throws IOException { + InputSource inputSource= resFile.getInputSource(); + String path=inputSource.getAlias(); + File file=toResJson(dir, path); + ResXmlBlock resXmlBlock=new ResXmlBlock(); + resXmlBlock.readBytes(inputSource.openStream()); + JSONObject jsonObject=resXmlBlock.toJson(); + jsonObject.write(file); + addDecoded(path); + } + private void writeRootFiles(File dir) throws IOException { + for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){ + writeRootFile(dir, inputSource); + } + } + private void writeRootFile(File dir, InputSource inputSource) throws IOException { + String path=inputSource.getAlias(); + if(hasDecoded(path)){ + return; + } + File file=toRootFile(dir, path); + File parent=file.getParentFile(); + if(parent!=null && !parent.exists()){ + parent.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); + addDecoded(path); + } + private void writeTable(File dir) throws IOException { + if(!splitTypes){ + writeTableSingle(dir); + return; + } + writeTableSplit(dir); + } + private void writeTableSplit(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + File splitDir= toJsonTableSplitDir(dir); + TableBlockJson tableBlockJson=new TableBlockJson(tableBlock); + tableBlockJson.writeJsonFiles(splitDir); + addDecoded(TableBlock.FILE_NAME); + } + private void writeTableSingle(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + File file= toJsonTableFile(dir); + tableBlock.toJson().write(file); + addDecoded(TableBlock.FILE_NAME); + } + private void writeResourceIds(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + ResourceIds resourceIds=new ResourceIds(); + resourceIds.loadTableBlock(tableBlock); + JSONObject jsonObject= resourceIds.toJson(); + File file=toResourceIds(dir); + jsonObject.write(file); + } + private void writeManifest(File dir) throws IOException { + if(!apkModule.hasAndroidManifestBlock()){ + return; + } + AndroidManifestBlock manifestBlock = apkModule.getAndroidManifestBlock(); + File file = toJsonManifestFile(dir); + manifestBlock.toJson().write(file); + addDecoded(AndroidManifestBlock.FILE_NAME); + } + private boolean hasDecoded(String path){ + return decodedPaths.contains(path); + } + private void addDecoded(String path){ + this.decodedPaths.add(path); + } + private File toJsonTableFile(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(file, name); + } + private File toJsonTableSplitDir(File dir){ + File file=new File(dir, apkModule.getModuleName()); + return new File(file, ApkUtil.SPLIT_JSON_DIRECTORY); + } + private File toResourceIds(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = ResourceIds.JSON_FILE_NAME; + return new File(file, name); + } + private File toUncompressedJsonFile(File dir){ + File file = new File(dir, apkModule.getModuleName()); + return new File(file, UncompressedFiles.JSON_FILE); + } + private File toJsonManifestFile(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(file, name); + } + private File toResJson(File dir, String path){ + File file=new File(dir, apkModule.getModuleName()); + file=new File(file, ApkUtil.RES_JSON_NAME); + path=path + ApkUtil.JSON_FILE_EXTENSION; + path=path.replace('/', File.separatorChar); + return new File(file, path); + } + private File toRootFile(File dir, String path){ + File file=new File(dir, apkModule.getModuleName()); + file=new File(file, ApkUtil.ROOT_NAME); + path=path.replace('/', File.separatorChar); + return new File(file, path); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/ApkJsonEncoder.java b/src/main/java/com/reandroid/lib/apk/ApkJsonEncoder.java new file mode 100644 index 0000000..23f8157 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/ApkJsonEncoder.java @@ -0,0 +1,110 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.APKArchive; +import com.reandroid.archive.FileInputSource; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.chunk.xml.AndroidManifestBlock; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class ApkJsonEncoder { + private APKArchive apkArchive; + public ApkJsonEncoder(){ + } + public ApkModule scanDirectory(File moduleDir){ + this.apkArchive=new APKArchive(); + String moduleName=moduleDir.getName(); + scanManifest(moduleDir); + scanTable(moduleDir); + scanResJsonDirs(moduleDir); + scanRootDirs(moduleDir); + ApkModule module=new ApkModule(moduleName, apkArchive); + loadUncompressed(module, moduleDir); + return module; + } + private void loadUncompressed(ApkModule module, File moduleDir){ + File jsonFile=toUncompressedJsonFile(moduleDir); + UncompressedFiles uf= module.getUncompressedFiles(); + try { + uf.fromJson(jsonFile); + } catch (IOException ignored) { + } + } + private void scanRootDirs(File moduleDir){ + File rootDir=toRootDir(moduleDir); + List jsonFileList=ApkUtil.recursiveFiles(rootDir); + for(File file:jsonFileList){ + scanRootFile(rootDir, file); + } + } + private void scanRootFile(File rootDir, File file){ + String path=ApkUtil.toArchivePath(rootDir, file); + FileInputSource inputSource=new FileInputSource(file, path); + apkArchive.add(inputSource); + } + private void scanResJsonDirs(File moduleDir){ + File resJsonDir=toResJsonDir(moduleDir); + List jsonFileList=ApkUtil.recursiveFiles(resJsonDir); + for(File file:jsonFileList){ + scanResJsonFile(resJsonDir, file); + } + } + private void scanResJsonFile(File resJsonDir, File file){ + JsonXmlInputSource inputSource=JsonXmlInputSource.fromFile(resJsonDir, file); + apkArchive.add(inputSource); + } + private void scanManifest(File moduleDir){ + File file=toJsonManifestFile(moduleDir); + if(!file.isFile()){ + return; + } + JsonManifestInputSource inputSource=JsonManifestInputSource.fromFile(moduleDir, file); + apkArchive.add(inputSource); + } + private void scanTable(File moduleDir) { + boolean splitFound=scanTableSplitJson(moduleDir); + if(splitFound){ + return; + } + scanTableSingleJson(moduleDir); + } + private boolean scanTableSplitJson(File moduleDir) { + File dir=toJsonTableSplitDir(moduleDir); + if(!dir.isDirectory()){ + return false; + } + SplitJsonTableInputSource inputSource=new SplitJsonTableInputSource(dir); + apkArchive.add(inputSource); + return true; + } + private void scanTableSingleJson(File moduleDir) { + File file=toJsonTableFile(moduleDir); + if(!file.isFile()){ + return; + } + SingleJsonTableInputSource inputSource= SingleJsonTableInputSource.fromFile(moduleDir, file); + apkArchive.add(inputSource); + } + private File toJsonTableFile(File dir){ + String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(dir, name); + } + private File toJsonManifestFile(File dir){ + String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(dir, name); + } + private File toUncompressedJsonFile(File dir){ + return new File(dir, UncompressedFiles.JSON_FILE); + } + private File toJsonTableSplitDir(File dir){ + return new File(dir, ApkUtil.SPLIT_JSON_DIRECTORY); + } + private File toResJsonDir(File dir){ + return new File(dir, ApkUtil.RES_JSON_NAME); + } + private File toRootDir(File dir){ + return new File(dir, ApkUtil.ROOT_NAME); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/ApkModule.java b/src/main/java/com/reandroid/lib/apk/ApkModule.java index 20a6725..e05f6ab 100644 --- a/src/main/java/com/reandroid/lib/apk/ApkModule.java +++ b/src/main/java/com/reandroid/lib/apk/ApkModule.java @@ -2,6 +2,8 @@ package com.reandroid.lib.apk; import com.reandroid.archive.APKArchive; import com.reandroid.archive.InputSource; +import com.reandroid.archive.ZipArchive; +import com.reandroid.archive.ZipSerializer; import com.reandroid.lib.arsc.array.PackageArray; import com.reandroid.lib.arsc.chunk.PackageBlock; import com.reandroid.lib.arsc.chunk.TableBlock; @@ -12,22 +14,36 @@ import com.reandroid.lib.arsc.pool.TableStringPool; import com.reandroid.lib.arsc.value.EntryBlock; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.*; public class ApkModule { + private final String moduleName; private final APKArchive apkArchive; private boolean loadDefaultFramework = true; private TableBlock mTableBlock; private AndroidManifestBlock mManifestBlock; - private ApkModule(APKArchive apkArchive){ + private final UncompressedFiles mUncompressedFiles; + ApkModule(String moduleName, APKArchive apkArchive){ + this.moduleName=moduleName; this.apkArchive=apkArchive; + this.mUncompressedFiles=new UncompressedFiles(); + this.mUncompressedFiles.add(apkArchive); } - public void writeTo(File file) throws IOException { - APKArchive archive=getApkArchive(); - archive.writeApk(file); + public String getModuleName(){ + return moduleName; + } + public void writeApk(File file) throws IOException { + ZipArchive archive=new ZipArchive(); + archive.addAll(getApkArchive().listInputSources()); + UncompressedFiles uf=getUncompressedFiles(); + uf.apply(archive); + ZipSerializer serializer=new ZipSerializer(archive.listInputSources(), false); + serializer.writeZip(file); + } + public UncompressedFiles getUncompressedFiles(){ + return mUncompressedFiles; } public void removeDir(String dirName){ getApkArchive().removeDir(dirName); @@ -164,48 +180,12 @@ public class ApkModule { public void setLoadDefaultFramework(boolean loadDefaultFramework) { this.loadDefaultFramework = loadDefaultFramework; } - public void convertToJson(File outDir) throws IOException { - Set convertedFiles=new HashSet<>(); - if(hasAndroidManifestBlock()){ - AndroidManifestBlock manifestBlock=getAndroidManifestBlock(); - String fileName=AndroidManifestBlock.FILE_NAME+ApkUtil.JSON_FILE_EXTENSION; - File file=new File(outDir, fileName); - manifestBlock.toJson().write(file); - convertedFiles.add(AndroidManifestBlock.FILE_NAME); - } - if(hasTableBlock()){ - TableBlock tableBlock=getTableBlock(); - String fileName=TableBlock.FILE_NAME+ApkUtil.JSON_FILE_EXTENSION; - File file=new File(outDir, fileName); - tableBlock.toJson().write(file); - convertedFiles.add(TableBlock.FILE_NAME); - } - List resFileList=listResFiles(); - for(ResFile resFile:resFileList){ - boolean convertOk=resFile.dumpToJson(outDir); - if(convertOk){ - convertedFiles.add(resFile.getFilePath()); - } - } - List allSources = getApkArchive().listInputSources(); - for(InputSource inputSource:allSources){ - String path=inputSource.getAlias(); - if(convertedFiles.contains(path)){ - continue; - } - path=path.replace('/', File.separatorChar); - File file=new File(outDir, path); - File dir=file.getParentFile(); - if(dir!=null && !dir.exists()){ - dir.mkdirs(); - } - FileOutputStream outputStream=new FileOutputStream(file); - inputSource.write(outputStream); - outputStream.close(); - } - } + public static ApkModule loadApkFile(File apkFile) throws IOException { + return loadApkFile(apkFile, ApkUtil.DEF_MODULE_NAME); + } + public static ApkModule loadApkFile(File apkFile, String moduleName) throws IOException { APKArchive archive=APKArchive.loadZippedApk(apkFile); - return new ApkModule(archive); + return new ApkModule(moduleName, archive); } } diff --git a/src/main/java/com/reandroid/lib/apk/ApkUtil.java b/src/main/java/com/reandroid/lib/apk/ApkUtil.java index 90605de..3af579a 100644 --- a/src/main/java/com/reandroid/lib/apk/ApkUtil.java +++ b/src/main/java/com/reandroid/lib/apk/ApkUtil.java @@ -1,5 +1,9 @@ package com.reandroid.lib.apk; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + public class ApkUtil { public static String replaceRootDir(String path, String dirName){ int i=path.indexOf('/')+1; @@ -12,5 +16,100 @@ public class ApkUtil { } return path; } - public static final String JSON_FILE_EXTENSION=".a.json"; + public static String toArchiveResourcePath(File dir, File file){ + String path = toArchivePath(dir, file); + if(path.endsWith(ApkUtil.JSON_FILE_EXTENSION)){ + int i2=path.length()-ApkUtil.JSON_FILE_EXTENSION.length(); + path=path.substring(0, i2); + } + return path; + } + public static String toArchivePath(File dir, File file){ + String dirPath = dir.getAbsolutePath()+File.separator; + String path = file.getAbsolutePath().substring(dirPath.length()); + path=path.replace(File.separatorChar, '/'); + return path; + } + public static List recursiveFiles(File dir, String ext){ + List results=new ArrayList<>(); + if(dir.isFile()){ + results.add(dir); + return results; + } + if(!dir.isDirectory()){ + return results; + } + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isFile()){ + if(ext!=null && !file.getName().endsWith(ext)){ + continue; + } + results.add(file); + continue; + } + results.addAll(recursiveFiles(file)); + } + return results; + } + public static List recursiveFiles(File dir){ + List results=new ArrayList<>(); + if(dir.isFile()){ + results.add(dir); + return results; + } + if(!dir.isDirectory()){ + return results; + } + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isFile()){ + results.add(file); + continue; + } + results.addAll(recursiveFiles(file)); + } + return results; + } + public static List listDirectories(File dir){ + List results=new ArrayList<>(); + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isDirectory()){ + results.add(file); + } + } + return results; + } + public static List listFiles(File dir, String ext){ + List results=new ArrayList<>(); + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isFile()){ + if(ext!=null && !file.getName().endsWith(ext)){ + continue; + } + results.add(file); + } + } + return results; + } + public static final String JSON_FILE_EXTENSION=".json"; + public static final String RES_JSON_NAME="res-json"; + public static final String ROOT_NAME="root"; + public static final String PACKAGE_JSON_FILE="package.json"; + public static final String SPLIT_JSON_DIRECTORY="resources"; + public static final String DEF_MODULE_NAME="base"; } diff --git a/src/main/java/com/reandroid/lib/apk/BlockInputSource.java b/src/main/java/com/reandroid/lib/apk/BlockInputSource.java index 9c5010a..84e80ab 100644 --- a/src/main/java/com/reandroid/lib/apk/BlockInputSource.java +++ b/src/main/java/com/reandroid/lib/apk/BlockInputSource.java @@ -1,24 +1,37 @@ package com.reandroid.lib.apk; +import com.reandroid.archive.ByteInputSource; import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.chunk.BaseChunk; +import com.reandroid.lib.arsc.chunk.TableBlock; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; -public class BlockInputSource extends InputSource { +public class BlockInputSource extends ByteInputSource{ private final T mBlock; public BlockInputSource(String name, T block) { - super(name); + super(new byte[0], name); this.mBlock=block; } public T getBlock() { + mBlock.refresh(); return mBlock; } @Override - public InputStream openStream(){ - T block=getBlock(); - block.refresh(); - return new ByteArrayInputStream(block.getBytes()); + public long getLength() throws IOException{ + Block block = getBlock(); + return block.countBytes(); + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getBlock().writeBytes(outputStream); + } + @Override + public byte[] getBytes() { + return getBlock().getBytes(); } } diff --git a/src/main/java/com/reandroid/lib/apk/JsonManifestInputSource.java b/src/main/java/com/reandroid/lib/apk/JsonManifestInputSource.java new file mode 100644 index 0000000..96e44c4 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/JsonManifestInputSource.java @@ -0,0 +1,21 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.chunk.xml.AndroidManifestBlock; + +import java.io.File; + +public class JsonManifestInputSource extends JsonXmlInputSource { + public JsonManifestInputSource(InputSource inputSource) { + super(inputSource); + } + AndroidManifestBlock newInstance(){ + return new AndroidManifestBlock(); + } + public static JsonManifestInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new JsonManifestInputSource(fileInputSource); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/JsonXmlInputSource.java b/src/main/java/com/reandroid/lib/apk/JsonXmlInputSource.java new file mode 100644 index 0000000..68660bb --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/JsonXmlInputSource.java @@ -0,0 +1,47 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.base.Block; +import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock; +import com.reandroid.lib.json.JSONObject; + +import java.io.*; + +public class JsonXmlInputSource extends InputSource { + private final InputSource inputSource; + public JsonXmlInputSource(InputSource inputSource) { + super(inputSource.getAlias()); + this.inputSource=inputSource; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getResXmlBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + ResXmlBlock resXmlBlock= getResXmlBlock(); + return new ByteArrayInputStream(resXmlBlock.getBytes()); + } + @Override + public long getLength() throws IOException{ + ResXmlBlock resXmlBlock = getResXmlBlock(); + return resXmlBlock.countBytes(); + } + private ResXmlBlock getResXmlBlock() throws IOException{ + ResXmlBlock resXmlBlock=newInstance(); + InputStream inputStream=inputSource.openStream(); + JSONObject jsonObject=new JSONObject(inputStream); + resXmlBlock.fromJson(jsonObject); + inputStream.close(); + return resXmlBlock; + } + ResXmlBlock newInstance(){ + return new ResXmlBlock(); + } + public static JsonXmlInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new JsonXmlInputSource(fileInputSource); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/ResourceIds.java b/src/main/java/com/reandroid/lib/apk/ResourceIds.java new file mode 100644 index 0000000..15c673d --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/ResourceIds.java @@ -0,0 +1,472 @@ +package com.reandroid.lib.apk; + + +import com.reandroid.lib.arsc.chunk.PackageBlock; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.group.EntryGroup; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ResourceIds { + private final Table mTable; + public ResourceIds(Table table){ + this.mTable=table; + } + public ResourceIds(){ + this(new Table()); + } + public JSONObject toJson(){ + return mTable.toJson(); + } + public void loadTableBlock(TableBlock tableBlock){ + for(PackageBlock packageBlock:tableBlock.listPackages()){ + loadPackageBlock(packageBlock); + } + } + public void loadPackageBlock(PackageBlock packageBlock){ + Collection entryGroupList = packageBlock.listEntryGroup(); + String name= packageBlock.getName(); + for(EntryGroup entryGroup:entryGroupList){ + Table.Package.Type.Entry entry= Table.Package.Type.Entry.fromEntryGroup(entryGroup); + mTable.add(entry); + if(name==null){ + continue; + } + Table.Package.Type type=entry.type; + if(type!=null && type.mPackage!=null){ + type.mPackage.name=name; + name=null; + } + } + } + + public static class Table{ + public final Map packageMap; + public Table(){ + this.packageMap = new HashMap<>(); + } + public void add(Package pkg){ + Package exist=this.packageMap.get(pkg.id); + if(exist!=null){ + exist.merge(pkg); + return; + } + this.packageMap.put(pkg.id, pkg); + } + public void add(Package.Type.Entry entry){ + if(entry==null){ + return; + } + byte pkgId=entry.getPackageId(); + Package pkg = packageMap.get(pkgId); + if(pkg==null){ + pkg=new Package(pkgId); + packageMap.put(pkgId, pkg); + } + pkg.add(entry); + } + public Package.Type.Entry getEntry(int resourceId){ + byte packageId = (byte) ((resourceId>>24) & 0xff); + byte typeId = (byte) ((resourceId>>16) & 0xff); + short entryId = (short) (resourceId & 0xff); + Package pkg = getPackage(packageId); + if(pkg == null){ + return null; + } + return getEntry(packageId, typeId, entryId); + } + public Package getPackage(byte packageId){ + return packageMap.get(packageId); + } + public Package.Type getType(byte packageId, byte typeId){ + Package pkg=getPackage(packageId); + if(pkg==null){ + return null; + } + return pkg.getType(typeId); + } + public Package.Type.Entry getEntry(byte packageId, byte typeId, short entryId){ + Package pkg=getPackage(packageId); + if(pkg==null){ + return null; + } + return pkg.getEntry(typeId, entryId); + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + JSONArray jsonArray=new JSONArray(); + for(Package pkg: packageMap.values()){ + jsonArray.put(pkg.toJson()); + } + jsonObject.put("packages", jsonArray); + return jsonObject; + } + public static Table fromJson(JSONObject jsonObject){ + Table table=new Table(); + JSONArray jsonArray= jsonObject.optJSONArray("packages"); + if(jsonArray!=null){ + int length= jsonArray.length(); + for(int i=0;i typeMap; + public Package(byte id){ + this.id = id; + this.typeMap = new HashMap<>(); + } + public void merge(Package pkg){ + if(pkg==this||pkg==null){ + return; + } + if(pkg.id!=this.id){ + throw new IllegalArgumentException("Different package id: "+this.id+"!="+pkg.id); + } + if(pkg.name!=null){ + this.name = pkg.name; + } + for(Type type:pkg.typeMap.values()){ + add(type); + } + } + public Type getType(byte typeId){ + return typeMap.get(typeId); + } + public void add(Type type){ + Byte typeId= type.id;; + Type exist=this.typeMap.get(typeId); + if(exist!=null){ + exist.merge(type); + return; + } + type.mPackage=this; + this.typeMap.put(typeId, type); + } + public Package.Type.Entry getEntry(byte typeId, short entryId){ + Package.Type type=getType(typeId); + if(type==null){ + return null; + } + return type.getEntry(entryId); + } + public void add(Type.Entry entry){ + if(entry==null){ + return; + } + if(entry.getPackageId()!=this.id){ + throw new IllegalArgumentException("Different package id: "+entry); + } + byte typeId=entry.getTypeId(); + Type type=typeMap.get(typeId); + if(type==null){ + type=new Type(typeId); + type.mPackage=this; + typeMap.put(typeId, type); + } + type.add(entry); + } + public String getHexId(){ + return String.format("0x%02x", id); + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", this.id); + if(this.name!=null){ + jsonObject.put("name", this.name); + } + JSONArray jsonArray=new JSONArray(); + for(Type type:typeMap.values()){ + jsonArray.put(type.toJson()); + } + jsonObject.put("types", jsonArray); + return jsonObject; + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Package aPackage = (Package) o; + return id == aPackage.id; + } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString(){ + return getHexId() + ", types=" + typeMap.size(); + } + + public static Package fromJson(JSONObject jsonObject){ + Package pkg=new Package((byte) jsonObject.getInt("id")); + pkg.name = jsonObject.optString("name", null); + JSONArray jsonArray = jsonObject.optJSONArray("types"); + int length = jsonArray.length(); + for(int i=0;i entryMap; + public Type(byte id){ + this.id = id; + this.entryMap = new HashMap<>(); + } + public byte getPackageId(){ + if(mPackage!=null){ + return mPackage.id; + } + return 0; + } + public void merge(Type type){ + if(type==this||type==null){ + return; + } + if(this.id!= type.id){ + throw new IllegalArgumentException("Different type ids: "+id+"!="+type.id); + } + if(type.name!=null){ + this.name=type.name; + } + for(Entry entry:type.entryMap.values()){ + Short entryId=entry.getEntryId(); + Entry existEntry=this.entryMap.get(entryId); + if(existEntry != null && Objects.equals(existEntry.name, entry.name)){ + continue; + } + this.entryMap.remove(entryId); + entry.type=this; + this.entryMap.put(entryId, entry); + } + } + public Entry getEntry(short entryId){ + return entryMap.get(entryId); + } + public String getHexId(){ + return String.format("0x%02x", id); + } + public void add(Entry entry){ + if(entry==null){ + return; + } + if(entry.getTypeId()!=this.id){ + throw new IllegalArgumentException("Different type id: "+entry); + } + short key=entry.getEntryId(); + Entry exist=entryMap.get(key); + if(exist!=null){ + if(Objects.equals(exist.name, entry.name)){ + return; + } + throw new IllegalArgumentException("Duplicate entry exist: "+exist+", entry: "+entry); + } + if(name == null){ + this.name = entry.typeName; + } + entry.type=this; + entryMap.put(key, entry); + } + + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("name", name); + JSONArray jsonArray=new JSONArray(); + for(Entry entry: entryMap.values()){ + jsonArray.put(entry.toJson()); + } + jsonObject.put("entries", jsonArray); + return jsonObject; + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Type that = (Type) o; + return id == that.id; + } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getHexId()); + if(name !=null){ + builder.append(" ").append(name); + } + builder.append(", entries=").append(entryMap.size()); + return builder.toString(); + } + + public static Type fromJson(JSONObject jsonObject){ + Type type = new Type((byte) jsonObject.getInt("id")); + type.name = jsonObject.optString("name", null); + JSONArray jsonArray = jsonObject.optJSONArray("entries"); + if(jsonArray!=null){ + int length=jsonArray.length(); + for(int i=0;i{ + public int resourceId; + public String typeName; + public String name; + public Type type; + public Entry(int resourceId, String typeName, String name){ + this.resourceId = resourceId; + this.typeName = typeName; + this.name = name; + } + public Entry(int resourceId, String name){ + this(resourceId, null, name); + } + public String getTypeName(){ + if(this.type!=null){ + return this.type.name; + } + return this.typeName; + } + public byte getPackageId(){ + if(this.type!=null){ + Package pkg=this.type.mPackage; + if(pkg!=null){ + return pkg.id; + } + } + return (byte) ((resourceId>>24) & 0xff); + } + public byte getTypeId(){ + if(this.type!=null){ + return this.type.id; + } + return (byte) ((resourceId>>16) & 0xff); + } + public short getEntryId(){ + return (short) (resourceId & 0xffff); + } + public int getResourceId(){ + return ((getPackageId() & 0xff)<<24) + | ((getTypeId() & 0xff)<<16) + | (getEntryId() & 0xffff); + } + public String getHexId(){ + return String.format("0x%08x", getResourceId()); + } + @Override + public int compareTo(Entry entry) { + return Integer.compare(getResourceId(), entry.getResourceId()); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Entry that = (Entry) o; + return getResourceId() == that.getResourceId(); + } + @Override + public int hashCode() { + return Objects.hash(getResourceId()); + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", getResourceId()); + jsonObject.put("name", name); + return jsonObject; + } + public String toXml(){ + StringBuilder builder=new StringBuilder(); + builder.append(""); + return builder.toString(); + } + @Override + public String toString(){ + return toJson().toString(); + } + public static Entry fromEntryGroup(EntryGroup entryGroup){ + return new Entry(entryGroup.getResourceId(), + entryGroup.getTypeName(), + entryGroup.getSpecName()); + } + public static Entry fromJson(JSONObject jsonObject){ + return new Entry(jsonObject.getInt("id"), + jsonObject.optString("type", null), + jsonObject.getString("name")); + } + public static Entry fromXml(String xmlElement){ + String element=xmlElement; + element=element.replaceAll("[\n\r\t]+", " "); + element=element.trim(); + String start="")){ + return null; + } + element=element.substring(start.length()).trim(); + Pattern pattern=PATTERN; + int id=0; + String type=null; + String name=null; + Matcher matcher=pattern.matcher(element); + while (matcher.find()){ + String attr=matcher.group("Attr").toLowerCase(); + String value=matcher.group("Value"); + element=matcher.group("Next"); + if(attr.equals("id")){ + id=Integer.decode(value); + }else if(attr.equals("name")){ + name=value; + }else if(attr.equals("type")){ + type=value; + } + matcher= pattern.matcher(element); + } + if(id==0){ + throw new IllegalArgumentException("Missing id: "+xmlElement); + } + if(name==null){ + throw new IllegalArgumentException("Missing name: "+xmlElement); + } + return new Entry(id, type, name); + } + private static final Pattern PATTERN=Pattern.compile("^\\s*(?[^\\s=\"]+)\\s*=\\s*\"(?[^\"]+)\"(?.*)$"); + } + } + + } + } + + public static final String JSON_FILE_NAME ="resource-ids.json"; +} diff --git a/src/main/java/com/reandroid/lib/apk/SingleJsonTableInputSource.java b/src/main/java/com/reandroid/lib/apk/SingleJsonTableInputSource.java new file mode 100644 index 0000000..c9751a1 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/SingleJsonTableInputSource.java @@ -0,0 +1,53 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.json.JSONObject; + +import java.io.*; + +public class SingleJsonTableInputSource extends InputSource { + private final InputSource inputSource; + private TableBlock mCache; + public SingleJsonTableInputSource(InputSource inputSource) { + super(inputSource.getAlias()); + this.inputSource=inputSource; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getTableBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + TableBlock tableBlock = getTableBlock(); + return new ByteArrayInputStream(tableBlock.getBytes()); + } + @Override + public long getLength() throws IOException{ + TableBlock tableBlock = getTableBlock(); + return tableBlock.countBytes(); + } + private TableBlock getTableBlock() throws IOException{ + if(mCache!=null){ + return mCache; + } + TableBlock tableBlock=newInstance(); + InputStream inputStream=inputSource.openStream(); + JSONObject jsonObject=new JSONObject(inputStream); + StringPoolBuilder poolBuilder=new StringPoolBuilder(); + poolBuilder.build(jsonObject); + poolBuilder.apply(tableBlock); + tableBlock.fromJson(jsonObject); + mCache=tableBlock; + return tableBlock; + } + TableBlock newInstance(){ + return new TableBlock(); + } + public static SingleJsonTableInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new SingleJsonTableInputSource(fileInputSource); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/SplitJsonTableInputSource.java b/src/main/java/com/reandroid/lib/apk/SplitJsonTableInputSource.java new file mode 100644 index 0000000..d99b168 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/SplitJsonTableInputSource.java @@ -0,0 +1,38 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.chunk.TableBlock; + +import java.io.*; + +public class SplitJsonTableInputSource extends InputSource { + private final File dir; + private TableBlock mCache; + public SplitJsonTableInputSource(File dir) { + super(TableBlock.FILE_NAME); + this.dir=dir; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getTableBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + TableBlock tableBlock = getTableBlock(); + return new ByteArrayInputStream(tableBlock.getBytes()); + } + @Override + public long getLength() throws IOException{ + TableBlock tableBlock = getTableBlock(); + return tableBlock.countBytes(); + } + private TableBlock getTableBlock() throws IOException { + if(mCache!=null){ + return mCache; + } + TableBlockJsonBuilder builder=new TableBlockJsonBuilder(); + TableBlock tableBlock=builder.scanDirectory(dir); + mCache=tableBlock; + return tableBlock; + } +} diff --git a/src/main/java/com/reandroid/lib/apk/StringPoolBuilder.java b/src/main/java/com/reandroid/lib/apk/StringPoolBuilder.java new file mode 100644 index 0000000..0e43601 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/StringPoolBuilder.java @@ -0,0 +1,135 @@ +package com.reandroid.lib.apk; + +import com.reandroid.lib.arsc.chunk.PackageBlock; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.pool.SpecStringPool; +import com.reandroid.lib.arsc.pool.TableStringPool; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; + +public class StringPoolBuilder { + private final Map> mSpecNameMap; + private final Set mTableStrings; + private byte mCurrentPackageId; + private JSONArray mStyledStrings; + public StringPoolBuilder(){ + this.mSpecNameMap = new HashMap<>(); + this.mTableStrings = new HashSet<>(); + } + public void apply(TableBlock tableBlock){ + applyTableString(tableBlock.getTableStringPool()); + for(byte pkgId:mSpecNameMap.keySet()){ + PackageBlock packageBlock=tableBlock.getPackageArray().getOrCreate(pkgId); + applySpecString(packageBlock.getSpecStringPool()); + } + } + private void applyTableString(TableStringPool stringPool){ + stringPool.fromJson(mStyledStrings); + stringPool.addStrings(getTableString()); + stringPool.refresh(); + } + private void applySpecString(SpecStringPool stringPool){ + byte pkgId= (byte) stringPool.getPackageBlock().getPackageId(); + stringPool.addStrings(getSpecString(pkgId)); + stringPool.refresh(); + } + public void scanDirectory(File resourcesDir) throws IOException { + mCurrentPackageId=0; + List pkgDirList=ApkUtil.listDirectories(resourcesDir); + for(File dir:pkgDirList){ + File pkgFile=new File(dir, ApkUtil.PACKAGE_JSON_FILE); + scanFile(pkgFile); + List jsonFileList=ApkUtil.recursiveFiles(dir, ".json"); + for(File file:jsonFileList){ + if(file.equals(pkgFile)){ + continue; + } + scanFile(file); + } + } + } + public void scanFile(File jsonFile) throws IOException { + FileInputStream inputStream=new FileInputStream(jsonFile); + JSONObject jsonObject=new JSONObject(inputStream); + build(jsonObject); + } + public void build(JSONObject jsonObject){ + scan(jsonObject); + } + public Set getTableString(){ + return mTableStrings; + } + public Set getSpecString(byte pkgId){ + return mSpecNameMap.get(pkgId); + } + private void scan(JSONObject jsonObject){ + if(jsonObject.has("is_array")){ + addSpecName(jsonObject.optString("name")); + } + if(jsonObject.has("value_type")){ + if("STRING".equals(jsonObject.getString("value_type"))){ + String data= jsonObject.getString("data"); + addTableString(data); + } + return; + }else if(jsonObject.has(PackageBlock.NAME_package_id)){ + mCurrentPackageId= (byte) jsonObject.getInt(PackageBlock.NAME_package_id); + } + Set keyList = jsonObject.keySet(); + for(String key:keyList){ + Object obj=jsonObject.get(key); + if(obj instanceof JSONObject){ + scan((JSONObject) obj); + continue; + } + if(obj instanceof JSONArray){ + JSONArray jsonArray = (JSONArray) obj; + if(TableBlock.NAME_styled_strings.equals(key)){ + this.mStyledStrings = jsonArray; + }else { + scan(jsonArray); + } + } + } + } + private void scan(JSONArray jsonArray){ + if(jsonArray==null){ + return; + } + for(Object obj:jsonArray.getArrayList()){ + if(obj instanceof JSONObject){ + scan((JSONObject) obj); + continue; + } + if(obj instanceof JSONArray){ + scan((JSONArray) obj); + } + } + } + private void addTableString(String name){ + if(name==null){ + return; + } + mTableStrings.add(name); + } + private void addSpecName(String name){ + if(name==null){ + return; + } + byte pkgId=mCurrentPackageId; + if(pkgId==0){ + throw new IllegalArgumentException("Current package id is 0"); + } + Set specNames=mSpecNameMap.get(pkgId); + if(specNames==null){ + specNames=new HashSet<>(); + mSpecNameMap.put(pkgId, specNames); + } + specNames.add(name); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/TableBlockJson.java b/src/main/java/com/reandroid/lib/apk/TableBlockJson.java new file mode 100644 index 0000000..bba8272 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/TableBlockJson.java @@ -0,0 +1,62 @@ +package com.reandroid.lib.apk; + +import com.reandroid.lib.arsc.chunk.PackageBlock; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.chunk.TypeBlock; +import com.reandroid.lib.arsc.container.SpecTypePair; +import com.reandroid.lib.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +public class TableBlockJson { + private final TableBlock tableBlock; + public TableBlockJson(TableBlock tableBlock){ + this.tableBlock=tableBlock; + } + public void writeJsonFiles(File outDir) throws IOException { + for(PackageBlock packageBlock: tableBlock.listPackages()){ + writeJsonFiles(outDir, packageBlock); + } + } + private void writeJsonFiles(File rootDir, PackageBlock packageBlock) throws IOException { + File pkgDir=new File(rootDir, getDirName(packageBlock)); + if(!pkgDir.exists()){ + pkgDir.mkdirs(); + } + File infoFile=new File(pkgDir, PackageBlock.JSON_FILE_NAME); + JSONObject jsonObject=new JSONObject(); + jsonObject.put(PackageBlock.NAME_package_id, packageBlock.getPackageId()); + jsonObject.put(PackageBlock.NAME_package_name, packageBlock.getPackageName()); + jsonObject.write(infoFile); + for(SpecTypePair specTypePair: packageBlock.listAllSpecTypePair()){ + for(TypeBlock typeBlock:specTypePair.getTypeBlockArray().listItems()){ + writeJsonFiles(pkgDir, typeBlock); + } + } + } + private void writeJsonFiles(File pkgDir, TypeBlock typeBlock) throws IOException { + String name= getFileName(typeBlock)+".json"; + File file=new File(pkgDir, name); + JSONObject jsonObject = typeBlock.toJson(); + jsonObject.write(file); + } + private String getFileName(TypeBlock typeBlock){ + StringBuilder builder=new StringBuilder(); + builder.append(String.format("0x%02x", typeBlock.getTypeId())); + String name= typeBlock.getTypeName(); + builder.append('-').append(name); + builder.append(typeBlock.getResConfig().getQualifiers()); + return builder.toString(); + } + private String getDirName(PackageBlock packageBlock){ + StringBuilder builder=new StringBuilder(); + builder.append(String.format("0x%02x", packageBlock.getPackageId())); + String name= packageBlock.getPackageName(); + if(name!=null){ + builder.append('-'); + builder.append(name); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/TableBlockJsonBuilder.java b/src/main/java/com/reandroid/lib/apk/TableBlockJsonBuilder.java new file mode 100644 index 0000000..571a723 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/TableBlockJsonBuilder.java @@ -0,0 +1,63 @@ +package com.reandroid.lib.apk; + +import com.reandroid.lib.arsc.chunk.PackageBlock; +import com.reandroid.lib.arsc.chunk.TableBlock; +import com.reandroid.lib.arsc.chunk.TypeBlock; +import com.reandroid.lib.arsc.value.ResConfig; +import com.reandroid.lib.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; + +public class TableBlockJsonBuilder { + private final StringPoolBuilder poolBuilder; + public TableBlockJsonBuilder(){ + poolBuilder=new StringPoolBuilder(); + } + public TableBlock scanDirectory(File resourcesDir) throws IOException { + if(!resourcesDir.isDirectory()){ + throw new IOException("No such directory: "+resourcesDir); + } + List pkgDirList=ApkUtil.listDirectories(resourcesDir); + if(pkgDirList.size()==0){ + throw new IOException("No package sub directory found in : "+resourcesDir); + } + TableBlock tableBlock=new TableBlock(); + poolBuilder.scanDirectory(resourcesDir); + poolBuilder.apply(tableBlock); + for(File pkgDir:pkgDirList){ + scanPackageDirectory(tableBlock, pkgDir); + } + tableBlock.refresh(); + return tableBlock; + } + private void scanPackageDirectory(TableBlock tableBlock, File pkgDir) throws IOException{ + File pkgFile=new File(pkgDir, "package.json"); + if(!pkgFile.isFile()){ + throw new IOException("Invalid package directory! Package file missing: "+pkgFile); + } + FileInputStream inputStream=new FileInputStream(pkgFile); + JSONObject jsonObject=new JSONObject(inputStream); + int id = jsonObject.getInt(PackageBlock.NAME_package_id); + String name=jsonObject.optString(PackageBlock.NAME_package_name); + PackageBlock pkg=tableBlock.getPackageArray().getOrCreate((byte) id); + pkg.setName(name); + List typeFileList=ApkUtil.listFiles(pkgDir, ".json"); + typeFileList.remove(pkgFile); + for(File typeFile:typeFileList){ + loadType(pkg, typeFile); + } + } + private void loadType(PackageBlock packageBlock, File typeJsonFile) throws IOException{ + FileInputStream inputStream=new FileInputStream(typeJsonFile); + JSONObject jsonObject=new JSONObject(inputStream); + int id= jsonObject.getInt("id"); + JSONObject configObj=jsonObject.getJSONObject("config"); + ResConfig resConfig=new ResConfig(); + resConfig.fromJson(configObj); + TypeBlock typeBlock=packageBlock.getSpecTypePairArray().getOrCreate((byte) id, resConfig); + typeBlock.fromJson(jsonObject); + } +} diff --git a/src/main/java/com/reandroid/lib/apk/UncompressedFiles.java b/src/main/java/com/reandroid/lib/apk/UncompressedFiles.java new file mode 100644 index 0000000..1a45e25 --- /dev/null +++ b/src/main/java/com/reandroid/lib/apk/UncompressedFiles.java @@ -0,0 +1,81 @@ +package com.reandroid.lib.apk; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive.ZipArchive; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONConvert; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.ZipEntry; + +public class UncompressedFiles implements JSONConvert { + private final Set mPathList; + public UncompressedFiles(){ + this.mPathList=new HashSet<>(); + } + public void apply(ZipArchive archive){ + for(InputSource inputSource:archive.listInputSources()){ + apply(inputSource); + } + } + public void apply(InputSource inputSource){ + if(contains(inputSource.getName()) || contains(inputSource.getAlias())){ + inputSource.setMethod(ZipEntry.STORED); + }else { + inputSource.setMethod(ZipEntry.DEFLATED); + } + } + public boolean contains(String path){ + return mPathList.contains(path); + } + public void add(ZipArchive zipArchive){ + for(InputSource inputSource: zipArchive.listInputSources()){ + add(inputSource); + } + } + public void add(InputSource inputSource){ + if(inputSource.getMethod()!=ZipEntry.STORED){ + return; + } + add(inputSource.getAlias()); + } + public void add(String path){ + if(path==null || path.length()==0){ + return; + } + path=path.replace(File.separatorChar, '/').trim(); + while (path.startsWith("/")){ + path=path.substring(1); + } + mPathList.add(path); + } + public void clear(){ + mPathList.clear(); + } + @Override + public JSONArray toJson() { + return new JSONArray(mPathList); + } + @Override + public void fromJson(JSONArray json) { + if(json==null){ + return; + } + int length = json.length(); + for(int i=0;i implements JSO } @Override public String toString(){ - return toJson().toString(4); + return getClass().getSimpleName()+": size="+childesCount(); } private static final String NAME_id="id"; } diff --git a/src/main/java/com/reandroid/lib/arsc/array/SpecTypePairArray.java b/src/main/java/com/reandroid/lib/arsc/array/SpecTypePairArray.java index 9cfbb5a..c228675 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/SpecTypePairArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/SpecTypePairArray.java @@ -4,6 +4,7 @@ import com.reandroid.lib.arsc.base.BlockArray; import com.reandroid.lib.arsc.chunk.TypeBlock; import com.reandroid.lib.arsc.container.SpecTypePair; import com.reandroid.lib.arsc.value.EntryBlock; +import com.reandroid.lib.arsc.value.ResConfig; import com.reandroid.lib.json.JSONConvert; import com.reandroid.lib.json.JSONArray; import com.reandroid.lib.json.JSONObject; @@ -61,6 +62,10 @@ public class SpecTypePairArray extends BlockArray implements JSONC } return pair.getTypeBlock(qualifiers); } + public TypeBlock getOrCreate(byte typeId, ResConfig resConfig){ + SpecTypePair pair=getOrCreate(typeId); + return pair.getTypeBlockArray().getOrCreate(resConfig); + } public SpecTypePair getOrCreate(byte typeId){ SpecTypePair pair=getPair(typeId); if(pair!=null){ @@ -143,6 +148,52 @@ public class SpecTypePairArray extends BlockArray implements JSONC } return results; } + public byte getSmallestTypeId(){ + SpecTypePair[] childes=getChildes(); + if(childes==null){ + return 0; + } + int result=0; + boolean firstFound=false; + for (int i=0;i extends OffsetBlockArray this.mUtf8=is_utf8; setEndBytes((byte)0x00); } + public List toStringList(){ + return new AbstractList() { + @Override + public String get(int i) { + T item=StringArray.this.get(i); + if(item==null){ + return null; + } + return item.getHtml(); + } + @Override + public int size() { + return childesCount(); + } + }; + } public List removeUnusedStrings(){ List allUnused=listUnusedStrings(); remove(allUnused); @@ -53,6 +70,7 @@ public abstract class StringArray extends OffsetBlockArray protected void refreshChildes(){ // Not required } + // Only styled strings @Override public JSONArray toJson() { return toJson(true); @@ -74,6 +92,9 @@ public abstract class StringArray extends OffsetBlockArray if(jsonObject==null){ continue; } + if(i>750){ + i=i+0; + } jsonArray.put(i, jsonObject); i++; } @@ -82,19 +103,9 @@ public abstract class StringArray extends OffsetBlockArray } return jsonArray; } + // Only styled strings @Override public void fromJson(JSONArray json) { - clearChildes(); - if(json==null){ - return; - } - int length = json.length(); - ensureSize(length); - for(int i=0; i implements JSONConvert } return typeBlock.getEntry(entryId); } + public TypeBlock getOrCreate(ResConfig resConfig){ + TypeBlock typeBlock=getTypeBlock(resConfig); + if(typeBlock!=null){ + return typeBlock; + } + byte id=getTypeId(); + typeBlock=createNext(); + typeBlock.setTypeId(id); + ResConfig config=typeBlock.getResConfig(); + config.copyFrom(resConfig); + return typeBlock; + } public TypeBlock getOrCreate(String qualifiers){ TypeBlock typeBlock=getTypeBlock(qualifiers); if(typeBlock!=null){ diff --git a/src/main/java/com/reandroid/lib/arsc/base/BlockArray.java b/src/main/java/com/reandroid/lib/arsc/base/BlockArray.java index f7dee78..ce09ef0 100755 --- a/src/main/java/com/reandroid/lib/arsc/base/BlockArray.java +++ b/src/main/java/com/reandroid/lib/arsc/base/BlockArray.java @@ -183,8 +183,23 @@ public abstract class BlockArray extends BlockContainer impl return false; } public void remove(Collection blockList){ + T[] items=elementData; + if(items==null || items.length==0){ + return; + } + int len=items.length; for(T block:blockList){ - remove(block, false); + if(block==null){ + continue; + } + int i=block.getIndex(); + if(i<0 || i>=len){ + continue; + } + if(items[i]!=block){ + continue; + } + items[i]=null; } trimNullBlocks(); } diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/BaseTypeBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/BaseTypeBlock.java index 5947302..6cbd485 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/BaseTypeBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/BaseTypeBlock.java @@ -62,6 +62,13 @@ abstract class BaseTypeBlock extends BaseChunk { } return null; } + public String getTypeName(){ + TypeString typeString=getTypeString(); + if(typeString==null){ + return null; + } + return typeString.get(); + } public TypeString getTypeString(){ if(mTypeString!=null){ if(mTypeString.getId()==getTypeId()){ diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java index d8fa9e4..ebc38bd 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java @@ -4,8 +4,11 @@ import com.reandroid.lib.arsc.array.LibraryInfoArray; import com.reandroid.lib.arsc.array.SpecTypePairArray; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.container.PackageLastBlocks; +import com.reandroid.lib.arsc.container.SingleBlockContainer; import com.reandroid.lib.arsc.container.SpecTypePair; import com.reandroid.lib.arsc.group.EntryGroup; +import com.reandroid.lib.arsc.io.BlockLoad; +import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.PackageName; import com.reandroid.lib.arsc.item.ReferenceItem; @@ -17,17 +20,19 @@ import com.reandroid.lib.arsc.value.LibraryInfo; import com.reandroid.lib.json.JSONConvert; import com.reandroid.lib.json.JSONObject; +import java.io.IOException; import java.util.*; -public class PackageBlock extends BaseChunk implements JSONConvert { +public class PackageBlock extends BaseChunk implements BlockLoad, JSONConvert { private final IntegerItem mPackageId; private final PackageName mPackageName; - private final IntegerItem mTypeStrings; - private final IntegerItem mLastPublicType; - private final IntegerItem mKeyStrings; - private final IntegerItem mLastPublicKey; + private final IntegerItem mTypeStringPoolOffset; + private final IntegerItem mTypeStringPoolCount; + private final IntegerItem mSpecStringPoolOffset; + private final IntegerItem mSpecStringPoolCount; + private final SingleBlockContainer mTypeIdOffsetContainer; private final IntegerItem mTypeIdOffset; private final TypeStringPool mTypeStringPool; @@ -44,13 +49,18 @@ public class PackageBlock extends BaseChunk implements JSONConvert super(ChunkType.PACKAGE, 3); this.mPackageId=new IntegerItem(); this.mPackageName=new PackageName(); - this.mTypeStrings=new IntegerItem(); - this.mLastPublicType=new IntegerItem(); - this.mKeyStrings=new IntegerItem(); - this.mLastPublicKey=new IntegerItem(); - this.mTypeIdOffset=new IntegerItem(); - this.mTypeStringPool=new TypeStringPool(false); + this.mTypeStringPoolOffset = new IntegerItem(); + this.mTypeStringPoolCount = new IntegerItem(); + this.mSpecStringPoolOffset = new IntegerItem(); + this.mSpecStringPoolCount = new IntegerItem(); + + this.mTypeIdOffsetContainer = new SingleBlockContainer<>(); + this.mTypeIdOffset = new IntegerItem(); + this.mTypeIdOffsetContainer.setItem(mTypeIdOffset); + + + this.mTypeStringPool=new TypeStringPool(false, mTypeIdOffset); this.mSpecStringPool=new SpecStringPool(true); this.mSpecTypePairArray=new SpecTypePairArray(); @@ -59,13 +69,15 @@ public class PackageBlock extends BaseChunk implements JSONConvert this.mEntriesGroup=new HashMap<>(); + mPackageId.setBlockLoad(this); + addToHeader(mPackageId); addToHeader(mPackageName); - addToHeader(mTypeStrings); - addToHeader(mLastPublicType); - addToHeader(mKeyStrings); - addToHeader(mLastPublicKey); - addToHeader(mTypeIdOffset); + addToHeader(mTypeStringPoolOffset); + addToHeader(mTypeStringPoolCount); + addToHeader(mSpecStringPoolOffset); + addToHeader(mSpecStringPoolCount); + addToHeader(mTypeIdOffsetContainer); addChild(mTypeStringPool); addChild(mSpecStringPool); @@ -73,6 +85,16 @@ public class PackageBlock extends BaseChunk implements JSONConvert addChild(mPackageLastBlocks); } + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender==mPackageId){ + int headerSize=getHeaderBlock().getHeaderSize(); + if(headerSize!=288){ + mTypeIdOffset.set(0); + mTypeIdOffsetContainer.setItem(null); + } + } + } public void removeEmpty(){ getSpecTypePairArray().removeEmptyPairs(); } @@ -115,33 +137,7 @@ public class PackageBlock extends BaseChunk implements JSONConvert public void setPackageName(String name){ mPackageName.set(name); } - public void setTypeStrings(int i){ - mTypeStrings.set(i); - } - public int getLastPublicType(){ - return mLastPublicType.get(); - } - public void setLastPublicType(int i){ - mLastPublicType.set(i); - } - public int getKeyStrings(){ - return mKeyStrings.get(); - } - public void setKeyStrings(int i){ - mKeyStrings.set(i); - } - public int getLastPublicKey(){ - return mLastPublicKey.get(); - } - public void setLastPublicKey(int i){ - mLastPublicKey.set(i); - } - public int getTypeIdOffset(){ - return mTypeIdOffset.get(); - } - public void setTypeIdOffset(int i){ - mTypeIdOffset.set(i); - } + public TypeStringPool getTypeStringPool(){ return mTypeStringPool; } @@ -250,9 +246,26 @@ public class PackageBlock extends BaseChunk implements JSONConvert return getSpecTypePairArray().listItems(); } - private void refreshKeyStrings(){ + private void refreshTypeStringPoolOffset(){ + int pos=countUpTo(mTypeStringPool); + mTypeStringPoolOffset.set(pos); + } + private void refreshTypeStringPoolCount(){ + mTypeStringPoolCount.set(mTypeStringPool.countStrings()); + } + private void refreshSpecStringPoolOffset(){ int pos=countUpTo(mSpecStringPool); - mKeyStrings.set(pos); + mSpecStringPoolOffset.set(pos); + } + private void refreshSpecStringCount(){ + mSpecStringPoolCount.set(mSpecStringPool.countStrings()); + } + private void refreshTypeIdOffset(){ + int smallestId=getSpecTypePairArray().getSmallestTypeId(); + if(smallestId>0){ + smallestId=smallestId-1; + } + mTypeIdOffset.set(smallestId); } public void onEntryAdded(EntryBlock entryBlock){ updateEntry(entryBlock); @@ -263,14 +276,18 @@ public class PackageBlock extends BaseChunk implements JSONConvert @Override protected void onChunkRefreshed() { - refreshKeyStrings(); + refreshTypeStringPoolOffset(); + refreshTypeStringPoolCount(); + refreshSpecStringPoolOffset(); + refreshSpecStringCount(); + refreshTypeIdOffset(); } @Override public JSONObject toJson() { JSONObject jsonObject=new JSONObject(); - jsonObject.put(NAME_id, getId()); - jsonObject.put(NAME_name, getName()); + jsonObject.put(NAME_package_id, getId()); + jsonObject.put(NAME_package_name, getName()); jsonObject.put(NAME_specs, getSpecTypePairArray().toJson()); LibraryInfoArray libraryInfoArray = mLibraryBlock.getLibraryInfoArray(); if(libraryInfoArray.childesCount()>0){ @@ -280,8 +297,8 @@ public class PackageBlock extends BaseChunk implements JSONConvert } @Override public void fromJson(JSONObject json) { - setId(json.getInt(NAME_id)); - setName(json.getString(NAME_name)); + setId(json.getInt(NAME_package_id)); + setName(json.getString(NAME_package_name)); getSpecTypePairArray().fromJson(json.getJSONArray(NAME_specs)); LibraryInfoArray libraryInfoArray = mLibraryBlock.getLibraryInfoArray(); libraryInfoArray.fromJson(json.optJSONArray(NAME_libraries)); @@ -302,8 +319,9 @@ public class PackageBlock extends BaseChunk implements JSONConvert return builder.toString(); } - private static final String NAME_id="id"; - private static final String NAME_name="name"; + public static final String NAME_package_id = "package_id"; + public static final String NAME_package_name = "package_name"; + public static final String JSON_FILE_NAME = "package.json"; private static final String NAME_specs="specs"; private static final String NAME_libraries="libraries"; } diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java index 542fa3e..e59d3ee 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java @@ -130,10 +130,6 @@ public class TableBlock extends BaseChunk implements JSONConvert { } @Override public void fromJson(JSONObject json) { - JSONArray jsonArray= json.optJSONArray(NAME_styled_strings); - if(jsonArray!=null){ - getTableStringPool().fromJson(jsonArray); - } getPackageArray().fromJson(json.getJSONArray(NAME_packages)); refresh(); } @@ -190,5 +186,5 @@ public class TableBlock extends BaseChunk implements JSONConvert { public static final String FILE_NAME="resources.arsc"; private static final String NAME_packages="packages"; - private static final String NAME_styled_strings="styled_strings"; + public static final String NAME_styled_strings="styled_strings"; } diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/BaseXmlChunk.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/BaseXmlChunk.java index 0f71db2..f64b196 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/xml/BaseXmlChunk.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/BaseXmlChunk.java @@ -40,7 +40,7 @@ public class BaseXmlChunk extends BaseChunk { return mLineNumber.get(); } public void setCommentReference(int val){ - mLineNumber.set(val); + mCommentReference.set(val); } public int getCommentReference(){ return mCommentReference.get(); @@ -119,6 +119,21 @@ public class BaseXmlChunk extends BaseChunk { public String getUri(){ return getString(getNamespaceReference()); } + public String getComment(){ + return getString(getCommentReference()); + } + public void setComment(String comment){ + if(comment==null||comment.length()==0){ + setCommentReference(-1); + }else { + String old=getComment(); + if(comment.equals(old)){ + return; + } + ResXmlString xmlString = getOrCreateResXmlString(comment); + setCommentReference(xmlString.getIndex()); + } + } public ResXmlElement getParentResXmlElement(){ Block parent=getParent(); @@ -137,6 +152,14 @@ public class BaseXmlChunk extends BaseChunk { @Override protected void onChunkRefreshed() { + } + @Override + public void onChunkLoaded(){ + super.onChunkLoaded(); + if(mCommentReference.get()!=-1){ + String junk=getString(mCommentReference.get()); + System.out.println(junk); + } } @Override public String toString(){ diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java index 5a18555..8987bc0 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java @@ -160,7 +160,7 @@ public class ResXmlBlock extends BaseChunk implements JSONConvert { buildResourceIds(attributeList); Set allStrings=recursiveStrings(json.optJSONObject(ResXmlBlock.NAME_element)); ResXmlStringPool stringPool = getStringPool(); - stringPool.addAllStrings(allStrings); + stringPool.addStrings(allStrings); stringPool.refresh(); } private void buildResourceIds(List attributeList){ diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlElement.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlElement.java index b7df458..b10f38a 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlElement.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlElement.java @@ -316,6 +316,20 @@ public class ResXmlElement extends FixedBlockContainer implements JSONConvert"+txt+"<"; + return txt; } return super.toString(); } diff --git a/src/main/java/com/reandroid/lib/arsc/item/IntegerArray.java b/src/main/java/com/reandroid/lib/arsc/item/IntegerArray.java index 4f8ba78..a7d47bd 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/IntegerArray.java +++ b/src/main/java/com/reandroid/lib/arsc/item/IntegerArray.java @@ -89,13 +89,6 @@ public class IntegerArray extends BlockItem { public final int size(){ return getBytesLength()/4; } - final void add(int value){ - int len=getBytesLength(); - len=len + 4; - setBytesLength(len, false); - int pos=size()-1; - put(pos, value); - } public final void put(int index, int value){ int i=index*4; byte[] bts = getBytesInternal(); @@ -104,7 +97,6 @@ public class IntegerArray extends BlockItem { bts[i+1]= (byte) (value >>> 8 & 0xff); bts[i]= (byte) (value & 0xff); } - @Override public void onBytesChanged() { diff --git a/src/main/java/com/reandroid/lib/arsc/item/SpecString.java b/src/main/java/com/reandroid/lib/arsc/item/SpecString.java index d51cb9e..a727c87 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/SpecString.java +++ b/src/main/java/com/reandroid/lib/arsc/item/SpecString.java @@ -6,6 +6,7 @@ public class SpecString extends StringItem { } @Override public StyleItem getStyle(){ + // Spec (resource name) don't have style unless to obfuscate/confuse other decompilers return null; } } diff --git a/src/main/java/com/reandroid/lib/arsc/item/StringItem.java b/src/main/java/com/reandroid/lib/arsc/item/StringItem.java index 25ee7d9..7969a18 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/StringItem.java +++ b/src/main/java/com/reandroid/lib/arsc/item/StringItem.java @@ -173,7 +173,7 @@ public class StringItem extends BlockItem implements JSONConvert { if(styleItem==null){ return false; } - return !styleItem.isNull(); + return styleItem.getSpanInfoList().size()>0; } public StyleItem getStyle(){ BaseStringPool stringPool=getStringPool(); @@ -362,6 +362,6 @@ public class StringItem extends BlockItem implements JSONConvert { private final CharsetDecoder UTF16LE_DECODER = StandardCharsets.UTF_16LE.newDecoder(); private final CharsetDecoder UTF8_DECODER = StandardCharsets.UTF_8.newDecoder(); - private static final String NAME_string="string"; - private static final String NAME_style="style"; + public static final String NAME_string="string"; + public static final String NAME_style="style"; } diff --git a/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java b/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java index 9ae950c..88567b2 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java +++ b/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java @@ -153,6 +153,9 @@ public class StyleItem extends IntegerArray implements JSONConvert { @Override public StyleSpanInfo get(int i) { int ref=getStringRef(i); + if(ref<=0){ + return null; + } return new StyleSpanInfo( getStringFromPool(ref), getFirstChar(i), @@ -192,7 +195,7 @@ public class StyleItem extends IntegerArray implements JSONConvert { return null; } List spanInfoList = getSpanInfoList(); - if(spanInfoList.size()==0){ + if(isEmpty(spanInfoList)){ return str; } StringBuilder builder=new StringBuilder(); @@ -202,6 +205,9 @@ public class StyleItem extends IntegerArray implements JSONConvert { char ch=allChars[i]; boolean lastAppend=false; for(StyleSpanInfo info:spanInfoList){ + if(info==null){ + continue; + } boolean isLast=(info.getLast()==i); if(info.getFirst()==i || isLast){ if(isLast && !lastAppend){ @@ -221,6 +227,17 @@ public class StyleItem extends IntegerArray implements JSONConvert { } return builder.toString(); } + private boolean isEmpty(List spanInfoList){ + if(spanInfoList.size()==0){ + return true; + } + for(StyleSpanInfo spanInfo:spanInfoList){ + if(spanInfo!=null){ + return false; + } + } + return true; + } @Override public void onBytesChanged() { } @@ -262,10 +279,16 @@ public class StyleItem extends IntegerArray implements JSONConvert { JSONArray jsonArray=new JSONArray(); int i=0; for(StyleSpanInfo spanInfo:getSpanInfoList()){ + if(spanInfo==null){ + continue; + } JSONObject jsonObjectSpan=spanInfo.toJson(); jsonArray.put(i, jsonObjectSpan); i++; } + if(i==0){ + return null; + } jsonObject.put(NAME_spans, jsonArray); return jsonObject; } @@ -295,5 +318,5 @@ public class StyleItem extends IntegerArray implements JSONConvert { private static final int INTEGERS_COUNT = 3; private static final int END_VALUE=0xFFFFFFFF; - private static final String NAME_spans="spans"; + public static final String NAME_spans="spans"; } diff --git a/src/main/java/com/reandroid/lib/arsc/item/TypeString.java b/src/main/java/com/reandroid/lib/arsc/item/TypeString.java index 3e087d7..6733928 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/TypeString.java +++ b/src/main/java/com/reandroid/lib/arsc/item/TypeString.java @@ -10,6 +10,7 @@ public class TypeString extends StringItem { } @Override public StyleItem getStyle(){ + // Type don't have style unless to obfuscate/confuse other decompilers return null; } } diff --git a/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java b/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java index 72d0e87..ca21131 100755 --- a/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java +++ b/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java @@ -78,7 +78,7 @@ public class StyleSpanInfo implements JSONConvert { return mTag +" ("+ mFirst +", "+ mLast +")"; } - private static final String NAME_tag="tag"; - private static final String NAME_first="first"; - private static final String NAME_last="last"; + public static final String NAME_tag="tag"; + public static final String NAME_first="first"; + public static final String NAME_last="last"; } diff --git a/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java b/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java index b750bc7..21b3f68 100755 --- a/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java +++ b/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java @@ -9,9 +9,10 @@ import com.reandroid.lib.arsc.group.StringGroup; import com.reandroid.lib.arsc.io.BlockLoad; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.*; -import com.reandroid.lib.arsc.pool.builder.StyleBuilder; +import com.reandroid.lib.arsc.model.StyleSpanInfo; import com.reandroid.lib.json.JSONConvert; import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.*; @@ -66,31 +67,60 @@ public abstract class BaseStringPool extends BaseChunk imp mUniqueMap=new HashMap<>(); } - public void addAllStrings(Collection stringList){ + public List toStringList(){ + return getStringsArray().toStringList(); + } + public void addStrings(Collection stringList){ + if(stringList==null || stringList.size()==0){ + return; + } + Set uniqueSet; + if(stringList instanceof HashSet){ + uniqueSet=(HashSet)stringList; + }else { + uniqueSet=new HashSet<>(stringList); + } + refreshUniqueIdMap(); + Set keySet=mUniqueMap.keySet(); + for(String key:keySet){ + uniqueSet.remove(key); + } List sortedList=new ArrayList<>(stringList); sortedList.sort(this); - addAllStrings(sortedList); + insertStrings(sortedList); } - public void addAllStrings(List sortedStringList){ - // call refreshUniqueIdMap() just in case elements modified somewhere - refreshUniqueIdMap(); - - for(String str:sortedStringList){ - getOrCreate(str); + private void insertStrings(List stringList){ + StringArray stringsArray = getStringsArray(); + int initialSize=stringsArray.childesCount(); + stringsArray.ensureSize(initialSize + stringList.size()); + int size=stringsArray.childesCount(); + int j=0; + for (int i=initialSize;i stringArray=getStringsArray(); - for(T stringItem:stringArray.listItems()){ - if(StyleBuilder.hasStyle(stringItem)){ - StyleBuilder.buildStyle(stringItem); + mUniqueMap.clear(); + T[] allChildes=getStrings(); + if(allChildes==null){ + return; + } + int max=allChildes.length; + for(int i=0;i group= getOrCreateGroup(str); + group.add(item); } } public List removeUnusedStrings(){ @@ -175,30 +205,6 @@ public abstract class BaseStringPool extends BaseChunk imp mCountStrings.set(mArrayStrings.childesCount()); return item; } - private void reUpdateUniqueMap(){ - mUniqueMap.clear(); - T[] allChildes=getStrings(); - if(allChildes==null){ - return; - } - int max=allChildes.length; - for(int i=0;i group= getOrCreateGroup(str); - group.add(item); - } public final StyleItem getStyle(int index){ return mArrayStyles.get(index); } @@ -259,7 +265,7 @@ public abstract class BaseStringPool extends BaseChunk imp } @Override public void onChunkLoaded() { - reUpdateUniqueMap(); + refreshUniqueIdMap(); } @Override @@ -272,16 +278,121 @@ public abstract class BaseStringPool extends BaseChunk imp public JSONArray toJson() { return getStringsArray().toJson(); } + //Only for styled strings @Override public void fromJson(JSONArray json) { - getStringsArray().fromJson(json); - refreshUniqueIdMap(); + if(json==null){ + return; + } + loadStyledStrings(json); refresh(); } + public void loadStyledStrings(JSONArray jsonArray) { + //Styled strings should be at first rows of string pool thus we clear all before adding + getStringsArray().clearChildes(); + getStyleArray().clearChildes(); + + List styledStringList = StyledString.fromJson(jsonArray); + loadText(styledStringList); + Map tagIndexMap = loadStyleTags(styledStringList); + loadStyles(styledStringList, tagIndexMap); + refreshUniqueIdMap(); + } + private void loadText(List styledStringList) { + StringArray stringsArray = getStringsArray(); + int size=styledStringList.size(); + stringsArray.ensureSize(size); + for(int i=0;i loadStyleTags(List styledStringList) { + Map indexMap=new HashMap<>(); + List tagList=new ArrayList<>(getStyleTags(styledStringList)); + tagList.sort(this); + StringArray stringsArray = getStringsArray(); + int tagsSize = tagList.size(); + int initialSize = stringsArray.childesCount(); + stringsArray.ensureSize(initialSize + tagsSize); + for(int i=0;i styledStringList, Map tagIndexMap){ + StyleArray styleArray = getStyleArray(); + int size=styledStringList.size(); + styleArray.ensureSize(size); + for(int i=0;i getStyleTags(List styledStringList){ + Set results=new HashSet<>(); + for(StyledString ss:styledStringList){ + for(StyleSpanInfo spanInfo:ss.spanInfoList){ + results.add(spanInfo.getTag()); + } + } + return results; + } @Override public int compare(String s1, String s2) { return s1.compareTo(s2); } + + private static class StyledString{ + final String text; + final List spanInfoList; + StyledString(String text, List spanInfoList){ + this.text=text; + this.spanInfoList=spanInfoList; + } + @Override + public String toString(){ + return text; + } + static List fromJson(JSONArray jsonArray){ + int length = jsonArray.length(); + List results=new ArrayList<>(); + for(int i=0;i]+>)([^<]+)(]+>)(.*)$"); } diff --git a/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java b/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java index c8e853a..832984a 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java +++ b/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java @@ -52,6 +52,9 @@ public abstract class BaseResValue extends BlockItem implements JSONConvert { private ShortItem mHeaderSize; - private ShortItem mFlags; + private ByteItem mFlagEntryType; + private ByteItem mByteFlagsB; private IntegerItem mSpecReference; private BaseResValue mResValue; private boolean mUnLocked; public EntryBlock() { super(); } - public ResValueInt setValueAsBoolean(boolean val){ ResValueInt resValueInt; BaseResValue res = getResValue(); @@ -227,12 +227,11 @@ public class EntryBlock extends Block implements JSONConvert { removeTableReferences(); removeSpecReferences(); } - - public short getFlags(){ - return mFlags.get(); + private void setEntryTypeFlag(byte b){ + mFlagEntryType.set(b); } - public void setFlags(short sh){ - mFlags.set(sh); + private void setByteFlagsB(byte b){ + mByteFlagsB.set(b); } public IntegerItem getSpecReferenceBlock(){ return mSpecReference; @@ -260,8 +259,8 @@ public class EntryBlock extends Block implements JSONConvert { setNull(true); }else { setNull(false); - boolean is_complex=(resValue instanceof ResValueBag); - setFlagComplex(is_complex); + boolean is_bag=(resValue instanceof ResValueBag); + setEntryTypeBag(is_bag); updatePackage(); updateSpecRef(); } @@ -271,7 +270,7 @@ public class EntryBlock extends Block implements JSONConvert { return; } if(resValue!=null){ - resValue.setIndex(3); + resValue.setIndex(4); resValue.setParent(this); } if(mResValue!=null){ @@ -284,14 +283,14 @@ public class EntryBlock extends Block implements JSONConvert { mResValue=resValue; } - public void setFlagComplex(boolean is_complex){ + public void setEntryTypeBag(boolean is_complex){ if(is_complex){ - if(!isFlagsComplex()){ - setFlags(FLAG_COMPLEX); + if(!isEntryTypeBag()){ + setEntryTypeFlag(FLAG_VALUE_BAG); } }else { - if(isFlagsComplex()){ - setFlags(FLAG_INT); + if(isEntryTypeBag()){ + setEntryTypeFlag(FLAG_VALUE_INT); } } refreshHeaderSize(); @@ -314,7 +313,13 @@ public class EntryBlock extends Block implements JSONConvert { private void setName(String name){ PackageBlock packageBlock=getPackageBlock(); EntryGroup entryGroup = packageBlock.getEntryGroup(getResourceId()); - entryGroup.renameSpec(name); + if(entryGroup!=null){ + entryGroup.renameSpec(name); + return; + } + SpecStringPool specStringPool= packageBlock.getSpecStringPool(); + SpecString specString=specStringPool.getOrCreate(name); + setSpecReference(specString.getIndex()); } public String getTypeName(){ TypeString typeString=getTypeString(); @@ -437,15 +442,18 @@ public class EntryBlock extends Block implements JSONConvert { } mUnLocked =true; this.mHeaderSize =new ShortItem(); - this.mFlags =new ShortItem(); + this.mFlagEntryType =new ByteItem(); + this.mByteFlagsB=new ByteItem(); this.mSpecReference = new IntegerItem(); mHeaderSize.setIndex(0); - mFlags.setIndex(1); - mSpecReference.setIndex(2); + mFlagEntryType.setIndex(1); + mByteFlagsB.setIndex(2); + mSpecReference.setIndex(3); mHeaderSize.setParent(this); - mFlags.setParent(this); + mFlagEntryType.setParent(this); + mByteFlagsB.setParent(this); mSpecReference.setParent(this); } @@ -456,16 +464,19 @@ public class EntryBlock extends Block implements JSONConvert { removeAllReferences(); mUnLocked =false; mHeaderSize.setParent(null); - mFlags.setParent(null); + mFlagEntryType.setParent(null); + mByteFlagsB.setParent(null); mSpecReference.setParent(null); mHeaderSize.setIndex(-1); - mFlags.setIndex(-1); + mFlagEntryType.setIndex(-1); + mByteFlagsB.setIndex(-1); mSpecReference.setIndex(-1); removeResValue(); this.mHeaderSize =null; - this.mFlags =null; + this.mFlagEntryType =null; + this.mByteFlagsB =null; this.mSpecReference =null; } private void removeResValue(){ @@ -476,7 +487,7 @@ public class EntryBlock extends Block implements JSONConvert { } } private void refreshHeaderSize(){ - if(isFlagsComplex()){ + if(isEntryTypeBag()){ mHeaderSize.set(HEADER_COMPLEX); }else { mHeaderSize.set(HEADER_INT); @@ -487,21 +498,15 @@ public class EntryBlock extends Block implements JSONConvert { return; } BaseResValue resValue; - if(isFlagsComplex()){ + if(isEntryTypeBag()){ resValue=new ResValueBag(); }else { resValue=new ResValueInt(); } setResValueInternal(resValue); } - private boolean isFlagsComplex(){ - return ((mFlags.get() & FLAG_COMPLEX_MASK) != 0); - } - public boolean isArray(){ - return ((mFlags.get() & FLAG_COMPLEX_MASK) != 0); - } - public void setIsArray(boolean is_array){ - setFlagComplex(is_array); + private boolean isEntryTypeBag(){ + return ((mFlagEntryType.get() & FLAG_BAG_ENTRY) != 0); } @Override public void setNull(boolean is_null){ @@ -525,7 +530,8 @@ public class EntryBlock extends Block implements JSONConvert { return null; } byte[] results=mHeaderSize.getBytes(); - results=addBytes(results, mFlags.getBytes()); + results=addBytes(results, mFlagEntryType.getBytes()); + results=addBytes(results, mByteFlagsB.getBytes()); results=addBytes(results, mSpecReference.getBytes()); results=addBytes(results, mResValue.getBytes()); return results; @@ -558,7 +564,8 @@ public class EntryBlock extends Block implements JSONConvert { } counter.addCount(countBytes()); mHeaderSize.onCountUpTo(counter); - mFlags.onCountUpTo(counter); + mFlagEntryType.onCountUpTo(counter); + mByteFlagsB.onCountUpTo(counter); mSpecReference.onCountUpTo(counter); mResValue.onCountUpTo(counter); } @@ -568,7 +575,8 @@ public class EntryBlock extends Block implements JSONConvert { return 0; } int result=mHeaderSize.writeBytes(stream); - result+=mFlags.writeBytes(stream); + result+= mFlagEntryType.writeBytes(stream); + result+=mByteFlagsB.writeBytes(stream); result+= mSpecReference.writeBytes(stream); result+=mResValue.writeBytes(stream); return result; @@ -612,12 +620,14 @@ public class EntryBlock extends Block implements JSONConvert { setNull(false); removeResValue(); mHeaderSize.readBytes(reader); - mFlags.readBytes(reader); + mFlagEntryType.readBytes(reader); + mByteFlagsB.readBytes(reader); mSpecReference.readBytes(reader); createResValue(); mResValue.readBytes(reader); updatePackage(); updateSpecRef(); + mResValue.onDataLoaded(); } @Override public JSONObject toJson() { @@ -626,7 +636,7 @@ public class EntryBlock extends Block implements JSONConvert { } JSONObject jsonObject=new JSONObject(); jsonObject.put(NAME_name, getSpecString().get()); - jsonObject.put(NAME_is_array, isArray()); + jsonObject.put(NAME_is_array, isEntryTypeBag()); jsonObject.put(NAME_value, getResValue().toJson()); return jsonObject; } @@ -636,16 +646,16 @@ public class EntryBlock extends Block implements JSONConvert { setNull(true); return; } - setName(json.getString(NAME_name)); - setNull(false); BaseResValue baseResValue; if(json.getBoolean(NAME_is_array)){ - baseResValue=new ResValueInt(); - }else { baseResValue=new ResValueBag(); + }else { + baseResValue=new ResValueInt(); } setResValue(baseResValue); + setName(json.getString(NAME_name)); baseResValue.fromJson(json.getJSONObject(NAME_value)); + mResValue.onDataLoaded(); } @Override public String toString(){ @@ -698,10 +708,10 @@ public class EntryBlock extends Block implements JSONConvert { return builder.toString(); } - private final static short FLAG_COMPLEX_MASK = 0x0001; + private final static byte FLAG_BAG_ENTRY = 0x01; - private final static short FLAG_COMPLEX = 0x0001; - private final static short FLAG_INT = 0x0000; + private final static byte FLAG_VALUE_BAG = 0x0001; + private final static byte FLAG_VALUE_INT = 0x00; private final static short HEADER_COMPLEX=0x0010; private final static short HEADER_INT=0x0008; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java b/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java index 4feceb3..fc03c1e 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java @@ -28,6 +28,13 @@ public class ResConfig extends FixedBlockContainer implements BlockLoad, JSONCon this.configSize.setBlockLoad(this); this.mValuesContainer.setBlockLoad(this); } + public void copyFrom(ResConfig resConfig){ + if(resConfig==this||resConfig==null){ + return; + } + setConfigSize(resConfig.getConfigSize()); + mValuesContainer.putByteArray(0, resConfig.mValuesContainer.toArray()); + } @Override public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { if(sender==configSize){ @@ -980,11 +987,11 @@ public class ResConfig extends FixedBlockContainer implements BlockLoad, JSONCon return keyboard; } } - return NOKEYS; + return NONE; } public static Keyboard fromName(String name){ if(name==null){ - return NOKEYS; + return NONE; } name=name.trim().toUpperCase(); if(name.equals("12KEY")){ @@ -995,7 +1002,7 @@ public class ResConfig extends FixedBlockContainer implements BlockLoad, JSONCon return keyboard; } } - return NOKEYS; + return NONE; } } public enum Navigation{ diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java index adf2d33..14a6c3d 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java @@ -129,6 +129,12 @@ public class ResValueBag extends BaseResValue { mResValueBagItemArray.readBytes(reader); } @Override + void onDataLoaded() { + for(ResValueBagItem item: mResValueBagItemArray.listItems()){ + item.refreshTableReference(); + } + } + @Override public JSONObject toJson() { if(isNull()){ return null; @@ -147,9 +153,6 @@ public class ResValueBag extends BaseResValue { @Override public String toString(){ - if(getParent()!=null){ - return toJson().toString(4); - } StringBuilder builder=new StringBuilder(); builder.append(getClass().getSimpleName()); builder.append(": parent="); diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java index 912068a..16c4521 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java @@ -102,14 +102,23 @@ public class ResValueBagItem extends BaseResValueItem{ setInt(OFFSET_DATA, data); afterDataValueChanged(); } + @Override + public void onSetReference(int data){ + setInt(OFFSET_DATA, data); + } private void beforeDataValueChanged(){ if(getValueType()==ValueType.STRING){ removeTableReference(); } } private void afterDataValueChanged(){ + refreshTableReference(); + } + void refreshTableReference(){ if(getValueType()==ValueType.STRING){ addTableReference(getTableStringReference()); + }else { + removeTableReference(); } } public short getIdHigh(){ @@ -195,6 +204,14 @@ public class ResValueBagItem extends BaseResValueItem{ builder.append(String.format("0x%08x", getId())); builder.append(", data="); builder.append(String.format("0x%08x", getData())); + if(vt==ValueType.STRING){ + TableString ts=getTableString(getData()); + if(ts==null){ + builder.append(" Null table string"); + }else { + builder.append(" \"").append(ts.getHtml()).append("\""); + } + } return builder.toString(); } private static final int OFFSET_ID=0; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java index 84af73e..42a86d6 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java @@ -26,6 +26,16 @@ public class ResValueInt extends BaseResValueItem { return getData()!=0; } @Override + void onDataLoaded() { + if(getValueType()==ValueType.STRING){ + if(!hasTableReference()){ + addTableReference(getTableStringReference()); + } + }else { + removeTableReference(); + } + } + @Override public void setHeaderSize(short size) { setShort(OFFSET_SIZE, size); } @@ -74,6 +84,10 @@ public class ResValueInt extends BaseResValueItem { } } @Override + public void onSetReference(int data){ + setInt(OFFSET_DATA, data); + } + @Override public JSONObject toJson() { if(isNull()){ return null; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueItem.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueItem.java index cba06ce..1e997e6 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueItem.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueItem.java @@ -13,4 +13,5 @@ public interface ResValueItem { int getData(); void setData(int data); + void onSetReference(int data); } diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueReference.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueReference.java index 3fd22dd..3ceb312 100644 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueReference.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueReference.java @@ -2,6 +2,8 @@ package com.reandroid.lib.arsc.value; import com.reandroid.lib.arsc.item.ReferenceItem; +import java.util.Objects; + public class ResValueReference implements ReferenceItem { private final BaseResValueItem resValueItem; public ResValueReference(BaseResValueItem resValueItem){ @@ -12,10 +14,21 @@ public class ResValueReference implements ReferenceItem { } @Override public void set(int val) { - resValueItem.setData(val); + resValueItem.onSetReference(val); } @Override public int get() { return resValueItem.getData(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResValueReference that = (ResValueReference) o; + return Objects.equals(resValueItem, that.resValueItem); + } + @Override + public int hashCode() { + return Objects.hash(resValueItem); + } } diff --git a/src/main/java/com/reandroid/lib/common/ROArrayList.java b/src/main/java/com/reandroid/lib/common/ROArrayList.java deleted file mode 100755 index 968eae8..0000000 --- a/src/main/java/com/reandroid/lib/common/ROArrayList.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.reandroid.lib.common; - -import java.util.AbstractList; - -public class ROArrayList extends AbstractList { - private final T[] elementData; - public ROArrayList(T[] elementData){ - this.elementData=elementData; - } - @Override - public T get(int i) { - return this.elementData[i]; - } - @Override - public int size() { - if(this.elementData==null){ - return 0; - } - return this.elementData.length; - } -} diff --git a/src/main/java/com/reandroid/lib/common/ROSingleList.java b/src/main/java/com/reandroid/lib/common/ROSingleList.java deleted file mode 100755 index ee21c40..0000000 --- a/src/main/java/com/reandroid/lib/common/ROSingleList.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.reandroid.lib.common; - -import java.util.AbstractList; - -public class ROSingleList extends AbstractList { - private final T item; - public ROSingleList(T item){ - this.item=item; - } - @Override - public T get(int i) { - if(i==0 && this.item!=null){ - return this.item; - } - throw new ArrayIndexOutOfBoundsException(getClass().getSimpleName()+": "+i); - } - - @Override - public int size() { - if(this.item==null){ - return 0; - } - return 1; - } -} diff --git a/src/main/java/com/reandroid/lib/json/JSONArray.java b/src/main/java/com/reandroid/lib/json/JSONArray.java index 8d52c70..f216dc4 100644 --- a/src/main/java/com/reandroid/lib/json/JSONArray.java +++ b/src/main/java/com/reandroid/lib/json/JSONArray.java @@ -1,6 +1,7 @@ package com.reandroid.lib.json; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Array; @@ -112,6 +113,16 @@ public class JSONArray extends JSONItem implements Iterable { } this.myArrayList = new ArrayList(initialCapacity); } + public JSONArray(InputStream inputStream) throws JSONException { + this(new JSONTokener(inputStream)); + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + public ArrayList getArrayList(){ + return myArrayList; + } @Override public Iterator iterator() { diff --git a/src/main/java/com/reandroid/lib/json/JSONObject.java b/src/main/java/com/reandroid/lib/json/JSONObject.java index 963a369..4d40253 100644 --- a/src/main/java/com/reandroid/lib/json/JSONObject.java +++ b/src/main/java/com/reandroid/lib/json/JSONObject.java @@ -1,10 +1,7 @@ package com.reandroid.lib.json; -import java.io.Closeable; +import java.io.*; -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -211,6 +208,14 @@ public class JSONObject extends JSONItem{ this.map = new HashMap(initialCapacity); } + + public JSONObject(InputStream inputStream) throws JSONException { + this(new JSONTokener(inputStream)); + try { + inputStream.close(); + } catch (IOException ignored) { + } + } public JSONObject accumulate(String key, Object value) throws JSONException { testValidity(value); Object object = this.opt(key); diff --git a/src/main/resources/fwk/android_resources_30.arsc b/src/main/resources/fwk/android_resources_30.arsc old mode 100755 new mode 100644 index bc187719e23c07e830d251968f3b7e27e2821d72..e24b958714549230a03c692cedc7ec651fb3ec8d GIT binary patch delta 45630 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 A&Hw-a delta 47088 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