Skip to content

Commit 3108bb5

Browse files
committed
Add auto start instrumentation
1 parent 0e80dc0 commit 3108bb5

File tree

22 files changed

+285
-187
lines changed

22 files changed

+285
-187
lines changed

embrace-android-sdk/api/embrace-android-sdk.api

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
public final class io/embrace/android/embracesdk/AutoStartInstrumentationHook {
2+
public static fun _preOnCreate (Landroid/app/Application;)V
3+
}
4+
15
public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/embracesdk/internal/api/SdkApi {
26
public static final field Companion Lio/embrace/android/embracesdk/Embrace$Companion;
37
public fun activityLoaded (Landroid/app/Activity;)V

embrace-android-sdk/embrace-proguard.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
## Keep gradle plugin hooks
5151
-keep class io.embrace.android.embracesdk.okhttp3.** { *; }
5252
-keep class io.embrace.android.embracesdk.ViewSwazzledHooks { *; }
53+
-keep class io.embrace.android.embracesdk.AutoStartInstrumentationHook { *; }
5354
-keep class io.embrace.android.embracesdk.WebViewClientSwazzledHooks { *; }
5455
-keep class io.embrace.android.embracesdk.WebViewChromeClientSwazzledHooks { *; }
5556
-keep class io.embrace.android.embracesdk.fcm.swazzle.callback.com.android.fcm.FirebaseSwazzledHooks { *; }

embrace-android-sdk/lint-baseline.xml

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<issues format="6" by="lint 8.7.3" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.3)" variant="all" version="8.7.3">
2+
<issues format="6" by="lint 8.9.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.9.1)" variant="all" version="8.9.1">
3+
4+
<issue
5+
id="EmbracePublicApiPackageRule"
6+
message="Don&apos;t put classes in the io.embrace.android.embracesdk package unless they&apos;re part of the public API. Please move the new class to an appropriate package or (if you&apos;re adding to the public API) suppress this error via the lint baseline file."
7+
errorLine1="public final class AutoStartInstrumentationHook {"
8+
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
9+
<location
10+
file="src/main/java/io/embrace/android/embracesdk/AutoStartInstrumentationHook.java"
11+
line="12"
12+
column="20"/>
13+
</issue>
314

415
<issue
516
id="EmbracePublicApiPackageRule"
@@ -67,6 +78,17 @@
6778
column="88"/>
6879
</issue>
6980

81+
<issue
82+
id="UnknownNullness"
83+
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
84+
errorLine1=" public static void _preOnCreate(android.app.Application application) {"
85+
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
86+
<location
87+
file="src/main/java/io/embrace/android/embracesdk/AutoStartInstrumentationHook.java"
88+
line="17"
89+
column="37"/>
90+
</issue>
91+
7092
<issue
7193
id="UnknownNullness"
7294
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.embrace.android.embracesdk;
2+
3+
import androidx.annotation.NonNull;
4+
5+
import io.embrace.android.embracesdk.annotation.InternalApi;
6+
import io.embrace.android.embracesdk.internal.EmbraceInternalApi;
7+
8+
/**
9+
* @hide
10+
*/
11+
@InternalApi
12+
public final class AutoStartInstrumentationHook {
13+
14+
private AutoStartInstrumentationHook() {
15+
}
16+
17+
public static void _preOnCreate(android.app.Application application) {
18+
try {
19+
Embrace.getInstance().start(application);
20+
} catch (Exception exception) {
21+
logError(exception);
22+
}
23+
}
24+
25+
private static void logError(@NonNull Throwable throwable) {
26+
EmbraceInternalApi.getInstance().getInternalInterface().logInternalError(throwable);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.embrace.test.fixtures
2+
3+
import android.app.Application
4+
5+
class TestApplication : Application() {
6+
override fun onCreate() {
7+
super.onCreate()
8+
}
9+
}

embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTestCases.kt

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.embrace.gradle.plugin.instrumentation
22

3+
import io.embrace.android.gradle.plugin.instrumentation.visitor.ApplicationClassAdapter
34
import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter
45
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter
56
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnLongClickClassAdapter
@@ -27,6 +28,7 @@ import io.embrace.test.fixtures.MissingInterfaceOnLongClickListener
2728
import io.embrace.test.fixtures.MissingOverrideOnClickListener
2829
import io.embrace.test.fixtures.MissingOverrideOnLongClickListener
2930
import io.embrace.test.fixtures.NoOverrideWebViewClient
31+
import io.embrace.test.fixtures.TestApplication
3032
import io.embrace.test.fixtures.VirtualMethodRefNamedOnClick
3133
import okhttp3.OkHttpClient
3234
import org.objectweb.asm.ClassVisitor
@@ -47,6 +49,10 @@ private val okHttpFactory: ClassVisitorFactory = { visitor ->
4749
OkHttpClassAdapter(ASM_API_VERSION, visitor) {}
4850
}
4951

52+
private val applicationFactory: ClassVisitorFactory = { visitor ->
53+
ApplicationClassAdapter(ASM_API_VERSION, visitor)
54+
}
55+
5056
/**
5157
* Declares the test cases for bytecode in [InstrumentedBytecodeTest]. You should define the
5258
* input class, the expected output, and the [ClassVisitor] which will instrument the bytecode.
@@ -60,15 +66,23 @@ internal fun instrumentedBytecodeTestCases(): List<BytecodeTestParams> {
6066
.plus(onLongClickInnerTestCases)
6167
.plus(webclientTestCases)
6268
.plus(okHttpTestCases)
69+
.plus(applicationTestCases)
6370
.distinct() // filter out any unintentional duplicate test cases
6471
.sortedBy(BytecodeTestParams::simpleClzName)
6572
}
6673

74+
private val applicationTestCases = listOf(
75+
TestApplication::class
76+
).map {
77+
BytecodeTestParams(it.java, factory = applicationFactory)
78+
}
79+
6780
private val okHttpTestCases = listOf(
6881
OkHttpClient.Builder::class
6982
).map {
7083
BytecodeTestParams(it.java, factory = okHttpFactory)
7184
}
85+
7286
private val webclientTestCases = listOf(
7387
CustomWebViewClient::class,
7488
ExtendedCustomWebViewClient::class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// class version 55.0 (55)
2+
// access flags 0x31
3+
public final class io/embrace/test/fixtures/TestApplication extends android/app/Application {
4+
5+
// compiled from: TestApplication.kt
6+
7+
@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"})
8+
9+
// access flags 0x1
10+
public <init>()V
11+
L0
12+
LINENUMBER 5 L0
13+
ALOAD 0
14+
INVOKESPECIAL android/app/Application.<init> ()V
15+
RETURN
16+
L1
17+
LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L1 0
18+
MAXSTACK = 1
19+
MAXLOCALS = 1
20+
21+
// access flags 0x1
22+
public onCreate()V
23+
ALOAD 0
24+
INVOKESTATIC io/embrace/android/embracesdk/AutoStartInstrumentationHook._preOnCreate (Landroid/app/Application;)V
25+
L0
26+
LINENUMBER 7 L0
27+
ALOAD 0
28+
INVOKESPECIAL android/app/Application.onCreate ()V
29+
L1
30+
LINENUMBER 8 L1
31+
RETURN
32+
L2
33+
LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L2 0
34+
MAXSTACK = 1
35+
MAXLOCALS = 1
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.app
2+
3+
import android.app.Application
4+
5+
class ApplicationFixture : Application() {
6+
override fun onCreate() {
7+
8+
}
9+
}

embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/BytecodeInstrumentationTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class BytecodeInstrumentationTest {
2222
"/com/example/app/OnLongClickListenerFixture",
2323
"/okhttp3/OkHttpClient\$Builder",
2424
"/com/example/app/FcmServiceFixture",
25+
"/com/example/app/ApplicationFixture"
2526
)
2627
private val defaultArgs = listOf("-x", "lintVitalRelease")
2728

embrace-gradle-plugin-integration-tests/src/test/resources/bytecode-instrumentation-enabled.json

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@
4545
"embraceHook": "invoke-static {p1}, Lio/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks;->_onMessageReceived(Lcom/google/firebase/messaging/RemoteMessage;)V"
4646
}
4747
]
48+
},
49+
{
50+
"className": "ApplicationFixture",
51+
"methods": [
52+
{
53+
"signature": "onCreate()V",
54+
"embraceHook": "invoke-static {p0}, Lio/embrace/android/embracesdk/AutoStartInstrumentationHook;->_preOnCreate(Landroid/app/Application;)V"
55+
}
56+
]
4857
}
4958
]
5059
}

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/api/EmbraceBytecodeInstrumentation.kt

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ abstract class EmbraceBytecodeInstrumentation @Inject internal constructor(objec
4242
val firebasePushNotificationsEnabled: Property<Boolean> =
4343
objectFactory.property(Boolean::class.java)
4444

45+
/**
46+
* Whether Embrace should automatically start in the Application class. Defaults to true.
47+
*/
48+
val autoStartEnabled: Property<Boolean> = objectFactory.property(Boolean::class.java)
49+
4550
/**
4651
* A list of string patterns that are used to filter classes during bytecode instrumentation. For example, 'com.example.foo.*'
4752
* would avoid instrumenting any classes in the 'com.example.foo' package.

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt

+2
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@ interface InstrumentationBehavior {
3131
* A list of string regexes that are used to filter classes during bytecode instrumentation
3232
*/
3333
val ignoredClasses: List<String>
34+
35+
val autoStartEnabled: Boolean
3436
}

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt

+4
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ class InstrumentationBehaviorImpl(
4141
override val ignoredClasses: List<String> by lazy {
4242
instrumentation.classIgnorePatterns.get().plus(extension.classSkipList.get())
4343
}
44+
45+
override val autoStartEnabled: Boolean by lazy {
46+
enabled && (instrumentation.autoStartEnabled.orNull ?: true)
47+
}
4448
}

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTaskRegistration.kt

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class AsmTaskRegistration : EmbraceTaskRegistration {
3939
params.shouldInstrumentOkHttp.set(behavior.instrumentation.okHttpEnabled)
4040
params.shouldInstrumentOnLongClick.set(behavior.instrumentation.onLongClickEnabled)
4141
params.shouldInstrumentOnClick.set(behavior.instrumentation.onClickEnabled)
42+
params.shouldAutoStart.set(behavior.instrumentation.autoStartEnabled)
4243

4344
project.afterEvaluate {
4445
// Find the Asm transformation task by name and make it depend on encodeSharedObjectFilesTask

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt

+3
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,7 @@ interface BytecodeInstrumentationParams : InstrumentationParameters {
5555

5656
@get:Input
5757
val shouldInstrumentOnClick: Property<Boolean>
58+
59+
@get:Input
60+
val shouldAutoStart: Property<Boolean>
5861
}

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.embrace.android.gradle.plugin.instrumentation
33
import com.android.build.api.instrumentation.ClassContext
44
import com.android.build.api.instrumentation.InstrumentationContext
55
import io.embrace.android.gradle.plugin.instrumentation.config.ConfigClassVisitorFactory
6+
import io.embrace.android.gradle.plugin.instrumentation.visitor.ApplicationClassAdapter
67
import io.embrace.android.gradle.plugin.instrumentation.visitor.FirebaseMessagingServiceClassAdapter
78
import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter
89
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter
@@ -31,6 +32,11 @@ internal fun createClassVisitorImpl(
3132

3233
// chain our own visitors to avoid unlikely (but possible) cases such as a custom
3334
// WebViewClient implementing an OnClickListener
35+
if (parameters.get().shouldAutoStart.get() && ApplicationClassAdapter.accept(classContext)) {
36+
visitor = ApplicationClassAdapter(api, visitor)
37+
logger { "Added ApplicationClassAdapter for $className." }
38+
}
39+
3440
if (parameters.get().shouldInstrumentFirebaseMessaging.get() &&
3541
FirebaseMessagingServiceClassAdapter.accept(classContext)
3642
) {

embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ data class SdkLocalConfig(
106106
val appExitInfoConfig: AppExitInfoLocalConfig? = null,
107107

108108
@Json(name = "app_framework")
109-
val appFramework: String? = null
109+
val appFramework: String? = null,
110+
111+
/**
112+
* Whether auto-start instrumentation should be enabled or not
113+
*/
114+
@Json(name = "auto_start")
115+
val autoStart: Boolean? = null
110116
) : Serializable {
111117

112118
private companion object {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.embrace.android.gradle.plugin.instrumentation.visitor
2+
3+
import com.android.build.api.instrumentation.ClassContext
4+
import org.objectweb.asm.ClassVisitor
5+
import org.objectweb.asm.MethodVisitor
6+
7+
/**
8+
* Visits the Application class and returns an [ApplicationMethodAdapter] for the onCreate method.
9+
*/
10+
class ApplicationClassAdapter(
11+
api: Int,
12+
nextClassVisitor: ClassVisitor?,
13+
) : ClassVisitor(api, nextClassVisitor) {
14+
15+
companion object : ClassVisitFilter {
16+
private const val METHOD_NAME = "onCreate"
17+
private const val METHOD_DESC = "()V"
18+
19+
override fun accept(classContext: ClassContext) = true
20+
}
21+
22+
override fun visitMethod(
23+
access: Int,
24+
name: String,
25+
desc: String,
26+
signature: String?,
27+
exceptions: Array<String>?
28+
): MethodVisitor? {
29+
val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
30+
31+
return if (METHOD_NAME == name && METHOD_DESC == desc) {
32+
ApplicationMethodAdapter(api, nextMethodVisitor)
33+
} else {
34+
nextMethodVisitor
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.embrace.android.gradle.plugin.instrumentation.visitor
2+
3+
import org.objectweb.asm.MethodVisitor
4+
import org.objectweb.asm.Opcodes
5+
6+
/**
7+
* Visits the onCreate method and inserts a call to AutoStartInstrumentationHook._preOnCreate at the very start
8+
* of the method. This ensures that Embrace is started before any other initialization code runs.
9+
*/
10+
internal class ApplicationMethodAdapter(
11+
api: Int,
12+
methodVisitor: MethodVisitor?
13+
) : MethodVisitor(api, methodVisitor) {
14+
15+
override fun visitCode() {
16+
// invoke AutoStartInstrumentationHook$Application._preOnCreate()
17+
visitVarInsn(Opcodes.ALOAD, 0) // load 'this' onto the stack
18+
visitMethodInsn(
19+
Opcodes.INVOKESTATIC,
20+
"io/embrace/android/embracesdk/AutoStartInstrumentationHook",
21+
"_preOnCreate",
22+
"(Landroid/app/Application;)V",
23+
false
24+
)
25+
26+
// call super last to reduce chance of interference with other bytecode instrumentation
27+
super.visitCode()
28+
}
29+
}

embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class TestBytecodeInstrumentationParams(
1818
instrumentOkHttp: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_OKHTTP,
1919
instrumentOnLongClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_LONG_CLICK,
2020
instrumentOnClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_CLICK,
21+
shouldAutoStart: Boolean = false
2122
) : BytecodeInstrumentationParams {
2223

2324
override val config: Property<VariantConfig> =
@@ -42,4 +43,6 @@ class TestBytecodeInstrumentationParams(
4243
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnLongClick)
4344
override val shouldInstrumentOnClick: Property<Boolean> =
4445
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnClick)
46+
override val shouldAutoStart: Property<Boolean> =
47+
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(shouldAutoStart)
4548
}

0 commit comments

Comments
 (0)