From b83031506adce381d0aac5c64dbdbd9645b5a888 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Mon, 6 Sep 2021 12:35:34 +0200 Subject: [PATCH 1/4] Workaround for failing to install multiple variants on the emulator --- Jenkinsfile | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a187a7b99f..b1bf8bf960 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,7 +26,7 @@ mongoDbRealmCommandServerContainer = null emulatorContainer = null dockerNetworkId = UUID.randomUUID().toString() currentBranch = (env.CHANGE_BRANCH == null) ? env.BRANCH_NAME : env.CHANGE_BRANCH -isReleaseBranch = releaseBranches.contains(currentBranch) +isReleaseBranch = true // FIXME After testing releaseBranches.contains(currentBranch) // FIXME: Always used the emulator until we can enable more reliable devices // 'android' nodes have android devices attached and 'brix' are physical machines in Copenhagen. // nodeSelector = (releaseBranches.contains(currentBranch)) ? 'android' : 'docker-cph-03' // Switch to `brix` when all CPH nodes work: https://jira.mongodb.org/browse/RCI-14 @@ -82,7 +82,7 @@ try { def useEmulator = false def emulatorImage = "" def buildFlags = "" - def instrumentationTestTarget = "connectedAndroidTest" + def instrumentationTestTarget = ['connectedBaseDebugAndroidTest', 'connectedObjectServerDebugAndroidTest'] def deviceSerial = "" if (!isReleaseBranch) { @@ -91,7 +91,7 @@ try { emulatorImage = "system-images;android-29;default;x86" // Build core from source instead of doing it from binary buildFlags = "-PbuildTargetABIs=x86 -PenableLTO=false -PbuildCore=true" - instrumentationTestTarget = "connectedObjectServerDebugAndroidTest" + instrumentationTestTargets = ['connectedObjectServerDebugAndroidTest'] deviceSerial = "emulator-5554" } else { // Build main/release branch @@ -100,7 +100,7 @@ try { useEmulator = true emulatorImage = "system-images;android-29;default;x86" buildFlags = "-PenableLTO=true -PbuildCore=true" - instrumentationTestTarget = "connectedAndroidTest" + instrumentationTestTargets = ['connectedBaseDebugAndroidTest', 'connectedObjectServerDebugAndroidTest'] deviceSerial = "emulator-5554" } @@ -161,12 +161,12 @@ try { // Need to go to ANDROID_HOME due to https://askubuntu.com/questions/1005944/emulator-avd-does-not-launch-the-virtual-device sh "cd \$ANDROID_HOME/tools && emulator -avd CIEmulator -no-boot-anim -no-window -wipe-data -noaudio -partition-size 4098 &" try { - runBuild(buildFlags, instrumentationTestTarget) + runBuild(buildFlags, instrumentationTestTargets) } finally { sh "adb emu kill" } } else { - runBuild(buildFlags, instrumentationTestTarget) + runBuild(buildFlags, instrumentationTestTargets) } // Release the library if needed @@ -224,7 +224,7 @@ try { } // Runs all build steps -def runBuild(buildFlags, instrumentationTestTarget) { +def runBuild(buildFlags, instrumentationTestTargets) { stage('Build') { withCredentials([ @@ -321,7 +321,12 @@ def runBuild(buildFlags, instrumentationTestTarget) { try { backgroundPid = startLogCatCollector() forwardAdbPorts() - gradle('realm', "${instrumentationTestTarget} ${buildFlags}") + instrumentationTestTargets.each { target -> + // Attempt to work around com.android.ddmlib.InstallException, which installing + // multiple variants for tests. + sh "adb uninstall io.realm.test || true" + gradle('realm', "${target} ${buildFlags}") + } } finally { stopLogCatCollector(backgroundPid) storeJunitResults 'realm/realm-library/build/outputs/androidTest-results/connected/**/TEST-*.xml' From f729183ee1be0627a31d1e94ed20388bff6b8ad9 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Mon, 6 Sep 2021 22:49:03 +0200 Subject: [PATCH 2/4] Increase ADB timeout --- realm/realm-library/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/realm/realm-library/build.gradle b/realm/realm-library/build.gradle index ff9388d2eb..4d1d6e0472 100644 --- a/realm/realm-library/build.gradle +++ b/realm/realm-library/build.gradle @@ -114,6 +114,10 @@ android { abortOnError false } + adbOptions { + timeOutInMs 5 * 60 * 1000 // 5 minutes + } + flavorDimensions 'api' productFlavors { From 4b98e6ebf7e22b98d6c1398bcb58745943aa6dfb Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Tue, 7 Sep 2021 08:38:22 +0200 Subject: [PATCH 3/4] More ADB trickery --- Jenkinsfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index b1bf8bf960..9ef6ca51d3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -157,7 +157,9 @@ try { // TODO: We should wait until the emulator is online. For now assume it starts fast enough // before the tests will run, since the library needs to build first. sh """yes '\n' | avdmanager create avd -n CIEmulator -k '${emulatorImage}' --force""" + sh "adb kill-server" // https://stackoverflow.com/questions/56198290/problems-with-adb-exe sh "adb start-server" // https://stackoverflow.com/questions/56198290/problems-with-adb-exe + sh "adb root" // Need to go to ANDROID_HOME due to https://askubuntu.com/questions/1005944/emulator-avd-does-not-launch-the-virtual-device sh "cd \$ANDROID_HOME/tools && emulator -avd CIEmulator -no-boot-anim -no-window -wipe-data -noaudio -partition-size 4098 &" try { @@ -395,8 +397,7 @@ String startLogCatCollector() { timeout(time: 1, unit: 'MINUTES') { // Need ADB as root to clear all buffers: https://stackoverflow.com/a/47686978/1389357 sh 'adb devices' - sh """adb root - adb logcat -b all -c + sh """adb logcat -b all -c adb logcat -v time > 'logcat.txt' & echo \$! > pid """ From 476fa612e2582c1a0c2d6789317208ba147253d9 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 15 Sep 2021 18:55:15 +0200 Subject: [PATCH 4/4] Add support for Gradle Configuration Cache --- CHANGELOG.md | 2 +- gradle-plugin/build.gradle | 19 +++ .../main/groovy/io/realm/gradle/Realm.groovy | 5 +- realm-transformer/build.gradle | 9 +- .../ComputerIdentifierGenerator.java | 116 -------------- .../io/realm/transformer/RealmAnalytics.java | 124 --------------- .../transformer/UrlEncodedAnalytics.java | 55 ------- .../io/realm/analytics/AnalyticsData.kt | 91 +++++++++++ .../analytics/ComputerIdentifierGenerator.kt | 101 ++++++++++++ .../io/realm/analytics/RealmAnalytics.kt | 150 ++++++++++++++++++ .../io/realm/analytics/UrlEncodedAnalytics.kt | 61 +++++++ .../io/realm/transformer/RealmTransformer.kt | 98 ++++-------- .../realm/transformer/build/BuildTemplate.kt | 9 +- .../io/realm/transformer/build/FullBuild.kt | 5 +- .../transformer/build/IncrementalBuild.kt | 6 +- .../io/realm/transformer/ext/ProjectExt.kt | 47 +++++- 16 files changed, 518 insertions(+), 380 deletions(-) delete mode 100644 realm-transformer/src/main/java/io/realm/transformer/ComputerIdentifierGenerator.java delete mode 100644 realm-transformer/src/main/java/io/realm/transformer/RealmAnalytics.java delete mode 100644 realm-transformer/src/main/java/io/realm/transformer/UrlEncodedAnalytics.java create mode 100644 realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt create mode 100644 realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt create mode 100644 realm-transformer/src/main/kotlin/io/realm/analytics/RealmAnalytics.kt create mode 100644 realm-transformer/src/main/kotlin/io/realm/analytics/UrlEncodedAnalytics.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eef013558..b645231e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 10.8.1 (YYYY-MM-DD) ### Enhancements -* None. +* The Realm transformer now supports the Gradle Configuration Cache. (Isse [#7299](https://github.com/realm/realm-java/issues/7299)) ### Fixed * [RealmApp] Failing to refresh the access token due to a 401/403 error will now correctly emit an error with `ErrorCode.BAD_AUTHENTICATION` rather than `ErrorCode.PERMISSION_DENIED`. (Realm Core [#4881](https://github.com/realm/realm-core/issues/4881), since 10.6.1) diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle index b038c15b8a..8af8e9ce01 100644 --- a/gradle-plugin/build.gradle +++ b/gradle-plugin/build.gradle @@ -1,6 +1,9 @@ +import org.gradle.api.internal.classpath.ModuleRegistry + buildscript { def properties = new Properties() properties.load(new FileInputStream("${rootDir}/../dependencies.list")) + ext.kotlin_version = properties.get('KOTLIN') repositories { jcenter() @@ -11,9 +14,11 @@ buildscript { dependencies { classpath "org.jfrog.buildinfo:build-info-extractor-gradle:${properties.get('BUILD_INFO_EXTRACTOR_GRADLE')}" classpath "io.github.gradle-nexus:publish-plugin:${properties.get("GRADLE_NEXUS_PLUGIN")}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } +apply plugin: 'kotlin' apply plugin: 'groovy' apply plugin: 'maven' @@ -64,10 +69,24 @@ dependencies { testCompile gradleTestKit() testCompile 'junit:junit:4.12' testCompile "com.android.tools.build:gradle:${props.get("GRADLE_BUILD_TOOLS")}" + + // See https://github.com/gradle/gradle/issues/16774#issuecomment-893493869 + def toolingApiBuildersJar = (project as ProjectInternal).services.get(ModuleRegistry.class) + .getModule("gradle-tooling-api-builders") + .classpath + .asFiles + .first() + testRuntimeOnly(files(toolingApiBuildersJar)) +} + +compileGroovy { + dependsOn tasks.getByPath('compileKotlin') + classpath += files(compileKotlin.destinationDir) } //for Ant filter import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.internal.project.ProjectInternal task generateVersionClass(type: Copy) { from 'src/main/templates/Version.java' diff --git a/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy b/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy index cbd699e394..5675799f91 100644 --- a/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy +++ b/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy @@ -19,7 +19,6 @@ package io.realm.gradle import com.android.build.gradle.AppPlugin import com.android.build.gradle.LibraryPlugin import com.neenbedankt.gradle.androidapt.AndroidAptPlugin -import io.realm.gradle.RealmPluginExtension import io.realm.transformer.RealmTransformer import org.gradle.api.GradleException import org.gradle.api.Plugin @@ -70,6 +69,10 @@ class Realm implements Plugin { usesAptPlugin = true } + // Register transformer during the evaluations phase, so the Android Plugin + // is able to pick it up. The project is passed in in order to gather various + // metadata in `project.afterEvaluate { }`, but the transformer is not allowed + // to store a reference to it if we want to support the Gradle Configuration Cache. project.android.registerTransform(new RealmTransformer(project)) project.dependencies.add(dependencyConfigurationName, "io.realm:realm-annotations:${Version.VERSION}") diff --git a/realm-transformer/build.gradle b/realm-transformer/build.gradle index 8d7df5c5a0..69bca1bce3 100644 --- a/realm-transformer/build.gradle +++ b/realm-transformer/build.gradle @@ -53,7 +53,6 @@ dependencies { compile gradleApi() compile "io.realm:realm-annotations:${version}" compileOnly "com.android.tools.build:gradle:${properties.get("GRADLE_BUILD_TOOLS")}" - compileOnly 'com.android.tools.build:gradle:3.1.1' compile 'org.javassist:javassist:3.25.0-GA' compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" @@ -70,7 +69,7 @@ task generateVersionClass(type: Copy) { outputs.upToDateWhen { false } } -compileJava.dependsOn generateVersionClass +compileKotlin.dependsOn generateVersionClass apply from: "${rootDir}/../mavencentral-publications.gradle" apply from: "${rootDir}/../mavencentral-publish.gradle" @@ -94,3 +93,9 @@ java { withSourcesJar() withJavadocJar() } + +compileKotlin { + kotlinOptions { + freeCompilerArgs = ["-Xinline-classes"] + } +} diff --git a/realm-transformer/src/main/java/io/realm/transformer/ComputerIdentifierGenerator.java b/realm-transformer/src/main/java/io/realm/transformer/ComputerIdentifierGenerator.java deleted file mode 100644 index aa32099d22..0000000000 --- a/realm-transformer/src/main/java/io/realm/transformer/ComputerIdentifierGenerator.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016 Realm Inc. - * - * 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. - */ - -package io.realm.transformer; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.security.NoSuchAlgorithmException; -import java.util.Scanner; - -/** - * Generate a unique identifier for a computer. The method being used depends on the platform: - * - OS X: Mac address of en0 - * - Windows: BIOS identifier - * - Linux: Machine ID provided by the OS - */ -public class ComputerIdentifierGenerator { - - private static final String UNKNOWN = "unknown"; - - private static String OS = System.getProperty("os.name").toLowerCase(); - - public static String get() { - try { - if (isWindows()) { - return getWindowsIdentifier(); - } else if (isMac()) { - return getMacOsIdentifier(); - } else if (isLinux()) { - return getLinuxMacAddress(); - } else { - return UNKNOWN; - } - } catch (Exception e) { - return UNKNOWN; - } - } - - private static boolean isWindows() { - return (OS.contains("win")); - } - - private static boolean isMac() { - return (OS.contains("mac")); - } - - private static boolean isLinux() { - return (OS.contains("inux")); - } - - private static String getLinuxMacAddress() throws FileNotFoundException, NoSuchAlgorithmException { - File machineId = new File("/var/lib/dbus/machine-id"); - if (!machineId.exists()) { - machineId = new File("/etc/machine-id"); - } - if (!machineId.exists()) { - return UNKNOWN; - } - - Scanner scanner = null; - try { - scanner = new Scanner(machineId); - String id = scanner.useDelimiter("\\A").next(); - return Utils.hexStringify(Utils.sha256Hash(id.getBytes())); - } finally { - if (scanner != null) { - scanner.close(); - } - } - } - - private static String getMacOsIdentifier() throws SocketException, NoSuchAlgorithmException { - NetworkInterface networkInterface = NetworkInterface.getByName("en0"); - byte[] hardwareAddress = networkInterface.getHardwareAddress(); - return Utils.hexStringify(Utils.sha256Hash(hardwareAddress)); - } - - private static String getWindowsIdentifier() throws IOException, NoSuchAlgorithmException { - Runtime runtime = Runtime.getRuntime(); - Process process = runtime.exec(new String[] { "wmic", "csproduct", "get", "UUID" }); - - String result = null; - InputStream is = process.getInputStream(); - Scanner sc = new Scanner(process.getInputStream()); - try { - while (sc.hasNext()) { - String next = sc.next(); - if (next.contains("UUID")) { - result = sc.next().trim(); - break; - } - } - } finally { - is.close(); - } - - return result==null?UNKNOWN:Utils.hexStringify(Utils.sha256Hash(result.getBytes())); - } -} diff --git a/realm-transformer/src/main/java/io/realm/transformer/RealmAnalytics.java b/realm-transformer/src/main/java/io/realm/transformer/RealmAnalytics.java deleted file mode 100644 index 56a0c8df29..0000000000 --- a/realm-transformer/src/main/java/io/realm/transformer/RealmAnalytics.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2015 Realm Inc. - * - * 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. - */ - -package io.realm.transformer; - -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketException; -import java.net.URL; -import java.security.NoSuchAlgorithmException; -import java.util.Set; - -// Asynchronously submits build information to Realm when the annotation -// processor is running -// -// To be clear: this does *not* run when your app is in production or on -// your end-user's devices; it will only run when you build your app from source. -// -// Why are we doing this? Because it helps us build a better product for you. -// None of the data personally identifies you, your employer or your app, but it -// *will* help us understand what Realm version you use, what host OS you use, -// etc. Having this info will help with prioritizing our time, adding new -// features and deprecating old features. Collecting an anonymized bundle & -// anonymized MAC is the only way for us to count actual usage of the other -// metrics accurately. If we don't have a way to deduplicate the info reported, -// it will be useless, as a single developer building their app on Windows ten -// times would report 10 times more than a single developer that only builds -// once from Mac OS X, making the data all but useless. No one likes sharing -// data unless it's necessary, we get it, and we've debated adding this for a -// long long time. Since Realm is a free product without an email signup, we -// feel this is a necessary step so we can collect relevant data to build a -// better product for you. -// -// Currently the following information is reported: -// - What version of Realm is being used -// - What OS you are running on -// - An anonymized MAC address and bundle ID to aggregate the other information on. -public class RealmAnalytics { - private static final String TOKEN = "ce0fac19508f6c8f20066d345d360fd0"; - private static final String EVENT_NAME = "Run"; - private static final String JSON_TEMPLATE - = "{\n" - + " \"event\": \"%EVENT%\",\n" - + " \"properties\": {\n" - + " \"token\": \"%TOKEN%\",\n" - + " \"distinct_id\": \"%USER_ID%\",\n" - + " \"Anonymized MAC Address\": \"%USER_ID%\",\n" - + " \"Anonymized Bundle ID\": \"%APP_ID%\",\n" - + " \"Binding\": \"java\",\n" - + " \"Target\": \"%TARGET%\",\n" - + " \"Language\": \"%LANGUAGE%\",\n" - + " \"Sync Version\": %SYNC_VERSION%,\n" - + " \"Realm Version\": \"%REALM_VERSION%\",\n" - + " \"Host OS Type\": \"%OS_TYPE%\",\n" - + " \"Host OS Version\": \"%OS_VERSION%\",\n" - + " \"Target OS Type\": \"android\",\n" - + " \"Target OS Version\": \"%TARGET_SDK%\",\n" - + " \"Target OS Minimum Version\": \"%MIN_SDK%\"\n" - + " }\n" - + "}"; - - // The list of packages the model classes reside in - private Set packages; - - private boolean usesKotlin; - private boolean usesSync; - private String targetSdk; - private String minSdk; - private String target;; - - public RealmAnalytics(Set packages, boolean usesKotlin, boolean usesSync, String targetSdk, String minSdk, String target) { - this.packages = packages; - this.usesKotlin = usesKotlin; - this.usesSync = usesSync; - this.targetSdk = targetSdk; - this.minSdk = minSdk; - this.target = target; - } - - public String generateJson() throws SocketException, NoSuchAlgorithmException { - return JSON_TEMPLATE - .replaceAll("%EVENT%", EVENT_NAME) - .replaceAll("%TOKEN%", TOKEN) - .replaceAll("%USER_ID%", ComputerIdentifierGenerator.get()) - .replaceAll("%APP_ID%", getAnonymousAppId()) - .replaceAll("%TARGET%", target) - .replaceAll("%LANGUAGE%", usesKotlin ? "kotlin" : "java") - .replaceAll("%SYNC_VERSION%", usesSync ? "\"" + Version.SYNC_VERSION + "\"": "null") - .replaceAll("%REALM_VERSION%", Version.VERSION) - .replaceAll("%OS_TYPE%", System.getProperty("os.name")) - .replaceAll("%OS_VERSION%", System.getProperty("os.version")) - .replaceAll("%TARGET_SDK%", targetSdk) - .replaceAll("%MIN_SDK%", minSdk); - } - - /** - * Computes an anonymous app/library id from the packages containing RealmObject classes - * @return the anonymous app/library id - * @throws NoSuchAlgorithmException - */ - public String getAnonymousAppId() throws NoSuchAlgorithmException { - StringBuilder stringBuilder = new StringBuilder(); - for (String modelPackage : packages) { - stringBuilder.append(modelPackage).append(":"); - } - byte[] packagesBytes = stringBuilder.toString().getBytes(); - - return Utils.hexStringify(Utils.sha256Hash(packagesBytes)); - } -} diff --git a/realm-transformer/src/main/java/io/realm/transformer/UrlEncodedAnalytics.java b/realm-transformer/src/main/java/io/realm/transformer/UrlEncodedAnalytics.java deleted file mode 100644 index e53de802cd..0000000000 --- a/realm-transformer/src/main/java/io/realm/transformer/UrlEncodedAnalytics.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.realm.transformer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketException; -import java.net.URL; -import java.security.NoSuchAlgorithmException; - - - -public class UrlEncodedAnalytics { - - private String prefix; - private String suffix; - - public UrlEncodedAnalytics(String prefix, String suffix) { - this.prefix = prefix; - this.suffix = suffix; - } - - public void execute(RealmAnalytics analytics) { - try { - URL url = getUrl(analytics); - - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.connect(); - connection.getResponseCode(); - } catch (Exception ignored) { - } - } - - private URL getUrl(RealmAnalytics analytics) throws - MalformedURLException, - SocketException, - NoSuchAlgorithmException, - UnsupportedEncodingException { - return new URL(prefix + Utils.base64Encode(analytics.generateJson()) + suffix); - } - - public static class Segment extends UrlEncodedAnalytics { - private static final String ADDRESS_PREFIX = - "https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?data="; - private static final String ADDRESS_SUFFIX = ""; - - public Segment() { - super(ADDRESS_PREFIX, ADDRESS_SUFFIX); - } - } - -} diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt new file mode 100644 index 0000000000..505f7dd3ca --- /dev/null +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Realm Inc. + * + * 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. + */ +package io.realm.analytics + +import io.realm.transformer.Utils +import io.realm.transformer.Version +import java.net.SocketException +import java.security.NoSuchAlgorithmException + +inline class PublicAppId(val id: String) { + fun anonymize(): String { + val idBytes: ByteArray = id.toByteArray() + return Utils.hexStringify(Utils.sha256Hash(idBytes)) + } +} + +/** + * Class wrapping data we want to send as analytics data. + */ +data class AnalyticsData( + val appId: PublicAppId, + val usesKotlin: Boolean, + val usesSync: Boolean, + val targetSdk: String, + val minSdk: String, + val target:String, + val gradleVersion: String, + val agpVersion: String +) { + + private val TOKEN = "ce0fac19508f6c8f20066d345d360fd0" + private val EVENT_NAME = "Run" + private val JSON_TEMPLATE = """ + { + "event": "%EVENT%", + "properties": { + "token": "%TOKEN%", + "distinct_id": "%USER_ID%", + "Anonymized MAC Address": "%USER_ID%", + "Anonymized Bundle ID": "%APP_ID%", + "Binding": "java", + "Target": "%TARGET%", + "Language": "%LANGUAGE%", + "Sync Version": %SYNC_VERSION%, + "Realm Version": "%REALM_VERSION%", + "Host OS Type": "%OS_TYPE%", + "Host OS Version": "%OS_VERSION%", + "Target OS Type": "android", + "Target OS Version": "%TARGET_SDK%", + "Target OS Minimum Version": "%MIN_SDK%", + "Gradle version": "%GRADLE_VERSION%", + "Android Gradle Plugin Version": "%AGP_VERSION%" + } + } + """.trimIndent() + + @Throws(SocketException::class, NoSuchAlgorithmException::class) + fun generateJson(): String { + return JSON_TEMPLATE + .replace("%EVENT%".toRegex(), EVENT_NAME) + .replace("%TOKEN%".toRegex(), TOKEN) + .replace("%USER_ID%".toRegex(), ComputerIdentifierGenerator.get()) + .replace("%APP_ID%".toRegex(), appId.anonymize()) + .replace("%TARGET%".toRegex(), target) + .replace("%LANGUAGE%".toRegex(), if (usesKotlin) "kotlin" else "java") + .replace( + "%SYNC_VERSION%".toRegex(), + if (usesSync) "\"" + Version.SYNC_VERSION + "\"" else "null" + ) + .replace("%REALM_VERSION%".toRegex(), Version.VERSION) + .replace("%OS_TYPE%".toRegex(), System.getProperty("os.name")) + .replace("%OS_VERSION%".toRegex(), System.getProperty("os.version")) + .replace("%TARGET_SDK%".toRegex(), targetSdk) + .replace("%MIN_SDK%".toRegex(), minSdk) + .replace("%GRADLE_VERSION%".toRegex(), gradleVersion) + .replace("%AGP_VERSION%".toRegex(), agpVersion) + } +} \ No newline at end of file diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt new file mode 100644 index 0000000000..8d007c825f --- /dev/null +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2021 Realm Inc. + * + * 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. + */ +package io.realm.analytics + +import io.realm.transformer.Utils +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.net.NetworkInterface +import java.net.SocketException +import java.security.NoSuchAlgorithmException +import java.util.* + +/** + * Generate a unique identifier for a computer. The method being used depends on the platform: + * - OS X: Mac address of en0 + * - Windows: BIOS identifier + * - Linux: Machine ID provided by the OS + */ +class ComputerIdentifierGenerator { + companion object { + private const val UNKNOWN = "unknown" + private val OS: String = System.getProperty("os.name").toLowerCase() + private val isWindows: Boolean = OS.contains("win") + private val isMac: Boolean = OS.contains("mac") + private val isLinux: Boolean = OS.contains("inux") + + fun get(): String { + return try { + when { + isWindows -> getWindowsIdentifier() + isMac -> getMacOsIdentifier() + isLinux -> getLinuxMacAddress() + else -> UNKNOWN + } + } catch (e: Exception) { + UNKNOWN + } + } + + @Throws(FileNotFoundException::class, NoSuchAlgorithmException::class) + private fun getLinuxMacAddress(): String { + var machineId = File("/var/lib/dbus/machine-id") + if (!machineId.exists()) { + machineId = File("/etc/machine-id") + } + if (!machineId.exists()) { + return UNKNOWN + } + var scanner: Scanner? = null + return try { + scanner = Scanner(machineId) + val id = scanner.useDelimiter("\\A").next() + Utils.hexStringify(Utils.sha256Hash(id.toByteArray())) + } finally { + scanner?.close() + } + } + + @Throws(SocketException::class, NoSuchAlgorithmException::class) + private fun getMacOsIdentifier(): String { + val networkInterface = NetworkInterface.getByName("en0") + val hardwareAddress = networkInterface.hardwareAddress + return Utils.hexStringify(Utils.sha256Hash(hardwareAddress)) + } + + @Throws(IOException::class, NoSuchAlgorithmException::class) + private fun getWindowsIdentifier(): String { + val runtime = Runtime.getRuntime() + val process = runtime.exec(arrayOf("wmic", "csproduct", "get", "UUID")) + var result: String? = null + val `is` = process.inputStream + val sc = Scanner(process.inputStream) + try { + while (sc.hasNext()) { + val next = sc.next() + if (next.contains("UUID")) { + result = sc.next().trim { it <= ' ' } + break + } + } + } finally { + `is`.close() + } + return if (result == null) UNKNOWN else Utils.hexStringify(Utils.sha256Hash(result.toByteArray())) + } + } +} \ No newline at end of file diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/RealmAnalytics.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/RealmAnalytics.kt new file mode 100644 index 0000000000..d97a383b01 --- /dev/null +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/RealmAnalytics.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2021 Realm Inc. + * + * 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. + */ +package io.realm.analytics + +import io.realm.transformer.CONNECT_TIMEOUT +import io.realm.transformer.READ_TIMEOUT +import io.realm.transformer.Utils +import io.realm.transformer.ext.getAgpVersion +import io.realm.transformer.ext.getAppId +import io.realm.transformer.ext.getMinSdk +import io.realm.transformer.ext.getTargetSdk +import org.gradle.api.Project +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +// Package level logger +val logger: Logger = LoggerFactory.getLogger("realm-logger") + +/** + * Asynchronously submits build information to Realm as part of running + * the Gradle build. + * + * To be clear: this does *not* run when your app is in production or on + * your end-user's devices; it will only run when you build your app from source. + * + * Why are we doing this? Because it helps us build a better product for you. + * None of the data personally identifies you, your employer or your app, but it + * *will* help us understand what Realm version you use, what host OS you use, + * etc. Having this info will help with prioritizing our time, adding new + * features and deprecating old features. Collecting an anonymized bundle & + * anonymized MAC is the only way for us to count actual usage of the other + * metrics accurately. If we don't have a way to deduplicate the info reported, + * it will be useless, as a single developer building their app on Windows ten + * times would report 10 times more than a single developer that only builds + * once from Mac OS X, making the data all but useless. No one likes sharing + * data unless it's necessary, we get it, and we've debated adding this for a + * long long time. Since Realm is a free product without an email signup, we + * feel this is a necessary step so we can collect relevant data to build a + * better product for you. + * + * Currently the following information is reported: + * - What version of Realm is being used + * - What OS you are running on + * - An anonymized MAC address and bundle ID to aggregate the other information on. + * + */ +class RealmAnalytics { + + private var data: AnalyticsData? = null + + /** + * Sends the analytics. + * + * @param inputs the inputs provided by the Transform API + * @param inputModelClasses a list of ctClasses describing the Realm models + */ + public fun execute() { + try { + // If there is no data, analytics was disabled, so exit early. + val analyticsData: AnalyticsData = data ?: return + + val pool = Executors.newFixedThreadPool(1); + try { + pool.execute { UrlEncodedAnalytics.create().execute(analyticsData) } + pool.awaitTermination(CONNECT_TIMEOUT + READ_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (e: InterruptedException) { + pool.shutdownNow() + } + } catch (e: Exception) { + // Analytics failing for any reason should not crash the build + logger.debug("Error happened when evaluating Realm Analytics: $e") + } + } + + public fun calculateAnalyticsData(project: Project): Boolean { + if (!isAnalyticsEnabled(project)) { + return false + } + + // Language specific data + // Should be safe to iterate the configurations as we are way beyond the configuration + // phase + var containsKotlin = false + outer@ + for (conf in project.configurations) { + for (artifact in conf.resolvedConfiguration.resolvedArtifacts) { + if (artifact.name.startsWith("kotlin-stdlib")) { + containsKotlin = true + break@outer + } + } + } + + // Android specific data + val appId: String = project.getAppId() + val targetSdk: String = project.getTargetSdk() + val minSdk: String = project.getMinSdk() + val target = + when { + project.plugins.findPlugin("com.android.application") != null -> { + "app" + } + project.plugins.findPlugin("com.android.library") != null -> { + "library" + } + else -> { + "unknown" + } + } + val gradleVersion = project.gradle.gradleVersion + val agpVersion = project.getAgpVersion() + + // Realm specific data + val sync: Boolean = Utils.isSyncEnabled(project) + + data = AnalyticsData( + appId = PublicAppId(appId), + usesKotlin = containsKotlin, + usesSync = sync, + targetSdk = targetSdk, + minSdk = minSdk, + target = target, + gradleVersion = gradleVersion, + agpVersion = agpVersion + ) + return true + } + + private fun isAnalyticsEnabled(project: Project): Boolean { + val env = System.getenv() + return !project.gradle.startParameter.isOffline + && env["REALM_DISABLE_ANALYTICS"] == null + && env["CI"] == null + } +} \ No newline at end of file diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/UrlEncodedAnalytics.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/UrlEncodedAnalytics.kt new file mode 100644 index 0000000000..fb8535ebad --- /dev/null +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/UrlEncodedAnalytics.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Realm Inc. + * + * 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. + */ +package io.realm.analytics + +import io.realm.transformer.Utils +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.SocketException +import java.net.URL +import java.security.NoSuchAlgorithmException + +class UrlEncodedAnalytics private constructor(private val prefix: String, private val suffix: String) { + + /** + * Send the analytics event to the server. + */ + fun execute(analytics: AnalyticsData) { + try { + val url = getUrl(analytics) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connect() + connection.responseCode + } catch (ignored: Exception) { + } + } + + @Throws( + MalformedURLException::class, + SocketException::class, + NoSuchAlgorithmException::class, + UnsupportedEncodingException::class + ) + private fun getUrl(analytics: AnalyticsData): URL { + logger.error("Sending: \n${analytics.generateJson()}") + return URL(prefix + Utils.base64Encode(analytics.generateJson()) + suffix) + } + + companion object { + fun create(): UrlEncodedAnalytics { + val ADDRESS_PREFIX = + "https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?data=" + val ADDRESS_SUFFIX = "" + return UrlEncodedAnalytics(ADDRESS_PREFIX, ADDRESS_SUFFIX) + } + } +} \ No newline at end of file diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt index 9d2cb46b4c..a249e708f3 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt @@ -17,17 +17,16 @@ package io.realm.transformer import com.android.build.api.transform.* +import io.realm.analytics.RealmAnalytics import io.realm.transformer.build.BuildTemplate import io.realm.transformer.build.FullBuild import io.realm.transformer.build.IncrementalBuild -import io.realm.transformer.ext.getMinSdk -import io.realm.transformer.ext.getTargetSdk +import io.realm.transformer.ext.getBootClasspath import javassist.CtClass import org.gradle.api.Project import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit +import java.io.File // Package level logger val logger: Logger = LoggerFactory.getLogger("realm-logger") @@ -35,13 +34,36 @@ val logger: Logger = LoggerFactory.getLogger("realm-logger") val CONNECT_TIMEOUT = 4000L; val READ_TIMEOUT = 2000L; +// Wrapper for storing data from org.gradle.api.Project as we cannot store a class variable to it +// as that conflict with the Configuration Cache. +data class ProjectMetaData( + val isOffline: Boolean, + val bootClassPath: List) + /** * This class implements the Transform API provided by the Android Gradle plugin. */ -class RealmTransformer(val project: Project) : Transform() { - - val logger: Logger = LoggerFactory.getLogger("realm-logger") +class RealmTransformer(project: Project) : Transform() { + private val logger: Logger = LoggerFactory.getLogger("realm-logger") + private val metadata: ProjectMetaData + private lateinit var analytics: RealmAnalytics + + init { + // Fetch project metadata when registering the transformer, as the Project is not + // available during execution time when using the Configuration Cache. + metadata = ProjectMetaData( + // Plugin requirements + project.gradle.startParameter.isOffline, + project.getBootClasspath() + ) + // We need to fetch analytics data at evaluation time as the Project class is not + // available at execution time when using the Configuration Cache + project.afterEvaluate { + this.analytics = RealmAnalytics() + this.analytics.calculateAnalyticsData(project) + } + } override fun getName(): String { return "RealmTransformer" } @@ -94,8 +116,8 @@ class RealmTransformer(val project: Project) : Transform() { val timer = Stopwatch() timer.start("Realm Transform time") - val build: BuildTemplate = if (isIncremental) IncrementalBuild(project, outputProvider!!, this) - else FullBuild(project, outputProvider!!, this) + val build: BuildTemplate = if (isIncremental) IncrementalBuild(metadata, outputProvider!!, this) + else FullBuild(metadata, outputProvider!!, this) build.prepareOutputClasses(inputs!!) timer.splitTime("Prepare output classes") @@ -119,62 +141,6 @@ class RealmTransformer(val project: Project) : Transform() { private fun exitTransform(inputs: Collection, outputModelClasses: Set, timer: Stopwatch) { timer.stop() - this.sendAnalytics(inputs, outputModelClasses) - } - - /** - * Sends the analytics - * - * @param inputs the inputs provided by the Transform API - * @param inputModelClasses a list of ctClasses describing the Realm models - */ - private fun sendAnalytics(inputs: Collection, outputModelClasses: Set) { - try { - val disableAnalytics: Boolean = project.gradle.startParameter.isOffline || "true".equals(System.getenv()["REALM_DISABLE_ANALYTICS"], ignoreCase = true) - if (inputs.isEmpty() || disableAnalytics) { - // Don't send analytics for incremental builds or if they have been explicitly disabled. - return - } - - var containsKotlin = false - // Should be safe to iterate the configurations as we are way beyond the configuration - // phase - outer@ - for (configuration in project.configurations) { - for (dependency in configuration.dependencies) { - if (dependency.name.startsWith("kotlin-stdlib")) { - containsKotlin = true - break@outer - } - } - } - - val packages: Set = outputModelClasses.map { it.packageName }.toSet() - val targetSdk: String? = project.getTargetSdk() - val minSdk: String? = project.getMinSdk() - val sync: Boolean = Utils.isSyncEnabled(project) - val target = - if (project.plugins.findPlugin("com.android.application") != null) { - "app" - } else if (project.plugins.findPlugin("com.android.library") != null) { - "library" - } else { - "unknown" - } - - val analytics = RealmAnalytics(packages, containsKotlin, sync, targetSdk, minSdk, target) - - val pool = Executors.newFixedThreadPool(1); - try { - pool.execute { UrlEncodedAnalytics.Segment().execute(analytics) } - pool.awaitTermination(CONNECT_TIMEOUT + READ_TIMEOUT, TimeUnit.MILLISECONDS); - } catch (e: InterruptedException) { - pool.shutdownNow() - } - } catch (e: Exception) { - // Analytics failing for any reason should not crash the build - logger.debug("Could not send analytics: $e") - } + analytics.execute() } - } diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt index 463b3e06ea..30920ed79e 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt @@ -21,10 +21,7 @@ import com.android.build.api.transform.Transform import com.android.build.api.transform.TransformInput import com.android.build.api.transform.TransformOutputProvider import com.google.common.io.Files -import io.realm.transformer.BytecodeModifier -import io.realm.transformer.ManagedClassPool -import io.realm.transformer.logger -import io.realm.transformer.Utils +import io.realm.transformer.* import io.realm.transformer.ext.getBootClasspath import javassist.ClassPool import javassist.CtClass @@ -36,7 +33,7 @@ import java.util.regex.Pattern * Abstract class defining the structure of doing different types of builds. * */ -abstract class BuildTemplate(val project: Project, val outputProvider: TransformOutputProvider, val transform: Transform) { +abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: TransformOutputProvider, val transform: Transform) { protected lateinit var inputs: MutableCollection protected lateinit var classPool: ManagedClassPool @@ -161,7 +158,7 @@ abstract class BuildTemplate(val project: Project, val outputProvider: Transform */ private fun addBootClassesToClassPool(classPool: ClassPool) { try { - project.getBootClasspath().forEach { + metadata.bootClassPath.forEach { val path: String = it.absolutePath logger.debug("Add boot class $path to class pool.") classPool.appendClassPath(path) diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt index 0536bbb4ff..a905f83fd8 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt @@ -21,6 +21,7 @@ import com.android.build.api.transform.Format import com.android.build.api.transform.TransformInput import com.android.build.api.transform.TransformOutputProvider import io.realm.transformer.BytecodeModifier +import io.realm.transformer.ProjectMetaData import io.realm.transformer.RealmTransformer import io.realm.transformer.ext.safeSubtypeOf import io.realm.transformer.logger @@ -30,8 +31,8 @@ import org.gradle.api.Project import java.io.File import java.util.jar.JarFile -class FullBuild(project: Project, outputProvider: TransformOutputProvider, transformer: RealmTransformer) - : BuildTemplate(project, outputProvider, transformer) { +class FullBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvider, transformer: RealmTransformer) + : BuildTemplate(metadata, outputProvider, transformer) { private val allModelClasses: ArrayList = arrayListOf() diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt index 4bdb65193e..4df3caa692 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt @@ -23,17 +23,17 @@ import com.android.build.api.transform.TransformInput import com.android.build.api.transform.TransformOutputProvider import io.realm.annotations.RealmClass import io.realm.transformer.BytecodeModifier +import io.realm.transformer.ProjectMetaData import io.realm.transformer.RealmTransformer import io.realm.transformer.ext.safeSubtypeOf import io.realm.transformer.logger import javassist.CtClass import javassist.NotFoundException -import org.gradle.api.Project import java.io.File import java.util.jar.JarFile -class IncrementalBuild(project: Project, outputProvider: TransformOutputProvider, transform: RealmTransformer) - : BuildTemplate(project, outputProvider, transform) { +class IncrementalBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvider, transform: RealmTransformer) + : BuildTemplate(metadata, outputProvider, transform) { override fun prepareOutputClasses(inputs: MutableCollection) { this.inputs = inputs; diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt index d33fc33e98..ee533aa6db 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt @@ -20,18 +20,57 @@ import com.android.build.gradle.BaseExtension import org.gradle.api.Project import java.io.File +/** + * Attempts to determine the best possible unique AppId for this project. + */ +fun Project.getAppId(): String { + // Use the Root project name, usually set in `settings.gradle` + // This means that we don't treat apps with multiple flavours as different, nor + // if a project contains more than one app (probably unlikely). + // This seems acceptable. These cases would just show up as more builds for the + // same AppId. + return this.rootProject.name +} + /** * Returns the `targetSdk` property for this project if it is available. */ -fun Project.getTargetSdk(): String? { - return getAndroidExtension(this).defaultConfig?.targetSdkVersion?.apiString +fun Project.getTargetSdk(): String { + return getAndroidExtension(this).defaultConfig.targetSdkVersion?.apiString ?: "unknown" } /** * Returns the `minSdk` property for this project if it is available. */ -fun Project.getMinSdk(): String? { - return getAndroidExtension(this).defaultConfig?.minSdkVersion?.apiString +fun Project.getMinSdk(): String { + return getAndroidExtension(this).defaultConfig.minSdkVersion?.apiString ?: "unknown" +} + +/** + * Returns the version of the Android Gradle Plugin that is used. + */ +fun Project.getAgpVersion(): String { + // This API is only available from AGP 7.0.0. And it is a bit unclear exactly which part of + // this is actually stable. Also, there appear to be problems with depending on AGP 7.* on + // the compile classpath (it cannot load BaseExtension). + // + // So for now, this code assumes that we are compiling against AGP 4.1 and uses reflection + // to try to grap the AGP version. + // + // This is done with a best-effort, but we just + // accept finding the version isn't possible if anything goes wrong. + return try { + val extension = this.extensions.getByName("androidComponents") as Object + val method = extension.`class`.getMethod("getPluginVersion") + val version = method.invoke(extension) + if (version != null) { + return version.toString() + } else { + return "unknown" + } + } catch (e: Exception) { + "unknown" + } } /**