From 4665c6731780524b85d6e41053c57ed87ea55676 Mon Sep 17 00:00:00 2001 From: venkat24 Date: Thu, 13 Jul 2017 04:46:36 +0530 Subject: [PATCH 1/4] Add Android Export Handler - exportAndroid handler which initializes new Cordova project Add a new handler, `exportAndroid`. This takes the source as a parameter, and performs the same steps that the compile route performs, and calls `compileIfNeeded`. After compilation, the `initCordovaProject` function is invoked, which merely creates a stub cordova project, by making a subprocess call to the `cordova` executable. At this commit, it does not build the project. --- codeworld-server/src/AndroidExport.hs | 37 +++++++++++++++++++++++++++ codeworld-server/src/Main.hs | 17 ++++++++++++ codeworld-server/src/Util.hs | 8 +++++- 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 codeworld-server/src/AndroidExport.hs diff --git a/codeworld-server/src/AndroidExport.hs b/codeworld-server/src/AndroidExport.hs new file mode 100644 index 000000000..61cd2a69d --- /dev/null +++ b/codeworld-server/src/AndroidExport.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE OverloadedStrings #-} + +{- + Copyright 2017 The CodeWorld Authors. All rights reserved. + + 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. +-} + +module AndroidExport where + +import System.Process +import System.Directory +import System.FilePath + +import Util + +initCordovaProject :: BuildMode -> ProgramId ->IO () +initCordovaProject mode programId = do + putStrLn $ androidRootDir mode + checkIfBuildExists <- doesDirectoryExist $ androidRootDir mode sourceBase programId + case checkIfBuildExists of + True -> do + putStrLn "Build Exists" + False -> do + createDirectory $ androidRootDir mode sourceBaseDir programId + readProcess "cordova" ["create", (androidRootDir mode sourceBase programId)] "" + putStrLn "Build Doesn't Exist" diff --git a/codeworld-server/src/Main.hs b/codeworld-server/src/Main.hs index 9f03ad557..540fbe28d 100644 --- a/codeworld-server/src/Main.hs +++ b/codeworld-server/src/Main.hs @@ -22,6 +22,7 @@ module Main where import Compile +import AndroidExport import Control.Applicative import Control.Monad import Control.Monad.Trans @@ -120,6 +121,7 @@ site clientId = ("shareContent", shareContentHandler clientId), ("moveProject", moveProjectHandler clientId), ("compile", compileHandler), + ("exportAndroid", exportAndroidHandler), ("saveXMLhash", saveXMLHashHandler), ("loadXML", loadXMLHandler), ("loadSource", loadSourceHandler), @@ -354,6 +356,21 @@ runHandler = do modifyResponse $ setContentType "text/javascript" serveFile (buildRootDir mode targetFile programId) +exportAndroidHandler :: Snap() +exportAndroidHandler = do + mode <- getBuildMode + Just source <- getParam "source" + let programId = sourceToProgramId source + deployId = sourceToDeployId source + success <- liftIO $ do + ensureProgramDir mode programId + B.writeFile (buildRootDir mode sourceFile programId) source + writeDeployLink mode deployId programId + compileIfNeeded mode programId + unless success $ modifyResponse $ setResponseCode 500 + modifyResponse $ setContentType "text/plain" + liftIO $ initCordovaProject mode programId + runMessageHandler :: Snap () runMessageHandler = do mode <- getBuildMode diff --git a/codeworld-server/src/Util.hs b/codeworld-server/src/Util.hs index 69c3a706c..fdb0b9281 100644 --- a/codeworld-server/src/Util.hs +++ b/codeworld-server/src/Util.hs @@ -41,8 +41,8 @@ import System.Posix.Files import Model -newtype BuildMode = BuildMode String deriving Eq newtype ProgramId = ProgramId { unProgramId :: Text } deriving Eq +newtype BuildMode = BuildMode String deriving Eq newtype ProjectId = ProjectId { unProjectId :: Text } deriving Eq newtype DeployId = DeployId { unDeployId :: Text } deriving Eq newtype DirId = DirId { unDirId :: Text} deriving Eq @@ -57,6 +57,9 @@ clientIdPath = "web/clientId.txt" buildRootDir :: BuildMode -> FilePath buildRootDir (BuildMode m) = "data" m "user" +androidRootDir :: BuildMode -> FilePath +androidRootDir (BuildMode m) = "data" m "android" + shareRootDir :: BuildMode -> FilePath shareRootDir (BuildMode m) = "data" m "share" @@ -66,6 +69,9 @@ projectRootDir (BuildMode m) = "data" m "projects" deployRootDir :: BuildMode -> FilePath deployRootDir (BuildMode m) = "data" m "deploy" +sourceBaseDir :: ProgramId -> FilePath +sourceBaseDir (ProgramId p) = let s = T.unpack p in take 3 s + sourceBase :: ProgramId -> FilePath sourceBase (ProgramId p) = let s = T.unpack p in take 3 s s From 8ce22848affe7084b45f2bb4cacee7e9b8649adf Mon Sep 17 00:00:00 2001 From: venkat24 Date: Sun, 3 Sep 2017 08:37:05 +0530 Subject: [PATCH 2/4] Implement android builds and include assets - Create `android-resources` directory that holds app assets - Initialize `android-template` for using as a base for app builds - Handler copies compiled `js` files into template and invokes `cordova` A template directory is initialized with `cordova create` and assets are copied in from `android-resources`. Includes main view HTML, CSS, and App Icons. The handler clones the template directory to `data//android/` and copies in the `js` files from the compilation step. `cordova build` is invoked and the generated `.apk` file is fetched as a binary blob and downloaded. --- android-resources/config.xml | 27 +++++ .../icon/android/mipmap-hdpi/codeworld.png | Bin 0 -> 3071 bytes .../icon/android/mipmap-mdpi/codeworld.png | Bin 0 -> 1768 bytes .../icon/android/mipmap-xhdpi/codeworld.png | Bin 0 -> 4153 bytes .../icon/android/mipmap-xxhdpi/codeworld.png | Bin 0 -> 7718 bytes .../icon/android/mipmap-xxxhdpi/codeworld.png | Bin 0 -> 10902 bytes android-resources/www/css/index.css | 105 ++++++++++++++++++ android-resources/www/index.html | 99 +++++++++++++++++ codeworld-server/src/AndroidExport.hs | 38 +++++-- codeworld-server/src/Main.hs | 34 +++--- codeworld-server/src/Util.hs | 14 ++- web/env.html | 1 + web/js/codeworld.js | 48 ++++++++ web/js/codeworld_shared.js | 17 +++ 14 files changed, 360 insertions(+), 23 deletions(-) create mode 100644 android-resources/config.xml create mode 100644 android-resources/res/icon/android/mipmap-hdpi/codeworld.png create mode 100644 android-resources/res/icon/android/mipmap-mdpi/codeworld.png create mode 100644 android-resources/res/icon/android/mipmap-xhdpi/codeworld.png create mode 100644 android-resources/res/icon/android/mipmap-xxhdpi/codeworld.png create mode 100644 android-resources/res/icon/android/mipmap-xxxhdpi/codeworld.png create mode 100644 android-resources/www/css/index.css create mode 100644 android-resources/www/index.html diff --git a/android-resources/config.xml b/android-resources/config.xml new file mode 100644 index 000000000..652479092 --- /dev/null +++ b/android-resources/config.xml @@ -0,0 +1,27 @@ + + + CodeWorld App + + CodeWorld App + + + CodeWorld + + + + + + + + + + + + + + + + + + + diff --git a/android-resources/res/icon/android/mipmap-hdpi/codeworld.png b/android-resources/res/icon/android/mipmap-hdpi/codeworld.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5398238c373b2dde4aabd0c585b909e29fd3b8 GIT binary patch literal 3071 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 KMNUMnLSTYAa6aAu literal 0 HcmV?d00001 diff --git a/android-resources/res/icon/android/mipmap-xhdpi/codeworld.png b/android-resources/res/icon/android/mipmap-xhdpi/codeworld.png new file mode 100644 index 0000000000000000000000000000000000000000..4ce28182a47f60c405db611724cfd706ae05291f GIT binary patch literal 4153 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 D5uB>< literal 0 HcmV?d00001 diff --git a/android-resources/res/icon/android/mipmap-xxhdpi/codeworld.png b/android-resources/res/icon/android/mipmap-xxhdpi/codeworld.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ef186b70e77cd18c329bc5f2283f207761ac09 GIT binary patch literal 7718 zcmV+>9@*iEP)X%A+*|ibuwQlyB+vp<0>|2i+ARPZARB^&TEGSwgIvkNT>yDfD&px`(PU& zn}LK{zy>Ix=CcpB0kRoLs0D0*5^6sCU>hKtfrMJX1}LHCvkz`9fLiOkc8?|20!m_s zpL}#OJDSg>{Y-Y9?eke{0ftBWY3*#0qmluT2n_v9Z%5>lE|ZLFB=jZI{9qB7FaY{p z{98h*lxXKNFw&4AL!7f`&(13;DH+(KM~|slSy_`MNg5zYQf``eV7q>OMNw*HSuPC* zgCA5?RlT!ux>I;1wvJ$v?yEi5d&JtHII5=oMB+O^f} zi|e+4qA2yjU~t93g9o24E-qdNfFRPUlBw291Rw&3-mdEE>Z`hU@BXMHN%^+PGS&X= zQCLwFrLwB3>i&X)f;Rw!A|Q#T&2YV}0Mz2=j2=DO{l*(_oL5|2{E8$=Jq=&gF4*oZ zpeV|r($doDzxvg$R&Cm}DPUEJ%mO4j$(^pYOA! zOPBuZEw|iK5m6#CXY@AwvI>w3fP2lFH51M{>#X-ANpc&$qFu1Vw1A>0fpzQFT|QyL zgf#$2g~*(lGG%4~YVmsjWL8#IKGCaJuiH9Ii`W%0-2zorRWIh}=idjwXGMjq0OTa~ z0g&bQ`!~DY?!KmLZ6DBKS|AVz?Du#)qX0BTREQ%pLv=I*5GM##^93L~91icb&6lBe z?y!Z(j5!>RegGN-Ge!^@dRYM|gUlBJq$oBi(+`3xa+5J2C4 z`z>b9oQdzh{~n<|#89YE3G*|~Ek2%arlw&>o#RysYJ5+ri2K7ION*|KFAHf)$`&e~gv z(h8tx@Q}lN&pr2G^XAQ_;At!M(N=2s@ZngvaG?rOdn-}e02B?LZ@&2kciwrY8f~?P zVrb8qi28`j;9W-bVUVZgd3>h+{ z-OO3a0~8G&@^$XN|Nb_Er!{~h$9mMLQF!K=XK?13XSRzHr8GdXqAfC8+qZ8wZMND< zmCOLwyLWFaU%nitoN`K=LzMCWaiWm(JQQu+b=O_Gd7gL)J*)7Tfx&9Vn;@cL7s?Z{ zV@(knzgjC}ow5LFGt8VMB(t??)25iYws_-Mr=4~h1`HT*!gPyxaK&<_>3XDwd-m*U zDQ4E$rUOe#N>p>k(U~}QW&>m*y{ZQf`8;H{NP*%35-S=TJa{nfzWZ*F1Iy!5RNia{ ziRP8y{^5ro@aUtDst}olhs#qP`uFdzMrV|CBqh?0otbDxLq$_gfwUbX%QE)v-HRDB zW~dHxBJfbiJZsi0j2$}`G|*|KbHwY5=9TJ6DTO(6=3w{k-6z%4XuZj7O_?$UOO`BA zr-JAWz#1aMV4;)(NDB|$*)*T$cfb3cdRNAJ#p@u7a&Eu0gM|ruBB6`v&2Zp?A#oNoa!XP zL-nP(N2HoGmE?&ho=^vNW6gx5a}gjDCr(u1IrY?2RfQzT8FU+9Lcrr6HRe%VbWOWR6z7}`_h~7<2P5AQ5FEMZ4JpB0MkGd^D z$5VYKO`4>pCdjcTzlZ9{b!}}&I;9kd?`&3|#>PfWo;+E%17i6+)LWEKA%}V7$dT$D z&8Cas(W;LTzekS%5g-DIz!6o5ZUAzu$$Zk3TCGn+%5upim#Fn5pNH$ob=3~ETCHcS z`!^*3(u=&PBWNQ0#TQ?E5mf#tq%ds8M3Y7k(R5pK z=*ib1vqcU*gDsk^L>znr?&b+aQc{8V&LtB@Nj1I1oSqWU-v}T|I1)7MQ^azdjY1R6 z2NIQ$eIgMet#3q0QF4?E3JO#uZ z1W0_mhks_rJ`vMuRDIGGpjgf{0kmz~wx|GEiM+ICiq4@|2R-MS%Zk}+5fbeq&uwA0 zM8ML{gvE=*2p|fTqxnB(0TLBOghBi{7z(2)5JGO28?-Et5%thcB{4gkVn$_Rl}#&v z;=#jF7y~2@oSGd^iUv+oz>gz!hp=|nHf-4ScN}lnhGT&+d_e_=YEOp~-H_`TgQ??( z;eruGDCn94kDFEo{0S@(77;ch;3W$jK2KTz6m2X?Y)tNOio)~&B0tEe`e^+a`dy|; zW0lo-@0-uC=ErZ4EB_N-R|NuL7Y_S+!!Ntw&8Wh@!^2U!|15+99`x&ygMTS0#&wg1 zVQ}BBF&9^84|vkT2hwJu_3l7XnGyv^7am#0s$J#y>nDrRS^fY{97Beq4v?Bq8|;G0 zrv8u=KWZ8a&@1m7Y}k4O0zNl_O)fY?3I^rpVa`qGVba(Ea0!nzx3$Bp*LEqXKw=^> z9za^NmIy$45tJ_&!t$-<_|vLIn3S^^vLd5V@nE-a1bSrb!bcS|ke|67d9E^SuQ(sM zp8cpkJP?_#8q^=_j$l&;5D1}1jt76e_cB~iGQeJ3A=zsra)hFqipZQAf{5}!u>gt| z#?u6MaRi>nzv7uNu^!DCWEm^AmtpQFAET$V23b-KPI0Y;3>md@Cpe@={7^p*Az4CU z?nc!5yJA=6IjE>Q10kOi2!?>5tO7)z{rVrlGq>H03rbEs>G2)ggeBnJSpg`TLrqbb z6@bJ#aNcJBkvd%a=9k!0TMeHp7oA*(aaqP=$dqajQWSM}lH;N7aH!@ifCIIsV^7sM zRM!kd*ym`5haidol-+F)MvVyLh3WHAl;8CPh~mMWK=Z~tH0?}SyhtojfH-4J3i#Zo z8}R7*eF!+Sfea@GWvs<7+>2G^2`SCn+o*2^IP5FLsvWZsR#k_f37-Uz(@}@w$**DB zfZ3RN`E~HPo98@pgxDSsr42ywz~S4>3P4)ZMP~G9T^%OB@GO4x4+C6I!0A8_R~atK zei|O>h`Jg#1LT+8`0C(Q*n4waO;w}_~+hXP#iA6N$F(=6bZ<4R$*Y)XBd>T3LhW% z1CIN8!WFJp!SOYDA%$dw0}2oz|EKw_1VV614agt00^UA5;P+)=Qr3f*cgt^1nCNEZ zIkiMVN_&9x!g#X)vD3_RcPfum;779;Go7#%s+Nrx1QTScd8U`48-<$wOGmXoiPCqU|qe+l{aS7kuzYwa9i= zpjXbf*jYXaL0=}EVLw8C2b2JykTV?tD6i;i^c?vSWM@$IoieM(;geba3vX8Q(;|8c zZ(3|lQ-`Ic0>yHu$%(cCkT{ig`@T}lSoSXV9e1HV)SU*g)VD*Dp(qj*b+>f|N|-{i zFmgTR=$ZR1I(OO$Z*Do70@+n#lnY995 zZ>BnXQg0@rDH1FePHBL&lj@B`V#xq#SNUE%viu=@Svm?0!LA4+LscJ_!w(5z1m!F! zv~2Z6fya3S<3>M$uoPCq`asBqBUOV?{`Cb2)_H+Y2;QE10LhPx!cydS&&A4_ z?;tCy86a`eTU&&8z}(0#Z~U0_f1;qquR&t?1lyEfmFp-Fqfu z|GtT8ot;tuVc7|%!-r77qpFc2hmqTP5Bdy$AFk}9YNSQ*5J+|Vic$UXMNoVWNRCE~ znNpAEX554Ry?$DS!&aaKNU{P@tT3KdF*6&+ivZC$7%kJX`2FQrSu-0>_fh!$dDyo3 zIyBXFgVPy6DCmYNHIc2Qn*E^=dY=9%x(;5e!V?NR&{$o7>Q$2=AInlR)1Hhl?)c3p z{C3*K$jO=|eiwDW=DInxw{Spi6V7?8{uz4snGh=*VMAC&r6qHCXT zP`33F94a3UZ>LH$9_yu=F)QLvPiBxa9eHPXkD~Cx|Av%RhohUvqV~HpAve04lZl})y5)KB*C#K< z=#hg@cy6EbKz1hD;@uF>gqa;owE~d1E)fI7&!+v~>aCdj+PA2w4Md=cJl3P`p!pL( z?#`9SpY#@vZykZ-n?^&SgpeGDBOFHm!aO|iZ{uyH>$Fa2skjjfPy8slwxS zN*F(~05@DY923S4R{b8)KND9=YwKsiA_>dXSe=7aNSxBjVSFrr^afL{0>luBbzqR1 zntUkRe-JCz@4@RImEmA@17ylVtN9>OAsVpv!;x8!u-^rzrwRQAxNyzHi8yEU87L|$ zKvrgR)>*p(nf9qvi$ZScu(Vd7XkolrfJ7CdbLuS=#S{Gr>Vp-P*u1?O@2}m3z@QHi z3dJXgCtd>^JQ?Es4BOj8s_D}fma z-y$yKA`YIm zm+zDE0I>rrE^!wR9v%y`0+1MyX(3{Gv`e*cIX1uI6Q^vJw9B_>;b~9xNogO50iv~& zjX2R_7@sIWJiH+~)*?I{@{7?U{mcMyGLWZeXg}>ucYz#5QOYcNZ8`_(h4E$q5~DDy z0EsH32N8$*;#xmkcflmIY@wjdE^EQjF0hcS_B3;zl=Xqk0HhsEC4e~6u`=6FJfAq- zL;JJ1;+UvFqFK`VI1C>xa~CloEy~r_Ph_OM35ynmi2!MR4(_ygcc~skqAKzKWBFyG zPekQtt>|gfrm4#h)AGLEx^+8g!Z6+rRe;Z7VeVgwc~ zTBI(gE3OZnNW`PPwpO9EDFM;kk@DPlO9KAnn#k@fwXf@7#0GRkvNDeMpnJ$fh2|dghK3k_QePz!g_qp}Q?Psm^V;-G+xB zei(UqdF=vGN&>`N1<*p{wAqT$Fkaia6DCYhcg&%^!Wg1tMm~BfB<7HKm&I6HbdnO? zaKjClJ$p8K^yty%5Ty)2972nmmC($ zH{X2o2`Xd+8m&`%{`u$Q@y8!mqqElBY=z7g?NLS>&P97gt3R39n{K*EHM?G~x7D}1 zOsPB&n=tLvQi6y!DWRQhXq4T|OWRQtojw2j^Cy@yvtfO#XsfieRP|>FBHq?1R(P-X zT}UCPPMwNZUU@~`Q=_#9TT?=T7#zhN9i&tYj zmq^a^Q%^l*c?(Q#SlXYI_7gMeq{cFt9Imfky*godh71{kKmPHL_~kEu*)kQx-tyAm z?ZBNRhQU0Ei=UF9N$#{482K`yIkVyu8L5_$^CqJVor6O7=bn2G=bUqnnpm_q-8Irf znlfy{#E3=@B2psSR-4S3kq*@QEk>$lI zpQ#YhR+3gs6(3^+v}Bg))vFg4ELecCW5=ovvp9c=m8Z2G=tv6V=l@ub9#`=oVv|PZ zjHXD;pFdw!A|st_1{yPEja-|`Pzr&zC1;;UYqr{I%kPv1h|L9qBrd_C%@dIlQFKOm zBBLYsMmoDS;Gt-XGR*{!s62W*OY`CJz)%g+!3zLGXiLvwZmrFMqhXcO0Etd(JS8HNM$c+dLXYMFX@@$0-4s8d0#m^Pz8QaSQ#2%aaOe6qtS4}bTRH(`36tW_fY#Sog*QT%t` zeOH}Yap=$?PzoTHuVuvjGy)L$F$B)IapQ31l~<}WBFSut&&7+Ox^|z8*2wpytq-LA z;$osv%vft7;`v1M{5nnWI(qb|3XphwvVhTUoJ(-fq_E=RV)dyDacDvu<7#V%S?~L% zl>)`{iL_a39tIJgC^ZX^5y-?ao9FM)C+Bk*8u1ymjsQ({S; zW&HT@4+{XPd&IPs0@1$Lq(Ef0vR{7r<;hoFb=8YmSy}Wq`}H$xfrf^L;L@c_r_Y!% z;}ZZ40DKV>rf%qIZ`AQs<5!I z>(8a}?0N#_<>fWYmMyz$_UzeT_!W|K3$Xwjmn zlO|2NtEi}`o7?R+drl+uw96!^1pDinbz`<{F588db2)ak{=#bNaOY`T?|0?45km;iET~r@K{*M}2nyf9GF@lIp8L1FKl;w82y(34C?0L;K z*Nh)Lc<{uoUAvy<@puYcE|=HjHSFR^Y=KZH)adv7D~=pFvSa7Yooilu?X`7Vwrr^m z1Of~W!9(gp(U&Rpu>ueu!#N>_h@5JA6HxR?f2TK_yd*Y>eJT?zKuSVya*7EKQW=7U z-V|}so8jTm-LU#30ucK{6p;}q6vmTD6W{bd5g@Z_XQG|#qBW*-1zTAjF>@dv_)#GU8}8;*pwQzx7u5&J%j#g)-GyAjZ#}nZK74QD2m#9 zQ?o`8@udIj|K@r32$_%)c)g3E}tY ztQ#o+zyWn7dHn#(-B4)yjq$0|(f&WTiNvCacyr1q+0li(E;_L>P7o@#Ju~rpICYtz zu$v;;We3o`S4xL^MIg#*c?hwcn2;_g6YXxC{^WB6=vmgY_h%PFW^;|rFFtIWwgkP; zX_hV3KJ%F?Z#k>l2yAR^hFd)ayBmY6C1?GneE2|WU^Q61GgJ*+ztt;zTNnx#9e&RR zhYr7I>vU`5M0F!`GEkhzoG{$S!&HEd$lVm23btV65cynePf-LP}<dJtGh^d`#L?H86KQl#PEOdCva(Hb0M7))q9!wLK92-ken;0M ze7*luc*iEgnlx~((e_9v;`syf{ga?z(1DjBYG3qoh;}Q)dgI$@e(@D(Rhd0SP$)CL z@85w>e2mmhN6qZ+Q5Wzz5ZC`I%-DNN3Cg;zA?Zv2;tJgw^@&W3A7dgR=0X#PcRxUA*DLYWo(D3#J>l${{Zj2r+D!=>0U zh7`Lg$|Vp1bTQ3$Wj4f{_Yq#Xn9D%VxInHGN&%A8Z3W*^tM;NH?iwPg`M;D#nwqofqZ>Qu-w z`^Td`0b^{)w*`j&5xm4d%nNMe08`8m~`!>PS8^|Ku}P-v+gYS;fsR} z#5J?D-*Ak%Yk?HFZM2t^>efp}s}Nd?79VF7@cccI$YP!1L@QG|j8}?^a2Y9HjkA#T za>wdA4oc7MG(Shy<}k}y5CyWNdEZs!FcT z$>}!{SI2tCTcbQ$fXYivjtEYaB1D7;zkv>CP2+y@IG|VG)w7gLUtj+v4FU%%&GR!= zNkh_MK;SIU^`{}Zc%FPUnNOo+IIRdN-L%)+7&acdfgCAe4`|lSf+5X1NGhOChoQux z1J{{l&R5f3`F{kTtp|f+F+MbRe;YNoc!!12G9z5MFc~oWRo(pu+Pl&J&^Poc_!>%suZ~T*i5oFR2 z-^pCBBq2Op%#&zq6A~LCO@p%>($Z$W(Z<%fP{PV#*g*p*^eiiRKps%dy!gYPH;xD2U|=Ypde;$p`MzD66Kb6bIAE@4 zUkrViV&wtsNqn^4h?dvPt;915doVDS+B&}FL@9%7^DR1}Gehuvdhg5f%vG$PHHzu| zjJ>ZiA@a!Mm0fIg${t~RMC5@})CuPL3x%-ta(ApVDc-JYXY8C{S>N7b(nCl5#@_81 z9q3+pULV7Wig-XDflk!LNCzIR+S1{)Xi+v`@`P_;kxKM#ca#GoizT*b(#fw=6!!LImS6X(eJ#ZV3H~wm#gHtciHXVJ1=%t8%jcNY-F~IS45&|ECxZ2V-Zg3gb zb(a|BuA@AGX`|&>2!2u0+uM6A0oHCdLzM~A;6MX>=Sm-Y1#${JHZ-}VOTnZm zCMhBDF4*5xZ4zrw!byos-@SW~`R2{Hxkx2QZPEF+TduTuB?^Jye_Q7^+oYS<-RZsd zSaGMfF%N7BzW&B&MY&B;;HM6DTH)CxgSSAUsad}8mFC*Sgne2pr5K6SJFYG()0tx> zfu#Vk3`O@VleIu{mtRS{*g;ub1z8wgk@Yzau*^xC4u(Gxt%jNAw?%CdJ-!W&ahAV8 zi$AllU@0#zr&}H*$L_c62$=`(Ht-4y8+Bo3!6adF$gx{W4!v|tCGyPJ1~ugU$+keT zz0T{fxf^_~av=O#rj=ndLZmU8M~VZrIscsru$JzwZ=y)=yLtvGuty$S8e*=a5jYTs zYNGxpS#}X7&}7zIeHS%BevANHKZ)*)iH;`vwxpZR+`Ijb%ROG*;ghnJMCY(<_}A&L zt#YxEmv$?+D;RW1x(#Djc*W__QwJrQJ2F^$=kTJ- zw-2jT!3<>dcLV5Z|G-X|gv~w+gi*9mH^laNjVLqTm6B?FYp^1CXB78=v+?ccUV+3v z(DhKGb(+7~$*hlPQJO85NN3LHEkeyie>aAbqR@eP{jB_I@ zfMakWdodGIiv&Ib;zMs58B|QI1T+>4w7BG0`dlB^If{?-3;(qfVUH-gk!4U&r^uP3 zr!8yX8;t!!t^LyH=#p5O-2zUcPXZRbtUJi?)S9w-2my zs>F#h)M_g+C#o9b)g!af#GJjZwZ)y|?(??wY=kq;*iXC>`%5{Ho&r$=fsQL7w5XnG zvE3mRC6{K>4`UaxvkyjYF5e{52JrqQ;#GeP9z|L801A{CgnlanJSAQUQm_PRBX&~X z^<3@X@{U$wuf6_;G0**5k-6>WJ~)(O*aL)6VpuC#z)YY7GUV5IW)RlWiuHk(%e9gT zn;_%e6b(+7&^iHxog(gMBcBCESP-SifFZ;FkxT$qFNV!L2a@O^!$co5E^XiAgMLT& zha0z_|1l^RGjYJ-lATQm2*GBcJro&`@jwWfx>-bmH0R*d=(^|U z^qE)F7OD8FTT2wA!;Ab1yYKF;2Hm)^-k=zJ$%s9{q=|%BYug3Nx=0)Y?&)*tJdLU` zW`u{4AgLt}G0eXJF@dm@FvM|J^`$L0>MCd@D5T}WKID2d$Y;PT5!p?YG?OH8DXif2 zQ_H7sAttJWymCR5snz9D0ebJbGJBc8kKXZ(yqCeMdH?y9{}z&#gRbaI-_3m0{&RcBXmdhcqp87_tR&Np?0<~uG>ObS`|oy znqFB{c? zaJ z^RLb1E~ur_WnyBM73zXXb&*_1y<%X7i09plpDu3BGE@uuED`x!=t9R2w%gx#y`sEXL|Z zE$!FRAqCowUeJWsB*RwJQnU7Qz}e%MpqKit1!W5pQj`iQ{yg5Ymnmz`7|?=O*;%Gd zc&L=y#a3HZPg0AV=B4WQg2g5|4X>~%ckBWpQNhqHzlI@YRD$S+p5%Rs;!NU?%V6dR zbcuXkEnvD>x-c0t$d5nd%NYONwL|rFpATsBxU+8>Pdk;Q422OPLOm%NE4pfzk2t{A zLreLSPo;s&otDD7cPy#^yZj~@RDt$Jx~EDj{qMz^B78)bW8Mv`Azl`TptmFVR3JCK z{v*x*BD(Ju8RYr62F+51;s%Hn9QJ3|?^(e7O4)I>3MWa}uhVjVCB3_RT}0L= z`TVu-)-y#r5h|zx7!eE_sgz9ixV1{@d`m;$e;cp2zBqk%qnoF@*u^c)qF^VF2WlM! z7)A`43;Y0c?SKyffMGlYuixpad_z{W5MD}7q{yG&Y-;X|GsIXyCrE$}BZM_k zHNFo~E57;;mN{B4$~v_*66Y-u-L0YCjwlprEw;8^(#*|mBqL|P2i;a&!xpYfj$J+~ z2p(aaY;q)|o(jYLACct}F)ra3$Ln_#l9u#n$%DKOqVKi6n7F;UP^vLqdFk9^ss=WO zqvk*II$VNN8vxS|M7&-$SdKDy&$7~;iSpapLmN;G@G=~@IWPD?&i`Q$qYx7Ly{q%H zO!V^y)1zjabHrA9w2={ijHmSma7q?;xiUyj93%=xr(PtR)fxDrvrjWuehUpPcjO4^ zMF)Qhqz-ET=75=^vnS+`Nk@uDfjKarisw$ z8d{Ec4$1qO8~>%+b<8-Dc(ivsp?mgFm|a@>(UO%pz@Opg2L0x7!TSG|yg-~+u;%AF z5unv-%2EE z7D*5jbAj%#qiC@vx5l90gTU}xrAqrWnc)$a>jJ8fsRuXy1VC{2XpK3Tp@k5gv0SHg zvbi5}^74B^0kcnd-8)(DetWHACFleakl8CJ#e5gY%IFTczS3O51FVG; zy76W-?hV<0pOR4MN&sXw7^||F`0_b^iiT*CDn6G z;Jt$=s`I`I)Bhr~U(~L4tV7hI-IM7=73nbCpaG{4-2<&=LNR8*EmHBLaJg7D1^L+6twFBouLbgLT^ z1=St5XRF~ye_eDJbiUia%;Jg(Ti@7MT6kgUCaJH`K=Oy>6WKc*hR=g-EGgVI+vLr& zVfCFWzA~d5ER?dBvqfadKSM#O03SQbl3)UbUE`4+g%?wqK6#uR&_xjt-8a(jrohdPI>A zZ;6*#r=urAzlP-|2MSy>Q?rhn+k?05aYOn9hEF;jipu`BU{MdlAkMb# zs$&D^p1j+4`kmmpGoD;3R({Yu7B909HfygnS9MgkmZ=J)OGOl=`&`qcV0(MJSo%x- z1!(g5rjf%HDO!9ETGL2Ne#OUHY3?PxP2(3qK^xF62O$bn^^N=BTUurmi;043F(2Mw z7b=-LaJI7GI~8iV(-q34`l#m%ZF-(@l0VPfYfIV02{*IC*B!eTds=xsf+(W@7gj%e zs0@UiGImC=m_$xbo8s^%owk{tg7<>)q(c5=AS-BxMNo|kv3WHtJ!XRpl&&TqSbkyT z;=o(uBi(}cR*}(F+DqQwxc?_#>|QpBefyWy>u8QFk<$=!|MBx}66O%$#^0zWpBz@c zenYI&`6WxVN#%#z0h-2P7voIo%>ruUDc={=0W+=}?HL%l6 zb%J+9C3pb}7P+?KhcQ&4hBm&=P;_*0cp7ljl##rSepC)}FD)-AGm#=oNjBm_)+;95 zW_@=`#rt6CXktlls+9H7!S2JdOZ241SW< zzDiH11^l2G|CbNdfhuFv!l?g3M`+gBxv9+GdG0Z(iz02#!fiaq2OX7{S+`fe%O?Ds zXp1AVLdPreYm>4s@#68}GOoWcUunfUh9;cKXsDj4ngWT%4zCo9xlo4x zf(%#{fZ)k$Gz=kS-d$`8n&Z~YYh~syKf9~B5@0h28d(~tqmInF-amrq_@M-=sjzp3XZZV4fGtv*ARDii<0I}TN57bdg z)i7;v=R(ciTC3X=s(JG^Dl@;auWV#qh6T;HSAW*1cB6^eqohk|zugQw-ZzRcBc)V6 ztIpH4v}E1iwKwbds6Bt8KCwj8_JaMN$|gMelFEg$6u@}y{+NY_*}7h6nYI5O8?)ZW zlMGx9d4x23hE}y7m20!gsl2a#pY@uip0lQGBap;QE?r-e0=hmLKfMA zTYa8KRgE)G9#NyxAj2`*slE{dcCX6Vtmj&MKR^0{z4s~DdEA6DVe0-qE^*(EmVh|o ziAbrxFp}McopLObE9$a8|4NnAAN8lw?Z)bQQDNbol9H0ct=G5?hh={+a=Vs)X*0JC z@czjOAtXi(mCEPAQ5iQEqty;LVV$2W?d}crb=g4WbnF-3F9cY-Mp$|>2B-sn-xkyl zg12Zf!>C3Ox9x%RtCm?5W5)dgPZEv_3he?Gxql#E_rOWZGt=LX zx|O7+!Gt8b5inf{EM(ff>0rt8SyP=EavRdsM!Y#%nzUQ`<vQ{FIf=eE;DC z^k0RF=R(-L3DzEbmPilRQ-ebnVW0s>uN~-^@FuxZu1b&;CWk6YPrmJ)9j{&%f9s#Q z54VT%9{GOxGxce_A+39fX6p{U8kkcZ5W_fP?j|D+yXl+uT6Pk%rTR&;{gwgRZkzUc zwRfdg^u&`s37GJWJpW}vwb;pT=Esl6A;%-ClKei_114YuSv~Y6l&6)u+v8%@L1a?Uv&))U)lF(MD!&?w{+Ekct|Zpi0E+c zefv0a&UR68En^VtyV&h2M?oy|ZHaC))$Lcp9a#2v6JymD(r%Nrp#T2+?@;z-14&Pb zqO}0jIsz~-Ffg23b65*3poO!B{P=Il zs48uZ6sejLs0I=aT=6GX7XK7^e3BQi9Q*lsS!pN~nSlH~1)^m}B*Sv z*(`LU?OU)L8sIEWAZ4+tasCStjSMVLGk?+E$6V$O>X5(;7Pl*E42f*Tavo1|MKMoA z^8p##^%P;hemtR5JgH9ex(-Afn4j?Mx$dImkl()pyo)EdTSAwl5pIQa)O8l#% zyQrEJQpPbqaeJRIdAFxk*6uLuYRAfU>la7G0GHy$>F$i}c)0m(^RnWQ|-#R9pBLgo$F0otuX=uTivDpE0LXR!^#ZJZ#}e?F^Cmg7>>a`7 z+CRgaBSm`Dbb<$?2PD66=x}_q?#cLyVV}1Bcd$-eJpq#PYQTt!KU_XCP@_Yo6_b%k z+Q!kM@H`XN6V(<4GIAzF(!w0WB6PF96BdJN1;j9Zk0`?qw5M3JU+4LtXRUP?Sa0yY zOZ4XQ+Un|JNv7qOGTox=UG^=X4fKZ}z2xd(GKjV56LXZIc|e>RGGZskv%}eR-Vjja z4A}lqI>x%9e@qo)S1NNIdN$`MJ!5-=O{a8{K*=gjxkPr+)cn2b;7u4xn>elx_(ay~ zzi${V!IVNUU&`<)*@^8y!yjgXIhmQ!j`@KkM&@4sk%xr^8_O{y3&9pikQ7GiK*5u$ zqnpZ8Zl>S3O_GP=M5sPA;NYHAF_4>yA zMYp(FI=3zMr8|K{8{nWL=sW2@4mj{uOpx@I7HGi*nZ12Pi5u9x%c$nbs^ltEz-0jk zMFbfllOO}Ph9mVHX76%XO(oKPD*t9ij=S*{w{6t<)G^+9FfvUPlTw-|$$%myW#f(0 zNI0mdsoP(r*0si+&Ul*j)Um*3Kp)3nRQ`_tkv61FbS2chN;kf{BsI9WAz!L!g{lW` z>8!D}Y?3>hfPdR+{zeh^IOr7{g1B2;46__gEDe8r`Y68RPM~7ulnOO zIYBy za;~9F!5h{!f2U`BdLJReow)n+0V}T^{A{-rUFGMW`ep?6gl8dRElP)_Tg6EjmF zB7DE>)T}@EX4S+Q05jtkem3NWQa?_I?egP=JDT3EZ>VF0^D8EHT_ZUty>=(Em40Tcy>8EZGKiD;9 zu1nNLT-87N%rn|Ws6Zq&;+R;98ma|Tm8yO~aD4V-cwotq5jcw}3d z0-w}I`5dLVtwTwaB27LyJe#g}-Zr=WBrFa0Q`i~(#qGZkDn&wlhiQM+hM6_&jjQMo zI?;nh*mVp1RykkgAqSn4yK!%+y7NMv`^@KO^KF5~bvDGO`wKLWJVl#2u7 zw1|@of9+^}M|MIeJ}DnNh%ui1AUg8%^%fFf9b*U8M_PE9Yqvz9?F68UaF9}`MFKP{ zV`8Q~MHnpv8D@{z+;b9iO@$H&iW-Q3urQc`)`xCk5t|nWwuw)T+h1lffCiIhCQR2J z(o1wwMw$T@>{$&0k-t5^+o4k3GZ?bYQO5hyq-Ay$!-X3fjJ)5Lf0HK$#$(brS?a?F z1a(T-9sZm24B~ILkDK|5II8DCa?(xZ#zwCFCyg}?wt$7 zij}F-;`ia~>Lgxs-L1{R*&lXmW5dJ4I{NxPrU}3k0KBCH_}VuSVi$aATU(o6PZ8ob zAh?@PeQQCjBj}~bBjLQHiLo)as>;f?m`}i#D$1b3@T%Q&p(AS4B-w8#1B#LQNa;6| zPrXrP+Th~c8FuACgq%j37u3U1q(HMwIdPV3@aZNkhl=<@3Oh^j#&ecmZz$jXS!R+g zJev>RAJHvRY3GK)XQxF0S~$N7sx5f0d1FkKE!2mVswY*L4^+ze{9sXjHvIakGvs94 zD14C&1{rpN6nq}ZeW(JM-ZiCq*@-*q56^w^^Wg?-|8(C6QvMoS0no?K1dKn`DcpAO zMYSnx?C8kGL!Q4eifn) z&3Xg)2DwjtB<{}?4qtk8+arK){p{})y7(%bhYZsyZ$}&hxHU4xk$UrJ=gZg;LV+Zo z{rUES;^J<%OZ#R?0nqf+i~bD3=#3%tbtx%Z$a8gpweJ}14n)nBdn@8Q9f3U>iwvkC z`E*ZzGE2Hm9-a}}Cz`4)TJtsf4@Zvf)Du1P^ZVT5@H(Wy0y@lVmTmet2QMKhS+4h_ z(S7E0?e7o*5s1f~&)6az`($~kDosE%dpazke|C0(UC=eeG-AR4%gEg{W(27y)8ii8(HGczobM3wWssL z)7CdE(8cwg^bDv*uoAcT@p%BDNv)Gl$H9Ix?HjGuc**N8yc{`$B-JB4{j0+~R3043 z^pjILB{Ixd^~+CPo*AT=!(WeogAXtzuA&;qYFIxd?WRd~`jytAqkSNq-D+C;o6lSz zK`^YA8ASF6mlNj5fpAa4-*DZAfR5ELzltc2aXcIV5uzem@1*WTX;od28^ns!OrtMj z25qKchLGCmfsKv1e8N+XdXUoCyq_~M$IP;_gDzj2P_iDU0)nr)3_DZuDU^_m3vKb; z=u97uT#`H5;CZfP=-n2%r + + + + CodeWorld + + + + + + + + + + diff --git a/codeworld-server/src/AndroidExport.hs b/codeworld-server/src/AndroidExport.hs index 61cd2a69d..a764453e1 100644 --- a/codeworld-server/src/AndroidExport.hs +++ b/codeworld-server/src/AndroidExport.hs @@ -24,14 +24,36 @@ import System.FilePath import Util -initCordovaProject :: BuildMode -> ProgramId ->IO () +buildAndroid :: BuildMode -> ProgramId -> IO() +buildAndroid mode programId = do + initCordovaProject mode programId + copySource mode programId + buildApk mode programId + return () + + +initCordovaProject :: BuildMode -> ProgramId -> IO () initCordovaProject mode programId = do - putStrLn $ androidRootDir mode - checkIfBuildExists <- doesDirectoryExist $ androidRootDir mode sourceBase programId + let buildDir = androidBuildDir mode programId + checkIfBuildExists <- doesDirectoryExist buildDir case checkIfBuildExists of - True -> do - putStrLn "Build Exists" + True -> return () False -> do - createDirectory $ androidRootDir mode sourceBaseDir programId - readProcess "cordova" ["create", (androidRootDir mode sourceBase programId)] "" - putStrLn "Build Doesn't Exist" + checkIfParentExists <- doesDirectoryExist $ androidRootDir mode sourceParent programId + case checkIfParentExists of + True -> return () + False -> do + createDirectory $ androidRootDir mode sourceParent programId + copyDirIfExists "android-template" (androidRootDir mode sourceBase programId) + return () + +copySource :: BuildMode -> ProgramId -> IO () +copySource mode programId = do + copyFile (buildRootDir mode targetFile programId) (androidBuildDir mode programId "www" "js" "runjs.js") + +buildApk :: BuildMode -> ProgramId -> IO () +buildApk mode programId = do + currwd <- getCurrentDirectory + setCurrentDirectory $ androidBuildDir mode programId + readProcess "cordova" ["build", "android"] "" + setCurrentDirectory currwd diff --git a/codeworld-server/src/Main.hs b/codeworld-server/src/Main.hs index 540fbe28d..bb68a3197 100644 --- a/codeworld-server/src/Main.hs +++ b/codeworld-server/src/Main.hs @@ -122,6 +122,7 @@ site clientId = ("moveProject", moveProjectHandler clientId), ("compile", compileHandler), ("exportAndroid", exportAndroidHandler), + ("getAndroid", getAndroidHandler), ("saveXMLhash", saveXMLHashHandler), ("loadXML", loadXMLHandler), ("loadSource", loadSourceHandler), @@ -321,7 +322,7 @@ compileHandler = do writeDeployLink mode deployId programId compileIfNeeded mode programId unless success $ modifyResponse $ setResponseCode 500 - modifyResponse $ setContentType "text/plain" + modifyResponse $ setContentType "application/json" let result = CompileResult (unProgramId programId) (unDeployId deployId) writeLBS (encode result) @@ -358,18 +359,25 @@ runHandler = do exportAndroidHandler :: Snap() exportAndroidHandler = do - mode <- getBuildMode - Just source <- getParam "source" - let programId = sourceToProgramId source - deployId = sourceToDeployId source - success <- liftIO $ do - ensureProgramDir mode programId - B.writeFile (buildRootDir mode sourceFile programId) source - writeDeployLink mode deployId programId - compileIfNeeded mode programId - unless success $ modifyResponse $ setResponseCode 500 - modifyResponse $ setContentType "text/plain" - liftIO $ initCordovaProject mode programId + mode <- getBuildMode + Just source <- getParam "source" + let programId = sourceToProgramId source + deployId = sourceToDeployId source + success <- liftIO $ do + ensureProgramDir mode programId + B.writeFile (buildRootDir mode sourceFile programId) source + writeDeployLink mode deployId programId + compileIfNeeded mode programId + unless success $ modifyResponse $ setResponseCode 500 + liftIO $ buildAndroid mode programId + let result = CompileResult (unProgramId programId) (unDeployId deployId) + writeLBS (encode result) + +getAndroidHandler :: Snap() +getAndroidHandler = do + mode <- getBuildMode + programId <- getHashParam True mode + serveFileAs "application/vnd.android.package-archive" (apkFile mode programId) runMessageHandler :: Snap () runMessageHandler = do diff --git a/codeworld-server/src/Util.hs b/codeworld-server/src/Util.hs index fdb0b9281..7046bec8b 100644 --- a/codeworld-server/src/Util.hs +++ b/codeworld-server/src/Util.hs @@ -60,6 +60,16 @@ buildRootDir (BuildMode m) = "data" m "user" androidRootDir :: BuildMode -> FilePath androidRootDir (BuildMode m) = "data" m "android" +androidBuildDir :: BuildMode -> ProgramId -> FilePath +androidBuildDir mode programId = androidRootDir mode sourceBase programId + +apkFile :: BuildMode -> ProgramId -> FilePath +apkFile mode programId = + androidRootDir mode + sourceBase programId + "platforms" "android" "build" "outputs" "apk" + "android-debug" <.> "apk" + shareRootDir :: BuildMode -> FilePath shareRootDir (BuildMode m) = "data" m "share" @@ -69,8 +79,8 @@ projectRootDir (BuildMode m) = "data" m "projects" deployRootDir :: BuildMode -> FilePath deployRootDir (BuildMode m) = "data" m "deploy" -sourceBaseDir :: ProgramId -> FilePath -sourceBaseDir (ProgramId p) = let s = T.unpack p in take 3 s +sourceParent :: ProgramId -> FilePath +sourceParent (ProgramId p) = let s = T.unpack p in take 3 s sourceBase :: ProgramId -> FilePath sourceBase (ProgramId p) = let s = T.unpack p in take 3 s s diff --git a/web/env.html b/web/env.html index 8c2f4502f..eb9330875 100644 --- a/web/env.html +++ b/web/env.html @@ -97,6 +97,7 @@   Stop +   Android   Run diff --git a/web/js/codeworld.js b/web/js/codeworld.js index 634e712b0..d69d13e5a 100644 --- a/web/js/codeworld.js +++ b/web/js/codeworld.js @@ -731,6 +731,54 @@ function goto(line, col) { codeworldEditor.focus(); } +function exportAndroid() { + var src = window.codeworldEditor.getValue(); + var data = new FormData(); + data.append('source', src); + data.append('mode', window.buildMode); + + sendHttp('POST', 'exportAndroid', data, function(request) { + if(request.status != 200) { + alert("Android build FAILED"); + return; + } + var response = JSON.parse(request.response); + var hash = response.hash; + + var data = new FormData(); + data.append('hash', hash); + data.append('mode', window.buildMode); + var props = {}; + props.responseType = "blob"; + + sendHttpWithProps('POST', 'getAndroid', data, props, function(request) { + if(request.status != 200) { + alert("Android fetch FAILED"); + return; + } + console.log("Success"); + var blob = request.response; + var d = new Date(); + var filename = 'codeworld_app_' + + d.toDateString().split(' ').join('_') + '_' + + d.getHours() +':'+ d.getMinutes() +':'+ d.getSeconds() + +'.apk'; + var a = document.createElement("a"); + document.body.appendChild(a); + a.style = "display: none"; + + var url = window.URL.createObjectURL(blob); + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + + a.remove(); + }); + + }); +} + function compile() { run('', '', 'Compiling...', false); diff --git a/web/js/codeworld_shared.js b/web/js/codeworld_shared.js index 66911fe87..de7b1ca22 100644 --- a/web/js/codeworld_shared.js +++ b/web/js/codeworld_shared.js @@ -39,6 +39,23 @@ function sendHttp(method, url, body, callback) { request.send(body); } +function sendHttpWithProps(method, url, body, properties, callback) { + var request = new XMLHttpRequest(); + + if (callback) { + request.onreadystatechange = function() { + if (request.readyState == 4) callback(request); + }; + } + + request.open(method, url, true); + for(var prop in properties) { + request[prop] = properties[prop]; + } + + request.send(body); +} + function registerStandardHints(successFunc) { function createHint(line, wordStart, wordEnd, cname) { From 9936c65d792cf9fc6a4d6ef13dfad6c1adcdd80b Mon Sep 17 00:00:00 2001 From: venkat24 Date: Sun, 3 Sep 2017 08:48:48 +0530 Subject: [PATCH 3/4] Modify build scripts to install Cordova - Add Cordova and JDK installation to install.sh - Build Android template project building - Set android build tools paths in base.sh List of Dependencies for Android Exports - - Cordova >= 6.2.2 - Android Build Tools - Android SDK >= 19 - Gradle >= 3.4 - All executables in path and $ANROID_HOME set --- base.sh | 9 +++++++++ build.sh | 4 ++++ install.sh | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/base.sh b/base.sh index 0ffd8d49c..21f31a586 100755 --- a/base.sh +++ b/base.sh @@ -19,6 +19,14 @@ export PATH=$BUILD/bin:$PATH export LANG=${LANG:-C.UTF-8} export PREFIX=$BUILD +export ANDROID_HOME=$BUILD/Android/Sdk +export PATH=$BUILD/Android/Sdk/tools:$PATH +export PATH=$BUILD/Android/Sdk/platform-tools:$PATH +export PATH=$BUILD/Android/Sdk/tools/bin:$PATH +export PATH=$BUILD/Android/Sdk/tools/lib:$PATH + +export PATH=$BUILD/Cordova/node_modules/cordova/bin:$PATH + function run { OLD_PWD=$PWD cd $1 @@ -38,3 +46,4 @@ function run { function cabal_install { cabal install --force-reinstalls --global --prefix=$BUILD $@ } + diff --git a/build.sh b/build.sh index 526742615..aeafe99c7 100755 --- a/build.sh +++ b/build.sh @@ -18,6 +18,9 @@ cwd=$(pwd) source base.sh +# Create Cordova template +run ./android-template cordova build android + run . cabal update # Install the codeworld-base and codeworld-api packages @@ -53,3 +56,4 @@ run . cabal_install ./codeworld-server \ # Build the JavaScript client code for FunBlocks, the block-based UI. run . cabal_install --ghcjs ./funblocks-client + diff --git a/install.sh b/install.sh index 87e0cefe4..d2392dd1e 100755 --- a/install.sh +++ b/install.sh @@ -52,6 +52,11 @@ then run . sudo yum install -y patch run . sudo yum install -y autoconf run . sudo yum install -y automake + + # Needed for Cordova + run . sudo yum install -y java-1.8.0-openjdk + # TODO: Gradle for yum + elif type apt-get > /dev/null 2> /dev/null then echo Detected 'apt-get': Installing packages from there. @@ -84,6 +89,11 @@ then run . sudo apt-get install -y autoconf run . sudo apt-get install -y automake run . sudo apt-get install -y libtinfo-dev + + # Needed for Cordova + run . sudo apt-get install -y default-jdk + run . sudo apt-get install -y gradle + elif type zypper > /dev/null 2> /dev/null then echo Detected 'zypper': Installing packages from there. @@ -113,9 +123,13 @@ then run . sudo zypper -n install patch run . sudo zypper -n install autoconf run . sudo zypper -n install automake + + # Needed for Cordova + # TODO: OpenJDK, JRE and Gradle for Zypper else echo "WARNING: Could not find package manager." echo "Make sure necessary packages are installed." + fi # Choose the right GHC download @@ -169,7 +183,6 @@ run $BUILD cabal_install ./ghcjs run $BUILD rm -rf ghcjs run . ghcjs-boot --dev --ghcjs-boot-dev-branch ghc-8.0 --shims-dev-branch ghc-8.0 --no-prof --no-haddock -run $BUILD rm -rf downloads # Install and build CodeMirror editor. @@ -187,5 +200,30 @@ function build_codemirror { run $BUILD/CodeMirror build_codemirror +# Install Android SDK + +run $DOWNLOADS wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip +run $BUILD unzip $DOWNLOADS/sdk-tools-linux-3859397.zip +run $BUILD mkdir -p Android/Sdk +run $BUILD mv tools Android/Sdk/ +run $BUILD yes | ./Android/Sdk/tools/android update + +# Install Apache Cordova +run $BUILD mkdir Cordova +run $BUILD/Cordova npm install -s cordova + +# Create a fresh template project and copy in files +run . cordova create android-template +run android-template cordova platform add android +run . rm -rf android-template/www android-template/res +run . cp android-resources/* android-template/ -rf + +# Install the Android SDK +run $BUILD sdkmanager "build-tools;26.0.0" + +## Remove downloads directory +run $BUILD rm -rf downloads + # Go ahead and run a first build, which installs more local packages. ./build.sh + From 59ee7ab89fa11c111af0058b2e8aebf1486808e4 Mon Sep 17 00:00:00 2001 From: venkat24 Date: Sun, 3 Sep 2017 08:39:19 +0530 Subject: [PATCH 4/4] Allow user to set App Name - Add XML Parser TagSoup to set the App Name in `config.xml` - Add UI components to receive App Name input --- codeworld-server/codeworld-server.cabal | 3 ++ codeworld-server/src/AndroidExport.hs | 54 ++++++++++++++++++------- codeworld-server/src/Main.hs | 6 ++- codeworld-server/src/Util.hs | 3 ++ web/css/codeworld.css | 9 +++++ web/env.html | 2 +- web/js/codeworld.js | 52 +++++++++++++++++++++--- 7 files changed, 107 insertions(+), 22 deletions(-) diff --git a/codeworld-server/codeworld-server.cabal b/codeworld-server/codeworld-server.cabal index 65acbc532..815d8d237 100644 --- a/codeworld-server/codeworld-server.cabal +++ b/codeworld-server/codeworld-server.cabal @@ -21,6 +21,7 @@ Executable codeworld-server base64-bytestring, bytestring, codeworld-compiler, + containers, cryptonite, data-default, directory, @@ -35,6 +36,8 @@ Executable codeworld-server regex-tdfa, snap-core, snap-server, + strict, + tagsoup, temporary, text, unix diff --git a/codeworld-server/src/AndroidExport.hs b/codeworld-server/src/AndroidExport.hs index a764453e1..a4cd3da29 100644 --- a/codeworld-server/src/AndroidExport.hs +++ b/codeworld-server/src/AndroidExport.hs @@ -18,38 +18,62 @@ module AndroidExport where -import System.Process -import System.Directory -import System.FilePath +import Data.Maybe +import qualified Data.Map as M +import System.Process +import System.Directory +import System.FilePath +import qualified System.IO.Strict as ST +import Text.HTML.TagSoup import Util -buildAndroid :: BuildMode -> ProgramId -> IO() -buildAndroid mode programId = do +buildAndroid :: BuildMode -> ProgramId -> AppProps -> IO () +buildAndroid mode programId appProps = do + let appName = fromJust $ M.lookup "appName" appProps initCordovaProject mode programId copySource mode programId + setAppName mode programId appName buildApk mode programId return () - initCordovaProject :: BuildMode -> ProgramId -> IO () initCordovaProject mode programId = do + let rootDir = androidRootDir mode + checkIfRootExists <- doesDirectoryExist rootDir + if not checkIfRootExists + then do + createDirectory $ androidRootDir mode + else return () let buildDir = androidBuildDir mode programId checkIfBuildExists <- doesDirectoryExist buildDir - case checkIfBuildExists of - True -> return () - False -> do + if not checkIfBuildExists + then do checkIfParentExists <- doesDirectoryExist $ androidRootDir mode sourceParent programId - case checkIfParentExists of - True -> return () - False -> do + if not checkIfParentExists + then do createDirectory $ androidRootDir mode sourceParent programId copyDirIfExists "android-template" (androidRootDir mode sourceBase programId) - return () + else return () + else return () copySource :: BuildMode -> ProgramId -> IO () -copySource mode programId = do - copyFile (buildRootDir mode targetFile programId) (androidBuildDir mode programId "www" "js" "runjs.js") +copySource mode programId = + copyFile + (buildRootDir mode targetFile programId) + (androidBuildDir mode programId "www" "js" "runjs.js") + +setAppName :: BuildMode -> ProgramId -> String -> IO () +setAppName mode programId appName = do + let configFileName = androidBuildDir mode programId "config.xml" + configContents <- ST.readFile configFileName + let tagSoup = parseTags configContents + let newNameTag = [TagOpen "name" [], TagText appName] + writeFile configFileName (renderTags $ newSoup tagSoup newNameTag) + where newSoup soup insertTag = takeWhile nameId soup + ++ insertTag + ++ drop 2 (dropWhile nameId soup) + nameId = (~/= (""::String)) buildApk :: BuildMode -> ProgramId -> IO () buildApk mode programId = do diff --git a/codeworld-server/src/Main.hs b/codeworld-server/src/Main.hs index bb68a3197..9632c3ebf 100644 --- a/codeworld-server/src/Main.hs +++ b/codeworld-server/src/Main.hs @@ -34,6 +34,7 @@ import qualified Data.ByteString.Lazy as LB import Data.Char (isSpace) import Data.List import Data.Maybe +import qualified Data.Map.Strict as M import Data.Monoid import qualified Data.Text as T import qualified Data.Text.IO as T @@ -361,6 +362,8 @@ exportAndroidHandler :: Snap() exportAndroidHandler = do mode <- getBuildMode Just source <- getParam "source" + maybeAppName <- getParam "appName" + let appName = BC.unpack $ fromMaybe (BC.pack "CodeWorld App") maybeAppName let programId = sourceToProgramId source deployId = sourceToDeployId source success <- liftIO $ do @@ -369,7 +372,8 @@ exportAndroidHandler = do writeDeployLink mode deployId programId compileIfNeeded mode programId unless success $ modifyResponse $ setResponseCode 500 - liftIO $ buildAndroid mode programId + let appProps = M.fromList [("appName", appName)] + liftIO $ buildAndroid mode programId appProps let result = CompileResult (unProgramId programId) (unDeployId deployId) writeLBS (encode result) diff --git a/codeworld-server/src/Util.hs b/codeworld-server/src/Util.hs index 7046bec8b..5b5674f57 100644 --- a/codeworld-server/src/Util.hs +++ b/codeworld-server/src/Util.hs @@ -30,6 +30,7 @@ import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as LB import Data.Maybe import Data.Monoid +import qualified Data.Map.Strict as M import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -48,6 +49,8 @@ newtype DeployId = DeployId { unDeployId :: Text } deriving Eq newtype DirId = DirId { unDirId :: Text} deriving Eq newtype ShareId = ShareId { unShareId :: Text } deriving Eq +type AppProps = M.Map String String + autocompletePath :: FilePath autocompletePath = "web/codeworld-base.txt" diff --git a/web/css/codeworld.css b/web/css/codeworld.css index ccf232820..c03dded6d 100644 --- a/web/css/codeworld.css +++ b/web/css/codeworld.css @@ -157,6 +157,15 @@ body { cursor: pointer; } +button { + border: none; +} + +button:disabled, +button[disabled] { + background-color: #cccccc !important; +} + .cw-button { border-radius: 4px; cursor: pointer; diff --git a/web/env.html b/web/env.html index eb9330875..0134b08b8 100644 --- a/web/env.html +++ b/web/env.html @@ -97,7 +97,7 @@   Stop -   Android +   Run diff --git a/web/js/codeworld.js b/web/js/codeworld.js index d69d13e5a..d43e890e0 100644 --- a/web/js/codeworld.js +++ b/web/js/codeworld.js @@ -690,11 +690,13 @@ function run(hash, dhash, msg, error) { runner.contentWindow.location.replace(loc); if (!!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia) { document.getElementById('startRecButton').style.display = ''; + document.getElementById('exportAndroidButton').style.display = ''; } } else { runner.contentWindow.location.replace('about:blank'); document.getElementById('runner').style.display = 'none'; document.getElementById('startRecButton').style.display = 'none'; + document.getElementById('exportAndroidButton').style.display = 'none'; } if (hash || msg) { @@ -732,14 +734,51 @@ function goto(line, col) { } function exportAndroid() { + sweetAlert({ + title: "App Information", + text: "App Name", + type: "input", + showCancelButton: true, + confirmButtonText: "Build App", + closeOnConfirm: false, + inputPlaceholder: "CodeWorld App" + }, + function(inputValue){ + if (inputValue === false) { + return false; + } + if (inputValue === "") { + swal.showInputError("Please enter a name for your app"); + return false; + } + compileAndExportAndroid({ + appName: inputValue, + }); + sweetAlert({ + title: "Please Wait", + text: "Your app is being built", + imageUrl: "https://upload.wikimedia.org/wikipedia/commons/b/b1/Loading_icon.gif", + showConfirmButton: false, + allowOutsideClick: false, + allowEscapeKey: false + }); + }); +} + +function compileAndExportAndroid(appProps) { var src = window.codeworldEditor.getValue(); var data = new FormData(); data.append('source', src); data.append('mode', window.buildMode); + for(var prop in appProps) { + data.append(prop, appProps[prop]); + } + document.getElementById('exportAndroidButton').disabled = true; sendHttp('POST', 'exportAndroid', data, function(request) { if(request.status != 200) { - alert("Android build FAILED"); + sweetAlert("Android Build Failed", "Something went wrong!", "error"); + document.getElementById('exportAndroidButton').disabled = false; return; } var response = JSON.parse(request.response); @@ -748,17 +787,19 @@ function exportAndroid() { var data = new FormData(); data.append('hash', hash); data.append('mode', window.buildMode); - var props = {}; + var props = {}; props.responseType = "blob"; sendHttpWithProps('POST', 'getAndroid', data, props, function(request) { if(request.status != 200) { - alert("Android fetch FAILED"); + sweetAlert("Android Fetch Failed", "Something went wrong!", "error"); + document.getElementById('exportAndroidButton').disabled = false; return; } - console.log("Success"); + swal("App Built!", "Your CodeWorld app will now be downloaded", "success"); + var blob = request.response; - var d = new Date(); + var d = new Date(); var filename = 'codeworld_app_' + d.toDateString().split(' ').join('_') + '_' + d.getHours() +':'+ d.getMinutes() +':'+ d.getSeconds() @@ -774,6 +815,7 @@ function exportAndroid() { window.URL.revokeObjectURL(url); a.remove(); + document.getElementById('exportAndroidButton').disabled = false; }); });