#lang racket/base
(require racket/file
racket/runtime-path
racket/path
racket/contract
racket/list
(only-in xml xexpr->string)
"../../js-vm/private/module-record.rkt"
"../../js-vm/private/compile-moby-module.rkt"
"../../js-vm/private/write-module-records.rkt"
"../../js-vm/private/write-runtime.rkt"
"android-permission.rkt"
"../config.rkt"
"../utils.rkt"
"../compile-helpers.rkt")
(define-runtime-path phonegap-path "../../support/phonegap-fork/android-1.5")
(define-runtime-path icon-path "../../support/icons/icon.png")
(define-runtime-path javascript-support-path "../../support/js")
(define (build-android-package program-name program-path)
(with-temporary-directory
(lambda (dir)
(build-android-package-in-path program-name
program-path
dir)
(get-apk-in-dest dir))))
(define (get-apk-in-dest dest)
(get-file-bytes
(first (find-files (lambda (a-path)
(equal? (filename-extension a-path)
#"apk"))
dest))))
(define (build-android-package-in-path name program-path dest)
(let ([module-records (get-compiled-modules program-path)])
(prepare-android-package-src-structure name
(module-records-android-permissions module-records)
dest)
(write-assets program-path module-records (build-path dest "assets"))
(write-local.properties dest)
(unless (file-exists? (current-ant-bin-path))
(error 'build-android-package-in-path
"The Apache ant binary appears to be missing from the current PATH."))
(unless (directory-exists? (current-android-sdk-path))
(error 'build-android-package-in-path
"The Android SDK could not be found."))
(run-ant-build.xml dest "debug")))
(define (write-local.properties dest)
(call-with-output-file (build-path dest "local.properties")
(lambda (op)
(fprintf op "sdk.dir=~a~n"
(path->string (current-android-sdk-path)))
(fprintf op "sdk-location=~a~n"
(path->string (current-android-sdk-path))))
#:exists 'replace))
(define (prepare-android-package-src-structure name android-permissions dest)
(make-directory* dest)
(copy-directory/files* phonegap-path dest)
(write-java-class-stubs name android-permissions dest)
(write-icon dest))
(define (module-records-android-permissions module-records)
(let ([ht (make-hash)])
(for ([a-record (in-list module-records)])
(for ([a-permission (module-record-permissions a-record)])
(for ([translated-permission
(permission->android-permissions a-permission (lambda () empty))])
(hash-set! ht translated-permission #t))))
(for/list ([k (in-hash-keys ht)])
k)))
(define (write-icon dest)
(make-directory* (build-path dest "res" "drawable"))
(copy-or-overwrite-file icon-path
(build-path dest "res" "drawable" "icon.png")))
(define (write-assets program-path module-records assets-path)
(make-directory* assets-path)
(write-index.html module-records (build-path assets-path "index.html"))
(copy-support-js-files assets-path)
(call-with-output-file (build-path assets-path "runtime.js")
(lambda (op)
(write-runtime "browser" op))
#:exists 'replace)
(copy-or-overwrite-file (build-path phonegap-path "assets" "phonegap.js")
(build-path assets-path "phonegap.js"))
(write-program.js program-path module-records assets-path))
(define (write-index.html module-records index.html-path)
(call-with-output-file index.html-path
(lambda (op)
(let ([header
#<<EOF
<html>
<head>
<script type="text/javascript" charset="utf-8" src="phonegap.js"></script>
<script type="text/javascript" charset="utf-8" src="runtime.js"></script>
<script type="text/javascript" charset="utf-8" src="evaluator.js"></script>
EOF
]
[footer
#<<EOF
<script type="text/javascript" charset="utf-8" src="program.js"></script>
<script type="text/javascript" charset="utf-8" src="main.js"></script>
</head>
<body onload="evaluator.setGasLimit(100); mainPageLoad();">
<div id="history"></div>
</body>
</html>
EOF
])
(display header op)
(for ([i (in-range (length module-records))])
(fprintf op "<script type=\"text/javascript\" charset=\"utf-8\" src=\"module~a.js\"></script>\n" i))
(display footer op)))
#:exists 'replace))
(define (write-java-class-stubs name android-permissions dest)
(let* ([normal-name (normalize-name name)]
[classname (upper-camel-case normal-name)]
[package (string-append "plt.moby." classname)])
(write-android-manifest
(build-path dest "AndroidManifest.xml")
#:name name
#:package package
#:activity-class (string-append package "." classname)
#:permissions android-permissions)
(let* ([build-xml-bytes (get-file-bytes (build-path dest "build.xml"))]
[build-xml-bytes (regexp-replace #rx"DroidGap"
build-xml-bytes
(string->bytes/utf-8 classname))])
(call-with-output-file (build-path dest "build.xml")
(lambda (op) (write-bytes build-xml-bytes op))
#:exists 'replace))
(let* ([strings-xml-bytes
(get-file-bytes (build-path dest "res" "values" "strings.xml"))]
[strings-xml-bytes
(regexp-replace #rx"DroidGap"
strings-xml-bytes
(string->bytes/utf-8 (xexpr->string name)))])
(call-with-output-file (build-path dest "res" "values" "strings.xml")
(lambda (op) (write-bytes strings-xml-bytes op))
#:exists 'replace))
(make-directory* (build-path dest "src" "plt" "moby" classname))
(let* ([middleware
(get-file-bytes
(build-path dest "src" "com" "phonegap" "demo" "DroidGap.java"))]
[middleware
(regexp-replace
#rx"package com.phonegap.demo;\n"
middleware
(string->bytes/utf-8
(format "package plt.moby.~a;\nimport com.phonegap.demo.*;\n"
classname)))]
[middleware
(regexp-replace #rx"DroidGap"
middleware
(string->bytes/utf-8 classname))])
(call-with-output-file (build-path dest "src" "plt" "moby"
classname
(format "~a.java" classname))
(lambda (op)
(write-bytes middleware op))
#:exists 'replace)
(delete-file (build-path dest "src" "com" "phonegap" "demo"
"DroidGap.java")))))
(define (normalize-name a-name)
(let ([a-name (regexp-replace* #px"[^\\w\\s]+" a-name "")])
(cond
[(or (= (string-length a-name) 0)
(not (char-alphabetic? (string-ref a-name 0))))
(string-append "_" a-name)]
[else
a-name])))
(define (write-android-manifest path
#:name name
#:package package
#:activity-class activity-class
#:permissions (permissions '()))
(call-with-output-file path
(lambda (op)
(display (get-android-manifest #:name name
#:package package
#:activity-class activity-class
#:permissions permissions) op))
#:exists 'replace))
(define (get-android-manifest #:name name
#:package package
#:activity-class activity-class
#:permissions (permissions '())
#:version-code (version-code "1")
#:version-name (version-name "1.0.0"))
(let ([AndroidManifest.xml
`(manifest
((xmlns:android "http://schemas.android.com/apk/res/android")
(package ,package)
(android:versionCode ,version-code)
(android:versionName ,version-name))
(uses-sdk ((android:minSdkVersion "3")))
,@(map (lambda (p)
`(uses-permission ((android:name ,p))))
permissions)
(application
((android:label "@string/app_name")
(android:icon "@drawable/icon"))
(activity ((android:name ,activity-class)
(android:label ,name)
(android:configChanges
"keyboardHidden|orientation"))
(intent-filter
()
(action ((android:name "android.intent.action.MAIN")))
(category
((android:name
"android.intent.category.LAUNCHER")))))
(activity ((android:name "plt.playlist.PickPlaylist")
(android:label "PickPlaylist")
(android:configChanges
"keyboardHidden|orientation"))
(action ((android:name "android.intent.action.PICK")))
(category ((android:name
"android.intent.category.DEFAULT"))))))])
(xexpr->string AndroidManifest.xml)))
(define (get-compiled-modules program-path)
(let*-values ([(a-path) (normalize-path program-path)]
[(base-dir file dir?) (split-path a-path)]
[(module-records) (compile-moby-modules a-path)])
module-records))
(define (write-program.js a-path module-records dest-dir)
(for ([r module-records]
[i (in-naturals)])
(call-with-output-file (build-path dest-dir
(format "module~a.js"
i))
(lambda (op)
(fprintf op
"MODULES[~s] = ~a\n"
(symbol->string (module-record-name r))
(encode-module-record r)))
#:exists 'replace))
(call-with-output-file (build-path dest-dir "program.js")
(lambda (op)
(for ([r module-records])
(cond
[(string=? (path->string (module-record-path r))
(if (string? a-path) a-path (path->string a-path)))
(fprintf op "var programModuleName = ~s;\n\n"
(symbol->string (module-record-name r)))]
[else
(void)])))
#:exists 'replace))
(provide/contract [build-android-package
(string? path-string? . -> . bytes?)]
[build-android-package-in-path
(string? path-string? path-string? . -> . any)]
[prepare-android-package-src-structure
(string? (listof string?) path-string? . -> . any)]
[write-assets
(path? (listof module-record?) path? . -> . any)]
[write-local.properties
(path-string? . -> . any)]
[get-apk-in-dest
(path? . -> . bytes?)]
[module-records-android-permissions
((listof module-record?) . -> . (listof string?))]
)