import '../Interfaces' ; import { Interfaces } from '../Interfaces' ; import * as path from 'path' ; import { getDownloadLink , getFileDownloadLink } from '../GitListDir' ; const request = require ( 'request' ) ; import * as fs from 'fs-extra' ; import { spawn } from 'child_process' ; import { getManifest } from './Html/ManifestBuilder' ; import { updateIcon } from './Html/updateIcon' ; import { updateAppName } from './Html/updateAppName' ; import { createIfNotExist } from './Html/createIfNotExist' ; import { downloadGithubFile } from './Html/downloadGithubFile' ; import { downloadGithubRepo } from './Html/downloadGithubRepo' ; import { LoadingBar } from './Html/ProgressBar' ; import { javaVersion } from './Html/getJavaVersion' ; import { getUpdate , getUpdateMessage } from './Html/checkForUpdates' ; import { updateTheme } from "./Html/updateAppTheme" ; import { updateApkVersion } from './Html/updateApkVersion' ; const chalk = require ( 'chalk' ) ; const admZip = require ( 'adm-zip' ) ; /**
 * Webview module for creating HTML based Android-Js project
 */ export class Webview implements Interfaces . IBuilderModule { env : Interfaces . IEnv ; user : string = 'android-js' ; sdk : Interfaces . IGithubRepoLink = { user : 'android-js' , repo : 'androidjs-sdk' } ; example : Interfaces . IGithubRepoLink = { user : 'android-js' , repo : 'webview-app-template' } ; apk_tool : Interfaces . GithubFileLink = { user : 'android-js' , repo : 'androidjs-builder' , dir : 'build_tools' , file : 'apktool.jar' } ; apk_signer : Interfaces . GithubFileLink = { user : 'android-js' , repo : 'androidjs-builder' , dir : 'build_tools' , file : 'uber-apk-signer-1.0.0.jar' } ; manifist = { platformBuildVersionCode : "30" , platformBuildVersionName : "11" , application : { 'android:allowBackup' : "true" , 'android:appComponentFactory' : "android.support.v4.app.CoreComponentFactory" , 'android:debuggable' : "false" , 'android:icon' : "@mipmap/ic_launcher" , 'android:label' : "@string/app_name" , 'android:roundIcon' : "@mipmap/ic_launcher_round" , 'android:supportsRtl' : "true" , 'android:theme' : "@style/AppTheme" , 'android:usesCleartextTraffic' : "true" , 'android:requestLegacyExternalStorage' : "true" } , activity : { 'android:configChanges' : "keyboard|keyboardHidden|orientation|screenSize" , 'android:name' : "com.android.js.webview.MainActivity" } } ; context : { } ; installModule ( env , context ) { this . env = env ; this . context = context ; this . context . updateSdk = ( ) => require ( './Html/updateSdk' ) . updateSdk ( this . env ) ; env . sdk = this . sdk ; getUpdate ( env ) ; console . log ( "--release=" , env . release ) ; this . manifist . application [ "android:debuggable" ] = ( ! env . release ) . toString ( ) ; return 0 ; } create ( ) { let examplesFolder = createIfNotExist ( path . join ( this . env . builder . cache , '..' , 'examples' ) ) ; let exampleFilePath = path . join ( examplesFolder , this . example . repo + '.zip' ) ; let projectPath = path . join ( this . env . project . dir , this . env . project . name ) ; const exampleRepoDownloadLink = getDownloadLink ( this . example . user , this . example . repo ) ; const exampleFolderPath = path . join ( examplesFolder , this . example . repo ) ; if ( fs . existsSync ( projectPath ) ) { console . log ( `ERROR: directory is not empty ${ projectPath }` ) ; process . exit ( ) ; } if ( ! fs . existsSync ( exampleFolderPath ) || this . env . force ) { console . log ( 'Downloading:' , exampleRepoDownloadLink ) ; downloadsdk ( { url : getDownloadLink ( this . example . user , this . example . repo ) , repo : this . example . repo , targetFolder : path . join ( this . env . builder . cache , '..' , 'examples' ) , targetZip : path . join ( this . env . builder . cache , '..' , 'examples' , this . example . repo + ".zip" ) , zipFolder : this . example . repo + '-master/' , retry : 3 } , ( error ) => { if ( error ) { console . log ( error ) ; } else { if ( this . env . builder . debug ) console . log ( 'Creating Project...' ) ; fs . copySync ( exampleFolderPath , projectPath ) ; this . updatePackageJson ( ) ; console . log ( `$cd ${ this . env . project . name }` ) ; console . log ( `$npm install` ) ; console . log ( `$npm run start:dev` ) ; console . log ( `$npm run build` ) ; } } ) ; } else { if ( this . env . builder . debug ) console . log ( 'Creating Project...' ) ; fs . copySync ( exampleFolderPath , projectPath ) ; this . updatePackageJson ( ) ; console . log ( chalk . green ( `   $cd ${ this . env . project . name }` ) ) ; console . log ( chalk . green ( `   $npm install` ) ) ; console . log ( chalk . green ( `   $npm run start:dev` ) ) ; console . log ( chalk . green ( `   $npm run build` ) ) ; } return 0 ; } updatePackageJson ( ) { let projectPath = path . join ( this . env . project . dir , this . env . project . name ) ; const _package = require ( path . join ( projectPath , 'package.json' ) ) ; _package . name = this . env . project . name ; _package [ 'app-name' ] = this . env . project . name ; _package [ 'project-type' ] = this . env . project . type ; _package [ 'project-name' ] = this . env . project . name ; _package [ 'scripts' ] [ 'build' ] = 'androidjs build' ; _package [ "dist-path" ] = "./dist" ; _package [ 'theme' ] = { "fullScreen" : true } ; fs . writeFileSync ( path . join ( this . env . project . dir , this . env . project . name , 'package.json' ) , JSON . stringify ( _package , null , 4 ) ) ; } downloadSDK ( callback , force = false ) { const sdkZip = path . join ( this . env . builder . cache , this . sdk . repo + '.zip' ) ; const sdkFolder = path . join ( this . env . builder . cache , this . sdk . repo ) ; if ( ! force && fs . existsSync ( sdkFolder ) ) { callback ( ) ; } else { downloadsdk ( { url : getDownloadLink ( this . sdk . user , this . sdk . repo ) , repo : this . sdk . repo , targetFolder : this . env . builder . cache , targetZip : sdkZip , zipFolder : this . sdk . repo + '-master/' , retry : 4 } , callback ) ; } } updateFiles ( callback ) { const sdkZip = path . join ( this . env . builder . cache , this . sdk . repo + '.zip' ) ; const sdkFolder = path . join ( this . env . builder . cache , this . sdk . repo ) ; const assetsFolder = path . join ( sdkFolder , 'assets' ) ; const myappFolder = path . join ( assetsFolder , 'myapp' ) ; try { this . env . project . package = require ( path . join ( this . env . project . dir , 'package.json' ) ) ; } catch ( e ) { console . log ( `failed to load package:` , e . message ) ; process . exit ( ) ; } try { if ( ! fs . existsSync ( assetsFolder ) ) { fs . mkdirSync ( assetsFolder ) ; fs . mkdirSync ( myappFolder ) ; } else { fs . removeSync ( myappFolder ) ; fs . mkdirSync ( myappFolder ) ; } } catch ( e ) { if ( ! fs . existsSync ( sdkFolder ) ) { console . log ( chalk . red ( 'Can not find ' + sdkFolder ) ) ; console . log ( "Try using '--force' command" ) ; console . log ( "$ androidjs build --force" ) ; } else { console . log ( "Failed to create assets" , e ) ; } process . exit ( ) ; } try { console . log ( 'copying assets ...' ) ; fs . copySync ( this . env . project . dir , myappFolder ) ; } catch ( e ) { console . log ( `failed to copy assets:` , e ) ; process . exit ( ) ; } if ( fs . existsSync ( path . join ( myappFolder , 'dist' ) ) ) { fs . removeSync ( path . join ( myappFolder , 'dist' ) ) ; } let permissions = [ ] ; if ( this . env . project . package . permission ) { permissions = [ ... this . env . project . package . permission ] ; } let deep_links = [ ] ; if ( this . env . project . package [ "deep-link" ] ) { deep_links = [ ... this . env . project . package [ "deep-link" ] ] ; } let screenOrientation = this . env . project . package [ 'screenOrientation' ] || null ; updateIcon ( this . env , this ) ; updateAppName ( this . env , this ) ; updateApkVersion ( this . env , this ) ; const manifestFileData = getManifest ( this . env , this , permissions , deep_links , screenOrientation ) ; fs . writeFileSync ( path . join ( sdkFolder , 'AndroidManifest.xml' ) , manifestFileData ) ; /**
         * Downloading Build Tools
         **/ callback ( ) ; } rebuildApk ( callback ) { const progress = new LoadingBar ( ) ; const apkToolLink = getFileDownloadLink ( this . apk_tool . user , this . apk_tool . repo , this . apk_tool . dir , this . apk_tool . file ) ; const apkToolFilePath = path . join ( this . env . builder . dir , 'build_tools' , this . apk_tool . file ) ; const apkSignerLink = getFileDownloadLink ( this . apk_signer . user , this . apk_signer . repo , this . apk_signer . dir , this . apk_signer . file ) ; const apkSignerFilePath = path . join ( this . env . builder . cache , this . apk_signer . file ) ; const sdkFolderPath = path . join ( this . env . builder . cache , this . sdk . repo ) ; const cacheFolderPath = this . env . builder . cache ; const buildApkFilePath = path . join ( this . env . builder . cache , this . env . project . package . name + '.apk' ) ; let sdkConfig = { } ; try { sdkConfig = require ( path . join ( sdkFolderPath , 'config.json' ) ) ; } catch ( e ) { } let args_ = [ '-jar' , apkToolFilePath , 'b' , sdkFolderPath , '-o' , buildApkFilePath , '--frame-path' , cacheFolderPath ] ; const proc = spawn ( 'java' , args_ , { cwd : cacheFolderPath } ) ; let log = "" ; if ( sdkConfig . version ) { console . log ( `Using SDK: ${ sdkConfig . version }` ) ; } console . log ( "Building Apk ..." ) ; try { proc . stdout . on ( 'data' , data => { if ( progress . isRunning === false ) { if ( this . env . builder . debug ) { console . log ( "Build process started:" ) ; } progress . message = 'building ...' ; progress . start ( ) ; } else { progress . chunksDownloaded = ( parseInt ( progress . chunksDownloaded ) + 1 ) . toString ( ) ; } } ) ; proc . stderr . on ( 'data' , ( data ) => { log += `${ data }` ; } ) ; proc . on ( 'close' , ( code ) => { if ( code === 0 ) { progress . stop ( ) ; progress . clear ( ) ; callback ( ) ; } else { progress . stop ( { message : 'non zero exit code: failed to build apk' } ) ; console . error ( log ) ; process . exit ( ) ; } } ) ; } catch ( e ) { console . log ( e ) ; } } build ( ) { const sdkZip = path . join ( this . env . builder . cache , this . sdk . repo + '.zip' ) ; const sdkFolder = path . join ( this . env . builder . cache , this . sdk . repo ) ; const assetsFolder = path . join ( sdkFolder , 'assets' ) ; const myappFolder = path . join ( assetsFolder , 'myapp' ) ; const projectDistFolderPath = path . join ( this . env . project . dir , 'dist' ) ; javaVersion ( ( error , home ) => { if ( error ) { console . log ( error . message ) ; process . exit ( ) ; } else { this . downloadSDK ( ( ) => { this . updateFiles ( ( error ) => { updateTheme ( this . env , ( err ) => { this . rebuildApk ( ( error ) => { this . sign ( ( ) => { try { if ( this . env . project . package [ 'dist-path' ] ) { let dist = this . env . project . package [ 'dist-path' ] ; if ( dist [ 0 ] === '.' ) { dist = path . join ( this . env . project . dir , dist ) ; } if ( ! fs . existsSync ( dist ) ) fs . mkdirSync ( dist ) ; fs . copyFileSync ( path . join ( this . env . builder . cache , this . env . project . package . name + '-aligned-debugSigned.apk' ) , path . join ( dist , this . env . project . package . name + '.apk' ) ) ; } else { if ( ! fs . existsSync ( projectDistFolderPath ) ) fs . mkdirSync ( projectDistFolderPath ) ; fs . copyFileSync ( path . join ( this . env . builder . cache , this . env . project . package . name + '-aligned-debugSigned.apk' ) , path . join ( projectDistFolderPath , this . env . project . package . name + '.apk' ) ) ; } getUpdateMessage ( ) ; } catch ( e ) { console . log ( "failed to move apk to dist folder" ) ; process . exit ( ) ; } } ) ; } ) ; } ) ; } ) ; } , this . env . force || false ) ; } } ) ; return 0 ; } sign ( callback ) { const progress = new LoadingBar ( ) ; let apksigner_path = path . join ( this . env . builder . dir , 'build_tools' , this . apk_signer . file ) ; let apk_file_path = path . join ( this . env . builder . cache , this . env . project . package . name + '.apk' ) ; let build_working_dir = path . join ( this . env . builder . cache ) ; let args_ = [ '-jar' , apksigner_path , '--apks' , apk_file_path ] ; if ( this . env . release === false ) { args_ . push ( '--debug' ) ; console . log ( "Generating apk in debug mode. use '--release' to generate release build" ) ; } else { console . log ( "Generating apk in release mode." ) ; } const proc = spawn ( 'java' , args_ , { cwd : build_working_dir } ) ; proc . stdout . on ( 'data' , data => { if ( progress . isRunning === false ) { progress . message = 'signing ...' ; progress . start ( ) ; } else { progress . chunksDownloaded = ( parseInt ( progress . chunksDownloaded ) + 1 ) . toString ( ) ; } } ) ; proc . stderr . on ( 'data' , ( data ) => { progress . stop ( { message : 'error on signing apk' } ) ; if ( this . env . builder . debug ) { console . log ( `${ data }` ) ; } process . exit ( ) ; } ) ; proc . on ( 'close' , ( code ) => { if ( code === 0 ) { progress . stop ( ) ; if ( this . env . release ) { console . log ( chalk . green ( ` Resign the apk with your own private keystore.` ) ) ; } callback ( ) ; } else { progress . stop ( { message : "failed to sign apk" , code } ) ; process . exit ( ) ; } } ) ; } } interface downloadGithubArgs { url : string ; targetZip : string ; repo : string ; targetFolder : string ; zipFolder : string ; retry : number ; } interface IGithubRepoLink { user : string ; repo : string ; } function downloadsdk ( args , callback ) { args . retry -= 1 ; let state = { data : 0 , moveCursor : false , responseCode : null , fileStream : fs . createWriteStream ( args . targetZip ) , progress : new LoadingBar ( ) } ; request . get ( { url : args . url , headers : { 'User-Agent' : 'request' } } ) . on ( 'response' , response => { state . responseCode = response . statusCode ; if ( response . statusCode === 200 ) { if ( args . recursive ) { console . log ( `re-trying` ) ; state . progress . message = "Downloading:" ; state . progress . start ( ) ; } else { console . log ( "Downloading:" , args . url ) ; state . progress . message = "Downloading:" ; state . progress . start ( ) ; } } } ) . on ( 'data' , data => { state . data += data . length ; state . progress . chunksDownloaded = `${ Math . floor ( state . data * 0.001 ) } KB` ; state . progress . message = `Data: ` ; } ) . on ( 'error' , error => { if ( state . responseCode === null ) { console . log ( `Failed to download: ${ args . url },\nCheck your internet connection` ) ; } else { state . progress . stop ( { message : `Failed to download: ${ args . url }` } ) ; console . log ( error ) ; } } ) . on ( 'end' , ( ) => { state . fileStream . close ( ) ; try { let zip = new admZip ( args . targetZip ) ; if ( fs . existsSync ( path . join ( args . targetFolder , args . zipFolder ) ) ) { fs . removeSync ( path . join ( args . targetFolder , args . zipFolder ) ) ; } zip . extractEntryTo ( args . zipFolder , args . targetFolder , true , true ) ; if ( fs . existsSync ( path . join ( args . targetFolder , args . repo ) ) ) { fs . removeSync ( path . join ( args . targetFolder , args . repo ) ) ; } fs . renameSync ( path . join ( args . targetFolder , args . zipFolder ) , path . join ( args . targetFolder , args . repo ) ) ; state . progress . stop ( ) ; callback ( ) ; } catch ( e ) { if ( args . retry > 0 ) { state . progress . stop ( { message : "failed to download, retrying.." } ) ; downloadsdk ( { ... args , recursive : true } , callback ) ; } else { state . progress . stop ( { message : "failed to download" } ) ; callback ( { message : "failed to download" } ) ; } } } ) . pipe ( state . fileStream ) ; }