5A*^D?_vXQ)`!1yHWNvrP2bph?Vb7R-G4Na>;`SSj>n;ap#c}VLCZvRf
zmItB8zq`@AX)Z$O?0X=QH7%~dOe
zHw_7zgNW?OE~McYuyR&bD=_kfUGJgr@z{u$C<|_OlExl`A&+l_NX3uHsug(
zA;E-TxsHSzL+`b0rN4Eeed^=@AG%PmBduc~BO?>@8!XxYgQNT)bB4y)c1V&j1fvV(
zXdqRx_3})5V+s`ML4^)=<^ji!>Pr>t{qcoNC!L5**Y=ORZfR*b7tJc&_8=7pzhL(M
z{09b22k%wjZZ2SqSuHIsFPfOJUn=)6crQke?LywnlRFEMb8jWhoy3WlOtV>
z-{+{32Moh0oTla#6bM)Q50x>4EKa2UI07%5>fVJypij4U(u)p23Eq=T^jXInQ0?pq
z)f02A;o*#7vn`-LSIe!>VE3o+IRQZ8EZs6qC#=nO`{0%7_o`oHB5huZq-fTOy&0(D
z>tac-UU^^rEXHpE%Yo~yq{B>F&h5&B8Pz`Qz+99b8-ZNAe%)yaUmLAp+4npY(XXvQ
z8dVf%_;d{%Y@Td{g@?N-@855~xCIyvBlZ%Ctn#39a8?@3Z=t}uv$P`EyKIK43V|O-V04|}HaCRXD;`%eKkVJ4&8APTs
zQBqz^<_By&JUnv0#Nc*OAQ*fD4dwzX%L;xzmsKXS3kC^iNb`#I~`U|ix?I9!qU?9rSWF`
zDZ|G23Pf`!=iV7s-{}uUVONUXcfX7l3e3%7fiHU%AP)s(Iv5SMc2PiDnN5iahF32*
zmA+rOeEB0c#xNk-WtD8qi~ORRli>Z#6%KZG>%$hnPi1>kzhy(JL;@;np!`7akHI%p
zKrE~xjO0P+UkTOy9q-P)zYL|9t13>`Ww3{kaH)P7rjGPv|EX+IjU0MG$2|F+2C-1-ezxrp
z19(L-F3wz$V!<)C&Cs#CP{oxFh~hSGEB?ETSAa
zZQTF2<%Jv4^#(PfjsVns
z0>C|SL)iGFzRu0eT&`;_{H8gxRV0rxPkIgbB2Xn@VsvtnddJL+7xH#MFCDT|E>PNS
zG|2mH2f&?XL)JGo=y_g~XYZBf+c?jWKRTejM7b3Fq2TRfG@)e;7XI*D<4z|ckVm`-
zn`htn0It9d(fE?QDH|{0(cIRy;<cO
z<2$>$IJZjI_d~9_d`nJN7B)-}eW>4Cmc?hJt~U?eL%gqYn?$^WH;|j}WO%ga*eW&|00)xfuyluVkzBcM3dkc^w_rg^)^cgEEKO;E{Bz
zQ}n#9A#%JBQ=u@IeDVuXCa5^}W87|n-3Gk|3od{nJ%yk&kDY*zz76i&7u}
zaAlM}xr*7bqY{!e-~6Bi_3DdW`YT~X^K)h`p;VDI;Ku1(A(n?vpRU%{)or}`#x978
z;N;;`WClDNJ9XSi=JVkWXh3?OO|YV#DMDa#5v%3W;cjww^Frb3->FFC)>-bX{C&Al
zaL)wOM?JhF@<8=y5M?+w9Aq)>jlhEQ9SoIvDI3SfBYZazqpw@@$Zfx&Ga&AFL!$jNvk92{r8ZvGpZCBKd6
z#El$~@#d?qm(qOF_7wHi<7@oIN2Ne^wDZ2lb)=w*Kv*
zn%52z!|Rvf&Z^SVqxWRQMeWixvZz+YZcL=FeCZ}*mf>b5S7Zx1jZV!x$%-&C%g#hVOH-#P-8pr^9XnJlGi0;1{1gG2@`V`(!*3_VrHoVOe|vMs->u&jDH4!Qpqn(#
z;yXL=-1J|Aw$e{5VE@sUQDtIBSb`_mbCppo48Gd&*Pc{G(Ug4vhYuy3?M{KrEgoL}
zW_#zmd3+&-w}#2NO+d|atlkAdwUNX&cGxfG0Li?4&t6X@qS&2m}m`WheQ%8j>xGG=$-G>dd
zjQ_y;4faDdHL~@c4byR}c&q5gZ2!6>>dJ@(P;8%HTJk&uZ`K}i(jrE4;#7!Kc2G(s
zLqSW$zuT~D118Gmqs})(h7#6b)itXsLRaWsfBSI`Q$#q_#A043L(0o)cvDC*gbL?f
zK2W`Xd|NEv3>xycWw{p;s4u{Df@bj$~ySXa4)W`
zAEyUb*S7w^LP2c5A#J`=Q_&TXbUqh2jA16iFP%1KImRT`r4)%?hBZwUEFBO1X?mWL
z5@ki)$xdVxGVs#N=Z6p3ci#$W<^r}4laU7xm_@|Ielp_TK&L}W;Kiz0a`}X(O@!gZ
zod%k=s}RvftqUsGrMTJRwcYxqEcQ29C;LR0Oge7{xNbC%NDx>VT$=8B{f__35=9b#vV`sNrU0n@NCeW#Wg}dk@
zGKA40!tE|P1T?!?veNf(wisF2pA2WR&~bp$g<+`vIDd@k{+lPkCM;rj@NAn!SXx;z
zevbhZ`BX+3m2j<%fY&@icoT}Pb49#w1VI_a&&psa1@;{1H)M!Zl{1q)oN&+5*eUgP
zzNnVM+n3&zR*j>9+Q|t56uqp8DqH8)RacMs?F%J{ZCwAqn!Y>`%J%#Fo{cef*_Q?_
zwidFqGQ&f*iZFyUQnIuVNh)K;l2W1KNvJ_~Nt+~MR8qFAg%rw`vad77c+Wh)@B2Tu
z`?~IPuIrr7a{N4qobEx#M6yO6Tsq^cxm|JADwF6f95odwe*k&m-x&uIY0>|7MUeNt
zu1+{}73kHqAX8lobRFn>PKb%3x`EPUNB919I7Yym`7geYIgbw-1Jc=|=&Ous
z0nn1))quPNsO~8rfMJ+?4Mzaw>6G|C5slfKr*1`(IazQdSM-BtrA!N({b((+^*RkM
z|1~nbcol`EJ>d8zoBUy#t%45XwLVM&^3IxdabAjjNzzvkbIOTife&b2TtsBf4t!*0
zrVhqXeq0#<+~v`eClXA3gymTl;n!5M;}26k@gO8~c&0#11e^@^(&Qr&T{a%3s#;YL
zrEe6Kl-$Y9S7ik<4FL|{)XsJ+yp>zyu;Oy)kbDzotID1WQ@-8f)XvIPXNo2_3ekNU
zNv5np<0L^QF|!lGl;m(SqjD|s8v)<1$cUZX?o_nx<_N&_Ukoqa1)hDAvn1!P9oI2^
z5$uNgoAFMmg#I~j0|${niZ>4EQs!FnSts2i>IkAc`7dc#A4J86^MED$z-5U#w@@Us
z@^I>NqcD1C4fc{{l&AN&6SF4bI*eF^6ZQg#KVQ
z2N%v}jc0AyvSm9kzbo;77fLqGm2}PWUw#YT$xx~PmJrK1zBui8px7mSYfn2KnG!o3mWs+Y>J}P
z&j@3&bib$YtR&|9TTC$x{y0M
zoMTX1eEQp-_xTSPj=n#N@Ww<)aLOW`=$B6iZr9s0XMeIk5+|H%X>L9U4s_wPY~RCA
zUg#qbg2)Zh?}X+NX#Akay6g?Ef1;)z+wtYFTxNvMK3u+=(xYcdKC0^sNGvrAJ0yPf>BuOa6A
zfO*nPEnG?fDKVVeZC9#!{xiI>+!eLC0E+^I;)ouaN4_U_VCMd`1+2AEI$e2OyPIw6PH%NhO0mdjXBT9^}_SjCi}_-!oXG8fyv)WIQ;DRr?(hE
zu_TT4>-We@n~Q7nb9uCxZTp4?4u{6t+M!wgIrKGa)Q<9_T5^VGh08A#O#CcpU$&Jj
zfdU-zrSXNS{s^mkHtJBGLgUwzLG-I&cKumxf-o_vPp^JF!-MQtOa49PS!%(UQ$5r3
z;#2%}I?)PiA5T~pg+j>NqiDW{yGhh;D2XC?gLmxG%|}WKM(w)esDmqNNTHK>#$(MX
zHOYRkwKWpksM3}K|G?)Ucity;Qd;{B73Y2p3`~CmZ&{^GdIWWPlQU&5wvzP7Z5~WW
z=!&?Vxt1GoZBgaW34Vvri%9g!iIm}oPsYV&W-iyl(o!F)6?cAe{kt=wHg47Dcr(?t
zmB?8+N=X&Bzn?|Es4F-XYZy&>5pf$&UJuJG9N`}G@=Af6EQS!MZRvJ${~>DET_Ns7C>k7{+@%
zfNMtezdtiR9I-cMw#25E6vy+t_(x=Pz3hX085EEk*C21oR3ZGy4`)}e_i@|#3#=e&
z^NtD>2-cH#Ac{YDqIZ-TN}Q
zmql@SiFQ;(OFE1q@jRuAAe}tD+p{DOi;;x5h?FA^s8WhFu22d!(~Pih&|}T%6&1_3
zAj-I=YcPH}InOpn?ob7#}woVA@2S|Db1XSO@PKR3a=OKWgBoy!D3aCrLZHtAQUdf-4
z+mi>QCxFse_?5?`jR}p2KgUY8WdA$n$56Cjf!=)_jYji=xl3m=RjxQ~yN1MJ$ygYh
z(#>vn-CKG<`nQOX&{432Q6uJBE!|7c@jU#sm@|1@fByRj7WCH&s2Nk!!o31TGN=Z=
z^%Ba~lO4jgf3`lgmOwn4o0((xKC+qMAwC$budAB~9TcT8ZF|5?*=B0z)xP0Cp6;=|
zs27r`xOQ}@6l%7dy|i@P(fx*jq5Y4Ico1sn%8l6h$aNaQ=efZ@)j)e}3zz6}c3d;H
zJ&6>;_kf7>y%b=@
z1?`8?`1gvkbaN!;%i#5Bdu4cwVKd^GbC$sd3+<@L$dv2XZHMZJ5ta9wx9o_JH)lOm
zC;Lk}^{S}$sdw3JLBC2p@*0ki!`fomM-}CXB8cvZDyrM)I+^8zIjhi$3Xe42d%m}Z
zy;wBA>b{cHv~bsLrJsA$Sq<&9U&KiV{e!kV9tu3#EoDVTPMLv83gL{s9DK*)RaEVT
zhv&VYc1=z~SCCx|eYAeZj@2WjNn00gakvBAqq16P=?aX~O;MU#6kfh1%i_8kB!(UU
zIHYW;8nxfXXtWtkZPE7D-0f$Uz?_Pi3(La5)z0@gT5hho-mf}YX&9Qsh
zHd+zBK~jDD{CQG8RoN=7)b|MD0LN4K6>iReNz$aam{{)R)e#WaFl=U49MYIsvy+7}
zvjKQW5+}(F|4`(O#C>n|jM$~0bIu#kvRjW-_JY`Nl*Pb=YHm<1CD>fTN{GTuwr$Yv
zzSX(kQ}gq0evOV^2m$Wt&x=5w@$u&B;LStV!w1*!V25Nkpp$Nj{u>~9s>!eJx9S?y
z_gXni?=;mn7q(V&XXGEhb%a!;J-n4R7pcAh?zoVTOwMuvJ`B2$>TliMIpT^2qx%h^
zu$Y@vd*FS0c2f00Nb$*nA7dJmv$3BEWve}V_VA#x
z4xbU;TB&=$;v%5-2Z*?TVZDXF`VDKW9A?UmKa`)G|tW+l^bifwSyu
z*>}b-Q$l81RS`k#T}Ca7Yn|+*p_fljPp?`A6{fLMG?zl*G<@Sq=Per+&j+Sa!>2bz
z^VizpK+?7Pp_cJf)|TDo)L*qG_NJlnKblsL&>
zrawzOog|{3@$Xmx!yzr>r3yNs19xt`Cu9jw-8XFEl{9Hl7S+hBZj7{O
zhTQ!gT#hfjLkp2a=pio`!JaBq#)F12dEdQl-YAm307b2@q_B>pRuP=2^*~pmj>StCc1#^}O|LxVVk*T)|y8eS;!w4>azmNn7>w2)D5aaPC
zdH2Q81xDmlb*nP0IR<%hBakF?DJ+I`EkaK2E(tD`my4kDy_YroWve(_r(sb?XW@7sIA1(X}FxLtl!iV}J7CQ=r?6
zBLG<{2VRca1~srtaxbS;lwDN6_^?Y{pxyCo#_QJzyAlb}V#H>lfMC1#3>X0q5EfF~
z`o-b>w#WSa`&Y0P!K0AIy6N_18xNB<1CrDtoKU9#M4)a3Ugk5fYq?tG^Ct;x+m)5n
znH9|i7#q3?-O-85w-xfGsDjexogDC|6<2fdoaN=dtZC-CV~KJ?UUX2vIu2S+Zn?$=
zg4wM<_#{|Etl_eAukV|`>rW|ev2*=MO}k8j=yMj8%qZAl-(HDOv{0qE$kO)*7(fG+
z|LrY*u4!N}SLNkKPF11%LK|6D8LNKY;TAE3v&3e92l>b!nbhI!kkDn)%GHHtr2T3U
zl$Dj9;bl01bydFNG;^^XAwv0^B?d(Xx5t!&kXRCmJ8Fw)`Y7eEA^
zw*R?QT0Y+(xUA{S))8i}>l2Ssc)Eq`YC|5AYH?D{*H)Y&;{jap8HAd~*zY`F_set$
zM>flwg(Jv^PqTNBywF7Ros`8(K=frj;DNo0GdI?G@7>2>e5d02mIDll&*v_Nca!vorA}rl_s^Wl>ipu^l
z-fp<8R>6kYnF+EfJBs9lOXa*vX^S1@Bauy$Z)BE
ztreBs8P0jpa&psF>1Z%FEaGVEU)SzgH3oofD~#e~@`c)L|Ffd_~y?+9cDB|1fCYhvw*2zg$Gl+%?ze8t>o
zC-9bweWAg@%QRS2H!zuQkNcM>G0T)8PV#pSMTVm`C?0h$(1!ffmaM1PV$g2D@qA)w7l-A%iASF>JLS^07VL^cUra8G?fwItTIoHqbj
zyI52|-w?1;oYc#qP}uAE5BIepQw%b4;(W<+F}~#FHuQUbeL4y~eG};X??Tn2ErZsS
z#gaqfsjbtFPB(|og65U`8v4s;;OO0T^7oR=yOl9*ZjO6IgW?YkKX}*WVVz{~{z{^W
zZD?bE<6Rc^I%-djf0B~Z(8&A4r&2b9b68D{?q!N3?WWaFH>dz$gWt*Pd-bd25O^w@
zwRg~J)k$nLbF7z>{J&jrHOP4SZz;;dzhH}0N5^IeGobl2+{-=sfPo+m3=Eh(%`l)O
zM$MwnJmis|Ewl1fLJ-$0fAt3-h`jZ6B|kru9Qs}&KjA_d%-jE%7(>%di805F
z`74Mfh7w-5NQvg3ccb^0-Mx7jfB2!v5TyUnO5IXj>p!u2t`dj7A;g0F3Ga2^2*b#=
zO;AM#am#?^Zh&UI+Lb5PsoJ}>dcLeAr|K=WyZFC{-`k5B>nL=~qyLmP4e?|ApjZn_
zfS<+V8RXLSiHV5_2h%m8(l%CBo*+OJ-ze;b(|a=5S=lydB2{g$?>c7*Y^akJuV1%4
zd%y*OP>*x#w2t$l?Ers!_J^+&2CZn$Mb$joq?mEqEu(%O_5NExwQ-u!o#}rjT~b{S
zth?fO(iN=mCxnpeoBuxamAm;j$LIdWMJVodA^H2hJoZT2M&mA(cLs6`3zs4CLeBo-
zr(UBI)=5KAH3zp#+gjMk7SF#7b4gShd#DW1WR*$?y^>Mg8&|J-I^3rloJg5@7#D(;
zz_8Naa+CV*Qs&wp(S}DymzrR_Ef|qicDeEnIMqxy%yLuTu5`Yc=SQBay$#vJfNWx6
z-??Kz%|)msnajwDA6#;ZBalT9M_|$p
zx6N9&>SzaTkhDEYvkAco2y6qWZ5djcs%L=%W|q9hgdCr0#qG5HVYCp~FF)F=`FEbq
zTd=1JZ^Rs+N4GHnivhy~8x0KaegrJYx@
zgRAs#-qnBQhgT=z*p`CHOAA7qf44sUb@%OxN}=S0Wel7FL+a$t1E?@JvYX+fSKjun
z;q;$}j-HNTVf3cJju;OHa_*y{hP#*IzGL>q5$s!4EpVET3J>WZdWcBax4S{hNrhQi
zCIzV^P0g!-j6UJ7o?kY-sRLj=4^YB~CkxJuv=1%~;b`?U-1@gG`;()Wm7LUi#YbQ}
z!wkrse-ZC_0B~_9*JFX9*EGT3OGJA0{xzj2Q4}r3j7^&}
zwyI?WWk~1&7Ib#9a<(}ZO81JTChge1CuhKvFMQg`z|Y=B(kdp(ArA3EM&JIv^D4=V
z>#%_oQj(||5*k_!*7?NQPa4b%`ycEHYlT9-uP~=Fi`t10J$BMgzgvNJR6d$qT7gUI
z6x58Dao&J6=)~vWB-oeE!T5{3CTL`v;DQl{vLKlt7~mL3k1HPa{5L^mqn>J$EL|Sy
z4B;3QMYh?k-(h{>BHT2?pMP|Mo^9{UH{Qk9;|<#E&hHDQpQd$!xpBWPB@eNS+`o(0
ziAR~@eZOzcIDprbB@`A)+G}79as{t~KTwpp&AT2=n+@uLqTyf6%M@IDn5JCu{W=Nx=3fr
zv2nJjXo8<%D*yTf*d6WOY>w{T2A6~C!&pnWs5%7;^xV`frFRAX4}4A-k%
z6%OU(biA&+UquY%H1Ks8196{S4c~ERK84pSM_BHl!9;lQoVY&C_<7tk1Q$h-#b8M+
zIZ%Bc7nj520U6}>3V^#3p!0c|XCHO!CvP`lr=!Boky8!n=NMtT!u@`&-FcaF#Cpm)
zcPM{}z~0gYyAUi$aNIZi5JIca{n;rep_(ox+M>uZRI
z5Pz51$4$LM(3EQgUug`#0Bn@$>3HVXokdYXnUh3=9BO*al~j3}liiFoQk?
z4_VL}+D<6b^32R$tKDu_HC5fLeS4Si#~OQ0*X~{v9{nzMZb*!I;vf4rg5jY~tCDNi
zHiLPV2E&Y&Z(pxEd6?_H$@SU#3m^h38db>8Y+tQjXf`rv_;cge(YDrNZ>zJ?Zthpk
zzY6*H<_l(w(0JZ&LDG&$v&38ZS8r9ONIW#ZX}8`+sw1WL?hfjsB3|K`NossZSj@LQ!jckakI;Sm{bJQ@+mIUXv`NJdYr%P0LkE#5cr#nPJCCt9^`U4RURn{cx+
z^U>-VxCW>_Jw46EkVCQTcEG4KVnWcG4Z8p
z4t~>+C?tG*gD%)6ae9rx<$0T*%o5khU%1d+|JqcJgCUQBe<~l2V$HPQ&i=
z;;2HbrhY}h#-wC)oT=jIS?1xcUnZ}g#9khF@aqQ?7^-G)&*WKJT9yF$Sosy4my^)I
zlmYFwrxV;)*T34tR{gp%B}OC89eWdA-m^HrI3#@ea>A@6SL9u(qolg5Ma_+IHm>6xxo;{ZVpZ@u!qr=I<29*)BfU0kQ
z=|&%EGSquT9b&~)iqD;Y_+b3o!|}+k#=D=CAPX>&^%djNkG{UuBLM-Eq+Ppq9XWIQ
zv~l#v%&|VIXEIYby!+eTqLoAUsJtsCk<6nhf)3b06V$i0R>g$(ZdeO%Rc-93S_u@{
z`sd@7v@X@-H!NIc+pmE<16MHIO1JM*Q0gx~T^W1I$R0_XA_C5!`
z&qf%R)bLo@GXmx5i$l?|ny7s)Qi!7t;qfM`qfg$w-Rbj+8Ok`5d8Q^ltjoc8I5x~n
zCLvE#H{#LYSz`&JFdOPT3S1o&BOT5?D5DREi;9Nx5k0#bCJJ;h+UE-wp`$F`Y>#CLly~dcT
zF>-Nqzpb9{sZ-oqK;L%5nO!%vbnjNwcJOfg=q?W)S>7vgModJccUz>cK78O{V8%X!
zE+u|ZCJEIDlOmk>>cu)|NmYX8t%ps>$E;4IKkGZ<$2x+gchyJ9TRJFggCcmJbsttGu#vMauQ)tBnZl1ZYY1_tMVHFD~{O6tTrc
z2Kzuq_QxG4#_|{7G|}r_)0-yJGJ>YM4^=3!gE9kbxzEZ^>XH@s2pPzrarKYC7ZYnK4UsB2n_uqtYlPEM14k<8Wt1b6TK=ns|X1|a8Fe8bP
zh|hM$^~y5r1de<6-+Mnsek{@9KyWgBatvhs4Qj%my6mNE$bhBtX{RNo_&C|XesgU&A6&CXJ%17O(s`AWv;pYl6o
z-)(UcR%~v-g?(`Cqw;`eY`0S$-Iz
zi59Kx!DwyMC8=U29>Bl65!qxT9qF^qtp84=x|e-`Y5t;B`Ahjx!>(oe>M8XpIjt6I
zS73l0HHybIAAR2VSy#kSP~nMs;yR3Qw8$&FHG7iPu(o^5618!u@=JrNmQrII#5>b>
zpW9Corkmgv6sPVUM^+22Tne2$X}lsm`n(LDpx;1YbQsk{Ub#fvd^w9Q@xBoISo)Qh
z#O)~cNPF?KR=BW(tYy{J^jAJ4>qFm1(5u(rC%f;oh$k=?Ef?%L$m5gDoUot;x^ucajpj}KQHk|M`n6df_9bQqNFh2*!4#cZ`?p_MR
z?Gx)5E=F}D&TW|`gJCLLnI_(rxE?b)N8x1b~
z-+Lcc$lr_qyEZaK`u%%Wa#V`yl6j4W$h>0JY(v-;WUI@ZNlTsQYW8fnG>-NrgMVGU
zyNo%`T8u}z@{=M_`lF5MK9Ou?-^l(u(ZsOmfjUWsl}hF@|KOJXEvT((M9iDR-LcM#
zdi`}AcF(a((I)S78*sDnD~M$V6&FYD#g?uqD=cL24oMHm#JUF{g0l=vqXfWkGByn&q+~
zFo$)T=PBXDi6id$bdP`kZ0O$rUeO#EBg`3756
za@TGY_QM{ZWoCa<+DflQTfkY&z_ew3jSNmp#6j8LaBzBvO%!K&ppTgS@MyJdalu?d
z!#BI~={7@I%_pNqAZYjQk%y=z65ww9AS-I&~9;*sJ#ZM6V
zdh$z&K`X{afmCJRWQ>c%VOXPTjOm@&taqWkY(mF|-fE4<7U&~Ul>(f@U2-UsO!&b6
zsA;apnrK-`VzdMwt6EtnCS$ckOv3U{EXA+p`wVRv;CT#j);1&IA`4jfE+Vs~c!nLp
zD>*XdzYxl}7MX-Qy~iQ&ef;u$d>7IA@yNGpiJx7&BXp*BACtdwG$mEo)QaGs&3~!L
z{xvZvJDhfVjm$R9W}*|Ba^-gm`UK&r?;7JIvad4i#LRB@--(IchE)&APG4tG*HT(L
zVLK6ig8!hMY64n8iRwb-KkbhXJ|NA%j_cKjRbMIJnv-!=wZlQ(Kdjlmguiu1HSy0?
iCFUPM4fi~074z-Zc;TTE)+q4Sh_%H5^T%eMk^c|201A@;
literal 0
HcmV?d00001
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
new file mode 100644
index 000000000..4cf695f27
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+/**
+ * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public interface ItemTouchHelperAdapter {
+
+ /**
+ * Called when an item has been dragged far enough to trigger a move. This is called every time
+ * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
+ * adjusting the underlying data to reflect this move.
+ *
+ * @param fromPosition The start position of the moved item.
+ * @param toPosition Then resolved position of the moved item.
+ * @return True if the item was moved to the new adapter position.
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
+ */
+ boolean onItemMove(int fromPosition, int toPosition);
+
+
+ /**
+ * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
+ * adjusting the underlying data to reflect this removal.
+ *
+ * @param position The position of the item dismissed.
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
+ */
+ void onItemDismiss(int position);
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
new file mode 100644
index 000000000..e20014de5
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+/**
+ * Interface to notify an item ViewHolder of relevant callbacks from {@link
+ * ItemTouchHelper.Callback}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public interface ItemTouchHelperViewHolder {
+
+ /**
+ * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
+ * Implementations should update the item view to indicate it's active state.
+ */
+ void onItemSelected();
+
+
+ /**
+ * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
+ * state should be cleared.
+ */
+ void onItemClear();
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
new file mode 100644
index 000000000..163f94de0
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.support.v7.widget.RecyclerView;
+
+/**
+ * Listener for manual initiation of a drag.
+ */
+public interface OnStartDragListener {
+
+ /**
+ * Called when a view is requesting a start of a drag.
+ *
+ * @param viewHolder The holder of the view to drag.
+ */
+ void onStartDrag(RecyclerView.ViewHolder viewHolder);
+
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
new file mode 100644
index 000000000..2d281d5ff
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.graphics.Canvas;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+/**
+ * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
+ * swipe-to-dismiss. Drag events are automatically started by an item long-press.
+ *
+ * Expects the RecyclerView.Adapter
to listen for {@link
+ * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder
to implement
+ * {@link ItemTouchHelperViewHolder}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
+
+ public static final float ALPHA_FULL = 1.0f;
+
+ private final ItemTouchHelperAdapter mAdapter;
+
+ public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ // Set movement flags based on the layout manager
+ if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
+ final int swipeFlags = 0;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ } else {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+ final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ }
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType()) {
+ return false;
+ }
+
+ // Notify the adapter of the move
+ mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
+ return true;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
+ // Notify the adapter of the dismissal
+ mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
+ }
+
+ @Override
+ public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+ // Fade out the view as it is swiped out of the parent's bounds
+ final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
+ viewHolder.itemView.setAlpha(alpha);
+ viewHolder.itemView.setTranslationX(dX);
+ } else {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ }
+ }
+
+ @Override
+ public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+ // We only want the active item to change
+ if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Let the view holder know that this item is being moved or dragged
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemSelected();
+ }
+ }
+
+ super.onSelectedChanged(viewHolder, actionState);
+ }
+
+ @Override
+ public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+
+ viewHolder.itemView.setAlpha(ALPHA_FULL);
+
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Tell the view holder it's time to restore the idle state
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemClear();
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java
new file mode 100644
index 000000000..38f17d9c4
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java
@@ -0,0 +1,121 @@
+package com.v2ray.ang.util;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+
+public class AssetsUtil {
+ public static boolean copyAssetFolder(AssetManager assetManager,
+ String fromAssetPath, String toPath) {
+ try {
+ String[] files = assetManager.list(fromAssetPath);
+ new File(toPath).mkdirs();
+ boolean res = true;
+ for (String file : files)
+ if (file.contains("."))
+ res &= copyAsset(assetManager,
+ fromAssetPath + "/" + file,
+ toPath + "/" + file);
+ else
+ res &= copyAssetFolder(assetManager,
+ fromAssetPath + "/" + file,
+ toPath + "/" + file);
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public static boolean copyAsset(AssetManager assetManager,
+ String fromAssetPath, String toPath) {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ in = assetManager.open(fromAssetPath);
+ new File(toPath).createNewFile();
+ out = new FileOutputStream(toPath);
+ copyFile(in, out);
+ in.close();
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static String readTextFromAssets(AssetManager assetManager, String fileName) {
+ try {
+ InputStreamReader inputReader = new InputStreamReader(assetManager.open(fileName));
+ BufferedReader bufReader = new BufferedReader(inputReader);
+ String line;
+ String Result = "";
+ while ((line = bufReader.readLine()) != null)
+ Result += line;
+ return Result;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ public static String getAssetPath(Context context, String assetPath) {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ context.deleteFile(assetPath);
+
+ in = context.getAssets().open(assetPath);
+ out = context.openFileOutput(assetPath, MODE_PRIVATE);
+ copyFile(in, out);
+ in.close();
+
+ String path = context.getFilesDir().toString();
+ return path + "/" + assetPath;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "";
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static void copyFile(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java
new file mode 100644
index 000000000..eb69a0626
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.v2ray.ang.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ * Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+ /** Specify encoding (value is {@code true}). */
+ public final static boolean ENCODE = true;
+
+ /** Specify decoding (value is {@code false}). */
+ public final static boolean DECODE = false;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/'};
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '-', (byte) '_'};
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ /** The web safe decodabet */
+ private final static byte[] WEBSAFE_DECODABET =
+ {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param alphabet is the encoding alphabet
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff =
+ (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Equivalent to calling
+ * {@code encodeBytes(source, 0, source.length)}
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source The data to convert
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet the encoding alphabet
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet,
+ boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param maxLineLength maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+ int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff =
+ ((source[d + off] << 24) >>> 8)
+ | ((source[d + 1 + off] << 24) >>> 16)
+ | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+ | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded data.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source)
+ throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded byte array.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+ throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0 or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException(
+ "invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2)
+ || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException(
+ "padding byte '=' falsely signals end of encoded value "
+ + "at offset " + i);
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException(
+ "encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i
+ + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ throw new Base64DecoderException("single trailing character at offset "
+ + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java
new file mode 100644
index 000000000..b113e43f8
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.v2ray.ang.util;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java
new file mode 100644
index 000000000..e6320808d
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /** Returns the IAB result (error) that this exception signals. */
+ public IabResult getResult() { return mResult; }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java
new file mode 100644
index 000000000..911d20dae
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java
@@ -0,0 +1,979 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ */
+public class IabHelper {
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+
+ // Is setup done?
+ boolean mSetupDone = false;
+
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+
+ // Context we were passed during initialization
+ Context mContext;
+
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+
+ // The request code used to launch purchase flow
+ int mRequestCode;
+
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ void onIabSetupFinished(IabResult result);
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions.
+ mSubscriptionsSupported = false;
+ return;
+ }
+ logDebug("In-app billing version 3 supported for " + packageName);
+
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ } else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ }
+
+ mSetupDone = true;
+ } catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+// Intent serviceIntent = new Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND");
+// serviceIntent.setPackage("com.farsitel.bazaar");
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+ // service available to handle that Intent
+ mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ } else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device."));
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() {
+ logDebug("Disposing.");
+ mSetupDone = false;
+ if (mServiceConn != null) {
+ logDebug("Unbinding from service.");
+ if (mContext != null) mContext.unbindService(mServiceConn);
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed)
+ throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /**
+ * Returns whether subscriptions are supported.
+ */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused while
+ * the user interacts with Google Play, and the result will be delivered via the activity's
+ * {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+ * @param requestCode A request code (to differentiate from other responses --
+ * as in {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase data
+ * when the purchase completes. This extra data will be permanently bound to that purchase
+ * and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ Integer.valueOf(0), Integer.valueOf(0),
+ Integer.valueOf(0));
+ } catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ } catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ } catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ } else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ } else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException {
+ return queryInventory(querySkuDetails, moreSkus, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ } catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails,
+ final List moreSkus,
+ final QueryInventoryFinishedListener listener) {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreSkus);
+ } catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(true, null, listener);
+ }
+
+ public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(querySkuDetails, null, listener);
+ }
+
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume " + sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ } else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ void onConsumeMultiFinished(List purchases, List results);
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link consumeAsync}, but for multiple items at once.
+ *
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ } else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return ((Integer) o).intValue();
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return ((Integer) o).intValue();
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) {
+ if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+ operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+
+ void flagEndAsync() {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ }
+
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ } else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ } else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener) {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ } catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java
new file mode 100644
index 000000000..0fbe5b582
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java
@@ -0,0 +1,45 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ }
+ else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+ public int getResponse() { return mResponse; }
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+ public boolean isFailure() { return !isSuccess(); }
+ public String toString() { return "IabResult: " + getMessage(); }
+}
+
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java
new file mode 100644
index 000000000..ae13e74fb
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap();
+ Map mPurchaseMap = new HashMap();
+
+ Inventory() { }
+
+ /** Returns the listing details for an in-app product. */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /** Returns purchase information for a given product, or null if there is no purchase. */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /** Returns whether or not there exists a purchase of the given product. */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /** Return whether or not details about the given product are available. */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /** Returns a list of all owned product IDs. */
+ List getAllOwnedSkus() {
+ return new ArrayList(mPurchaseMap.keySet());
+ }
+
+ /** Returns a list of all owned product IDs of a given type */
+ List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /** Returns a list of all purchases. */
+ List getAllPurchases() {
+ return new ArrayList(mPurchaseMap.values());
+ }
+
+ void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java
new file mode 100644
index 000000000..ad4d54ae0
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java
@@ -0,0 +1,540 @@
+package com.v2ray.ang.util;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Reference to http://blog.csdn.net/way_ping_li/article/details/8487866
+ * and improved some features...
+ */
+public class LogRecorder {
+
+ public static final int LOG_LEVEL_NO_SET = 0;
+
+ public static final int LOG_BUFFER_MAIN = 1;
+ public static final int LOG_BUFFER_SYSTEM = 1 << 1;
+ public static final int LOG_BUFFER_RADIO = 1 << 2;
+ public static final int LOG_BUFFER_EVENTS = 1 << 3;
+ public static final int LOG_BUFFER_KERNEL = 1 << 4; // not be supported by now
+
+ public static final int LOG_BUFFER_DEFAULT = LOG_BUFFER_MAIN | LOG_BUFFER_SYSTEM;
+
+ public static final int INVALID_PID = -1;
+
+ public String mFileSuffix;
+ public String mFolderPath;
+ public int mFileSizeLimitation;
+ public int mLevel;
+ public List mFilterTags = new ArrayList<>();
+ public int mPID = INVALID_PID;
+
+ public boolean mUseLogcatFileOut = false;
+
+ private LogDumper mLogDumper = null;
+
+ public static final int EVENT_RESTART_LOG = 1001;
+
+ private RestartHandler mHandler;
+
+ private static class RestartHandler extends Handler {
+ final LogRecorder logRecorder;
+ public RestartHandler(LogRecorder logRecorder) {
+ this.logRecorder = logRecorder;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_RESTART_LOG) {
+ logRecorder.stop();
+ logRecorder.start();
+ }
+ }
+ }
+
+ public LogRecorder() {
+ mHandler = new RestartHandler(this);
+ }
+
+ public void start() {
+ // make sure the out folder exist
+ // TODO support multi-phase path
+ File file = new File(mFolderPath);
+ if (!file.exists()) {
+ file.mkdirs();
+ }
+
+ String cmdStr = collectLogcatCommand();
+
+ if (mLogDumper != null) {
+ mLogDumper.stopDumping();
+ mLogDumper = null;
+ }
+
+ mLogDumper = new LogDumper(mFolderPath, mFileSuffix, mFileSizeLimitation, cmdStr, mHandler);
+ mLogDumper.start();
+ }
+
+ public void stop() {
+ // TODO maybe should clean the log buffer first?
+ if (mLogDumper != null) {
+ mLogDumper.stopDumping();
+ mLogDumper = null;
+ }
+ }
+
+ private String collectLogcatCommand() {
+ StringBuilder stringBuilder = new StringBuilder();
+ final String SPACE = " ";
+ stringBuilder.append("logcat");
+
+ // TODO select ring buffer, -b
+
+ // TODO set out format
+ stringBuilder.append(SPACE);
+ stringBuilder.append("-v time");
+
+ // append tag filters
+ String levelStr = getLevelStr();
+
+ if (!mFilterTags.isEmpty()) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("-s");
+ for (int i = 0; i < mFilterTags.size(); i++) {
+ String tag = mFilterTags.get(i) + ":" + levelStr;
+ stringBuilder.append(SPACE);
+ stringBuilder.append(tag);
+ }
+ } else {
+ if (!TextUtils.isEmpty(levelStr)) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("*:" + levelStr);
+ }
+ }
+
+ // logcat -f , but the rotated count default is 4?
+ // can`t be sure to use that feature
+ if (mPID != INVALID_PID) {
+ mUseLogcatFileOut = false;
+ String pidStr = adjustPIDStr();
+ if (!TextUtils.isEmpty(pidStr)) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("|");
+ stringBuilder.append(SPACE);
+ stringBuilder.append("grep (" + pidStr + ")");
+ }
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private String getLevelStr() {
+ switch (mLevel) {
+ case 2:
+ return "V";
+ case 3:
+ return "D";
+ case 4:
+ return "I";
+ case 5:
+ return "W";
+ case 6:
+ return "E";
+ case 7:
+ return "F";
+ }
+
+ return "V";
+ }
+
+ /**
+ * Android`s user app pid is bigger than 1000.
+ *
+ * @return
+ */
+ private String adjustPIDStr() {
+ if (mPID == INVALID_PID) {
+ return null;
+ }
+
+ String pidStr = String.valueOf(mPID);
+ int length = pidStr.length();
+ if (length < 4) {
+ pidStr = " 0" + pidStr;
+ }
+
+ if (length == 4) {
+ pidStr = " " + pidStr;
+ }
+
+ return pidStr;
+ }
+
+
+ private class LogDumper extends Thread {
+ final String logPath;
+ final String logFileSuffix;
+ final int logFileLimitation;
+ final String logCmd;
+
+ final RestartHandler restartHandler;
+
+ private Process logcatProc;
+ private BufferedReader mReader = null;
+ private FileOutputStream out = null;
+
+ private boolean mRunning = true;
+ final private Object mRunningLock = new Object();
+
+ private long currentFileSize;
+
+ public LogDumper(String folderPath, String suffix,
+ int fileSizeLimitation, String command,
+ RestartHandler handler) {
+ logPath = folderPath;
+ logFileSuffix = suffix;
+ logFileLimitation = fileSizeLimitation;
+ logCmd = command;
+ restartHandler = handler;
+
+ String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
+ .format(new Date(System.currentTimeMillis()));
+ String fileName = (TextUtils.isEmpty(logFileSuffix)) ? date : (logFileSuffix + "-"+ date);
+ try {
+ out = new FileOutputStream(new File(logPath, fileName + ".log"));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void stopDumping() {
+ synchronized (mRunningLock) {
+ mRunning = false;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ logcatProc = Runtime.getRuntime().exec(logCmd);
+ mReader = new BufferedReader(new InputStreamReader(
+ logcatProc.getInputStream()), 1024);
+ String line = null;
+ while (mRunning && (line = mReader.readLine()) != null) {
+ if (!mRunning) {
+ break;
+ }
+ if (line.length() == 0) {
+ continue;
+ }
+ if (out != null && !line.isEmpty()) {
+ byte[] data = (line + "\n").getBytes();
+ out.write(data);
+ if (logFileLimitation != 0) {
+ currentFileSize += data.length;
+ if (currentFileSize > logFileLimitation*1024) {
+ restartHandler.sendEmptyMessage(EVENT_RESTART_LOG);
+ break;
+ }
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (logcatProc != null) {
+ logcatProc.destroy();
+ logcatProc = null;
+ }
+ if (mReader != null) {
+ try {
+ mReader.close();
+ mReader = null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ out = null;
+ }
+ }
+ }
+ }
+
+ public static class Builder {
+
+ /**
+ * context object
+ */
+ private Context mContext;
+
+ /**
+ * the folder name that we save log files to,
+ * just folder name, not the whole path,
+ * if set this, will save log files to /sdcard/$mLogFolderName folder,
+ * use /sdcard/$ApplicationName as default.
+ */
+ private String mLogFolderName;
+
+ /**
+ * the whole folder path that we save log files to,
+ * this setting`s priority is bigger than folder name.
+ */
+ private String mLogFolderPath;
+
+ /**
+ * the log file suffix,
+ * if this is sot, it will be appended to log file name automatically
+ */
+ private String mLogFileNameSuffix = "";
+
+ /**
+ * single log file size limitation,
+ * in k-bytes, ex. set to 16, is 16KB limitation.
+ */
+ private int mLogFileSizeLimitation = 0;
+
+ /**
+ * log level, see android.util.Log, 2 - 7,
+ * if not be set, will use verbose as default
+ */
+ private int mLogLevel = LogRecorder.LOG_LEVEL_NO_SET;
+
+ /**
+ * can set several filter tags
+ * logcat -s ActivityManager:V SystemUI:V
+ */
+ private List mLogFilterTags = new ArrayList<>();
+
+ /**
+ * filter through pid, by setting this with your APP PID,
+ * the log recorder will just record the APP`s own log,
+ * use one call: android.os.Process.myPid().
+ */
+ private int mPID = LogRecorder.INVALID_PID;
+
+ /**
+ * which log buffer to catch...
+ *
+ * Request alternate ring buffer, 'main', 'system', 'radio'
+ * or 'events'. Multiple -b parameters are allowed and the
+ * results are interleaved.
+ *
+ * The default is -b main -b system.
+ */
+ private int mLogBuffersSelected = LogRecorder.LOG_BUFFER_DEFAULT;
+
+ /**
+ * log output format, don`t support config yet, use $time format as default.
+ *
+ * Log messages contain a number of metadata fields, in addition to the tag and priority.
+ * You can modify the output format for messages so that they display a specific metadata
+ * field. To do so, you use the -v option and specify one of the supported output formats
+ * listed below.
+ *
+ * brief — Display priority/tag and PID of the process issuing the message.
+ * process — Display PID only.
+ * tag — Display the priority/tag only.
+ * thread - Display the priority, tag, and the PID(process ID) and TID(thread ID)
+ * of the thread issuing the message.
+ * raw — Display the raw log message, with no other metadata fields.
+ * time — Display the date, invocation time, priority/tag, and PID of
+ * the process issuing the message.
+ * threadtime — Display the date, invocation time, priority, tag, and the PID(process ID)
+ * and TID(thread ID) of the thread issuing the message.
+ * long — Display all metadata fields and separate messages with blank lines.
+ */
+ private int mLogOutFormat;
+
+ /**
+ * set log out folder name
+ *
+ * @param logFolderName folder name
+ * @return The same Builder.
+ */
+ public Builder setLogFolderName(String logFolderName) {
+ this.mLogFolderName = logFolderName;
+ return this;
+ }
+
+ /**
+ * set log out folder path
+ *
+ * @param logFolderPath out folder absolute path
+ * @return the same Builder
+ */
+ public Builder setLogFolderPath(String logFolderPath) {
+ this.mLogFolderPath = logFolderPath;
+ return this;
+ }
+
+ /**
+ * set log file name suffix
+ *
+ * @param logFileNameSuffix auto appened suffix
+ * @return the same Builder
+ */
+ public Builder setLogFileNameSuffix(String logFileNameSuffix) {
+ this.mLogFileNameSuffix = logFileNameSuffix;
+ return this;
+ }
+
+ /**
+ * set the file size limitation
+ *
+ * @param fileSizeLimitation file size limitation in KB
+ * @return the same Builder
+ */
+ public Builder setLogFileSizeLimitation(int fileSizeLimitation) {
+ this.mLogFileSizeLimitation = fileSizeLimitation;
+ return this;
+ }
+
+ /**
+ * set the log level
+ *
+ * @param logLevel log level, 2-7
+ * @return the same Builder
+ */
+ public Builder setLogLevel(int logLevel) {
+ this.mLogLevel = logLevel;
+ return this;
+ }
+
+ /**
+ * add log filterspec tag name, can add multiple ones,
+ * they use the same log level set by setLogLevel()
+ *
+ * @param tag tag name
+ * @return the same Builder
+ */
+ public Builder addLogFilterTag(String tag) {
+ mLogFilterTags.add(tag);
+ return this;
+ }
+
+ /**
+ * which process`s log
+ *
+ * @param mPID process id
+ * @return the same Builder
+ */
+ public Builder setPID(int mPID) {
+ this.mPID = mPID;
+ return this;
+ }
+
+ /**
+ * -b radio, -b main, -b system, -b events
+ * -b main -b system as default
+ *
+ * @param logBuffersSelected one of
+ * LOG_BUFFER_MAIN = 1 << 0;
+ * LOG_BUFFER_SYSTEM = 1 << 1;
+ * LOG_BUFFER_RADIO = 1 << 2;
+ * LOG_BUFFER_EVENTS = 1 << 3;
+ * LOG_BUFFER_KERNEL = 1 << 4;
+ * @return the same Builder
+ */
+ public Builder setLogBufferSelected(int logBuffersSelected) {
+ this.mLogBuffersSelected = logBuffersSelected;
+ return this;
+ }
+
+ /**
+ * sets log out format, -v parameter
+ *
+ * @param logOutFormat out format, like -v time
+ * @return the same Builder
+ */
+ public Builder setLogOutFormat(int logOutFormat) {
+ this.mLogOutFormat = mLogOutFormat;
+ return this;
+ }
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * call this only if mLogFolderName and mLogFolderPath not
+ * be set both.
+ *
+ * @return
+ */
+ private void applyAppNameAsOutfolderName() {
+ try {
+ String appName = mContext.getPackageName();
+ String versionName = mContext.getPackageManager().getPackageInfo(
+ appName, 0).versionName;
+ int versionCode = mContext.getPackageManager()
+ .getPackageInfo(appName, 0).versionCode;
+ mLogFolderName = appName + "-" + versionName + "-" + versionCode;
+ mLogFolderPath = applyOutfolderPath();
+ } catch (Exception e) {
+ }
+ }
+
+ private String applyOutfolderPath() {
+ String outPath = "";
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ outPath = Environment.getExternalStorageDirectory()
+ .getAbsolutePath() + File.separator + mLogFolderName;
+ }
+
+ return outPath;
+ }
+
+ /**
+ * Combine all of the options that have been set and return
+ * a new {@link LogRecorder} object.
+ */
+ public LogRecorder build() {
+ LogRecorder logRecorder = new LogRecorder();
+
+ // no folder name & folder path be set
+ if (TextUtils.isEmpty(mLogFolderName)
+ && TextUtils.isEmpty(mLogFolderPath)) {
+ applyAppNameAsOutfolderName();
+ }
+
+ // make sure out path be set
+ if (TextUtils.isEmpty(mLogFolderPath)) {
+ mLogFolderPath = applyOutfolderPath();
+ }
+
+ logRecorder.mFolderPath = mLogFolderPath;
+ logRecorder.mFileSuffix = mLogFileNameSuffix;
+ logRecorder.mFileSizeLimitation = mLogFileSizeLimitation;
+ logRecorder.mLevel = mLogLevel;
+ if (!mLogFilterTags.isEmpty()) {
+ for (int i = 0; i < mLogFilterTags.size(); i++) {
+ logRecorder.mFilterTags.add(mLogFilterTags.get(i));
+ }
+ }
+ logRecorder.mPID = mPID;
+
+ return logRecorder;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java
new file mode 100644
index 000000000..d5e59153e
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mSignature = signature;
+ }
+
+ public String getItemType() { return mItemType; }
+ public String getOrderId() { return mOrderId; }
+ public String getPackageName() { return mPackageName; }
+ public String getSku() { return mSku; }
+ public long getPurchaseTime() { return mPurchaseTime; }
+ public int getPurchaseState() { return mPurchaseState; }
+ public String getDeveloperPayload() { return mDeveloperPayload; }
+ public String getToken() { return mToken; }
+ public String getOriginalJson() { return mOriginalJson; }
+ public String getSignature() { return mSignature; }
+
+ @Override
+ public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java
new file mode 100644
index 000000000..1a16ac3ec
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java
@@ -0,0 +1,116 @@
+package com.v2ray.ang.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.RGBLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.GlobalHistogramBinarizer;
+import com.google.zxing.common.HybridBinarizer;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 描述:解析二维码图片
+ */
+public class QRCodeDecoder {
+ public static final Map HINTS = new EnumMap<>(DecodeHintType.class);
+
+ static {
+ List allFormats = new ArrayList<>();
+ allFormats.add(BarcodeFormat.AZTEC);
+ allFormats.add(BarcodeFormat.CODABAR);
+ allFormats.add(BarcodeFormat.CODE_39);
+ allFormats.add(BarcodeFormat.CODE_93);
+ allFormats.add(BarcodeFormat.CODE_128);
+ allFormats.add(BarcodeFormat.DATA_MATRIX);
+ allFormats.add(BarcodeFormat.EAN_8);
+ allFormats.add(BarcodeFormat.EAN_13);
+ allFormats.add(BarcodeFormat.ITF);
+ allFormats.add(BarcodeFormat.MAXICODE);
+ allFormats.add(BarcodeFormat.PDF_417);
+ allFormats.add(BarcodeFormat.QR_CODE);
+ allFormats.add(BarcodeFormat.RSS_14);
+ allFormats.add(BarcodeFormat.RSS_EXPANDED);
+ allFormats.add(BarcodeFormat.UPC_A);
+ allFormats.add(BarcodeFormat.UPC_E);
+ allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
+ HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
+ HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
+ HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
+ }
+
+ private QRCodeDecoder() {
+ }
+
+ /**
+ * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param picturePath 要解析的二维码图片本地路径
+ * @return 返回二维码图片里的内容 或 null
+ */
+ public static String syncDecodeQRCode(String picturePath) {
+ return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
+ }
+
+ /**
+ * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param bitmap 要解析的二维码图片
+ * @return 返回二维码图片里的内容 或 null
+ */
+ public static String syncDecodeQRCode(Bitmap bitmap) {
+ Result result = null;
+ RGBLuminanceSource source = null;
+ try {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int[] pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+ source = new RGBLuminanceSource(width, height, pixels);
+ result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
+ return result.getText();
+ } catch (Exception e) {
+ e.printStackTrace();
+ if (source != null) {
+ try {
+ result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
+ return result.getText();
+ } catch (Throwable e2) {
+ e2.printStackTrace();
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
+ *
+ * @param picturePath 本地图片文件路径
+ * @return
+ */
+ private static Bitmap getDecodeAbleBitmap(String picturePath) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(picturePath, options);
+ int sampleSize = options.outHeight / 400;
+ if (sampleSize <= 0) {
+ sampleSize = 1;
+ }
+ options.inSampleSize = sampleSize;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(picturePath, options);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java
new file mode 100644
index 000000000..50f02e3c4
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java
@@ -0,0 +1,119 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey Base64-encoded public key
+ * @throws IllegalArgumentException if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param signedData signed data from server
+ * @param signature server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ Signature sig;
+ try {
+ sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ }
+ return false;
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java
new file mode 100644
index 000000000..b15cd4728
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java
@@ -0,0 +1,58 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ String mItemType;
+ String mSku;
+ String mType;
+ String mPrice;
+ String mTitle;
+ String mDescription;
+ String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() { return mSku; }
+ public String getType() { return mType; }
+ public String getPrice() { return mPrice; }
+ public String getTitle() { return mTitle; }
+ public String getDescription() { return mDescription; }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
new file mode 100644
index 000000000..e54812cd4
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
@@ -0,0 +1,31 @@
+package com.v2ray.ang
+
+import android.app.Application
+//import com.squareup.leakcanary.LeakCanary
+import com.v2ray.ang.util.AngConfigManager
+import me.dozen.dpreference.DPreference
+import org.jetbrains.anko.defaultSharedPreferences
+
+class AngApplication : Application() {
+ companion object {
+ const val PREF_LAST_VERSION = "pref_last_version"
+ }
+
+ var firstRun = false
+ private set
+
+ val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") }
+
+ override fun onCreate() {
+ super.onCreate()
+
+// LeakCanary.install(this)
+
+ firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
+ if (firstRun)
+ defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
+
+ //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
+ AngConfigManager.inject(this)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
new file mode 100644
index 000000000..9f2278db6
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
@@ -0,0 +1,61 @@
+package com.v2ray.ang
+
+/**
+ *
+ * App Config Const
+ */
+object AppConfig {
+ const val ANG_PACKAGE = "com.v2ray.ang"
+ const val ANG_CONFIG = "ang_config"
+ const val PREF_CURR_CONFIG = "pref_v2ray_config"
+ const val PREF_CURR_CONFIG_GUID = "pref_v2ray_config_guid"
+ const val PREF_CURR_CONFIG_NAME = "pref_v2ray_config_name"
+ const val PREF_CURR_CONFIG_DOMAIN = "pref_v2ray_config_domain"
+ const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
+ const val VMESS_PROTOCOL: String = "vmess://"
+ const val SS_PROTOCOL: String = "ss://"
+ const val SOCKS_PROTOCOL: String = "socks://"
+ const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
+ const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
+ const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
+
+ const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
+ const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
+ const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
+ const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
+ const val TASKER_DEFAULT_GUID = "Default"
+
+ const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
+ const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
+ const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
+ const val TAG_AGENT = "proxy"
+ const val TAG_DIRECT = "direct"
+ const val TAG_BLOCKED = "block"
+
+ const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
+ const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
+ const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
+ const val promotionUrl = "https://1.2345345.xyz/ads.html"
+
+ const val DNS_AGENT = "1.1.1.1"
+ const val DNS_DIRECT = "223.5.5.5"
+
+ const val MSG_REGISTER_CLIENT = 1
+ const val MSG_STATE_RUNNING = 11
+ const val MSG_STATE_NOT_RUNNING = 12
+ const val MSG_UNREGISTER_CLIENT = 2
+ const val MSG_STATE_START = 3
+ const val MSG_STATE_START_SUCCESS = 31
+ const val MSG_STATE_START_FAILURE = 32
+ const val MSG_STATE_STOP = 4
+ const val MSG_STATE_STOP_SUCCESS = 41
+ const val MSG_STATE_RESTART = 5
+
+ object EConfigType {
+ val Vmess = 1
+ val Custom = 2
+ val Shadowsocks = 3
+ val Socks = 4
+ }
+
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
new file mode 100644
index 000000000..c51d78b65
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
@@ -0,0 +1,28 @@
+package com.v2ray.ang.dto
+
+data class AngConfig(
+ var index: Int,
+ var vmess: ArrayList,
+ var subItem: ArrayList
+) {
+ data class VmessBean(var guid: String = "123456",
+ var address: String = "v2ray.cool",
+ var port: Int = 10086,
+ var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
+ var alterId: Int = 64,
+ var security: String = "aes-128-cfb",
+ var network: String = "tcp",
+ var remarks: String = "def",
+ var headerType: String = "",
+ var requestHost: String = "",
+ var path: String = "",
+ var streamSecurity: String = "",
+ var configType: Int = 1,
+ var configVersion: Int = 1,
+ var testResult: String = "",
+ var subid: String = "")
+
+ data class SubItemBean(var id: String = "",
+ var remarks: String = "",
+ var url: String = "")
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
new file mode 100644
index 000000000..f99655a85
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
@@ -0,0 +1,9 @@
+package com.v2ray.ang.dto
+
+import android.graphics.drawable.Drawable
+
+data class AppInfo(val appName: String,
+ val packageName: String,
+ val appIcon: Drawable,
+ val isSystemApp: Boolean,
+ var isSelected: Int)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
new file mode 100644
index 000000000..ec1a87eca
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
@@ -0,0 +1,142 @@
+package com.v2ray.ang.dto
+
+data class V2rayConfig(
+ val stats: Any?=null,
+ val log: LogBean,
+ val policy: PolicyBean,
+ val inbounds: ArrayList,
+ var outbounds: ArrayList,
+ var dns: DnsBean,
+ val routing: RoutingBean) {
+
+ data class LogBean(val access: String,
+ val error: String,
+ val loglevel: String)
+
+ data class InboundBean(
+ var tag: String,
+ var port: Int,
+ var protocol: String,
+ var listen: String?=null,
+ val settings: InSettingsBean,
+ val sniffing: SniffingBean?) {
+
+ data class InSettingsBean(val auth: String? = null,
+ val udp: Boolean? = null,
+ val userLevel: Int? =null,
+ val address: String? = null,
+ val port: Int? = null,
+ val network: String? = null)
+
+ data class SniffingBean(var enabled: Boolean,
+ val destOverride: List)
+ }
+
+ data class OutboundBean(val tag: String,
+ var protocol: String,
+ var settings: OutSettingsBean?,
+ var streamSettings: StreamSettingsBean?,
+ var mux: MuxBean?) {
+
+ data class OutSettingsBean(var vnext: List?,
+ var servers: List?,
+ var response: Response) {
+
+ data class VnextBean(var address: String,
+ var port: Int,
+ var users: List) {
+
+ data class UsersBean(var id: String,
+ var alterId: Int,
+ var security: String,
+ var level: Int)
+ }
+
+ data class ServersBean(var address: String,
+ var method: String,
+ var ota: Boolean,
+ var password: String,
+ var port: Int,
+ var level: Int)
+
+ data class Response(var type: String)
+ }
+
+ data class StreamSettingsBean(var network: String,
+ var security: String,
+ var tcpSettings: TcpsettingsBean?,
+ var kcpsettings: KcpsettingsBean?,
+ var wssettings: WssettingsBean?,
+ var httpsettings: HttpsettingsBean?,
+ var tlssettings: TlssettingsBean?,
+ var quicsettings: QuicsettingBean?
+ ) {
+
+ data class TcpsettingsBean(var connectionReuse: Boolean = true,
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none",
+ var request: Any? = null,
+ var response: Any? = null)
+ }
+
+ data class KcpsettingsBean(var mtu: Int = 1350,
+ var tti: Int = 20,
+ var uplinkCapacity: Int = 12,
+ var downlinkCapacity: Int = 100,
+ var congestion: Boolean = false,
+ var readBufferSize: Int = 1,
+ var writeBufferSize: Int = 1,
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class WssettingsBean(var connectionReuse: Boolean = true,
+ var path: String = "",
+ var headers: HeadersBean = HeadersBean()) {
+ data class HeadersBean(var Host: String = "")
+ }
+
+ data class HttpsettingsBean(var host: List = ArrayList(), var path: String = "")
+
+ data class TlssettingsBean(var allowInsecure: Boolean = true,
+ var serverName: String = "")
+
+ data class QuicsettingBean(var security: String = "none",
+ var key: String = "",
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+ }
+
+ data class MuxBean(var enabled: Boolean)
+ }
+
+ //data class DnsBean(var servers: List)
+ data class DnsBean(var servers: List?=null,
+ var hosts: Map?=null
+ ) {
+ data class ServersBean(var address: String = "",
+ var port: Int = 0,
+ var domains: List?)
+ }
+
+ data class RoutingBean(var domainStrategy: String,
+ var rules: ArrayList) {
+
+ data class RulesBean(var type: String = "",
+ var ip: ArrayList? = null,
+ var domain: ArrayList? = null,
+ var outboundTag: String = "",
+ var port: String? = null,
+ var inboundTag: ArrayList? = null)
+ }
+
+ data class PolicyBean(var levels: Map,
+ var system: Any?=null) {
+ data class LevelBean(
+ var handshake: Int? = null,
+ var connIdle: Int? = null,
+ var uplinkOnly: Int? = null,
+ var downlinkOnly: Int? = null)
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
new file mode 100644
index 000000000..30618017b
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
@@ -0,0 +1,13 @@
+package com.v2ray.ang.dto
+
+data class VmessQRCode(var v: String = "",
+ var ps: String = "",
+ var add: String = "",
+ var port: String = "",
+ var id: String = "",
+ var aid: String = "",
+ var net: String = "",
+ var type: String = "",
+ var host: String = "",
+ var path: String = "",
+ var tls: String = "")
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt
new file mode 100644
index 000000000..f70b51301
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt
@@ -0,0 +1,244 @@
+package com.v2ray.ang.extension
+
+import android.app.Fragment
+import android.app.ProgressDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.database.Cursor
+import android.graphics.drawable.Drawable
+import android.support.v7.app.AlertDialog
+import android.view.KeyEvent
+import android.view.View
+import android.widget.ListAdapter
+
+
+fun Context.alertView(
+ title: String? = null,
+ view: View,
+ init: (KAlertDialogBuilder.() -> Unit)? = null
+) = KAlertDialogBuilder(this).apply {
+ if (title != null) title(title)
+ if (title != null) customView(view)
+ if (init != null) init()
+}
+
+fun Fragment.alert(
+ message: String,
+ title: String? = null,
+ init: (KAlertDialogBuilder.() -> Unit)? = null
+) = activity.alert(message, title, init)
+
+fun Context.alert(
+ message: String,
+ title: String? = null,
+ init: (KAlertDialogBuilder.() -> Unit)? = null
+) = KAlertDialogBuilder(this).apply {
+ if (title != null) title(title)
+ message(message)
+ if (init != null) init()
+}
+
+fun Fragment.alert(
+ message: Int,
+ title: Int? = null,
+ init: (KAlertDialogBuilder.() -> Unit)? = null
+) = activity.alert(message, title, init)
+
+fun Context.alert(
+ message: Int,
+ title: Int? = null,
+ init: (KAlertDialogBuilder.() -> Unit)? = null
+) = KAlertDialogBuilder(this).apply {
+ if (title != null) title(title)
+ message(message)
+ if (init != null) init()
+}
+
+
+fun Fragment.alert(init: KAlertDialogBuilder.() -> Unit): KAlertDialogBuilder = activity.alert(init)
+
+fun Context.alert(init: KAlertDialogBuilder.() -> Unit) = KAlertDialogBuilder(this).apply { init() }
+
+fun Fragment.progressDialog(
+ message: Int? = null,
+ title: Int? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = activity.progressDialog(message, title, init)
+
+fun Context.progressDialog(
+ message: Int? = null,
+ title: Int? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = progressDialog(false, message?.let { getString(it) }, title?.let { getString(it) }, init)
+
+fun Fragment.indeterminateProgressDialog(
+ message: Int? = null,
+ title: Int? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = activity.progressDialog(message, title, init)
+
+fun Context.indeterminateProgressDialog(
+ message: Int? = null,
+ title: Int? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = progressDialog(true, message?.let { getString(it) }, title?.let { getString(it) }, init)
+
+fun Fragment.progressDialog(
+ message: String? = null,
+ title: String? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = activity.progressDialog(message, title, init)
+
+fun Context.progressDialog(
+ message: String? = null,
+ title: String? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = progressDialog(false, message, title, init)
+
+fun Fragment.indeterminateProgressDialog(
+ message: String? = null,
+ title: String? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = activity.indeterminateProgressDialog(message, title, init)
+
+fun Context.indeterminateProgressDialog(
+ message: String? = null,
+ title: String? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = progressDialog(true, message, title, init)
+
+private fun Context.progressDialog(
+ indeterminate: Boolean,
+ message: String? = null,
+ title: String? = null,
+ init: (ProgressDialog.() -> Unit)? = null
+) = ProgressDialog(this).apply {
+ isIndeterminate = indeterminate
+ if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
+ if (message != null) setMessage(message)
+ if (title != null) setTitle(title)
+ if (init != null) init()
+ show()
+}
+
+fun Fragment.selector(
+ title: CharSequence? = null,
+ items: List,
+ onClick: (Int) -> Unit
+): Unit = activity.selector(title, items, onClick)
+
+fun Context.selector(
+ title: CharSequence? = null,
+ items: List,
+ onClick: (Int) -> Unit
+) {
+ with(KAlertDialogBuilder(this)) {
+ if (title != null) title(title)
+ items(items, onClick)
+ show()
+ }
+}
+
+class KAlertDialogBuilder(val ctx: Context) {
+
+ val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
+ protected var dialog: AlertDialog? = null
+
+ fun dismiss() {
+ dialog?.dismiss()
+ }
+
+ fun show(): KAlertDialogBuilder {
+ dialog = builder.create()
+ dialog!!.show()
+ return this
+ }
+
+ fun title(title: CharSequence) {
+ builder.setTitle(title)
+ }
+
+ fun title(resource: Int) {
+ builder.setTitle(resource)
+ }
+
+ fun message(title: CharSequence) {
+ builder.setMessage(title)
+ }
+
+ fun message(resource: Int) {
+ builder.setMessage(resource)
+ }
+
+ fun icon(icon: Int) {
+ builder.setIcon(icon)
+ }
+
+ fun icon(icon: Drawable) {
+ builder.setIcon(icon)
+ }
+
+ fun customTitle(title: View) {
+ builder.setCustomTitle(title)
+ }
+
+ fun customView(view: View) {
+ builder.setView(view)
+ }
+
+ fun cancellable(value: Boolean = true) {
+ builder.setCancelable(value)
+ }
+
+ fun onCancel(f: () -> Unit) {
+ builder.setOnCancelListener { f() }
+ }
+
+ fun onKey(f: (keyCode: Int, e: KeyEvent) -> Boolean) {
+ builder.setOnKeyListener({ dialog, keyCode, event -> f(keyCode, event) })
+ }
+
+ fun neutralButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit = { dismiss() }) {
+ neutralButton(ctx.getString(textResource), f)
+ }
+
+ fun neutralButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
+ builder.setNeutralButton(title, { dialog, which -> dialog.f() })
+ }
+
+ fun positiveButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit) {
+ positiveButton(ctx.getString(textResource), f)
+ }
+
+ fun positiveButton(title: String, f: DialogInterface.() -> Unit) {
+ builder.setPositiveButton(title, { dialog, which -> dialog.f() })
+ }
+
+ fun negativeButton(textResource: Int = android.R.string.cancel, f: DialogInterface.() -> Unit = { dismiss() }) {
+ negativeButton(ctx.getString(textResource), f)
+ }
+
+ fun negativeButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
+ builder.setNegativeButton(title, { dialog, which -> dialog.f() })
+ }
+
+ fun items(itemsId: Int, f: (which: Int) -> Unit) {
+ items(ctx.resources!!.getTextArray(itemsId), f)
+ }
+
+ fun items(items: List, f: (which: Int) -> Unit) {
+ items(items.toTypedArray(), f)
+ }
+
+ fun items(items: Array, f: (which: Int) -> Unit) {
+ builder.setItems(items, { dialog, which -> f(which) })
+ }
+
+ fun adapter(adapter: ListAdapter, f: (which: Int) -> Unit) {
+ builder.setAdapter(adapter, { dialog, which -> f(which) })
+ }
+
+ fun adapter(cursor: Cursor, labelColumn: String, f: (which: Int) -> Unit) {
+ builder.setCursor(cursor, { dialog, which -> f(which) }, labelColumn)
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
new file mode 100644
index 000000000..fa028c47d
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
@@ -0,0 +1,64 @@
+package com.v2ray.ang.extension
+
+import android.content.Context
+import android.os.Build
+import com.v2ray.ang.AngApplication
+import me.dozen.dpreference.DPreference
+import org.json.JSONObject
+import java.net.URLConnection
+
+/**
+ * Some extensions
+ */
+
+val Context.v2RayApplication: AngApplication
+ get() = applicationContext as AngApplication
+
+val Context.defaultDPreference: DPreference
+ get() = v2RayApplication.defaultDPreference
+
+
+fun JSONObject.putOpt(pair: Pair) = putOpt(pair.first, pair.second)!!
+fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) }
+
+const val threshold = 1000
+const val divisor = 1024F
+
+fun Long.toSpeedString() = toTrafficString() + "/s"
+
+fun Long.toTrafficString(): String {
+ if (this < threshold)
+ return "$this B"
+
+ val kib = this / divisor
+ if (kib < threshold)
+ return "${kib.toShortString()} KB"
+
+ val mib = kib / divisor
+ if (mib < threshold)
+ return "${mib.toShortString()} MB"
+
+ val gib = mib / divisor
+ if (gib < threshold)
+ return "${gib.toShortString()} GB"
+
+ val tib = gib / divisor
+ if (tib < threshold)
+ return "${tib.toShortString()} TB"
+
+ val pib = tib / divisor
+ if (pib < threshold)
+ return "${pib.toShortString()} PB"
+
+ return "∞"
+}
+
+private fun Float.toShortString(): String {
+ val s = toString()
+ if (s.length <= 4)
+ return s
+ return s.substring(0, 4).removeSuffix(".")
+}
+
+val URLConnection.responseLength: Long
+ get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt
new file mode 100644
index 000000000..dcfa46718
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt
@@ -0,0 +1,10 @@
+package com.v2ray.ang.extension
+
+import android.preference.Preference
+
+fun Preference.onClick(listener: () -> Unit) {
+ setOnPreferenceClickListener {
+ listener()
+ true
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
new file mode 100644
index 000000000..dca1dc38e
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
@@ -0,0 +1,35 @@
+package com.v2ray.ang.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.text.TextUtils
+import com.google.zxing.WriterException
+import com.v2ray.ang.AppConfig
+
+import com.v2ray.ang.util.Utils
+
+class TaskerReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+
+ try {
+ val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
+ val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
+ val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
+
+ if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
+ return
+ } else if (switch) {
+ if (guid == AppConfig.TASKER_DEFAULT_GUID) {
+ Utils.startVService(context)
+ } else {
+ Utils.startVService(context, guid)
+ }
+ } else {
+ Utils.stopVService(context)
+ }
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
new file mode 100644
index 000000000..eddc21467
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
@@ -0,0 +1,50 @@
+package com.v2ray.ang.receiver
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.widget.RemoteViews
+import com.v2ray.ang.R
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.util.Utils
+import org.jetbrains.anko.toast
+
+class WidgetProvider : AppWidgetProvider() {
+ /**
+ * 每次窗口小部件被更新都调用一次该方法
+ */
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+
+ val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
+ val intent = Intent(AppConfig.BROADCAST_ACTION_WIDGET_CLICK)
+ val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
+
+ for (appWidgetId in appWidgetIds) {
+ appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
+ }
+ }
+
+ /**
+ * 接收窗口小部件点击时发送的广播
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
+
+ val isRunning = Utils.isServiceRun(context, "com.v2ray.ang.service.V2RayVpnService")
+ if (isRunning) {
+// context.toast(R.string.toast_services_stop)
+ Utils.stopVService(context)
+ } else {
+// context.toast(R.string.toast_services_start)
+ Utils.startVService(context)
+ }
+ }
+ }
+
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
new file mode 100644
index 000000000..cbb824254
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
@@ -0,0 +1,96 @@
+package com.v2ray.ang.service
+
+import android.annotation.TargetApi
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.net.VpnService
+import android.os.Build
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.defaultDPreference
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.Utils
+import org.jetbrains.anko.toast
+import java.lang.ref.SoftReference
+
+
+@TargetApi(Build.VERSION_CODES.N)
+class QSTileService : TileService() {
+
+ fun setState(state: Int) {
+ if (state == Tile.STATE_INACTIVE) {
+ qsTile?.state = Tile.STATE_INACTIVE
+ qsTile?.label = getString(R.string.app_name)
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle)
+ } else if (state == Tile.STATE_ACTIVE) {
+ qsTile?.state = Tile.STATE_ACTIVE
+ qsTile?.label = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG")
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v)
+ }
+
+
+ qsTile?.updateTile()
+ }
+
+ override fun onStartListening() {
+ super.onStartListening()
+ setState(Tile.STATE_INACTIVE)
+ mMsgReceive = ReceiveMessageHandler(this)
+ registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
+ MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
+ }
+
+ override fun onStopListening() {
+ super.onStopListening()
+
+ unregisterReceiver(mMsgReceive)
+ mMsgReceive = null
+ }
+
+ override fun onClick() {
+ super.onClick()
+ when (qsTile.state) {
+ Tile.STATE_INACTIVE -> {
+ val intent = VpnService.prepare(this)
+ if (intent == null)
+ if (!Utils.startVService(this)) {
+ toast(R.string.app_tile_first_use)
+ }
+ }
+ Tile.STATE_ACTIVE -> {
+ Utils.stopVService(this)
+ }
+ }
+ }
+
+ private var mMsgReceive: BroadcastReceiver? = null
+
+ private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
+ internal var mReference: SoftReference = SoftReference(context)
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val context = mReference.get()
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING -> {
+ context?.setState(Tile.STATE_ACTIVE)
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ AppConfig.MSG_STATE_START_SUCCESS -> {
+ context?.setState(Tile.STATE_ACTIVE)
+ }
+ AppConfig.MSG_STATE_START_FAILURE -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
new file mode 100644
index 000000000..91ff3860a
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
@@ -0,0 +1,453 @@
+package com.v2ray.ang.service
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.net.*
+import android.net.VpnService
+import android.os.*
+import android.support.annotation.RequiresApi
+import android.support.v4.app.NotificationCompat
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.defaultDPreference
+import com.v2ray.ang.extension.toSpeedString
+import com.v2ray.ang.ui.MainActivity
+import com.v2ray.ang.ui.PerAppProxyActivity
+import com.v2ray.ang.ui.SettingsActivity
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.Utils
+import libv2ray.Libv2ray
+import libv2ray.V2RayVPNServiceSupportsSet
+import rx.Observable
+import rx.Subscription
+import java.net.InetAddress
+import java.io.IOException
+import java.io.File
+import java.io.FileDescriptor
+import java.io.FileInputStream
+import java.lang.ref.SoftReference
+import android.os.Build
+import android.annotation.TargetApi
+import android.util.Log
+import org.jetbrains.anko.doAsync
+
+class V2RayVpnService : VpnService() {
+ companion object {
+ const val NOTIFICATION_ID = 1
+ const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
+ const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
+
+ fun startV2Ray(context: Context) {
+ val intent = Intent(context.applicationContext, V2RayVpnService::class.java)
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+ }
+
+ private val v2rayPoint = Libv2ray.newV2RayPoint()
+ private val v2rayCallback = V2RayCallback()
+ private lateinit var configContent: String
+ private lateinit var mInterface: ParcelFileDescriptor
+ val fd: Int get() = mInterface.fd
+ private var mBuilder: NotificationCompat.Builder? = null
+ private var mSubscription: Subscription? = null
+ private var mNotificationManager: NotificationManager? = null
+
+
+
+ /**
+ * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
+ *
+ * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
+ * satisfies default network capabilities but only THE default network. Unfortunately we need to have
+ * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
+ *
+ * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
+ */
+ @TargetApi(28)
+ private val defaultNetworkRequest = NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .build()
+
+
+ private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
+ @TargetApi(28)
+ private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ setUnderlyingNetworks(arrayOf(network))
+ }
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
+ // it's a good idea to refresh capabilities
+ setUnderlyingNetworks(arrayOf(network))
+ }
+ override fun onLost(network: Network) {
+ setUnderlyingNetworks(null)
+ }
+ }
+ private var listeningForDefaultNetwork = false
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
+ StrictMode.setThreadPolicy(policy)
+ v2rayPoint.packageName = Utils.packagePath(applicationContext)
+ }
+
+ override fun onRevoke() {
+ stopV2Ray()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ cancelNotification()
+ }
+
+ fun setup(parameters: String) {
+
+ val prepare = VpnService.prepare(this)
+ if (prepare != null) {
+ return
+ }
+
+ // If the old interface has exactly the same parameters, use it!
+ // Configure a builder while parsing the parameters.
+ val builder = Builder()
+ val enableLocalDns = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
+
+ parameters.split(" ")
+ .map { it.split(",") }
+ .forEach {
+ when (it[0][0]) {
+ 'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt())
+ 's' -> builder.addSearchDomain(it[1])
+ 'a' -> builder.addAddress(it[1], Integer.parseInt(it[2]))
+ 'r' -> builder.addRoute(it[1], Integer.parseInt(it[2]))
+ 'd' -> builder.addDnsServer(it[1])
+ }
+ }
+
+ if(!enableLocalDns) {
+ Utils.getRemoteDnsServers(defaultDPreference)
+ .forEach {
+ builder.addDnsServer(it)
+ }
+ }
+
+ builder.setSession(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, ""))
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
+ defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)) {
+ val apps = defaultDPreference.getPrefStringSet(PerAppProxyActivity.PREF_PER_APP_PROXY_SET, null)
+ val bypassApps = defaultDPreference.getPrefBoolean(PerAppProxyActivity.PREF_BYPASS_APPS, false)
+ apps?.forEach {
+ try {
+ if (bypassApps)
+ builder.addDisallowedApplication(it)
+ else
+ builder.addAllowedApplication(it)
+ } catch (e: PackageManager.NameNotFoundException) {
+ //Logger.d(e)
+ }
+ }
+ }
+
+ // Close the old interface since the parameters have been changed.
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ }
+
+
+ if (Build.VERSION.SDK_INT >= 28) {
+ connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
+ listeningForDefaultNetwork = true
+ }
+
+ // Create a new interface using the builder and save the parameters.
+ mInterface = builder.establish()
+ sendFd()
+
+ if (defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SPEED_ENABLED, false)) {
+ mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
+ .subscribe {
+ val uplink = v2rayPoint.queryStats("socks", "uplink")
+ val downlink = v2rayPoint.queryStats("socks", "downlink")
+ updateNotification("${(uplink / 3).toSpeedString()} ↑ ${(downlink / 3).toSpeedString()} ↓")
+ }
+ }
+ }
+
+ fun shutdown() {
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ }
+ }
+
+ fun sendFd() {
+ val fd = mInterface.fileDescriptor
+ val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath
+
+ doAsync {
+ var tries = 0
+ while (true) try {
+ Thread.sleep(50L shl tries)
+ Log.d(packageName, "sendFd tries: " + tries.toString())
+ LocalSocket().use { localSocket ->
+ localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
+ localSocket.setFileDescriptorsForSend(arrayOf(fd))
+ localSocket.outputStream.write(42)
+ }
+ break
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ if (tries > 5) break
+ tries += 1
+ }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ startV2ray()
+ return START_STICKY
+ //return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun startV2ray() {
+ if (!v2rayPoint.isRunning) {
+
+ try {
+ registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE))
+ } catch (e: Exception) {
+ }
+
+ configContent = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "")
+ v2rayPoint.supportSet = v2rayCallback
+ v2rayPoint.configureFileContent = configContent
+ v2rayPoint.enableLocalDNS = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
+ v2rayPoint.forwardIpv6 = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_FORWARD_IPV6, false)
+ v2rayPoint.domainName = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, "")
+
+ try {
+ v2rayPoint.runLoop()
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ }
+
+ if (v2rayPoint.isRunning) {
+ MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_SUCCESS, "")
+ showNotification()
+ } else {
+ MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_FAILURE, "")
+ cancelNotification()
+ }
+ }
+ // showNotification()
+ }
+
+ private fun stopV2Ray(isForced: Boolean = true) {
+// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
+// val emptyInfo = VpnNetworkInfo()
+// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
+// saveVpnNetworkInfo(configName, info)
+ if (Build.VERSION.SDK_INT >= 28) {
+ if (listeningForDefaultNetwork) {
+ connectivity.unregisterNetworkCallback(defaultNetworkCallback)
+ listeningForDefaultNetwork = false
+ }
+ }
+ if (v2rayPoint.isRunning) {
+ try {
+ v2rayPoint.stopLoop()
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ }
+ }
+
+ MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "")
+ cancelNotification()
+
+ if (isForced) {
+ try {
+ unregisterReceiver(mMsgReceive)
+ } catch (e: Exception) {
+ }
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ }
+
+ stopSelf()
+ }
+ }
+
+ private fun showNotification() {
+ val startMainIntent = Intent(applicationContext, MainActivity::class.java)
+ val contentPendingIntent = PendingIntent.getActivity(applicationContext,
+ NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
+ stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE
+ stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
+
+ val stopV2RayPendingIntent = PendingIntent.getBroadcast(applicationContext,
+ NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val channelId =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ } else {
+ // If earlier version channel ID is not used
+ // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
+ ""
+ }
+
+ mBuilder = NotificationCompat.Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.ic_v)
+ .setContentTitle(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, ""))
+ .setContentText(getString(R.string.notification_action_more))
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setOnlyAlertOnce(true)
+ .setContentIntent(contentPendingIntent)
+ .addAction(R.drawable.ic_close_grey_800_24dp,
+ getString(R.string.notification_action_stop_v2ray),
+ stopV2RayPendingIntent)
+ //.build()
+
+ //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
+
+ startForeground(NOTIFICATION_ID, mBuilder?.build())
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel(): String {
+ val channelId = "RAY_NG_M_CH_ID"
+ val channelName = "V2rayNG Background Service"
+ val chan = NotificationChannel(channelId,
+ channelName, NotificationManager.IMPORTANCE_HIGH)
+ chan.lightColor = Color.DKGRAY
+ chan.importance = NotificationManager.IMPORTANCE_NONE
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ getNotificationManager().createNotificationChannel(chan)
+ return channelId
+ }
+
+ private fun cancelNotification() {
+ stopForeground(true)
+ mBuilder = null
+ mSubscription?.unsubscribe()
+ mSubscription = null
+ }
+
+ private fun updateNotification(contentText: String) {
+ if (mBuilder != null) {
+ mBuilder?.setContentText(contentText)
+ getNotificationManager().notify(NOTIFICATION_ID, mBuilder?.build())
+ }
+ }
+
+ private fun getNotificationManager(): NotificationManager {
+ if (mNotificationManager == null) {
+ mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
+ return mNotificationManager!!
+ }
+
+ private inner class V2RayCallback : V2RayVPNServiceSupportsSet {
+ override fun shutdown(): Long {
+ try {
+ this@V2RayVpnService.shutdown()
+ return 0
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ return -1
+ }
+ }
+
+ override fun prepare(): Long {
+ return 0
+ }
+
+ override fun protect(l: Long) = (if (this@V2RayVpnService.protect(l.toInt())) 0 else 1).toLong()
+
+ override fun onEmitStatus(l: Long, s: String?): Long {
+ //Logger.d(s)
+ return 0
+ }
+
+ override fun setup(s: String): Long {
+ //Logger.d(s)
+ try {
+ this@V2RayVpnService.setup(s)
+ return 0
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ return -1
+ }
+ }
+
+ override fun sendFd(): Long {
+ try {
+ this@V2RayVpnService.sendFd()
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ return -1
+ }
+ return 0
+ }
+ }
+
+ private var mMsgReceive = ReceiveMessageHandler(this@V2RayVpnService)
+
+ private class ReceiveMessageHandler(vpnService: V2RayVpnService) : BroadcastReceiver() {
+ internal var mReference: SoftReference = SoftReference(vpnService)
+
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val vpnService = mReference.get()
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_REGISTER_CLIENT -> {
+ //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
+
+ val isRunning = vpnService?.v2rayPoint!!.isRunning
+ && VpnService.prepare(vpnService) == null
+ if (isRunning) {
+ MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_RUNNING, "")
+ } else {
+ MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_NOT_RUNNING, "")
+ }
+ }
+ AppConfig.MSG_UNREGISTER_CLIENT -> {
+// vpnService?.mMsgSend = null
+ }
+ AppConfig.MSG_STATE_START -> {
+ //nothing to do
+ }
+ AppConfig.MSG_STATE_STOP -> {
+ vpnService?.stopV2Ray()
+ }
+ AppConfig.MSG_STATE_RESTART -> {
+ vpnService?.startV2ray()
+ }
+ }
+ }
+ }
+}
+
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
new file mode 100644
index 000000000..8ea4d87c0
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
@@ -0,0 +1,14 @@
+package com.v2ray.ang.ui
+
+import android.support.v7.app.AppCompatActivity
+import android.view.MenuItem
+
+abstract class BaseActivity : AppCompatActivity() {
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt
new file mode 100644
index 000000000..879d3d922
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt
@@ -0,0 +1,206 @@
+package com.v2ray.ang.ui
+
+import android.app.ActivityOptions
+import android.app.FragmentManager
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.support.design.widget.NavigationView
+import android.support.v4.view.GravityCompat
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.ActionBarDrawerToggle
+import android.support.v7.widget.Toolbar
+import android.util.Log
+import android.view.MenuItem
+import android.view.View
+import com.v2ray.ang.InappBuyActivity
+
+import com.v2ray.ang.R
+import org.jetbrains.anko.startActivity
+
+
+abstract class BaseDrawerActivity : BaseActivity() {
+ companion object {
+
+ private val TAG = "BaseDrawerActivity"
+ }
+
+ private var mToolbar: Toolbar? = null
+
+ private var mDrawerToggle: ActionBarDrawerToggle? = null
+
+ private var mDrawerLayout: DrawerLayout? = null
+
+ private var mToolbarInitialized: Boolean = false
+
+ private var mItemToOpenWhenDrawerCloses = -1
+
+ private val backStackChangedListener = FragmentManager.OnBackStackChangedListener { updateDrawerToggle() }
+
+ private val drawerListener = object : DrawerLayout.DrawerListener {
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
+ mDrawerToggle!!.onDrawerSlide(drawerView, slideOffset)
+ }
+
+ override fun onDrawerOpened(drawerView: View) {
+ mDrawerToggle!!.onDrawerOpened(drawerView)
+ //supportActionBar!!.setTitle(R.string.app_name)
+ }
+
+ override fun onDrawerClosed(drawerView: View) {
+ mDrawerToggle!!.onDrawerClosed(drawerView)
+
+ if (mItemToOpenWhenDrawerCloses >= 0) {
+ val extras = ActivityOptions.makeCustomAnimation(
+ this@BaseDrawerActivity, R.anim.fade_in, R.anim.fade_out).toBundle()
+ var activityClass: Class<*>? = null
+ when (mItemToOpenWhenDrawerCloses) {
+ R.id.server_profile -> activityClass = MainActivity::class.java
+ R.id.sub_setting -> activityClass = SubSettingActivity::class.java
+ R.id.settings -> activityClass = SettingsActivity::class.java
+ R.id.logcat -> {
+ startActivity()
+ return
+ }
+ R.id.donate -> {
+ startActivity()
+ return
+ }
+ }
+ if (activityClass != null) {
+ startActivity(Intent(this@BaseDrawerActivity, activityClass), extras)
+ finish()
+ }
+ }
+ }
+
+ override fun onDrawerStateChanged(newState: Int) {
+ mDrawerToggle!!.onDrawerStateChanged(newState)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "Activity onCreate")
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!mToolbarInitialized) {
+ throw IllegalStateException("You must run super.initializeToolbar at " + "the end of your onCreate method")
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ // Whenever the fragment back stack changes, we may need to update the
+ // action bar toggle: only top level screens show the hamburger-like icon, inner
+ // screens - either Activities or fragments - show the "Up" icon instead.
+ fragmentManager.addOnBackStackChangedListener(backStackChangedListener)
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ fragmentManager.removeOnBackStackChangedListener(backStackChangedListener)
+ }
+
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ super.onPostCreate(savedInstanceState)
+ mDrawerToggle!!.syncState()
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ mDrawerToggle!!.onConfigurationChanged(newConfig)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (mDrawerToggle != null && mDrawerToggle!!.onOptionsItemSelected(item)) {
+ return true
+ }
+ // If not handled by drawerToggle, home needs to be handled by returning to previous
+ if (item.itemId == android.R.id.home) {
+ onBackPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onBackPressed() {
+ // If the drawer is open, back will close it
+ if (mDrawerLayout != null && mDrawerLayout!!.isDrawerOpen(GravityCompat.START)) {
+ mDrawerLayout!!.closeDrawers()
+ return
+ }
+ // Otherwise, it may return to the previous fragment stack
+ val fragmentManager = fragmentManager
+ if (fragmentManager.backStackEntryCount > 0) {
+ fragmentManager.popBackStack()
+ } else {
+ // Lastly, it will rely on the system behavior for back
+ super.onBackPressed()
+ }
+ }
+
+ private fun updateDrawerToggle() {
+ if (mDrawerToggle == null) {
+ return
+ }
+ val isRoot = fragmentManager.backStackEntryCount == 0
+ mDrawerToggle!!.isDrawerIndicatorEnabled = isRoot
+
+ supportActionBar!!.setDisplayShowHomeEnabled(!isRoot)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(!isRoot)
+ supportActionBar!!.setHomeButtonEnabled(!isRoot)
+
+ if (isRoot) {
+ mDrawerToggle!!.syncState()
+ }
+ }
+
+ protected fun initializeToolbar() {
+ mToolbar = findViewById(R.id.toolbar) as Toolbar
+ if (mToolbar == null) {
+ throw IllegalStateException("Layout is required to include a Toolbar with id " + "'toolbar'")
+ }
+
+ // mToolbar.inflateMenu(R.menu.main);
+
+ mDrawerLayout = findViewById(R.id.drawer_layout) as DrawerLayout
+ if (mDrawerLayout != null) {
+ val navigationView = findViewById(R.id.nav_view) as NavigationView
+ ?: throw IllegalStateException("Layout requires a NavigationView " + "with id 'nav_view'")
+
+ // Create an ActionBarDrawerToggle that will handle opening/closing of the drawer:
+ mDrawerToggle = ActionBarDrawerToggle(this, mDrawerLayout,
+ mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
+
+ mDrawerLayout!!.addDrawerListener(drawerListener)
+
+ populateDrawerItems(navigationView)
+ setSupportActionBar(mToolbar)
+ updateDrawerToggle()
+ } else {
+ setSupportActionBar(mToolbar)
+ }
+
+ mToolbarInitialized = true
+ }
+
+ private fun populateDrawerItems(navigationView: NavigationView) {
+ navigationView.setNavigationItemSelectedListener { menuItem ->
+ menuItem.isChecked = true
+ mItemToOpenWhenDrawerCloses = menuItem.itemId
+ mDrawerLayout!!.closeDrawers()
+ true
+ }
+
+ if (MainActivity::class.java.isAssignableFrom(javaClass)) {
+ navigationView.setCheckedItem(R.id.server_profile)
+ } else if (SubSettingActivity::class.java.isAssignableFrom(javaClass)) {
+ navigationView.setCheckedItem(R.id.sub_setting)
+ } else if (SettingsActivity::class.java.isAssignableFrom(javaClass)) {
+ navigationView.setCheckedItem(R.id.settings)
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
new file mode 100644
index 000000000..e4d34e3f0
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
@@ -0,0 +1,21 @@
+package com.v2ray.ang.ui
+
+
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentStatePagerAdapter
+
+class FragmentAdapter(fm: FragmentManager, private val mFragments: List, private val mTitles: List) : FragmentStatePagerAdapter(fm) {
+
+ override fun getItem(position: Int): Fragment {
+ return mFragments[position]
+ }
+
+ override fun getCount(): Int {
+ return mFragments.size
+ }
+
+ override fun getPageTitle(position: Int): CharSequence? {
+ return mTitles[position]
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
new file mode 100644
index 000000000..872f32ea5
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
@@ -0,0 +1,72 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import com.v2ray.ang.R
+import com.v2ray.ang.util.Utils
+import kotlinx.android.synthetic.main.activity_logcat.*
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.toast
+import org.jetbrains.anko.uiThread
+
+import java.io.IOException
+import java.util.LinkedHashSet
+
+class LogcatActivity : BaseActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_logcat)
+
+ title = getString(R.string.title_logcat)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ logcat()
+ }
+
+ private fun logcat() {
+
+ try {
+ pb_waiting.visibility = View.VISIBLE
+
+ doAsync {
+ val lst = LinkedHashSet()
+ lst.add("logcat")
+ lst.add("-d")
+ lst.add("-v")
+ lst.add("time")
+ lst.add("-s")
+ lst.add("GoLog,tun2socks,com.v2ray.ang")
+ val process = Runtime.getRuntime().exec(lst.toTypedArray())
+// val bufferedReader = BufferedReader(
+// InputStreamReader(process.inputStream))
+// val allText = bufferedReader.use(BufferedReader::readText)
+ val allText = process.inputStream.bufferedReader().use { it.readText() }
+ uiThread {
+ tv_logcat.text = allText
+ tv_logcat.movementMethod = ScrollingMovementMethod()
+ pb_waiting.visibility = View.GONE
+ }
+ }
+ } catch (e: IOException) {
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_logcat, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.copy_all -> {
+ Utils.setClipboard(this, tv_logcat.text.toString())
+ toast(R.string.toast_success)
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
new file mode 100644
index 000000000..dfe3a006a
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
@@ -0,0 +1,573 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.content.*
+import android.net.Uri
+import android.net.VpnService
+import android.support.v7.widget.LinearLayoutManager
+import android.view.Menu
+import android.view.MenuItem
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.v2ray.ang.R
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.Utils
+import kotlinx.android.synthetic.main.activity_main.*
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.KeyEvent
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.V2rayConfigUtil
+import org.jetbrains.anko.*
+import java.lang.ref.SoftReference
+import java.net.URL
+import android.content.IntentFilter
+import android.support.design.widget.NavigationView
+import android.support.v4.view.GravityCompat
+import android.support.v7.app.ActionBarDrawerToggle
+import android.support.v7.widget.helper.ItemTouchHelper
+import android.util.Log
+import com.v2ray.ang.InappBuyActivity
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
+import com.v2ray.ang.util.AngConfigManager.configs
+
+class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
+ companion object {
+ private const val REQUEST_CODE_VPN_PREPARE = 0
+ private const val REQUEST_SCAN = 1
+ private const val REQUEST_FILE_CHOOSER = 2
+ private const val REQUEST_SCAN_URL = 3
+ }
+
+ var isRunning = false
+ set(value) {
+ field = value
+ adapter.changeable = !value
+ if (value) {
+ fab.imageResource = R.drawable.ic_v
+ tv_test_state.text = getString(R.string.connection_connected)
+ } else {
+ fab.imageResource = R.drawable.ic_v_idle
+ tv_test_state.text = getString(R.string.connection_not_connected)
+ }
+ hideCircle()
+ }
+
+ private val adapter by lazy { MainRecyclerAdapter(this) }
+ private var mItemTouchHelper: ItemTouchHelper? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ title = getString(R.string.title_server)
+ setSupportActionBar(toolbar)
+
+ fab.setOnClickListener {
+ if (isRunning) {
+ Utils.stopVService(this)
+ } else {
+ val intent = VpnService.prepare(this)
+ if (intent == null) {
+ startV2Ray()
+ } else {
+ startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE)
+ }
+ }
+ }
+ layout_test.setOnClickListener {
+ if (isRunning) {
+ val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808"))
+
+ tv_test_state.text = getString(R.string.connection_test_testing)
+ doAsync {
+ val result = Utils.testConnection(this@MainActivity, socksPort)
+ uiThread {
+ tv_test_state.text = Utils.getEditable(result)
+ }
+ }
+ } else {
+// tv_test_state.text = getString(R.string.connection_test_fail)
+ }
+ }
+
+ recycler_view.setHasFixedSize(true)
+ recycler_view.layoutManager = LinearLayoutManager(this)
+ recycler_view.adapter = adapter
+
+ val callback = SimpleItemTouchHelperCallback(adapter)
+ mItemTouchHelper = ItemTouchHelper(callback)
+ mItemTouchHelper?.attachToRecyclerView(recycler_view)
+
+
+ val toggle = ActionBarDrawerToggle(
+ this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
+ drawer_layout.addDrawerListener(toggle)
+ toggle.syncState()
+ nav_view.setNavigationItemSelectedListener(this)
+ }
+
+ fun startV2Ray() {
+ if (AngConfigManager.configs.index < 0) {
+ return
+ }
+ showCircle()
+// toast(R.string.toast_services_start)
+ if (!Utils.startVService(this)) {
+ hideCircle()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ isRunning = false
+
+// val intent = Intent(this.applicationContext, V2RayVpnService::class.java)
+// intent.`package` = AppConfig.ANG_PACKAGE
+// bindService(intent, mConnection, BIND_AUTO_CREATE)
+
+ mMsgReceive = ReceiveMessageHandler(this@MainActivity)
+ registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
+ MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (mMsgReceive != null) {
+ unregisterReceiver(mMsgReceive)
+ mMsgReceive = null
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ adapter.updateConfigList()
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when (requestCode) {
+ REQUEST_CODE_VPN_PREPARE ->
+ if (resultCode == RESULT_OK) {
+ startV2Ray()
+ }
+ REQUEST_SCAN ->
+ if (resultCode == RESULT_OK) {
+ importBatchConfig(data?.getStringExtra("SCAN_RESULT"))
+ }
+ REQUEST_FILE_CHOOSER -> {
+ if (resultCode == RESULT_OK) {
+ val uri = data!!.data
+ readContentFromUri(uri)
+ }
+ }
+ REQUEST_SCAN_URL ->
+ if (resultCode == RESULT_OK) {
+ importConfigCustomUrl(data?.getStringExtra("SCAN_RESULT"))
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.import_qrcode -> {
+ importQRcode(REQUEST_SCAN)
+ true
+ }
+ R.id.import_clipboard -> {
+ importClipboard()
+ true
+ }
+ R.id.import_manually_vmess -> {
+ startActivity("position" to -1, "isRunning" to isRunning)
+ adapter.updateConfigList()
+ true
+ }
+ R.id.import_manually_ss -> {
+ startActivity("position" to -1, "isRunning" to isRunning)
+ adapter.updateConfigList()
+ true
+ }
+ R.id.import_manually_socks -> {
+ startActivity("position" to -1, "isRunning" to isRunning)
+ adapter.updateConfigList()
+ true
+ }
+ R.id.import_config_custom_clipboard -> {
+ importConfigCustomClipboard()
+ true
+ }
+ R.id.import_config_custom_local -> {
+ importConfigCustomLocal()
+ true
+ }
+ R.id.import_config_custom_url -> {
+ importConfigCustomUrlClipboard()
+ true
+ }
+ R.id.import_config_custom_url_scan -> {
+ importQRcode(REQUEST_SCAN_URL)
+ true
+ }
+
+// R.id.sub_setting -> {
+// startActivity()
+// true
+// }
+
+ R.id.sub_update -> {
+ importConfigViaSub()
+ true
+ }
+
+ R.id.export_all -> {
+ if (AngConfigManager.shareAll2Clipboard() == 0) {
+ toast(R.string.toast_success)
+ } else {
+ toast(R.string.toast_failure)
+ }
+ true
+ }
+
+ R.id.ping_all -> {
+ for (k in 0 until configs.vmess.count()) {
+ configs.vmess[k].testResult = ""
+ adapter.updateConfigList()
+ }
+ for (k in 0 until configs.vmess.count()) {
+ if (configs.vmess[k].configType != AppConfig.EConfigType.Custom) {
+ doAsync {
+ configs.vmess[k].testResult = Utils.tcping(configs.vmess[k].address, configs.vmess[k].port)
+ uiThread {
+ adapter.updateSelectedItem(k)
+ }
+ }
+ }
+ }
+ true
+ }
+
+// R.id.settings -> {
+// startActivity("isRunning" to isRunning)
+// true
+// }
+// R.id.logcat -> {
+// startActivity()
+// true
+// }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+
+ /**
+ * import config from qrcode
+ */
+ fun importQRcode(requestCode: Int): Boolean {
+// try {
+// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
+// .addCategory(Intent.CATEGORY_DEFAULT)
+// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
+// } catch (e: Exception) {
+ RxPermissions(this)
+ .request(Manifest.permission.CAMERA)
+ .subscribe {
+ if (it)
+ startActivityForResult(requestCode)
+ else
+ toast(R.string.toast_permission_denied)
+ }
+// }
+ return true
+ }
+
+ /**
+ * import config from clipboard
+ */
+ fun importClipboard()
+ : Boolean {
+ try {
+ val clipboard = Utils.getClipboard(this)
+ importBatchConfig(clipboard)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importBatchConfig(server: String?, subid: String = "") {
+ val count = AngConfigManager.importBatchConfig(server, subid)
+ if (count > 0) {
+ toast(R.string.toast_success)
+ adapter.updateConfigList()
+ } else {
+ toast(R.string.toast_failure)
+ }
+ }
+
+ fun importConfigCustomClipboard()
+ : Boolean {
+ try {
+ val configText = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(configText)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ importCustomizeConfig(configText)
+ return true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from local config file
+ */
+ fun importConfigCustomLocal(): Boolean {
+ try {
+ showFileChooser()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importConfigCustomUrlClipboard()
+ : Boolean {
+ try {
+ val url = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(url)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ return importConfigCustomUrl(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from url
+ */
+ fun importConfigCustomUrl(url: String?): Boolean {
+ try {
+ if (!Utils.isValidUrl(url)) {
+ toast(R.string.toast_invalid_url)
+ return false
+ }
+ doAsync {
+ val configText = URL(url).readText()
+ uiThread {
+ importCustomizeConfig(configText)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * import config from sub
+ */
+ fun importConfigViaSub()
+ : Boolean {
+ try {
+ toast(R.string.title_sub_update)
+ val subItem = AngConfigManager.configs.subItem
+ for (k in 0 until subItem.count()) {
+ if (TextUtils.isEmpty(subItem[k].id)
+ || TextUtils.isEmpty(subItem[k].remarks)
+ || TextUtils.isEmpty(subItem[k].url)
+ ) {
+ continue
+ }
+ val id = subItem[k].id
+ val url = subItem[k].url
+ if (!Utils.isValidUrl(url)) {
+ continue
+ }
+ Log.d("Main", url)
+ doAsync {
+ val configText = URL(url).readText()
+ uiThread {
+ importBatchConfig(Utils.decode(configText), id)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * show file chooser
+ */
+ private fun showFileChooser() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "*/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+
+ try {
+ startActivityForResult(
+ Intent.createChooser(intent, getString(R.string.title_file_chooser)),
+ REQUEST_FILE_CHOOSER)
+ } catch (ex: android.content.ActivityNotFoundException) {
+ toast(R.string.toast_require_file_manager)
+ }
+ }
+
+ /**
+ * read content from uri
+ */
+ private fun readContentFromUri(uri: Uri) {
+ RxPermissions(this)
+ .request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .subscribe {
+ if (it) {
+ try {
+ val inputStream = contentResolver.openInputStream(uri)
+ val configText = inputStream.bufferedReader().readText()
+ importCustomizeConfig(configText)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else
+ toast(R.string.toast_permission_denied)
+ }
+ }
+
+ /**
+ * import customize config
+ */
+ fun importCustomizeConfig(server: String?) {
+ if (server == null) {
+ return
+ }
+ if (!V2rayConfigUtil.isValidConfig(server)) {
+ toast(R.string.toast_config_file_invalid)
+ return
+ }
+ val resId = AngConfigManager.importCustomizeConfig(server)
+ if (resId > 0) {
+ toast(resId)
+ } else {
+ toast(R.string.toast_success)
+ adapter.updateConfigList()
+ }
+ }
+
+// val mConnection = object : ServiceConnection {
+// override fun onServiceDisconnected(name: ComponentName?) {
+// }
+//
+// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
+// }
+// }
+
+ private
+ var mMsgReceive: BroadcastReceiver? = null
+
+ private class ReceiveMessageHandler(activity: MainActivity) : BroadcastReceiver() {
+ internal var mReference: SoftReference = SoftReference(activity)
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val activity = mReference.get()
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING -> {
+ activity?.isRunning = true
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING -> {
+ activity?.isRunning = false
+ }
+ AppConfig.MSG_STATE_START_SUCCESS -> {
+ activity?.toast(R.string.toast_services_success)
+ activity?.isRunning = true
+ }
+ AppConfig.MSG_STATE_START_FAILURE -> {
+ activity?.toast(R.string.toast_services_failure)
+ activity?.isRunning = false
+ }
+ AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ activity?.isRunning = false
+ }
+ }
+ }
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ moveTaskToBack(false)
+ return true
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ fun showCircle() {
+ fabProgressCircle?.show()
+ }
+
+ fun hideCircle() {
+ try {
+ Observable.timer(300, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (fabProgressCircle.isShown) {
+ fabProgressCircle.hide()
+ }
+ }
+ } catch (e: Exception) {
+ }
+ }
+
+ override fun onBackPressed() {
+ if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
+ drawer_layout.closeDrawer(GravityCompat.START)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ // Handle navigation view item clicks here.
+ when (item.itemId) {
+ //R.id.server_profile -> activityClass = MainActivity::class.java
+ R.id.sub_setting -> {
+ startActivity()
+ }
+ R.id.settings -> {
+ startActivity("isRunning" to isRunning)
+ }
+ R.id.feedback -> {
+ Utils.openUri(this, AppConfig.v2rayNGIssues)
+ }
+ R.id.promotion -> {
+ Utils.openUri(this, AppConfig.promotionUrl)
+ }
+ R.id.donate -> {
+ startActivity()
+ }
+ R.id.logcat -> {
+ startActivity()
+ }
+ }
+ drawer_layout.closeDrawer(GravityCompat.START)
+ return true
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
new file mode 100644
index 000000000..4ae5aadd4
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
@@ -0,0 +1,268 @@
+package com.v2ray.ang.ui
+
+import android.graphics.Color
+import android.support.v7.widget.RecyclerView
+import android.text.TextUtils
+import android.view.View
+import android.view.ViewGroup
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.AngConfig
+import com.v2ray.ang.helper.ItemTouchHelperAdapter
+import com.v2ray.ang.helper.ItemTouchHelperViewHolder
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.Utils
+import kotlinx.android.synthetic.main.item_qrcode.view.*
+import kotlinx.android.synthetic.main.item_recycler_main.view.*
+import org.jetbrains.anko.*
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+import com.v2ray.ang.extension.defaultDPreference
+
+class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter()
+ , ItemTouchHelperAdapter {
+ companion object {
+ private const val VIEW_TYPE_ITEM = 1
+ private const val VIEW_TYPE_FOOTER = 2
+ }
+
+ private var mActivity: MainActivity = activity
+ private lateinit var configs: AngConfig
+ private val share_method: Array by lazy {
+ mActivity.resources.getStringArray(R.array.share_method)
+ }
+
+ var changeable: Boolean = true
+ set(value) {
+ if (field == value)
+ return
+ field = value
+ notifyDataSetChanged()
+ }
+
+ init {
+ updateConfigList()
+ }
+
+ override fun getItemCount() = configs.vmess.count() + 1
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ if (holder is MainViewHolder) {
+ val configType = configs.vmess[position].configType
+ val remarks = configs.vmess[position].remarks
+ val subid = configs.vmess[position].subid
+ val address = configs.vmess[position].address
+ val port = configs.vmess[position].port
+ val test_result = configs.vmess[position].testResult
+
+ holder.name.text = remarks
+ holder.radio.isChecked = (position == configs.index)
+ holder.itemView.backgroundColor = Color.TRANSPARENT
+ holder.test_result.text = test_result
+
+ if (TextUtils.isEmpty(subid)) {
+ holder.subid.text = ""
+ } else {
+ holder.subid.text = "S"
+ }
+
+ if (configType == AppConfig.EConfigType.Vmess) {
+ holder.type.text = "vmess"
+ holder.statistics.text = "$address : $port"
+ holder.layout_share.visibility = View.VISIBLE
+ } else if (configType == AppConfig.EConfigType.Custom) {
+ holder.type.text = mActivity.getString(R.string.server_customize_config)
+ holder.statistics.text = ""//mActivity.getString(R.string.server_customize_config)
+ holder.layout_share.visibility = View.INVISIBLE
+ } else if (configType == AppConfig.EConfigType.Shadowsocks) {
+ holder.type.text = "shadowsocks"
+ holder.statistics.text = "$address : $port"
+ holder.layout_share.visibility = View.VISIBLE
+ } else if (configType == AppConfig.EConfigType.Socks) {
+ holder.type.text = "socks"
+ holder.statistics.text = "$address : $port"
+ holder.layout_share.visibility = View.VISIBLE
+ }
+
+ holder.layout_share.setOnClickListener {
+ mActivity.selector(null, share_method.asList()) { dialogInterface, i ->
+ try {
+ when (i) {
+ 0 -> {
+ val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null)
+ iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position))
+
+ mActivity.alert {
+ customView {
+ linearLayout {
+ addView(iv)
+ }
+ }
+ }.show()
+ }
+ 1 -> {
+ if (AngConfigManager.share2Clipboard(position) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+ 2 -> {
+ if (AngConfigManager.shareFullContent2Clipboard(position) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+ else ->
+ mActivity.toast("else")
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ holder.layout_edit.setOnClickListener {
+ if (configType == AppConfig.EConfigType.Vmess) {
+ mActivity.startActivity("position" to position, "isRunning" to !changeable)
+ } else if (configType == AppConfig.EConfigType.Custom) {
+ mActivity.startActivity("position" to position, "isRunning" to !changeable)
+ } else if (configType == AppConfig.EConfigType.Shadowsocks) {
+ mActivity.startActivity("position" to position, "isRunning" to !changeable)
+ } else if (configType == AppConfig.EConfigType.Socks) {
+ mActivity.startActivity("position" to position, "isRunning" to !changeable)
+ }
+ }
+ holder.layout_remove.setOnClickListener {
+ if (configs.index != position) {
+ if (AngConfigManager.removeServer(position) == 0) {
+ notifyItemRemoved(position)
+ updateSelectedItem(position)
+ }
+ }
+ }
+
+ holder.infoContainer.setOnClickListener {
+ if (changeable) {
+ AngConfigManager.setActiveServer(position)
+ } else {
+ mActivity.showCircle()
+ Utils.stopVService(mActivity)
+ AngConfigManager.setActiveServer(position)
+ Observable.timer(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ mActivity.showCircle()
+ if (!Utils.startVService(mActivity)) {
+ mActivity.hideCircle()
+ }
+ }
+
+ }
+ notifyDataSetChanged()
+ }
+ }
+ if (holder is FooterViewHolder) {
+ //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
+ if (true) {
+ holder.layout_edit.visibility = View.INVISIBLE
+ } else {
+ holder.layout_edit.setOnClickListener {
+ Utils.openUri(mActivity, AppConfig.promotionUrl)
+ }
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ when (viewType) {
+ VIEW_TYPE_ITEM ->
+ return MainViewHolder(parent.context.layoutInflater
+ .inflate(R.layout.item_recycler_main, parent, false))
+ else ->
+ return FooterViewHolder(parent.context.layoutInflater
+ .inflate(R.layout.item_recycler_footer, parent, false))
+ }
+ }
+
+ fun updateConfigList() {
+ configs = AngConfigManager.configs
+ notifyDataSetChanged()
+ }
+
+// fun updateSelectedItem() {
+// updateSelectedItem(configs.index)
+// }
+
+ fun updateSelectedItem(pos: Int) {
+ //notifyItemChanged(pos)
+ notifyItemRangeChanged(pos, itemCount - pos)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ if (position == configs.vmess.count()) {
+ return VIEW_TYPE_FOOTER
+ } else {
+ return VIEW_TYPE_ITEM
+ }
+ }
+
+ open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+
+ class MainViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder {
+ val subid = itemView.tv_subid
+ val radio = itemView.btn_radio!!
+ val name = itemView.tv_name!!
+ val test_result = itemView.tv_test_result!!
+ val type = itemView.tv_type!!
+ val statistics = itemView.tv_statistics!!
+ val infoContainer = itemView.info_container!!
+ val layout_edit = itemView.layout_edit!!
+ val layout_share = itemView.layout_share
+ val layout_remove = itemView.layout_remove!!
+
+ override fun onItemSelected() {
+ itemView.setBackgroundColor(Color.LTGRAY)
+ }
+
+ override fun onItemClear() {
+ itemView.setBackgroundColor(0)
+ }
+ }
+
+ class FooterViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder {
+ val layout_edit = itemView.layout_edit!!
+
+ override fun onItemSelected() {
+ itemView.setBackgroundColor(Color.LTGRAY)
+ }
+
+ override fun onItemClear() {
+ itemView.setBackgroundColor(0)
+ }
+ }
+
+ override fun onItemDismiss(position: Int) {
+ if (configs.index != position) {
+// mActivity.alert(R.string.del_config_comfirm) {
+// positiveButton(android.R.string.ok) {
+ if (AngConfigManager.removeServer(position) == 0) {
+ notifyItemRemoved(position)
+ }
+// }
+// show()
+// }
+ }
+ updateSelectedItem(position)
+ }
+
+ override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
+ AngConfigManager.swapServer(fromPosition, toPosition)
+ notifyItemMoved(fromPosition, toPosition)
+ //notifyDataSetChanged()
+ updateSelectedItem(if (fromPosition < toPosition) fromPosition else toPosition)
+ return true
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
new file mode 100644
index 000000000..5b20acc5a
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
@@ -0,0 +1,279 @@
+package com.v2ray.ang.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Context
+import android.os.Bundle
+import android.support.v7.widget.RecyclerView
+import android.text.TextUtils
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import com.dinuscxj.itemdecoration.LinearDividerItemDecoration
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.defaultDPreference
+import com.v2ray.ang.util.AppManagerUtil
+import kotlinx.android.synthetic.main.activity_bypass_list.*
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import java.text.Collator
+import java.util.*
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.dto.AppInfo
+import com.v2ray.ang.extension.v2RayApplication
+import com.v2ray.ang.util.Utils
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.toast
+import org.jetbrains.anko.uiThread
+import java.net.URL
+
+class PerAppProxyActivity : BaseActivity() {
+ companion object {
+ const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
+ const val PREF_BYPASS_APPS = "pref_bypass_apps"
+ }
+
+ private var adapter: PerAppProxyAdapter? = null
+ private var appsAll: List? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_bypass_list)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ val dividerItemDecoration = LinearDividerItemDecoration(
+ this, LinearDividerItemDecoration.LINEAR_DIVIDER_VERTICAL)
+ recycler_view.addItemDecoration(dividerItemDecoration)
+
+ val blacklist = defaultDPreference.getPrefStringSet(PREF_PER_APP_PROXY_SET, null)
+
+ AppManagerUtil.rxLoadNetworkAppList(this)
+ .subscribeOn(Schedulers.io())
+ .map {
+ if (blacklist != null) {
+ it.forEach { one ->
+ if ((blacklist.contains(one.packageName))) {
+ one.isSelected = 1
+ } else {
+ one.isSelected = 0
+ }
+ }
+ val comparator = object : Comparator {
+ override fun compare(p1: AppInfo, p2: AppInfo): Int = when {
+ p1.isSelected > p2.isSelected -> -1
+ p1.isSelected == p2.isSelected -> 0
+ else -> 1
+ }
+ }
+ it.sortedWith(comparator)
+ } else {
+ val comparator = object : Comparator {
+ val collator = Collator.getInstance()
+ override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
+ }
+ it.sortedWith(comparator)
+ }
+ }
+// .map {
+// val comparator = object : Comparator {
+// val collator = Collator.getInstance()
+// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
+// }
+// it.sortedWith(comparator)
+// }
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ appsAll = it
+ adapter = PerAppProxyAdapter(this, it, blacklist)
+ recycler_view.adapter = adapter
+ pb_waiting.visibility = View.GONE
+ }
+
+ recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ var dst = 0
+ val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3
+ override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
+ dst += dy
+ if (dst > threshold) {
+ header_view.hide()
+ dst = 0
+ } else if (dst < -20) {
+ header_view.show()
+ dst = 0
+ }
+ }
+
+ var hiding = false
+ fun View.hide() {
+ val target = -height.toFloat()
+ if (hiding || translationY == target) return
+ animate()
+ .translationY(target)
+ .setInterpolator(AccelerateInterpolator(2F))
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ hiding = false
+ }
+ })
+ hiding = true
+ }
+
+ var showing = false
+ fun View.show() {
+ val target = 0f
+ if (showing || translationY == target) return
+ animate()
+ .translationY(target)
+ .setInterpolator(DecelerateInterpolator(2F))
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ showing = false
+ }
+ })
+ showing = true
+ }
+ })
+
+ switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked ->
+ defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked)
+ }
+ switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)
+
+ switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked ->
+ defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked)
+ }
+ switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, false)
+
+ et_search.setOnEditorActionListener { v, actionId, event ->
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ //hide
+ var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
+
+ val key = v.text.toString().toUpperCase()
+ val apps = ArrayList()
+ if (TextUtils.isEmpty(key)) {
+ appsAll?.forEach {
+ apps.add(it)
+ }
+ } else {
+ appsAll?.forEach {
+ if (it.appName.toUpperCase().indexOf(key) >= 0) {
+ apps.add(it)
+ }
+ }
+ }
+ adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
+ recycler_view.adapter = adapter
+ adapter?.notifyDataSetChanged()
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ adapter?.let {
+ defaultDPreference.setPrefStringSet(PREF_PER_APP_PROXY_SET, it.blacklist)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_bypass_list, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.select_all -> adapter?.let {
+ val pkgNames = it.apps.map { it.packageName }
+ if (it.blacklist.containsAll(pkgNames)) {
+ it.apps.forEach {
+ val packageName = it.packageName
+ adapter?.blacklist!!.remove(packageName)
+ }
+ } else {
+ it.apps.forEach {
+ val packageName = it.packageName
+ adapter?.blacklist!!.add(packageName)
+ }
+
+ }
+ it.notifyDataSetChanged()
+ true
+ } ?: false
+ R.id.select_proxy_app -> {
+ selectProxyApp()
+
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun selectProxyApp() {
+ toast(R.string.msg_downloading_content)
+ val url = AppConfig.androidpackagenamelistUrl
+ doAsync {
+ val content = URL(url).readText()
+ uiThread {
+ Log.d("selectProxyApp", content)
+ selectProxyApp(content)
+ toast(R.string.toast_success)
+ }
+ }
+ }
+
+ private fun selectProxyApp(content: String): Boolean {
+ try {
+ var proxyApps = content
+ if (TextUtils.isEmpty(content)) {
+ val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
+ proxyApps = assets.lines().toString()
+ }
+ if (TextUtils.isEmpty(proxyApps)) {
+ return false
+ }
+
+ adapter?.blacklist!!.clear()
+
+ if (switch_bypass_apps.isChecked) {
+ adapter?.let {
+ it.apps.forEach block@{
+ val packageName = it.packageName
+ Log.d("selectProxyApp2", packageName)
+ if (proxyApps.indexOf(packageName) < 0) {
+ adapter?.blacklist!!.add(packageName)
+ println(packageName)
+ return@block
+ }
+ }
+ it.notifyDataSetChanged()
+ }
+ } else {
+ adapter?.let {
+ it.apps.forEach block@{
+ val packageName = it.packageName
+ Log.d("selectProxyApp3", packageName)
+ if (proxyApps.indexOf(packageName) >= 0) {
+ adapter?.blacklist!!.add(packageName)
+ println(packageName)
+ return@block
+ }
+ }
+ it.notifyDataSetChanged()
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt
new file mode 100644
index 000000000..1ffd7f854
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt
@@ -0,0 +1,99 @@
+package com.v2ray.ang.ui
+
+import android.graphics.Color
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.AppInfo
+import kotlinx.android.synthetic.main.item_recycler_bypass_list.view.*
+import org.jetbrains.anko.image
+import org.jetbrains.anko.layoutInflater
+import org.jetbrains.anko.textColor
+import java.util.*
+
+class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, blacklist: MutableSet