diff --git a/embrace-android-sdk/api/embrace-android-sdk.api b/embrace-android-sdk/api/embrace-android-sdk.api index 8e06c17e74..f9e9f30aad 100644 --- a/embrace-android-sdk/api/embrace-android-sdk.api +++ b/embrace-android-sdk/api/embrace-android-sdk.api @@ -1,3 +1,7 @@ +public final class io/embrace/android/embracesdk/AutoStartInstrumentationHook { + public static fun _preOnCreate (Landroid/app/Application;)V +} + public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/embracesdk/internal/api/SdkApi { public static final field Companion Lio/embrace/android/embracesdk/Embrace$Companion; public fun activityLoaded (Landroid/app/Activity;)V diff --git a/embrace-android-sdk/embrace-proguard.cfg b/embrace-android-sdk/embrace-proguard.cfg index 024a96b22e..7b1a58198e 100644 --- a/embrace-android-sdk/embrace-proguard.cfg +++ b/embrace-android-sdk/embrace-proguard.cfg @@ -50,6 +50,7 @@ ## Keep gradle plugin hooks -keep class io.embrace.android.embracesdk.okhttp3.** { *; } -keep class io.embrace.android.embracesdk.ViewSwazzledHooks { *; } +-keep class io.embrace.android.embracesdk.AutoStartInstrumentationHook { *; } -keep class io.embrace.android.embracesdk.WebViewClientSwazzledHooks { *; } -keep class io.embrace.android.embracesdk.WebViewChromeClientSwazzledHooks { *; } -keep class io.embrace.android.embracesdk.fcm.swazzle.callback.com.android.fcm.FirebaseSwazzledHooks { *; } diff --git a/embrace-android-sdk/lint-baseline.xml b/embrace-android-sdk/lint-baseline.xml index 143e25e8df..94e81b7994 100644 --- a/embrace-android-sdk/lint-baseline.xml +++ b/embrace-android-sdk/lint-baseline.xml @@ -1,5 +1,16 @@ - + + + + + + + + + OkHttpClassAdapter(ASM_API_VERSION, visitor) {} } +private val applicationFactory: ClassVisitorFactory = { visitor -> + ApplicationClassAdapter(ASM_API_VERSION, visitor) +} + /** * Declares the test cases for bytecode in [InstrumentedBytecodeTest]. You should define the * input class, the expected output, and the [ClassVisitor] which will instrument the bytecode. @@ -60,15 +66,23 @@ internal fun instrumentedBytecodeTestCases(): List { .plus(onLongClickInnerTestCases) .plus(webclientTestCases) .plus(okHttpTestCases) + .plus(applicationTestCases) .distinct() // filter out any unintentional duplicate test cases .sortedBy(BytecodeTestParams::simpleClzName) } +private val applicationTestCases = listOf( + TestApplication::class +).map { + BytecodeTestParams(it.java, factory = applicationFactory) +} + private val okHttpTestCases = listOf( OkHttpClient.Builder::class ).map { BytecodeTestParams(it.java, factory = okHttpFactory) } + private val webclientTestCases = listOf( CustomWebViewClient::class, ExtendedCustomWebViewClient::class, diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/TestApplication_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/TestApplication_expected.txt new file mode 100644 index 0000000000..50d0c1ed0b --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/TestApplication_expected.txt @@ -0,0 +1,36 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/TestApplication extends android/app/Application { + + // compiled from: TestApplication.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0008\u0010\u0003\u001a\u00020\u0004H\u0016\u00a8\u0006\u0005"}, d2={"Lio/embrace/test/fixtures/TestApplication;", "Landroid/app/Application;", "()V", "onCreate", "", "embrace-bytecode-instrumentation-tests_release"}) + + // access flags 0x1 + public ()V + L0 + LINENUMBER 5 L0 + ALOAD 0 + INVOKESPECIAL android/app/Application. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onCreate()V + ALOAD 0 + INVOKESTATIC io/embrace/android/embracesdk/AutoStartInstrumentationHook._preOnCreate (Landroid/app/Application;)V + L0 + LINENUMBER 7 L0 + ALOAD 0 + INVOKESPECIAL android/app/Application.onCreate ()V + L1 + LINENUMBER 8 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L2 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-instrumentation/src/main/kotlin/com/example/app/ApplicationFixture.kt b/embrace-gradle-plugin-integration-tests/fixtures/android-instrumentation/src/main/kotlin/com/example/app/ApplicationFixture.kt new file mode 100644 index 0000000000..43164f4788 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-instrumentation/src/main/kotlin/com/example/app/ApplicationFixture.kt @@ -0,0 +1,9 @@ +package com.example.app + +import android.app.Application + +class ApplicationFixture : Application() { + override fun onCreate() { + + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/BytecodeInstrumentationTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/BytecodeInstrumentationTest.kt index 26a0e68311..5eda50457a 100644 --- a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/BytecodeInstrumentationTest.kt +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/BytecodeInstrumentationTest.kt @@ -22,6 +22,7 @@ class BytecodeInstrumentationTest { "/com/example/app/OnLongClickListenerFixture", "/okhttp3/OkHttpClient\$Builder", "/com/example/app/FcmServiceFixture", + "/com/example/app/ApplicationFixture" ) private val defaultArgs = listOf("-x", "lintVitalRelease") diff --git a/embrace-gradle-plugin-integration-tests/src/test/resources/bytecode-instrumentation-enabled.json b/embrace-gradle-plugin-integration-tests/src/test/resources/bytecode-instrumentation-enabled.json index 665ba81d1c..e45f8a66fd 100644 --- a/embrace-gradle-plugin-integration-tests/src/test/resources/bytecode-instrumentation-enabled.json +++ b/embrace-gradle-plugin-integration-tests/src/test/resources/bytecode-instrumentation-enabled.json @@ -45,6 +45,15 @@ "embraceHook": "invoke-static {p1}, Lio/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks;->_onMessageReceived(Lcom/google/firebase/messaging/RemoteMessage;)V" } ] + }, + { + "className": "ApplicationFixture", + "methods": [ + { + "signature": "onCreate()V", + "embraceHook": "invoke-static {p0}, Lio/embrace/android/embracesdk/AutoStartInstrumentationHook;->_preOnCreate(Landroid/app/Application;)V" + } + ] } ] } diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/api/EmbraceBytecodeInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/api/EmbraceBytecodeInstrumentation.kt index 804e533490..3e47ad3b6e 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/api/EmbraceBytecodeInstrumentation.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/api/EmbraceBytecodeInstrumentation.kt @@ -42,6 +42,11 @@ abstract class EmbraceBytecodeInstrumentation @Inject internal constructor(objec val firebasePushNotificationsEnabled: Property = objectFactory.property(Boolean::class.java) + /** + * Whether Embrace should automatically start in the Application class. Defaults to true. + */ + val autoStartEnabled: Property = objectFactory.property(Boolean::class.java) + /** * A list of string patterns that are used to filter classes during bytecode instrumentation. For example, 'com.example.foo.*' * would avoid instrumenting any classes in the 'com.example.foo' package. diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt index 2744ea6ea3..0d086e4126 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt @@ -31,4 +31,6 @@ interface InstrumentationBehavior { * A list of string regexes that are used to filter classes during bytecode instrumentation */ val ignoredClasses: List + + val autoStartEnabled: Boolean } diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt index 2e750e13eb..aed4275443 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt @@ -41,4 +41,8 @@ class InstrumentationBehaviorImpl( override val ignoredClasses: List by lazy { instrumentation.classIgnorePatterns.get().plus(extension.classSkipList.get()) } + + override val autoStartEnabled: Boolean by lazy { + enabled && (instrumentation.autoStartEnabled.orNull ?: true) + } } diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTaskRegistration.kt index 8aab204ca5..e9b95f25d7 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTaskRegistration.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTaskRegistration.kt @@ -39,6 +39,7 @@ class AsmTaskRegistration : EmbraceTaskRegistration { params.shouldInstrumentOkHttp.set(behavior.instrumentation.okHttpEnabled) params.shouldInstrumentOnLongClick.set(behavior.instrumentation.onLongClickEnabled) params.shouldInstrumentOnClick.set(behavior.instrumentation.onClickEnabled) + params.shouldAutoStart.set(behavior.instrumentation.autoStartEnabled) project.afterEvaluate { // Find the Asm transformation task by name and make it depend on encodeSharedObjectFilesTask diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt index 1af12e5e73..b1453558ae 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt @@ -55,4 +55,7 @@ interface BytecodeInstrumentationParams : InstrumentationParameters { @get:Input val shouldInstrumentOnClick: Property + + @get:Input + val shouldAutoStart: Property } diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt index 13789f5a82..06a1dddc4d 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt @@ -3,6 +3,7 @@ package io.embrace.android.gradle.plugin.instrumentation import com.android.build.api.instrumentation.ClassContext import com.android.build.api.instrumentation.InstrumentationContext import io.embrace.android.gradle.plugin.instrumentation.config.ConfigClassVisitorFactory +import io.embrace.android.gradle.plugin.instrumentation.visitor.ApplicationClassAdapter import io.embrace.android.gradle.plugin.instrumentation.visitor.FirebaseMessagingServiceClassAdapter import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter @@ -31,6 +32,11 @@ internal fun createClassVisitorImpl( // chain our own visitors to avoid unlikely (but possible) cases such as a custom // WebViewClient implementing an OnClickListener + if (parameters.get().shouldAutoStart.get() && ApplicationClassAdapter.accept(classContext)) { + visitor = ApplicationClassAdapter(api, visitor) + logger { "Added ApplicationClassAdapter for $className." } + } + if (parameters.get().shouldInstrumentFirebaseMessaging.get() && FirebaseMessagingServiceClassAdapter.accept(classContext) ) { diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt index 92c0a5b7ca..9735618c41 100644 --- a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt @@ -106,7 +106,13 @@ data class SdkLocalConfig( val appExitInfoConfig: AppExitInfoLocalConfig? = null, @Json(name = "app_framework") - val appFramework: String? = null + val appFramework: String? = null, + + /** + * Whether auto-start instrumentation should be enabled or not + */ + @Json(name = "auto_start") + val autoStart: Boolean? = null ) : Serializable { private companion object { diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapter.kt new file mode 100644 index 0000000000..27ac3e7d69 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapter.kt @@ -0,0 +1,37 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Visits the Application class and returns an [ApplicationMethodAdapter] for the onCreate method. + */ +class ApplicationClassAdapter( + api: Int, + nextClassVisitor: ClassVisitor?, +) : ClassVisitor(api, nextClassVisitor) { + + companion object : ClassVisitFilter { + private const val METHOD_NAME = "onCreate" + private const val METHOD_DESC = "()V" + + override fun accept(classContext: ClassContext) = true + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME == name && METHOD_DESC == desc) { + ApplicationMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationMethodAdapter.kt new file mode 100644 index 0000000000..1cf47dd7fe --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationMethodAdapter.kt @@ -0,0 +1,29 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits the onCreate method and inserts a call to AutoStartInstrumentationHook._preOnCreate at the very start + * of the method. This ensures that Embrace is started before any other initialization code runs. + */ +internal class ApplicationMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // invoke AutoStartInstrumentationHook$Application._preOnCreate() + visitVarInsn(Opcodes.ALOAD, 0) // load 'this' onto the stack + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/AutoStartInstrumentationHook", + "_preOnCreate", + "(Landroid/app/Application;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt index 71d9e5ed9a..97b1d46366 100644 --- a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt @@ -18,6 +18,7 @@ class TestBytecodeInstrumentationParams( instrumentOkHttp: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_OKHTTP, instrumentOnLongClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_LONG_CLICK, instrumentOnClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_CLICK, + shouldAutoStart: Boolean = false ) : BytecodeInstrumentationParams { override val config: Property = @@ -42,4 +43,6 @@ class TestBytecodeInstrumentationParams( DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnLongClick) override val shouldInstrumentOnClick: Property = DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnClick) + override val shouldAutoStart: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(shouldAutoStart) } diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapterTest.kt new file mode 100644 index 0000000000..9449ed36a6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ApplicationClassAdapterTest.kt @@ -0,0 +1,45 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class ApplicationClassAdapterTest { + + private val adapter = ApplicationClassAdapter(ASM_API_VERSION, null) + + @Test + fun testOnCreateVisited() { + val visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onCreate", + "()V", + null, + emptyArray() + ) + assertTrue(visitor is ApplicationMethodAdapter) + } + + @Test + fun testMethodNotVisited() { + var visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onCreate", + "(Landroid/content/Context;)V", + null, + emptyArray() + ) + assertFalse(visitor is ApplicationMethodAdapter) + + visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onStart", + "()V", + null, + emptyArray() + ) + assertFalse(visitor is ApplicationMethodAdapter) + } +} diff --git a/embrace-test-fakes/lint-baseline.xml b/embrace-test-fakes/lint-baseline.xml index 333f47a062..f4b541243b 100644 --- a/embrace-test-fakes/lint-baseline.xml +++ b/embrace-test-fakes/lint-baseline.xml @@ -1,5 +1,5 @@ - + @@ -19,7 +19,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -30,7 +30,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -41,7 +41,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -52,7 +52,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -63,7 +63,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -74,7 +74,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -85,106 +85,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -232,83 +133,6 @@ column="9"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - -