diff --git a/CHANGELOG.md b/CHANGELOG.md index 49c6bff068..594b3f0dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased * Feat: Measure app start time (#1487) +* Feat: Automatic breadcrumbs logging for fragment lifecycle (#1522) ## 5.0.1 diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 1eca6aa7b9..141eb35545 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -33,6 +33,7 @@ object Config { val minSdkVersion = 14 val minSdkVersionOkHttp = 21 + val minSdkVersionFragment = 21 val minSdkVersionNdk = 16 val targetSdkVersion = sdkVersion val compileSdkVersion = sdkVersion @@ -82,6 +83,8 @@ object Config { val retrofit2Gson = "$retrofit2Group:converter-gson:$retrofit2Version" val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" + + val fragment = "androidx.fragment:fragment-ktx:1.3.4" } object AnnotationProcessors { diff --git a/sentry-android-fragment/.gitignore b/sentry-android-fragment/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/sentry-android-fragment/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sentry-android-fragment/api/sentry-android-fragment.api b/sentry-android-fragment/api/sentry-android-fragment.api new file mode 100644 index 0000000000..9d7818b76a --- /dev/null +++ b/sentry-android-fragment/api/sentry-android-fragment.api @@ -0,0 +1,38 @@ +public final class io/sentry/android/fragment/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/android/fragment/FragmentLifecycleIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { + public fun (Landroid/app/Application;)V + public fun close ()V + public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V +} + +public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : androidx/fragment/app/FragmentManager$FragmentLifecycleCallbacks { + public fun ()V + public fun (Lio/sentry/IHub;)V + public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun onFragmentAttached (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/content/Context;)V + public fun onFragmentCreated (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/os/Bundle;)V + public fun onFragmentDestroyed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentDetached (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentPaused (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentResumed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentSaveInstanceState (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/os/Bundle;)V + public fun onFragmentStarted (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentStopped (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V + public fun onFragmentViewCreated (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;)V + public fun onFragmentViewDestroyed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V +} + diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts new file mode 100644 index 0000000000..c877deae56 --- /dev/null +++ b/sentry-android-fragment/build.gradle.kts @@ -0,0 +1,83 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension + +plugins { + id("com.android.library") + kotlin("android") + jacoco + id(Config.QualityPlugins.gradleVersions) + id(Config.QualityPlugins.detektPlugin) +} + +android { + compileSdkVersion(Config.Android.compileSdkVersion) + + defaultConfig { + targetSdkVersion(Config.Android.targetSdkVersion) + minSdkVersion(Config.Android.minSdkVersionFragment) + + versionName = project.version.toString() + versionCode = project.properties[Config.Sentry.buildVersionCodeProp].toString().toInt() + + // for AGP 4.1 + buildConfigField("String", "VERSION_NAME", "\"$versionName\"") + } + + buildTypes { + getByName("debug") + getByName("release") { + consumerProguardFiles("proguard-rules.pro") + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + testOptions { + animationsDisabled = true + unitTests.apply { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + } + + lintOptions { + isWarningsAsErrors = true + isCheckDependencies = true + + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. + isCheckReleaseBuilds = false + } +} + +tasks.withType { + configure { + isIncludeNoLocationClasses = false + } +} + +kotlin { + explicitApi() +} + +dependencies { + api(project(":sentry")) + + implementation(Config.Libs.fragment) + + // tests + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) +} + +tasks.withType { + // Target version of the generated JVM bytecode. It is used for type resolution. + jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +configure { + buildUponDefaultConfig = true + allRules = true +} diff --git a/sentry-android-fragment/proguard-rules.pro b/sentry-android-fragment/proguard-rules.pro new file mode 100644 index 0000000000..83e417c46f --- /dev/null +++ b/sentry-android-fragment/proguard-rules.pro @@ -0,0 +1 @@ +-keep class io.sentry.android.fragment.** { *; } diff --git a/sentry-android-fragment/src/main/AndroidManifest.xml b/sentry-android-fragment/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c64a3ae4cb --- /dev/null +++ b/sentry-android-fragment/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt new file mode 100644 index 0000000000..a265b66614 --- /dev/null +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -0,0 +1,76 @@ +package io.sentry.android.fragment + +import android.app.Activity +import android.app.Application +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import io.sentry.IHub +import io.sentry.ILogger +import io.sentry.Integration +import io.sentry.SentryLevel.DEBUG +import io.sentry.SentryOptions +import java.io.Closeable + +class FragmentLifecycleIntegration(private val application: Application) : + ActivityLifecycleCallbacks, + Integration, + Closeable { + + private lateinit var hub: IHub + private lateinit var logger: ILogger + + override fun register(hub: IHub, options: SentryOptions) { + this.hub = hub + this.logger = options.logger + + application.registerActivityLifecycleCallbacks(this) + logger.log(DEBUG, "FragmentLifecycleIntegration installed.") + } + + override fun close() { + application.unregisterActivityLifecycleCallbacks(this) + if (::logger.isInitialized) { + logger.log(DEBUG, "FragmentLifecycleIntegration removed.") + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + (activity as? FragmentActivity) + ?.supportFragmentManager + ?.registerFragmentLifecycleCallbacks( + SentryFragmentLifecycleCallbacks(hub), + true + ) + } + + override fun onActivityStarted(activity: Activity) { + // no-op + } + + override fun onActivityResumed(activity: Activity) { + // no-op + } + + override fun onActivityPaused(activity: Activity) { + // no-op + } + + override fun onActivityStopped(activity: Activity) { + // no-op + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + // no-op + } + + override fun onActivityDestroyed(activity: Activity) { + /** + * It is not needed to unregister [SentryFragmentLifecycleCallbacks] as + * [androidx.fragment.app.FragmentManager] will do this on its own when it's destroyed. + * + * @see [androidx.fragment.app.FragmentManager.registerFragmentLifecycleCallbacks] + */ + // no-op + } +} diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt new file mode 100644 index 0000000000..55ad38e0d7 --- /dev/null +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt @@ -0,0 +1,94 @@ +package io.sentry.android.fragment + +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks +import io.sentry.Breadcrumb +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.SentryLevel.INFO + +@Suppress("TooManyFunctions") +class SentryFragmentLifecycleCallbacks( + private val hub: IHub = HubAdapter.getInstance() +) : FragmentLifecycleCallbacks() { + + override fun onFragmentAttached( + fragmentManager: FragmentManager, + fragment: Fragment, + context: Context + ) { + addBreadcrumb(fragment, "attached") + } + + override fun onFragmentSaveInstanceState( + fragmentManager: FragmentManager, + fragment: Fragment, + outState: Bundle + ) { + addBreadcrumb(fragment, "save instance state") + } + + override fun onFragmentCreated( + fragmentManager: FragmentManager, + fragment: Fragment, + savedInstanceState: Bundle? + ) { + addBreadcrumb(fragment, "created") + } + + override fun onFragmentViewCreated( + fragmentManager: FragmentManager, + fragment: Fragment, + view: View, + savedInstanceState: Bundle? + ) { + addBreadcrumb(fragment, "view created") + } + + override fun onFragmentStarted(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "started") + } + + override fun onFragmentResumed(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "resumed") + } + + override fun onFragmentPaused(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "paused") + } + + override fun onFragmentStopped(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "stopped") + } + + override fun onFragmentViewDestroyed(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "view destroyed") + } + + override fun onFragmentDestroyed(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "destroyed") + } + + override fun onFragmentDetached(fragmentManager: FragmentManager, fragment: Fragment) { + addBreadcrumb(fragment, "detached") + } + + private fun addBreadcrumb(fragment: Fragment, state: String) { + val breadcrumb = Breadcrumb().apply { + type = "navigation" + setData("state", state) + setData("screen", getFragmentName(fragment)) + category = "ui.fragment.lifecycle" + level = INFO + } + hub.addBreadcrumb(breadcrumb) + } + + private fun getFragmentName(fragment: Fragment): String { + return fragment.javaClass.simpleName + } +} diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt new file mode 100644 index 0000000000..23243651ba --- /dev/null +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt @@ -0,0 +1,77 @@ +package io.sentry.android.fragment + +import android.app.Activity +import android.app.Application +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import io.sentry.SentryOptions +import org.junit.Test + +class FragmentLifecycleIntegrationTest { + + private class Fixture { + val application = mock() + + fun getSut(): FragmentLifecycleIntegration { + return FragmentLifecycleIntegration(application) + } + } + + private val fixture = Fixture() + + @Test + fun `When register, it should register activity lifecycle callbacks`() { + val sut = fixture.getSut() + + sut.register(mock(), SentryOptions()) + + verify(fixture.application).registerActivityLifecycleCallbacks(sut) + } + + @Test + fun `When close, it should unregister lifecycle callbacks`() { + val sut = fixture.getSut() + + sut.register(mock(), SentryOptions()) + sut.close() + + verify(fixture.application).unregisterActivityLifecycleCallbacks(sut) + } + + @Test + fun `When FragmentActivity is created, it should register fragment lifecycle callbacks`() { + val sut = fixture.getSut() + val fragmentManager = mock() + val fragmentActivity = mock { + on { supportFragmentManager } doReturn fragmentManager + } + + sut.register(mock(), SentryOptions()) + sut.onActivityCreated(fragmentActivity, savedInstanceState = null) + + verify(fragmentManager).registerFragmentLifecycleCallbacks(check { fragmentCallbacks -> + fragmentCallbacks is SentryFragmentLifecycleCallbacks + }, eq(true)) + } + + @Test + fun `When not a FragmentActivity is created, it should not crash`() { + val sut = fixture.getSut() + val activity = mock() + + sut.register(mock(), SentryOptions()) + sut.onActivityCreated(activity, savedInstanceState = null) + } + + @Test + fun `When close is called without register, it should not crash`() { + val sut = fixture.getSut() + + sut.close() + } +} diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt new file mode 100644 index 0000000000..ec7f677157 --- /dev/null +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt @@ -0,0 +1,146 @@ +package io.sentry.android.fragment + +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import io.sentry.Breadcrumb +import io.sentry.Hub +import io.sentry.SentryLevel.INFO +import kotlin.test.assertEquals +import org.junit.Test + +class SentryFragmentLifecycleCallbacksTest { + + private class Fixture { + val fragmentManager = mock() + val hub = mock() + val fragment = mock() + val context = mock() + + fun getSut(): SentryFragmentLifecycleCallbacks { + return SentryFragmentLifecycleCallbacks(hub) + } + } + + private val fixture = Fixture() + + @Test + fun `When fragment is attached, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentAttached(fixture.fragmentManager, fixture.fragment, fixture.context) + + verifyBreadcrumbAdded("attached") + } + + @Test + fun `When fragment saved instance state, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentSaveInstanceState(fixture.fragmentManager, fixture.fragment, Bundle()) + + verifyBreadcrumbAdded("save instance state") + } + + @Test + fun `When fragment is created, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) + + verifyBreadcrumbAdded("created") + } + + @Test + fun `When fragments view is created, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentViewCreated( + fixture.fragmentManager, + fixture.fragment, + view = mock(), + savedInstanceState = null + ) + + verifyBreadcrumbAdded("view created") + } + + @Test + fun `When fragment is started, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentStarted(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("started") + } + + @Test + fun `When fragment is resumed, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentResumed(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("resumed") + } + + @Test + fun `When fragment is paused, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentPaused(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("paused") + } + + @Test + fun `When fragment is stopped, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentStopped(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("stopped") + } + + @Test + fun `When fragments view is destroyed, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentViewDestroyed(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("view destroyed") + } + + @Test + fun `When fragment is destroyed, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentDestroyed(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("destroyed") + } + + @Test + fun `When fragment is detached, it should add breadcrumb`() { + val sut = fixture.getSut() + + sut.onFragmentDetached(fixture.fragmentManager, fixture.fragment) + + verifyBreadcrumbAdded("detached") + } + + private fun verifyBreadcrumbAdded(expectedState: String) { + verify(fixture.hub).addBreadcrumb( + check { breadcrumb: Breadcrumb -> + assertEquals("ui.fragment.lifecycle", breadcrumb.category) + assertEquals("navigation", breadcrumb.type) + assertEquals(INFO, breadcrumb.level) + assertEquals(expectedState, breadcrumb.getData("state")) + assertEquals(fixture.fragment.javaClass.simpleName, breadcrumb.getData("screen")) + } + ) + } +} diff --git a/sentry-samples/sentry-samples-android/api/sentry-samples-android.api b/sentry-samples/sentry-samples-android/api/sentry-samples-android.api index 6be7374fb0..89b4771061 100644 --- a/sentry-samples/sentry-samples-android/api/sentry-samples-android.api +++ b/sentry-samples/sentry-samples-android/api/sentry-samples-android.api @@ -32,6 +32,28 @@ public final class io/sentry/samples/android/Repo { public fun ()V } +public final class io/sentry/samples/android/SampleFragment : androidx/fragment/app/DialogFragment { + public static final field Companion Lio/sentry/samples/android/SampleFragment$Companion; + public fun ()V + public static final fun newInstance ()Lio/sentry/samples/android/SampleFragment; + public fun onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; +} + +public final class io/sentry/samples/android/SampleFragment$Companion { + public final fun newInstance ()Lio/sentry/samples/android/SampleFragment; +} + +public final class io/sentry/samples/android/SampleInnerFragment : androidx/fragment/app/Fragment { + public static final field Companion Lio/sentry/samples/android/SampleInnerFragment$Companion; + public fun ()V + public static final fun newInstance ()Lio/sentry/samples/android/SampleInnerFragment; + public fun onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; +} + +public final class io/sentry/samples/android/SampleInnerFragment$Companion { + public final fun newInstance ()Lio/sentry/samples/android/SampleInnerFragment; +} + public final class io/sentry/samples/android/SecondActivity : androidx/appcompat/app/AppCompatActivity { public fun ()V } @@ -44,6 +66,7 @@ public final class io/sentry/samples/android/databinding/ActivityMainBinding : a public final field crashFromJava Landroid/widget/Button; public final field nativeCapture Landroid/widget/Button; public final field nativeCrash Landroid/widget/Button; + public final field openSampleFragment Landroid/widget/Button; public final field openSecondActivity Landroid/widget/Button; public final field sendMessage Landroid/widget/Button; public final field sendUserFeedback Landroid/widget/Button; @@ -69,3 +92,21 @@ public final class io/sentry/samples/android/databinding/ActivitySecondBinding : public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lio/sentry/samples/android/databinding/ActivitySecondBinding; } +public final class io/sentry/samples/android/databinding/FragmentSampleBinding : androidx/viewbinding/ViewBinding { + public final field container Landroid/widget/FrameLayout; + public static fun bind (Landroid/view/View;)Lio/sentry/samples/android/databinding/FragmentSampleBinding; + public synthetic fun getRoot ()Landroid/view/View; + public fun getRoot ()Landroid/widget/FrameLayout; + public static fun inflate (Landroid/view/LayoutInflater;)Lio/sentry/samples/android/databinding/FragmentSampleBinding; + public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lio/sentry/samples/android/databinding/FragmentSampleBinding; +} + +public final class io/sentry/samples/android/databinding/FragmentSampleInnerBinding : androidx/viewbinding/ViewBinding { + public final field sendMessage Landroid/widget/Button; + public static fun bind (Landroid/view/View;)Lio/sentry/samples/android/databinding/FragmentSampleInnerBinding; + public synthetic fun getRoot ()Landroid/view/View; + public fun getRoot ()Landroid/widget/FrameLayout; + public static fun inflate (Landroid/view/LayoutInflater;)Lio/sentry/samples/android/databinding/FragmentSampleInnerBinding; + public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lio/sentry/samples/android/databinding/FragmentSampleInnerBinding; +} + diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index a96afab7aa..a67076268e 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -92,6 +92,7 @@ dependencies { implementation(project(":sentry-android")) implementation(project(":sentry-android-okhttp")) + implementation(project(":sentry-android-fragment")) // how to exclude androidx if release health feature is disabled // implementation(project(":sentry-android")) { diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 981e8d4a9b..2ed1391d49 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index b73530f8e1..820bf8a354 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -156,6 +156,9 @@ protected void onCreate(Bundle savedInstanceState) { startActivity(new Intent(this, SecondActivity.class)); }); + binding.openSampleFragment.setOnClickListener( + view -> SampleFragment.newInstance().show(getSupportFragmentManager(), null)); + setContentView(binding.getRoot()); } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 8f79b8ebe0..810e4264fe 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,8 +2,8 @@ import android.app.Application; import android.os.StrictMode; - -// import io.sentry.android.core.SentryAndroid; +import io.sentry.android.core.SentryAndroid; +import io.sentry.android.fragment.FragmentLifecycleIntegration; /** Apps. main Application. */ public class MyApplication extends Application { @@ -15,14 +15,17 @@ public void onCreate() { // Example how to initialize the SDK manually which allows access to SentryOptions callbacks. // Make sure you disable the auto init via manifest meta-data: io.sentry.auto-init=false - // SentryAndroid.init( - // this, - // options -> { - // options.setBeforeSend(event -> { - // event.setTag("sample-key", "before-send"); - // }); - // options.setAnrTimeoutIntervalMillis(2000); - // }); + SentryAndroid.init( + this, + options -> { + // options.setBeforeSend( + // (event, hint) -> { + // event.setTag("sample-key", "before-send"); + // return event; + // }); + // options.setAnrTimeoutIntervalMillis(2000); + options.addIntegration(new FragmentLifecycleIntegration(MyApplication.this)); + }); } private void strictMode() { diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleFragment.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleFragment.kt new file mode 100644 index 0000000000..3b9df357b7 --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleFragment.kt @@ -0,0 +1,27 @@ +package io.sentry.samples.android + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import io.sentry.samples.android.databinding.FragmentSampleBinding + +class SampleFragment : DialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FragmentSampleBinding.inflate(inflater).apply { + childFragmentManager.beginTransaction() + .replace(R.id.container, SampleInnerFragment.newInstance()) + .commit() + }.root + } + + companion object { + @JvmStatic fun newInstance() = SampleFragment() + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleInnerFragment.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleInnerFragment.kt new file mode 100644 index 0000000000..638574fdaf --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SampleInnerFragment.kt @@ -0,0 +1,28 @@ +package io.sentry.samples.android + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import io.sentry.Sentry +import io.sentry.samples.android.databinding.FragmentSampleInnerBinding + +class SampleInnerFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FragmentSampleInnerBinding.inflate(inflater).apply { + this.sendMessage.setOnClickListener { + Sentry.captureMessage("Some message from inner Fragment Lifecycle events in breadcrumbs.") + } + }.root + } + + companion object { + @JvmStatic fun newInstance() = SampleInnerFragment() + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml index 011904a5c8..827228e4b6 100644 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml @@ -67,4 +67,9 @@ android:layout_height="wrap_content" android:id="@+id/open_second_activity" android:text="@string/open_second_activity"/> + +