import org.gradle.internal.os.OperatingSystem; import com.android.ddmlib.AndroidDebugBridge import com.android.ddmlib.IDevice import com.android.ddmlib.CollectingOutputReceiver import org.apache.tools.ant.taskdefs.condition.Os buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.0.0' } tasks.register("wrapper") } class XrAppPlugin implements Plugin { Project project = null void installApk( IDevice device, File apkFile, String applicationId ) { project.logger.quiet "Installing ${applicationId} on device ${device.serialNumber}" String toinstall = "/data/local/tmp/toinstall.apk" try { device.pushFile( apkFile.path, toinstall ) } catch ( Exception e ) { throw new RuntimeException( "Failed to push ${apkFile.path} to ${toinstall}. ${e}", e ) } while ( true ) { try { device.installRemotePackage( toinstall, true ) break } catch ( Exception e ) { project.logger.quiet "Failed to install ${applicationId} on device ${device.serialNumber} (${e}). Trying to uninstall first." } try { device.uninstallPackage( applicationId ) } catch ( Exception e ) { throw new RuntimeException( "Failed to uninstall ${applicationId}. ${e}", e ) } } } void stopApk( IDevice device, String packageName ) { CollectingOutputReceiver receiver = new CollectingOutputReceiver() device.executeShellCommand( "am force-stop ${packageName}", receiver ) } void runApk( IDevice device, manifestFile ) { CollectingOutputReceiver receiver = new CollectingOutputReceiver() def activityClass = new XmlSlurper().parse( manifestFile ).application.activity.find{ it.'intent-filter'.find{ filter -> return filter.action .find{it.'@android:name'.text() == 'android.intent.action.MAIN' } \ && ( filter.category.find{it.'@android:name'.text() == 'android.intent.category.LAUNCHER'} \ || filter.category.find{it.'@android:name'.text() == 'android.intent.category.INFO'} ) }}.'@android:name' def startActivity = "${project.android.defaultConfig.applicationId}/${activityClass}" project.logger.quiet "Starting \'$startActivity\' on ${project.deviceMap.size()} devices:" project.logger.quiet "- ${device.serialNumber}" device.executeShellCommand( "am start $startActivity", receiver ) } void apply( Project project ) { this.project = project // FIXME: The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead. project.task( "cleanWorkAround" ) { description "Workaround for .externalNativeBuild not being deleted on clean" }.doLast { project.delete project.file( ".externalNativeBuild" ) } project.android { compileSdkVersion 26 defaultConfig { minSdkVersion 24 targetSdkVersion 25 externalNativeBuild { ndk { } ndkBuild { def numProcs = Runtime.runtime.availableProcessors() arguments "V=0", "-j$numProcs", "-C$project.buildDir.parent", "APP_PLATFORM=android-24", "NDK_TOOLCHAIN_VERSION=clang", "APP_STL=c++_static" } } } externalNativeBuild { ndkBuild { path 'jni/Android.mk' } } signingConfigs { def keystorePath = (project.hasProperty('key.store')) ? new File(project.getProperty('key.store')) : project.file('android.debug.keystore') def keystorePassword = (project.hasProperty('key.store.password')) ? project.getProperty('key.store.password') : 'android' def keystoreKeyAlias = (project.hasProperty('key.alias')) ? project.getProperty('key.alias') : 'androiddebugkey' def keystoreKeyPassword = (project.hasProperty('key.alias.password')) ? project.getProperty('key.alias.password') : 'android' debug { storeFile keystorePath storePassword keystorePassword keyAlias keystoreKeyAlias keyPassword keystoreKeyPassword v2SigningEnabled true } release { storeFile keystorePath storePassword keystorePassword keyAlias keystoreKeyAlias keyPassword keystoreKeyPassword v2SigningEnabled true } } buildTypes { debug { signingConfig signingConfigs.debug debuggable true jniDebuggable true externalNativeBuild { ndkBuild { arguments "NDK_DEBUG=1","USE_ASAN=1" } } } release { signingConfig signingConfigs.release debuggable false jniDebuggable false externalNativeBuild { ndkBuild { arguments "NDK_DEBUG=0","USE_ASAN=0" } } } } } // WORKAROUND: On Mac OS X, running ndk-build clean with a high num of parallel executions // set may result in the following build error: rm: fts_read: No such file or directory. // Currently, there isn't a good way to specify numProcs=1 only on clean. So, in order // to work around the issue, delete the auto-generated .externalNativeBuild artifacts // (where $numProcs specified) before executing the clean task. project.clean.dependsOn project.cleanWorkAround project.clean { // remove the auto-generated debug keystore (currently generated by python build script) // delete "android.debug.keystore" } project.afterEvaluate { Task initDeviceList = project.task( "initDeviceList()" ).doLast { project.ext.deviceMap = [ : ] if (project.hasProperty( "should_install" ) == true) { AndroidDebugBridge.initIfNeeded( false ) AndroidDebugBridge bridge = AndroidDebugBridge.createBridge( project.android.getAdbExe().absolutePath, false ) long timeOut = 30000 // 30 sec int sleepTime = 1000 while ( !bridge.hasInitialDeviceList() && timeOut > 0 ) { sleep( sleepTime ) timeOut -= sleepTime } if ( timeOut <= 0 && !bridge.hasInitialDeviceList() ) { throw new RuntimeException( "Timeout getting device list.", null ) } // if a device is connected both physically and over the network, only include the physical ID if ( project.hasProperty( "should_install" ) == true ) { bridge.devices.split { it.getProperty( "ro.serialno" ) != it.serialNumber }.each { it.collectEntries( project.deviceMap, { [ ( it.getProperty( "ro.serialno" )) : it ] } ) } } } } project.task( "stopApk", dependsOn: initDeviceList ) { description "Stops app if currently running on device" }.doLast { project.deviceMap.each { deviceSerial, device -> stopApk( device, android.defaultConfig.applicationId ) } } project.android.applicationVariants.all { variant -> Task installAndRun = project.task( "installAndRun${variant.name.capitalize()}" ) { dependsOn variant.assembleProvider.get() dependsOn initDeviceList onlyIf { project.hasProperty( "should_install" ) } description "Installs and runs the APK file" }.doLast { variant.outputs.each { output -> if ( output.outputFile.exists() ) { if ( project.deviceMap.size() == 0 ) { project.logger.quiet "Install requested, but no devices found." } else { project.deviceMap.each { deviceSerial, device -> installApk( device, output.outputFile, project.android.defaultConfig.applicationId ) runApk( device, new File(output.processManifest.manifestOutputDirectory.get().asFile, "AndroidManifest.xml")) } } } } } variant.assembleProvider.get().finalizedBy installAndRun } } } } // Workaround to fix issue in Android Studio Chipmunk 2021.2.1 and later // where opening a project would result in a 'prepareKotlinBuildScriptModel' // not found error if (!tasks.findByName("prepareKotlinBuildScriptModel")) { tasks.register("prepareKotlinBuildScriptModel") {} } apply plugin: XrAppPlugin