2018-10-07 21:10:38 -03:00
#!/usr/bin/env node
/ *
Licensed to the Apache Software Foundation ( ASF ) under one
or more contributor license agreements . See the NOTICE file
distributed with this work for additional information
regarding copyright ownership . The ASF licenses this file
to you 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 .
* /
var android _versions = require ( 'android-versions' ) ;
var retry = require ( './retry' ) ;
var build = require ( './build' ) ;
var path = require ( 'path' ) ;
var Adb = require ( './Adb' ) ;
var AndroidManifest = require ( './AndroidManifest' ) ;
var events = require ( 'cordova-common' ) . events ;
var superspawn = require ( 'cordova-common' ) . superspawn ;
var CordovaError = require ( 'cordova-common' ) . CordovaError ;
var shelljs = require ( 'shelljs' ) ;
var android _sdk = require ( './android_sdk' ) ;
var check _reqs = require ( './check_reqs' ) ;
var os = require ( 'os' ) ;
var fs = require ( 'fs' ) ;
var child _process = require ( 'child_process' ) ;
// constants
var ONE _SECOND = 1000 ; // in milliseconds
var ONE _MINUTE = 60 * ONE _SECOND ; // in milliseconds
var INSTALL _COMMAND _TIMEOUT = 5 * ONE _MINUTE ; // in milliseconds
var NUM _INSTALL _RETRIES = 3 ;
var CHECK _BOOTED _INTERVAL = 3 * ONE _SECOND ; // in milliseconds
var EXEC _KILL _SIGNAL = 'SIGKILL' ;
function forgivingWhichSync ( cmd ) {
try {
return fs . realpathSync ( shelljs . which ( cmd ) ) ;
} catch ( e ) {
return '' ;
}
}
module . exports . list _images _using _avdmanager = function ( ) {
return superspawn . spawn ( 'avdmanager' , [ 'list' , 'avd' ] ) . then ( function ( output ) {
var response = output . split ( '\n' ) ;
var emulator _list = [ ] ;
for ( var i = 1 ; i < response . length ; i ++ ) {
// To return more detailed information use img_obj
var img _obj = { } ;
if ( response [ i ] . match ( /Name:\s/ ) ) {
img _obj [ 'name' ] = response [ i ] . split ( 'Name: ' ) [ 1 ] . replace ( '\r' , '' ) ;
if ( response [ i + 1 ] . match ( /Device:\s/ ) ) {
i ++ ;
img _obj [ 'device' ] = response [ i ] . split ( 'Device: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /Path:\s/ ) ) {
i ++ ;
img _obj [ 'path' ] = response [ i ] . split ( 'Path: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /Target:\s/ ) ) {
i ++ ;
if ( response [ i + 1 ] . match ( /ABI:\s/ ) ) {
img _obj [ 'abi' ] = response [ i + 1 ] . split ( 'ABI: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
// This next conditional just aims to match the old output of `android list avd`
// We do so so that we don't have to change the logic when parsing for the
// best emulator target to spawn (see below in `best_image`)
// This allows us to transitionally support both `android` and `avdmanager` binaries,
// depending on what SDK version the user has
if ( response [ i + 1 ] . match ( /Based\son:\s/ ) ) {
img _obj [ 'target' ] = response [ i + 1 ] . split ( 'Based on:' ) [ 1 ] ;
if ( img _obj [ 'target' ] . match ( /Tag\/ABI:\s/ ) ) {
img _obj [ 'target' ] = img _obj [ 'target' ] . split ( 'Tag/ABI:' ) [ 0 ] . replace ( '\r' , '' ) . trim ( ) ;
if ( img _obj [ 'target' ] . indexOf ( '(' ) > - 1 ) {
img _obj [ 'target' ] = img _obj [ 'target' ] . substr ( 0 , img _obj [ 'target' ] . indexOf ( '(' ) - 1 ) . trim ( ) ;
}
}
var version _string = img _obj [ 'target' ] . replace ( /Android\s+/ , '' ) ;
var api _level = android _sdk . version _string _to _api _level [ version _string ] ;
if ( api _level ) {
img _obj [ 'target' ] += ' (API level ' + api _level + ')' ;
}
}
}
if ( response [ i + 1 ] . match ( /Skin:\s/ ) ) {
i ++ ;
img _obj [ 'skin' ] = response [ i ] . split ( 'Skin: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
emulator _list . push ( img _obj ) ;
}
/ * T o j u s t r e t u r n a l i s t o f n a m e s u s e t h i s
if ( response [ i ] . match ( /Name:\s/ ) ) {
emulator _list . push ( response [ i ] . split ( 'Name: ' ) [ 1 ] . replace ( '\r' , '' ) ;
} * /
}
return emulator _list ;
} ) ;
} ;
module . exports . list _images _using _android = function ( ) {
return superspawn . spawn ( 'android' , [ 'list' , 'avd' ] ) . then ( function ( output ) {
var response = output . split ( '\n' ) ;
var emulator _list = [ ] ;
for ( var i = 1 ; i < response . length ; i ++ ) {
// To return more detailed information use img_obj
var img _obj = { } ;
if ( response [ i ] . match ( /Name:\s/ ) ) {
img _obj [ 'name' ] = response [ i ] . split ( 'Name: ' ) [ 1 ] . replace ( '\r' , '' ) ;
if ( response [ i + 1 ] . match ( /Device:\s/ ) ) {
i ++ ;
img _obj [ 'device' ] = response [ i ] . split ( 'Device: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /Path:\s/ ) ) {
i ++ ;
img _obj [ 'path' ] = response [ i ] . split ( 'Path: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /\(API\slevel\s/ ) || ( response [ i + 2 ] && response [ i + 2 ] . match ( /\(API\slevel\s/ ) ) ) {
i ++ ;
var secondLine = response [ i + 1 ] . match ( /\(API\slevel\s/ ) ? response [ i + 1 ] : '' ;
img _obj [ 'target' ] = ( response [ i ] + secondLine ) . split ( 'Target: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /ABI:\s/ ) ) {
i ++ ;
img _obj [ 'abi' ] = response [ i ] . split ( 'ABI: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
if ( response [ i + 1 ] . match ( /Skin:\s/ ) ) {
i ++ ;
img _obj [ 'skin' ] = response [ i ] . split ( 'Skin: ' ) [ 1 ] . replace ( '\r' , '' ) ;
}
emulator _list . push ( img _obj ) ;
}
/ * T o j u s t r e t u r n a l i s t o f n a m e s u s e t h i s
if ( response [ i ] . match ( /Name:\s/ ) ) {
emulator _list . push ( response [ i ] . split ( 'Name: ' ) [ 1 ] . replace ( '\r' , '' ) ;
} * /
}
return emulator _list ;
} ) ;
} ;
/ * *
* Returns a Promise for a list of emulator images in the form of objects
* {
name : < emulator _name > ,
device : < device > ,
path : < path _to _emulator _image > ,
target : < api _target > ,
abi : < cpu > ,
skin : < skin >
}
* /
module . exports . list _images = function ( ) {
2019-04-09 20:33:36 -04:00
return Promise . resolve ( ) . then ( function ( ) {
2018-10-07 21:10:38 -03:00
if ( forgivingWhichSync ( 'avdmanager' ) ) {
return module . exports . list _images _using _avdmanager ( ) ;
} else if ( forgivingWhichSync ( 'android' ) ) {
return module . exports . list _images _using _android ( ) ;
} else {
2019-04-09 20:33:36 -04:00
return Promise . reject ( new CordovaError ( 'Could not find either `android` or `avdmanager` on your $PATH! Are you sure the Android SDK is installed and available?' ) ) ;
2018-10-07 21:10:38 -03:00
}
} ) . then ( function ( avds ) {
// In case we're missing the Android OS version string from the target description, add it.
return avds . map ( function ( avd ) {
if ( avd . target && avd . target . indexOf ( 'Android API' ) > - 1 && avd . target . indexOf ( 'API level' ) < 0 ) {
var api _level = avd . target . match ( /\d+/ ) ;
if ( api _level ) {
var level = android _versions . get ( api _level ) ;
if ( level ) {
avd . target = 'Android ' + level . semver + ' (API level ' + api _level + ')' ;
}
}
}
return avd ;
} ) ;
} ) ;
} ;
/ * *
* Will return the closest avd to the projects target
* or undefined if no avds exist .
* Returns a promise .
* /
module . exports . best _image = function ( ) {
return this . list _images ( ) . then ( function ( images ) {
// Just return undefined if there is no images
if ( images . length === 0 ) return ;
var closest = 9999 ;
var best = images [ 0 ] ;
var project _target = parseInt ( check _reqs . get _target ( ) . replace ( 'android-' , '' ) ) ;
for ( var i in images ) {
var target = images [ i ] . target ;
if ( target && target . indexOf ( 'API level' ) > - 1 ) {
var num = parseInt ( target . split ( '(API level ' ) [ 1 ] . replace ( ')' , '' ) ) ;
if ( num === project _target ) {
return images [ i ] ;
} else if ( project _target - num < closest && project _target > num ) {
closest = project _target - num ;
best = images [ i ] ;
}
}
}
return best ;
} ) ;
} ;
// Returns a promise.
module . exports . list _started = function ( ) {
2019-04-09 20:33:36 -04:00
return Adb . devices ( { emulators : true } ) ;
2018-10-07 21:10:38 -03:00
} ;
// Returns a promise.
// TODO: we should remove this, there's a more robust method under android_sdk.js
module . exports . list _targets = function ( ) {
2019-04-09 20:33:36 -04:00
return superspawn . spawn ( 'android' , [ 'list' , 'targets' ] , { cwd : os . tmpdir ( ) } ) . then ( function ( output ) {
2018-10-07 21:10:38 -03:00
var target _out = output . split ( '\n' ) ;
var targets = [ ] ;
for ( var i = target _out . length ; i >= 0 ; i -- ) {
if ( target _out [ i ] . match ( /id:/ ) ) {
targets . push ( targets [ i ] . split ( ' ' ) [ 1 ] ) ;
}
}
return targets ;
} ) ;
} ;
/ *
* Gets unused port for android emulator , between 5554 and 5584
* Returns a promise .
* /
module . exports . get _available _port = function ( ) {
var self = this ;
return self . list _started ( ) . then ( function ( emulators ) {
for ( var p = 5584 ; p >= 5554 ; p -= 2 ) {
if ( emulators . indexOf ( 'emulator-' + p ) === - 1 ) {
events . emit ( 'verbose' , 'Found available port: ' + p ) ;
return p ;
}
}
throw new CordovaError ( 'Could not find an available avd port' ) ;
} ) ;
} ;
/ *
* Starts an emulator with the given ID ,
* and returns the started ID of that emulator .
* If no ID is given it will use the first image available ,
* if no image is available it will error out ( maybe create one ? ) .
* If no boot timeout is given or the value is negative it will wait forever for
* the emulator to boot
*
* Returns a promise .
* /
module . exports . start = function ( emulator _ID , boot _timeout ) {
var self = this ;
2019-04-09 20:33:36 -04:00
return Promise . resolve ( ) . then ( function ( ) {
if ( emulator _ID ) return Promise . resolve ( emulator _ID ) ;
2018-10-07 21:10:38 -03:00
return self . best _image ( ) . then ( function ( best ) {
if ( best && best . name ) {
events . emit ( 'warn' , 'No emulator specified, defaulting to ' + best . name ) ;
return best . name ;
}
var androidCmd = check _reqs . getAbsoluteAndroidCmd ( ) ;
2019-04-09 20:33:36 -04:00
return Promise . reject ( new CordovaError ( 'No emulator images (avds) found.\n' +
2018-10-07 21:10:38 -03:00
'1. Download desired System Image by running: ' + androidCmd + ' sdk\n' +
'2. Create an AVD by running: ' + androidCmd + ' avd\n' +
'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n' ) ) ;
} ) ;
} ) . then ( function ( emulatorId ) {
return self . get _available _port ( ) . then ( function ( port ) {
// Figure out the directory the emulator binary runs in, and set the cwd to that directory.
// Workaround for https://code.google.com/p/android/issues/detail?id=235461
var emulator _dir = path . dirname ( shelljs . which ( 'emulator' ) ) ;
var args = [ '-avd' , emulatorId , '-port' , port ] ;
// Don't wait for it to finish, since the emulator will probably keep running for a long time.
child _process
. spawn ( 'emulator' , args , { stdio : 'inherit' , detached : true , cwd : emulator _dir } )
. unref ( ) ;
// wait for emulator to start
events . emit ( 'log' , 'Waiting for emulator to start...' ) ;
return self . wait _for _emulator ( port ) ;
} ) ;
} ) . then ( function ( emulatorId ) {
2019-04-09 20:33:36 -04:00
if ( ! emulatorId ) { return Promise . reject ( new CordovaError ( 'Failed to start emulator' ) ) ; }
2018-10-07 21:10:38 -03:00
// wait for emulator to boot up
process . stdout . write ( 'Waiting for emulator to boot (this may take a while)...' ) ;
return self . wait _for _boot ( emulatorId , boot _timeout ) . then ( function ( success ) {
if ( success ) {
events . emit ( 'log' , 'BOOT COMPLETE' ) ;
// unlock screen
return Adb . shell ( emulatorId , 'input keyevent 82' ) . then ( function ( ) {
// return the new emulator id for the started emulators
return emulatorId ;
} ) ;
} else {
// We timed out waiting for the boot to happen
return null ;
}
} ) ;
} ) ;
} ;
/ *
* Waits for an emulator to boot on a given port .
* Returns this emulator ' s ID in a promise .
* /
module . exports . wait _for _emulator = function ( port ) {
var self = this ;
2019-04-09 20:33:36 -04:00
return Promise . resolve ( ) . then ( function ( ) {
2018-10-07 21:10:38 -03:00
var emulator _id = 'emulator-' + port ;
return Adb . shell ( emulator _id , 'getprop dev.bootcomplete' ) . then ( function ( output ) {
if ( output . indexOf ( '1' ) >= 0 ) {
return emulator _id ;
}
return self . wait _for _emulator ( port ) ;
} , function ( error ) {
if ( ( error && error . message &&
( error . message . indexOf ( 'not found' ) > - 1 ) ) ||
2019-03-09 14:12:15 -03:00
( error . message . indexOf ( 'device offline' ) > - 1 ) ||
2019-04-09 20:33:36 -04:00
( error . message . indexOf ( 'device still connecting' ) > - 1 ) ||
( error . message . indexOf ( 'device still authorizing' ) > - 1 ) ) {
2018-10-07 21:10:38 -03:00
// emulator not yet started, continue waiting
return self . wait _for _emulator ( port ) ;
} else {
// something unexpected has happened
throw error ;
}
} ) ;
} ) ;
} ;
/ *
* Waits for the core android process of the emulator to start . Returns a
* promise that resolves to a boolean indicating success . Not specifying a
* time _remaining or passing a negative value will cause it to wait forever
* /
module . exports . wait _for _boot = function ( emulator _id , time _remaining ) {
var self = this ;
return Adb . shell ( emulator _id , 'ps' ) . then ( function ( output ) {
if ( output . match ( /android\.process\.acore/ ) ) {
return true ;
} else if ( time _remaining === 0 ) {
return false ;
} else {
process . stdout . write ( '.' ) ;
2019-04-09 20:33:36 -04:00
return new Promise ( resolve => {
const delay = time _remaining < CHECK _BOOTED _INTERVAL ? time _remaining : CHECK _BOOTED _INTERVAL ;
setTimeout ( ( ) => {
const updated _time = time _remaining >= 0 ? Math . max ( time _remaining - CHECK _BOOTED _INTERVAL , 0 ) : time _remaining ;
resolve ( self . wait _for _boot ( emulator _id , updated _time ) ) ;
} , delay ) ;
2018-10-07 21:10:38 -03:00
} ) ;
}
} ) ;
} ;
/ *
* Create avd
* TODO : Enter the stdin input required to complete the creation of an avd .
* Returns a promise .
* /
module . exports . create _image = function ( name , target ) {
console . log ( 'Creating new avd named ' + name ) ;
if ( target ) {
return superspawn . spawn ( 'android' , [ 'create' , 'avd' , '--name' , name , '--target' , target ] ) . then ( null , function ( error ) {
console . error ( 'ERROR : Failed to create emulator image : ' ) ;
console . error ( ' Do you have the latest android targets including ' + target + '?' ) ;
console . error ( error ) ;
} ) ;
} else {
console . log ( 'WARNING : Project target not found, creating avd with a different target but the project may fail to install.' ) ;
// TODO: there's a more robust method for finding targets in android_sdk.js
return superspawn . spawn ( 'android' , [ 'create' , 'avd' , '--name' , name , '--target' , this . list _targets ( ) [ 0 ] ] ) . then ( function ( ) {
// TODO: This seems like another error case, even though it always happens.
console . error ( 'ERROR : Unable to create an avd emulator, no targets found.' ) ;
console . error ( 'Ensure you have targets available by running the "android" command' ) ;
2019-04-09 20:33:36 -04:00
return Promise . reject ( new CordovaError ( ) ) ;
2018-10-07 21:10:38 -03:00
} , function ( error ) {
console . error ( 'ERROR : Failed to create emulator image : ' ) ;
console . error ( error ) ;
} ) ;
}
} ;
module . exports . resolveTarget = function ( target ) {
return this . list _started ( ) . then ( function ( emulator _list ) {
if ( emulator _list . length < 1 ) {
2019-04-09 20:33:36 -04:00
return Promise . reject ( new CordovaError ( 'No running Android emulators found, please start an emulator before deploying your project.' ) ) ;
2018-10-07 21:10:38 -03:00
}
// default emulator
target = target || emulator _list [ 0 ] ;
if ( emulator _list . indexOf ( target ) < 0 ) {
2019-04-09 20:33:36 -04:00
return Promise . reject ( new CordovaError ( 'Unable to find target \'' + target + '\'. Failed to deploy to emulator.' ) ) ;
2018-10-07 21:10:38 -03:00
}
return build . detectArchitecture ( target ) . then ( function ( arch ) {
2019-04-09 20:33:36 -04:00
return { target : target , arch : arch , isEmulator : true } ;
2018-10-07 21:10:38 -03:00
} ) ;
} ) ;
} ;
/ *
* Installs a previously built application on the emulator and launches it .
* If no target is specified , then it picks one .
* If no started emulators are found , error out .
* Returns a promise .
* /
module . exports . install = function ( givenTarget , buildResults ) {
var target ;
// We need to find the proper path to the Android Manifest
2019-04-09 20:33:36 -04:00
const manifestPath = path . join ( _ _dirname , '..' , '..' , 'app' , 'src' , 'main' , 'AndroidManifest.xml' ) ;
const manifest = new AndroidManifest ( manifestPath ) ;
const pkgName = manifest . getPackageId ( ) ;
2018-10-07 21:10:38 -03:00
// resolve the target emulator
2019-04-09 20:33:36 -04:00
return Promise . resolve ( ) . then ( function ( ) {
2018-10-07 21:10:38 -03:00
if ( givenTarget && typeof givenTarget === 'object' ) {
return givenTarget ;
} else {
return module . exports . resolveTarget ( givenTarget ) ;
}
// set the resolved target
} ) . then ( function ( resolvedTarget ) {
target = resolvedTarget ;
// install the app
} ) . then ( function ( ) {
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed.
2019-04-09 20:33:36 -04:00
return Promise . resolve ( ) . then ( function ( ) {
2018-10-07 21:10:38 -03:00
var apk _path = build . findBestApkForArchitecture ( buildResults , target . arch ) ;
var execOptions = {
cwd : os . tmpdir ( ) ,
timeout : INSTALL _COMMAND _TIMEOUT , // in milliseconds
killSignal : EXEC _KILL _SIGNAL
} ;
events . emit ( 'log' , 'Using apk: ' + apk _path ) ;
events . emit ( 'log' , 'Package name: ' + pkgName ) ;
events . emit ( 'verbose' , 'Installing app on emulator...' ) ;
// A special function to call adb install in specific environment w/ specific options.
// Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
// to workaround sporadic emulator hangs
function adbInstallWithOptions ( target , apk , opts ) {
events . emit ( 'verbose' , 'Installing apk ' + apk + ' on ' + target + '...' ) ;
var command = 'adb -s ' + target + ' install -r "' + apk + '"' ;
2019-04-09 20:33:36 -04:00
return new Promise ( function ( resolve , reject ) {
2018-10-07 21:10:38 -03:00
child _process . exec ( command , opts , function ( err , stdout , stderr ) {
if ( err ) reject ( new CordovaError ( 'Error executing "' + command + '": ' + stderr ) ) ;
// adb does not return an error code even if installation fails. Instead it puts a specific
// message to stdout, so we have to use RegExp matching to detect installation failure.
else if ( /Failure/ . test ( stdout ) ) {
if ( stdout . match ( /INSTALL_PARSE_FAILED_NO_CERTIFICATES/ ) ) {
stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
' or sign and deploy the unsigned apk manually using Android tools.' ;
} else if ( stdout . match ( /INSTALL_FAILED_VERSION_DOWNGRADE/ ) ) {
stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
'\nEither uninstall an app or increment the versionCode.' ;
}
reject ( new CordovaError ( 'Failed to install apk to emulator: ' + stdout ) ) ;
} else resolve ( stdout ) ;
} ) ;
} ) ;
}
function installPromise ( ) {
return adbInstallWithOptions ( target . target , apk _path , execOptions ) . catch ( function ( error ) {
// CB-9557 CB-10157 only uninstall and reinstall app if the one that
// is already installed on device was signed w/different certificate
if ( ! /INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/ . test ( error . toString ( ) ) ) { throw error ; }
events . emit ( 'warn' , 'Uninstalling app from device and reinstalling it because the ' +
'currently installed app was signed with different key' ) ;
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed.
return Adb . uninstall ( target . target , pkgName ) . then ( function ( ) {
return adbInstallWithOptions ( target . target , apk _path , execOptions ) ;
} ) ;
} ) ;
}
return retry . retryPromise ( NUM _INSTALL _RETRIES , installPromise ) . then ( function ( output ) {
events . emit ( 'log' , 'INSTALL SUCCESS' ) ;
} ) ;
} ) ;
// unlock screen
} ) . then ( function ( ) {
events . emit ( 'verbose' , 'Unlocking screen...' ) ;
return Adb . shell ( target . target , 'input keyevent 82' ) ;
} ) . then ( function ( ) {
Adb . start ( target . target , pkgName + '/.' + manifest . getActivity ( ) . getName ( ) ) ;
// report success or failure
} ) . then ( function ( output ) {
events . emit ( 'log' , 'LAUNCH SUCCESS' ) ;
} ) ;
} ;