diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ActivityCallbacks.java b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityCallbacks.java index 6b17a4432..6c0ec11fc 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/ActivityCallbacks.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityCallbacks.java @@ -17,175 +17,110 @@ package com.splunk.rum; import android.app.Activity; -import android.app.Application; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import io.opentelemetry.api.trace.Tracer; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; -class ActivityCallbacks implements Application.ActivityLifecycleCallbacks { +class ActivityCallbacks implements DefaultingActivityLifecycleCallbacks { - private final Map tracersByActivityClassName = new HashMap<>(); - private final AtomicReference initialAppActivity = new AtomicReference<>(); - private final Tracer tracer; - private final VisibleScreenTracker visibleScreenTracker; - private final AppStartupTimer startupTimer; + private final ActivityTracerCache tracers; - ActivityCallbacks( - Tracer tracer, - VisibleScreenTracker visibleScreenTracker, - AppStartupTimer startupTimer) { - this.tracer = tracer; - this.visibleScreenTracker = visibleScreenTracker; - this.startupTimer = startupTimer; + ActivityCallbacks(ActivityTracerCache tracers) { + this.tracers = tracers; } @Override public void onActivityPreCreated( @NonNull Activity activity, @Nullable Bundle savedInstanceState) { - getTracer(activity).startActivityCreation().addEvent("activityPreCreated"); - - if (activity instanceof FragmentActivity) { - FragmentManager fragmentManager = - ((FragmentActivity) activity).getSupportFragmentManager(); - fragmentManager.registerFragmentLifecycleCallbacks( - new RumFragmentLifecycleCallbacks(tracer, visibleScreenTracker), true); - } + tracers.startActivityCreation(activity).addEvent("activityPreCreated"); } @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - startupTimer.startUiInit(); - addEvent(activity, "activityCreated"); + tracers.addEvent(activity, "activityCreated"); } @Override public void onActivityPostCreated( @NonNull Activity activity, @Nullable Bundle savedInstanceState) { - addEvent(activity, "activityPostCreated"); + tracers.addEvent(activity, "activityPostCreated"); } @Override public void onActivityPreStarted(@NonNull Activity activity) { - getTracer(activity) - .initiateRestartSpanIfNecessary(tracersByActivityClassName.size() > 1) - .addEvent("activityPreStarted"); + tracers.initiateRestartSpanIfNecessary(activity).addEvent("activityPreStarted"); } @Override public void onActivityStarted(@NonNull Activity activity) { - addEvent(activity, "activityStarted"); + tracers.addEvent(activity, "activityStarted"); } @Override public void onActivityPostStarted(@NonNull Activity activity) { - addEvent(activity, "activityPostStarted"); + tracers.addEvent(activity, "activityPostStarted"); } @Override public void onActivityPreResumed(@NonNull Activity activity) { - getTracer(activity).startSpanIfNoneInProgress("Resumed").addEvent("activityPreResumed"); + tracers.startSpanIfNoneInProgress(activity, "Resumed").addEvent("activityPreResumed"); } @Override public void onActivityResumed(@NonNull Activity activity) { - addEvent(activity, "activityResumed"); + tracers.addEvent(activity, "activityResumed"); } @Override public void onActivityPostResumed(@NonNull Activity activity) { - getTracer(activity) - .addEvent("activityPostResumed") + tracers.addEvent(activity, "activityPostResumed") .addPreviousScreenAttribute() .endSpanForActivityResumed(); - visibleScreenTracker.activityResumed(activity); } @Override public void onActivityPrePaused(@NonNull Activity activity) { - getTracer(activity).startSpanIfNoneInProgress("Paused").addEvent("activityPrePaused"); - visibleScreenTracker.activityPaused(activity); + tracers.startSpanIfNoneInProgress(activity, "Paused").addEvent("activityPrePaused"); } @Override public void onActivityPaused(@NonNull Activity activity) { - addEvent(activity, "activityPaused"); + tracers.addEvent(activity, "activityPaused"); } @Override public void onActivityPostPaused(@NonNull Activity activity) { - getTracer(activity).addEvent("activityPostPaused").endActiveSpan(); + tracers.addEvent(activity, "activityPostPaused").endActiveSpan(); } @Override public void onActivityPreStopped(@NonNull Activity activity) { - getTracer(activity).startSpanIfNoneInProgress("Stopped").addEvent("activityPreStopped"); + tracers.startSpanIfNoneInProgress(activity, "Stopped").addEvent("activityPreStopped"); } @Override public void onActivityStopped(@NonNull Activity activity) { - addEvent(activity, "activityStopped"); + tracers.addEvent(activity, "activityStopped"); } @Override public void onActivityPostStopped(@NonNull Activity activity) { - getTracer(activity).addEvent("activityPostStopped").endActiveSpan(); - } - - @Override - public void onActivityPreSaveInstanceState( - @NonNull Activity activity, @NonNull Bundle outState) { - // todo: add event - } - - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { - // todo: add event - } - - @Override - public void onActivityPostSaveInstanceState( - @NonNull Activity activity, @NonNull Bundle outState) { - // todo: add event + tracers.addEvent(activity, "activityPostStopped").endActiveSpan(); } @Override public void onActivityPreDestroyed(@NonNull Activity activity) { - getTracer(activity).startSpanIfNoneInProgress("Destroyed").addEvent("activityPreDestroyed"); + tracers.startSpanIfNoneInProgress(activity, "Destroyed").addEvent("activityPreDestroyed"); } @Override public void onActivityDestroyed(@NonNull Activity activity) { - addEvent(activity, "activityDestroyed"); + tracers.addEvent(activity, "activityDestroyed"); } @Override public void onActivityPostDestroyed(@NonNull Activity activity) { - getTracer(activity).addEvent("activityPostDestroyed").endActiveSpan(); - } - - private void addEvent(@NonNull Activity activity, String eventName) { - getTracer(activity).addEvent(eventName); - } - - private ActivityTracer getTracer(Activity activity) { - ActivityTracer activityTracer = - tracersByActivityClassName.get(activity.getClass().getName()); - if (activityTracer == null) { - activityTracer = - new ActivityTracer( - activity, - initialAppActivity, - tracer, - visibleScreenTracker, - startupTimer); - tracersByActivityClassName.put(activity.getClass().getName(), activityTracer); - } - return activityTracer; + tracers.addEvent(activity, "activityPostDestroyed").endActiveSpan(); } } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracer.java b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracer.java index 561320ac1..b2979e511 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracer.java @@ -52,11 +52,11 @@ class ActivityTracer { this.activeSpan = new ActiveSpan(visibleScreenTracker); } - ActivityTracer startSpanIfNoneInProgress(String action) { + ActivityTracer startSpanIfNoneInProgress(String spanName) { if (activeSpan.spanInProgress()) { return this; } - activeSpan.startSpan(() -> createSpan(action)); + activeSpan.startSpan(() -> createSpan(spanName)); return this; } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracerCache.java b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracerCache.java new file mode 100644 index 000000000..6bb9c5900 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ActivityTracerCache.java @@ -0,0 +1,91 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import android.app.Activity; +import androidx.annotation.VisibleForTesting; +import io.opentelemetry.api.trace.Tracer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * Encapsulates the fact that we have an ActivityTracer instance per Activity class, and provides + * convenience methods for adding events and starting spans. + */ +class ActivityTracerCache { + + private final Map tracersByActivityClassName = new HashMap<>(); + + private final Function tracerFactory; + + public ActivityTracerCache( + Tracer tracer, + VisibleScreenTracker visibleScreenTracker, + AppStartupTimer startupTimer) { + this(tracer, visibleScreenTracker, new AtomicReference<>(), startupTimer); + } + + @VisibleForTesting + ActivityTracerCache( + Tracer tracer, + VisibleScreenTracker visibleScreenTracker, + AtomicReference initialAppActivity, + AppStartupTimer startupTimer) { + this( + activity -> + new ActivityTracer( + activity, + initialAppActivity, + tracer, + visibleScreenTracker, + startupTimer)); + } + + @VisibleForTesting + ActivityTracerCache(Function tracerFactory) { + this.tracerFactory = tracerFactory; + } + + ActivityTracer addEvent(Activity activity, String eventName) { + return getTracer(activity).addEvent(eventName); + } + + ActivityTracer startSpanIfNoneInProgress(Activity activity, String spanName) { + return getTracer(activity).startSpanIfNoneInProgress(spanName); + } + + ActivityTracer initiateRestartSpanIfNecessary(Activity activity) { + boolean isMultiActivityApp = tracersByActivityClassName.size() > 1; + return getTracer(activity).initiateRestartSpanIfNecessary(isMultiActivityApp); + } + + ActivityTracer startActivityCreation(Activity activity) { + return getTracer(activity).startActivityCreation(); + } + + private ActivityTracer getTracer(Activity activity) { + ActivityTracer activityTracer = + tracersByActivityClassName.get(activity.getClass().getName()); + if (activityTracer == null) { + activityTracer = tracerFactory.apply(activity); + tracersByActivityClassName.put(activity.getClass().getName(), activityTracer); + } + return activityTracer; + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/AppStartupTimer.java b/splunk-otel-android/src/main/java/com/splunk/rum/AppStartupTimer.java index 2686953b9..48c8f8e5e 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/AppStartupTimer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/AppStartupTimer.java @@ -16,11 +16,16 @@ package com.splunk.rum; +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; import android.os.Handler; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; import io.opentelemetry.sdk.common.Clock; import java.util.concurrent.TimeUnit; @@ -60,8 +65,19 @@ Span start(Tracer tracer) { return appStart; } + /** Creates a lifecycle listener that starts the UI init when an activity is created. */ + Application.ActivityLifecycleCallbacks createLifecycleCallback() { + return new DefaultingActivityLifecycleCallbacks() { + @Override + public void onActivityCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + startUiInit(); + } + }; + } + /** Called when Activity is created. */ - void startUiInit() { + private void startUiInit() { if (uiInitStarted || isStartedFromBackground) { return; } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/Pre29ActivityCallbacks.java b/splunk-otel-android/src/main/java/com/splunk/rum/Pre29ActivityCallbacks.java index c2d125b22..6d7ab58c1 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/Pre29ActivityCallbacks.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/Pre29ActivityCallbacks.java @@ -17,106 +17,54 @@ package com.splunk.rum; import android.app.Activity; -import android.app.Application; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import io.opentelemetry.api.trace.Tracer; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; -class Pre29ActivityCallbacks implements Application.ActivityLifecycleCallbacks { - private final Tracer tracer; - private final VisibleScreenTracker visibleScreenTracker; - private final Map tracersByActivityClassName = new HashMap<>(); - private final AtomicReference initialAppActivity = new AtomicReference<>(); - private final AppStartupTimer appStartupTimer; +class Pre29ActivityCallbacks implements DefaultingActivityLifecycleCallbacks { + private final ActivityTracerCache tracers; - Pre29ActivityCallbacks( - Tracer tracer, - VisibleScreenTracker visibleScreenTracker, - AppStartupTimer appStartupTimer) { - this.tracer = tracer; - this.visibleScreenTracker = visibleScreenTracker; - this.appStartupTimer = appStartupTimer; + Pre29ActivityCallbacks(ActivityTracerCache tracers) { + this.tracers = tracers; } @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - appStartupTimer.startUiInit(); - getTracer(activity).startActivityCreation().addEvent("activityCreated"); - - if (activity instanceof FragmentActivity) { - FragmentManager fragmentManager = - ((FragmentActivity) activity).getSupportFragmentManager(); - fragmentManager.registerFragmentLifecycleCallbacks( - new RumFragmentLifecycleCallbacks(tracer, visibleScreenTracker), true); - } + tracers.startActivityCreation(activity).addEvent("activityCreated"); } @Override public void onActivityStarted(@NonNull Activity activity) { - getTracer(activity) - .initiateRestartSpanIfNecessary(tracersByActivityClassName.size() > 1) - .addEvent("activityStarted"); + tracers.initiateRestartSpanIfNecessary(activity).addEvent("activityStarted"); } @Override public void onActivityResumed(@NonNull Activity activity) { - getTracer(activity) - .startSpanIfNoneInProgress("Resumed") + tracers.startSpanIfNoneInProgress(activity, "Resumed") .addEvent("activityResumed") .addPreviousScreenAttribute() .endSpanForActivityResumed(); - visibleScreenTracker.activityResumed(activity); } @Override public void onActivityPaused(@NonNull Activity activity) { - getTracer(activity) - .startSpanIfNoneInProgress("Paused") + tracers.startSpanIfNoneInProgress(activity, "Paused") .addEvent("activityPaused") .endActiveSpan(); - visibleScreenTracker.activityPaused(activity); } @Override public void onActivityStopped(@NonNull Activity activity) { - getTracer(activity) - .startSpanIfNoneInProgress("Stopped") + tracers.startSpanIfNoneInProgress(activity, "Stopped") .addEvent("activityStopped") .endActiveSpan(); } - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { - // todo: add event - } - @Override public void onActivityDestroyed(@NonNull Activity activity) { - getTracer(activity) - .startSpanIfNoneInProgress("Destroyed") + tracers.startSpanIfNoneInProgress(activity, "Destroyed") .addEvent("activityDestroyed") .endActiveSpan(); } - - private ActivityTracer getTracer(Activity activity) { - ActivityTracer activityTracer = - tracersByActivityClassName.get(activity.getClass().getName()); - if (activityTracer == null) { - activityTracer = - new ActivityTracer( - activity, - initialAppActivity, - tracer, - visibleScreenTracker, - appStartupTimer); - tracersByActivityClassName.put(activity.getClass().getName(), activityTracer); - } - return activityTracer; - } } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/Pre29VisibleScreenLifecycleBinding.java b/splunk-otel-android/src/main/java/com/splunk/rum/Pre29VisibleScreenLifecycleBinding.java new file mode 100644 index 000000000..b8ad95f34 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/Pre29VisibleScreenLifecycleBinding.java @@ -0,0 +1,45 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import android.app.Activity; +import androidx.annotation.NonNull; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; + +/** + * An ActivityLifecycleCallbacks that is responsible for telling the VisibleScreenTracker when an + * activity has been resumed and when an activity has been paused. It's just a glue class designed + * for API level < 29. + */ +class Pre29VisibleScreenLifecycleBinding implements DefaultingActivityLifecycleCallbacks { + + private final VisibleScreenTracker visibleScreenTracker; + + Pre29VisibleScreenLifecycleBinding(VisibleScreenTracker visibleScreenTracker) { + this.visibleScreenTracker = visibleScreenTracker; + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + visibleScreenTracker.activityResumed(activity); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + visibleScreenTracker.activityPaused(activity); + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index 48a2ebeb6..35ebae9ae 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -19,6 +19,7 @@ import static com.splunk.rum.SplunkRum.APP_NAME_KEY; import static com.splunk.rum.SplunkRum.COMPONENT_ERROR; import static com.splunk.rum.SplunkRum.COMPONENT_KEY; +import static com.splunk.rum.SplunkRum.RUM_TRACER_NAME; import static com.splunk.rum.SplunkRum.RUM_VERSION_KEY; import static io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor.constant; import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.DEPLOYMENT_ENVIRONMENT; @@ -44,6 +45,8 @@ import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender; import io.opentelemetry.rum.internal.OpenTelemetryRum; import io.opentelemetry.rum.internal.OpenTelemetryRumBuilder; +import io.opentelemetry.rum.internal.instrumentation.InstrumentedApplication; +import io.opentelemetry.rum.internal.instrumentation.activity.RumFragmentActivityRegisterer; import io.opentelemetry.rum.internal.instrumentation.anr.AnrDetector; import io.opentelemetry.rum.internal.instrumentation.crash.CrashReporter; import io.opentelemetry.rum.internal.instrumentation.network.CurrentNetworkProvider; @@ -178,39 +181,114 @@ SplunkRum initialize( installCrashReporter(otelRumBuilder); } - otelRumBuilder.addInstrumentation( - instrumentedApplication -> { - Tracer tracer = - instrumentedApplication - .getOpenTelemetrySdk() - .getTracer(SplunkRum.RUM_TRACER_NAME); - Application.ActivityLifecycleCallbacks activityCallbacks; - if (Build.VERSION.SDK_INT < 29) { - activityCallbacks = - new Pre29ActivityCallbacks( - tracer, visibleScreenTracker, startupTimer); - } else { - activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); - } - instrumentedApplication - .getApplication() - .registerActivityLifecycleCallbacks(activityCallbacks); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "activityLifecycleCallbacksInitialized", timingClock.now())); - }); + // Lifecycle events instrumentation are always installed. + installLifecycleInstrumentations(otelRumBuilder, visibleScreenTracker); OpenTelemetryRum openTelemetryRum = otelRumBuilder.build(application); recordInitializationSpans( startTimeNanos, initializationEvents, - openTelemetryRum.getOpenTelemetry().getTracer(SplunkRum.RUM_TRACER_NAME)); + openTelemetryRum.getOpenTelemetry().getTracer(RUM_TRACER_NAME)); return new SplunkRum(openTelemetryRum, globalAttributesSpanAppender); } + private void installLifecycleInstrumentations( + OpenTelemetryRumBuilder otelRumBuilder, VisibleScreenTracker visibleScreenTracker) { + + installStartupTimerInstrumentation(otelRumBuilder); + installActivityLifecycleEventsInstrumentation(otelRumBuilder, visibleScreenTracker); + installFragmentLifecycleInstrumentation(otelRumBuilder, visibleScreenTracker); + installScreenTrackingInstrumentation(otelRumBuilder, visibleScreenTracker); + + otelRumBuilder.addInstrumentation( + instrumentedApp -> { + initializationEvents.add( + new InitializationEvent( + "activityLifecycleCallbacksInitialized", timingClock.now())); + }); + } + + private void installScreenTrackingInstrumentation( + OpenTelemetryRumBuilder otelRumBuilder, VisibleScreenTracker visibleScreenTracker) { + otelRumBuilder.addInstrumentation( + instrumentedApp -> { + Application.ActivityLifecycleCallbacks screenTrackingBinding = + buildScreenTrackingBinding(visibleScreenTracker); + instrumentedApp + .getApplication() + .registerActivityLifecycleCallbacks(screenTrackingBinding); + }); + } + + @NonNull + private Application.ActivityLifecycleCallbacks buildScreenTrackingBinding( + VisibleScreenTracker visibleScreenTracker) { + if (Build.VERSION.SDK_INT < 29) { + return new Pre29VisibleScreenLifecycleBinding(visibleScreenTracker); + } + return new VisibleScreenLifecycleBinding(visibleScreenTracker); + } + + private void installFragmentLifecycleInstrumentation( + OpenTelemetryRumBuilder otelRumBuilder, VisibleScreenTracker visibleScreenTracker) { + otelRumBuilder.addInstrumentation( + instrumentedApp -> { + Application.ActivityLifecycleCallbacks fragmentRegisterer = + buildFragmentRegisterer(visibleScreenTracker, instrumentedApp); + instrumentedApp + .getApplication() + .registerActivityLifecycleCallbacks(fragmentRegisterer); + }); + } + + @NonNull + private Application.ActivityLifecycleCallbacks buildFragmentRegisterer( + VisibleScreenTracker visibleScreenTracker, InstrumentedApplication instrumentedApp) { + Tracer tracer = instrumentedApp.getOpenTelemetrySdk().getTracer(RUM_TRACER_NAME); + RumFragmentLifecycleCallbacks fragmentLifecycle = + new RumFragmentLifecycleCallbacks(tracer, visibleScreenTracker); + if (Build.VERSION.SDK_INT < 29) { + return RumFragmentActivityRegisterer.createPre29(fragmentLifecycle); + } + return RumFragmentActivityRegisterer.create(fragmentLifecycle); + } + + private void installStartupTimerInstrumentation(OpenTelemetryRumBuilder otelRumBuilder) { + otelRumBuilder.addInstrumentation( + instrumentedApp -> { + instrumentedApp + .getApplication() + .registerActivityLifecycleCallbacks( + startupTimer.createLifecycleCallback()); + }); + } + + private void installActivityLifecycleEventsInstrumentation( + OpenTelemetryRumBuilder otelRumBuilder, VisibleScreenTracker visibleScreenTracker) { + otelRumBuilder.addInstrumentation( + instrumentedApp -> { + Application.ActivityLifecycleCallbacks activityCallbacks = + buildActivityEventsCallback(visibleScreenTracker, instrumentedApp); + instrumentedApp + .getApplication() + .registerActivityLifecycleCallbacks(activityCallbacks); + }); + } + + @NonNull + private Application.ActivityLifecycleCallbacks buildActivityEventsCallback( + VisibleScreenTracker visibleScreenTracker, InstrumentedApplication instrumentedApp) { + Tracer tracer = instrumentedApp.getOpenTelemetrySdk().getTracer(RUM_TRACER_NAME); + ActivityTracerCache tracers = + new ActivityTracerCache(tracer, visibleScreenTracker, startupTimer); + if (Build.VERSION.SDK_INT < 29) { + return new Pre29ActivityCallbacks(tracers); + } + return new ActivityCallbacks(tracers); + } + private Resource createResource() { // applicationName can't be null at this stage String applicationName = requireNonNull(builder.applicationName); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/VisibleScreenLifecycleBinding.java b/splunk-otel-android/src/main/java/com/splunk/rum/VisibleScreenLifecycleBinding.java new file mode 100644 index 000000000..73504d1eb --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/VisibleScreenLifecycleBinding.java @@ -0,0 +1,44 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import android.app.Activity; +import androidx.annotation.NonNull; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; + +/** + * An ActivityLifecycleCallbacks that is responsible for telling the VisibleScreenTracker when an + * activity has been resumed and when an activity has been paused. It's just a glue class. + */ +class VisibleScreenLifecycleBinding implements DefaultingActivityLifecycleCallbacks { + + private final VisibleScreenTracker visibleScreenTracker; + + VisibleScreenLifecycleBinding(VisibleScreenTracker visibleScreenTracker) { + this.visibleScreenTracker = visibleScreenTracker; + } + + @Override + public void onActivityPostResumed(@NonNull Activity activity) { + visibleScreenTracker.activityResumed(activity); + } + + @Override + public void onActivityPrePaused(@NonNull Activity activity) { + visibleScreenTracker.activityPaused(activity); + } +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java index 6531c7674..4df387b61 100644 --- a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java @@ -17,15 +17,12 @@ package io.opentelemetry.rum.internal; import android.app.Activity; -import android.app.Application; -import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -final class ApplicationStateWatcher implements Application.ActivityLifecycleCallbacks { +final class ApplicationStateWatcher implements DefaultingActivityLifecycleCallbacks { private final List applicationStateListeners = new CopyOnWriteArrayList<>(); @@ -33,12 +30,6 @@ final class ApplicationStateWatcher implements Application.ActivityLifecycleCall // figure out when the app goes into the background. private int numberOfOpenActivities = 0; - public ApplicationStateWatcher() {} - - @Override - public void onActivityCreated( - @NonNull Activity activity, @Nullable Bundle savedInstanceState) {} - @Override public void onActivityStarted(@NonNull Activity activity) { if (numberOfOpenActivities == 0) { @@ -49,12 +40,6 @@ public void onActivityStarted(@NonNull Activity activity) { numberOfOpenActivities++; } - @Override - public void onActivityResumed(@NonNull Activity activity) {} - - @Override - public void onActivityPaused(@NonNull Activity activity) {} - @Override public void onActivityStopped(@NonNull Activity activity) { if (--numberOfOpenActivities == 0) { @@ -64,12 +49,6 @@ public void onActivityStopped(@NonNull Activity activity) { } } - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} - - @Override - public void onActivityDestroyed(@NonNull Activity activity) {} - void registerListener(ApplicationStateListener listener) { applicationStateListeners.add(listener); } diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/DefaultingActivityLifecycleCallbacks.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/DefaultingActivityLifecycleCallbacks.java new file mode 100644 index 000000000..1dad341dd --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/DefaultingActivityLifecycleCallbacks.java @@ -0,0 +1,55 @@ +/* + * Copyright Splunk 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.opentelemetry.rum.internal; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Interface helper for implementations that don't need/want all the extra baggage of the full + * Application.ActivityLifecycleCallbacks interface. Implementations can choose which methods to + * implement. + */ +public interface DefaultingActivityLifecycleCallbacks + extends Application.ActivityLifecycleCallbacks { + + @Override + default void onActivityCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) {} + + @Override + default void onActivityStarted(@NonNull Activity activity) {} + + @Override + default void onActivityResumed(@NonNull Activity activity) {} + + @Override + default void onActivityPaused(@NonNull Activity activity) {} + + @Override + default void onActivityStopped(@NonNull Activity activity) {} + + @Override + default void onActivitySaveInstanceState( + @NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + default void onActivityDestroyed(@NonNull Activity activity) {} +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegisterer.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegisterer.java new file mode 100644 index 000000000..b4f104506 --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegisterer.java @@ -0,0 +1,68 @@ +/* + * Copyright Splunk 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.opentelemetry.rum.internal.instrumentation.activity; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; + +/** + * Registers the RumFragmentLifecycleCallbacks when an activity is created. There are just 2 factory + * methods here, one for API level before 29, and one for the rest. + */ +public class RumFragmentActivityRegisterer { + + private RumFragmentActivityRegisterer() {} + + public static Application.ActivityLifecycleCallbacks create( + FragmentManager.FragmentLifecycleCallbacks fragmentCallbacks) { + return new DefaultingActivityLifecycleCallbacks() { + @Override + public void onActivityPreCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (activity instanceof FragmentActivity) { + register((FragmentActivity) activity, fragmentCallbacks); + } + } + }; + } + + public static Application.ActivityLifecycleCallbacks createPre29( + FragmentManager.FragmentLifecycleCallbacks fragmentCallbacks) { + return new DefaultingActivityLifecycleCallbacks() { + @Override + public void onActivityCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (activity instanceof FragmentActivity) { + register((FragmentActivity) activity, fragmentCallbacks); + } + } + }; + } + + private static void register( + FragmentActivity activity, + FragmentManager.FragmentLifecycleCallbacks fragmentCallbacks) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + fragmentManager.registerFragmentLifecycleCallbacks(fragmentCallbacks, true); + } +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/slowrendering/SlowRenderListener.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/slowrendering/SlowRenderListener.java index 5b6e53b99..73fadd65b 100644 --- a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/slowrendering/SlowRenderListener.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/slowrendering/SlowRenderListener.java @@ -21,9 +21,7 @@ import static io.opentelemetry.rum.internal.instrumentation.slowrendering.SlowRenderingDetector.OPEN_TELEMETRY_RUM_LOG_TAG; import android.app.Activity; -import android.app.Application; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -33,10 +31,10 @@ import android.view.Window; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.rum.internal.DefaultingActivityLifecycleCallbacks; import java.time.Duration; import java.time.Instant; import java.util.concurrent.ConcurrentHashMap; @@ -46,7 +44,7 @@ import java.util.concurrent.TimeUnit; @RequiresApi(api = Build.VERSION_CODES.N) -class SlowRenderListener implements Application.ActivityLifecycleCallbacks { +class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { static final int SLOW_THRESHOLD_MS = 16; static final int FROZEN_THRESHOLD_MS = 700; @@ -105,13 +103,6 @@ void start() { TimeUnit.MILLISECONDS); } - @Override - public void onActivityCreated( - @NonNull Activity activity, @Nullable Bundle savedInstanceState) {} - - @Override - public void onActivityStarted(@NonNull Activity activity) {} - @Override public void onActivityResumed(@NonNull Activity activity) { PerActivityListener listener = new PerActivityListener(activity); @@ -130,15 +121,6 @@ public void onActivityPaused(@NonNull Activity activity) { } } - @Override - public void onActivityStopped(@NonNull Activity activity) {} - - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} - - @Override - public void onActivityDestroyed(@NonNull Activity activity) {} - static class PerActivityListener implements Window.OnFrameMetricsAvailableListener { private final Activity activity; diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/ActivityCallbacksTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/ActivityCallbacksTest.java index 30f103484..119dee91c 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/ActivityCallbacksTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/ActivityCallbacksTest.java @@ -36,37 +36,36 @@ class ActivityCallbacksTest { @RegisterExtension final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); - private Tracer tracer; + private ActivityTracerCache tracers; private VisibleScreenTracker visibleScreenTracker; - private final AppStartupTimer startupTimer = new AppStartupTimer(); @BeforeEach public void setup() { - tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + AppStartupTimer startupTimer = new AppStartupTimer(); visibleScreenTracker = mock(VisibleScreenTracker.class); + tracers = new ActivityTracerCache(tracer, visibleScreenTracker, startupTimer); } @Test void appStartup() { - startupTimer.start(tracer); - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); Activity activity = mock(Activity.class); testHarness.runAppStartupLifecycle(activity); - startupTimer.end(); List spans = otelTesting.getSpans(); - assertEquals(2, spans.size()); + assertEquals(1, spans.size()); - SpanData startupSpan = spans.get(0); + SpanData creationSpan = spans.get(0); - assertEquals("AppStart", startupSpan.getName()); - assertEquals("cold", startupSpan.getAttributes().get(SplunkRum.START_TYPE_KEY)); + // TODO: ADD THIS TEST TO THE NEW COMPONENT(S) + // assertEquals("AppStart", startupSpan.getName()); + // assertEquals("cold", startupSpan.getAttributes().get(SplunkRum.START_TYPE_KEY)); - SpanData creationSpan = spans.get(1); + // SpanData creationSpan = spans.get(1); assertEquals( activity.getClass().getSimpleName(), @@ -96,8 +95,7 @@ void appStartup() { @Test void activityCreation() { - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); @@ -146,8 +144,7 @@ private void startupAppAndClearSpans(ActivityCallbackTestHarness testHarness) { @Test void activityRestart() { - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); @@ -189,8 +186,7 @@ void activityRestart() { @Test void activityResumed() { when(visibleScreenTracker.getPreviouslyVisibleScreen()).thenReturn("previousScreen"); - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); @@ -225,8 +221,7 @@ void activityResumed() { @Test void activityDestroyedFromStopped() { - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); @@ -261,8 +256,7 @@ void activityDestroyedFromStopped() { @Test void activityDestroyedFromPaused() { - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); @@ -317,8 +311,7 @@ void activityDestroyedFromPaused() { @Test void activityStoppedFromRunning() { - ActivityCallbacks activityCallbacks = - new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + ActivityCallbacks activityCallbacks = new ActivityCallbacks(tracers); ActivityCallbackTestHarness testHarness = new ActivityCallbackTestHarness(activityCallbacks); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/ActivityTracerCacheTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/ActivityTracerCacheTest.java new file mode 100644 index 000000000..736d4325f --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/ActivityTracerCacheTest.java @@ -0,0 +1,138 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ActivityTracerCacheTest { + + @Mock Activity activity; + + @Mock ActivityTracer activityTracer; + @Mock Function tracerCreator; + AtomicReference initialActivity; + + @BeforeEach + void setup() { + initialActivity = new AtomicReference<>(); + } + + @Test + void addEventNewActivity() { + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(activityTracer.addEvent(anyString())).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + ActivityTracer result = underTest.addEvent(activity, "beep"); + assertSame(activityTracer, result); + verify(activityTracer).addEvent("beep"); + verifyNoMoreInteractions(tracerCreator); + } + + @Test + void addEventExistingActivity() { + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(activityTracer.addEvent(anyString())).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + ActivityTracer result1 = underTest.addEvent(activity, "beep1"); + ActivityTracer result2 = underTest.addEvent(activity, "beep2"); + ActivityTracer result3 = underTest.addEvent(activity, "beep3"); + assertSame(activityTracer, result1); + assertSame(activityTracer, result2); + assertSame(activityTracer, result3); + verify(activityTracer).addEvent("beep1"); + verify(activityTracer).addEvent("beep2"); + verify(activityTracer).addEvent("beep3"); + verify(tracerCreator).apply(activity); + } + + @Test + void startSpanIfNoneInProgress() { + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(activityTracer.startSpanIfNoneInProgress("wrenchy")).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + + ActivityTracer result = underTest.startSpanIfNoneInProgress(activity, "wrenchy"); + assertSame(activityTracer, result); + verify(activityTracer).startSpanIfNoneInProgress("wrenchy"); + verifyNoMoreInteractions(tracerCreator); + } + + @Test + void initiateRestartSpanIfNecessary_singleActivity() { + + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(activityTracer.initiateRestartSpanIfNecessary(false)).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + + ActivityTracer result = underTest.initiateRestartSpanIfNecessary(activity); + assertSame(activityTracer, result); + verify(activityTracer).initiateRestartSpanIfNecessary(false); + verifyNoMoreInteractions(tracerCreator); + } + + @Test + void initiateRestartSpanIfNecessary_multiActivity() { + Activity activity2 = new Activity() { + // to get a new class name used in the cache + }; + ActivityTracer activityTracer2 = mock(ActivityTracer.class); + + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(tracerCreator.apply(activity2)).thenReturn(activityTracer2); + when(activityTracer.addEvent(anyString())).thenReturn(activityTracer); + when(activityTracer.initiateRestartSpanIfNecessary(true)).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + + underTest.addEvent(activity, "foo"); + underTest.addEvent(activity2, "bar"); + ActivityTracer result = underTest.initiateRestartSpanIfNecessary(activity); + assertSame(activityTracer, result); + verify(activityTracer).initiateRestartSpanIfNecessary(true); + } + + @Test + void startActivityCreation() { + when(tracerCreator.apply(activity)).thenReturn(activityTracer); + when(activityTracer.startActivityCreation()).thenReturn(activityTracer); + + ActivityTracerCache underTest = new ActivityTracerCache(tracerCreator); + + ActivityTracer result = underTest.startActivityCreation(activity); + assertSame(activityTracer, result); + verify(activityTracer).startActivityCreation(); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/Pre29ActivityLifecycleCallbacksTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/Pre29ActivityLifecycleCallbacksTest.java index 85ce24cab..b217efd98 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/Pre29ActivityLifecycleCallbacksTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/Pre29ActivityLifecycleCallbacksTest.java @@ -35,37 +35,35 @@ class Pre29ActivityLifecycleCallbacksTest { @RegisterExtension final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); - private Tracer tracer; + private ActivityTracerCache tracers; + private VisibleScreenTracker visibleScreenTracker; - private final AppStartupTimer appStartupTimer = new AppStartupTimer(); @BeforeEach void setup() { - tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + AppStartupTimer appStartupTimer = new AppStartupTimer(); + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); visibleScreenTracker = mock(VisibleScreenTracker.class); + tracers = new ActivityTracerCache(tracer, visibleScreenTracker, appStartupTimer); } @Test void appStartup() { - appStartupTimer.start(tracer); - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); Activity activity = mock(Activity.class); testHarness.runAppStartupLifecycle(activity); - appStartupTimer.end(); List spans = otelTesting.getSpans(); - assertEquals(2, spans.size()); - - SpanData appStartSpan = spans.get(0); + assertEquals(1, spans.size()); - assertEquals("AppStart", appStartSpan.getName()); - assertEquals("cold", appStartSpan.getAttributes().get(SplunkRum.START_TYPE_KEY)); + SpanData creationSpan = spans.get(0); - SpanData creationSpan = spans.get(1); + // TODO: Add test to relevant components + // assertEquals("AppStart", appStartSpan.getName()); + // assertEquals("cold", appStartSpan.getAttributes().get(SplunkRum.START_TYPE_KEY)); assertEquals( activity.getClass().getSimpleName(), @@ -87,8 +85,7 @@ void appStartup() { @Test void activityCreation() { - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); startupAppAndClearSpans(testHarness); @@ -128,8 +125,7 @@ private void startupAppAndClearSpans(Pre29ActivityCallbackTestHarness testHarnes @Test void activityRestart() { - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); @@ -166,8 +162,7 @@ void activityRestart() { void activityResumed() { when(visibleScreenTracker.getPreviouslyVisibleScreen()).thenReturn("previousScreen"); - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); @@ -199,8 +194,7 @@ void activityResumed() { @Test void activityDestroyedFromStopped() { - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); @@ -232,8 +226,7 @@ void activityDestroyedFromStopped() { @Test void activityDestroyedFromPaused() { - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); @@ -283,8 +276,7 @@ void activityDestroyedFromPaused() { @Test void activityStoppedFromRunning() { - Pre29ActivityCallbacks rumLifecycleCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, appStartupTimer); + Pre29ActivityCallbacks rumLifecycleCallbacks = new Pre29ActivityCallbacks(tracers); Pre29ActivityCallbackTestHarness testHarness = new Pre29ActivityCallbackTestHarness(rumLifecycleCallbacks); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/Pre29VisibleScreenLifecycleBindingTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/Pre29VisibleScreenLifecycleBindingTest.java new file mode 100644 index 000000000..234ffc11c --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/Pre29VisibleScreenLifecycleBindingTest.java @@ -0,0 +1,50 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.app.Activity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class Pre29VisibleScreenLifecycleBindingTest { + @Mock Activity activity; + @Mock VisibleScreenTracker tracker; + + @Test + void postResumed() { + Pre29VisibleScreenLifecycleBinding underTest = + new Pre29VisibleScreenLifecycleBinding(tracker); + underTest.onActivityResumed(activity); + verify(tracker).activityResumed(activity); + verifyNoMoreInteractions(tracker); + } + + @Test + void prePaused() { + Pre29VisibleScreenLifecycleBinding underTest = + new Pre29VisibleScreenLifecycleBinding(tracker); + underTest.onActivityPaused(activity); + verify(tracker).activityPaused(activity); + verifyNoMoreInteractions(tracker); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/VisibleScreenLifecycleBindingTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/VisibleScreenLifecycleBindingTest.java new file mode 100644 index 000000000..29169b313 --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/VisibleScreenLifecycleBindingTest.java @@ -0,0 +1,48 @@ +/* + * Copyright Splunk 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 com.splunk.rum; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.app.Activity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class VisibleScreenLifecycleBindingTest { + + @Mock Activity activity; + @Mock VisibleScreenTracker tracker; + + @Test + void postResumed() { + VisibleScreenLifecycleBinding underTest = new VisibleScreenLifecycleBinding(tracker); + underTest.onActivityPostResumed(activity); + verify(tracker).activityResumed(activity); + verifyNoMoreInteractions(tracker); + } + + @Test + void prePaused() { + VisibleScreenLifecycleBinding underTest = new VisibleScreenLifecycleBinding(tracker); + underTest.onActivityPrePaused(activity); + verify(tracker).activityPaused(activity); + } +} diff --git a/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegistererTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegistererTest.java new file mode 100644 index 000000000..fa141703c --- /dev/null +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/instrumentation/activity/RumFragmentActivityRegistererTest.java @@ -0,0 +1,84 @@ +/* + * Copyright Splunk 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.opentelemetry.rum.internal.instrumentation.activity; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.Application; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RumFragmentActivityRegistererTest { + + @Mock FragmentManager.FragmentLifecycleCallbacks fragmentCallbacks; + + @Test + void createHappyPath() { + FragmentActivity activity = mock(FragmentActivity.class); + FragmentManager manager = mock(FragmentManager.class); + + when(activity.getSupportFragmentManager()).thenReturn(manager); + + Application.ActivityLifecycleCallbacks underTest = + RumFragmentActivityRegisterer.create(fragmentCallbacks); + + underTest.onActivityPreCreated(activity, null); + verify(manager).registerFragmentLifecycleCallbacks(fragmentCallbacks, true); + } + + @Test + void callbackIgnoresNonFragmentActivity() { + Activity activity = mock(Activity.class); + + Application.ActivityLifecycleCallbacks underTest = + RumFragmentActivityRegisterer.create(fragmentCallbacks); + + underTest.onActivityPreCreated(activity, null); + } + + @Test + void createPre29HappyPath() { + FragmentActivity activity = mock(FragmentActivity.class); + FragmentManager manager = mock(FragmentManager.class); + + when(activity.getSupportFragmentManager()).thenReturn(manager); + + Application.ActivityLifecycleCallbacks underTest = + RumFragmentActivityRegisterer.createPre29(fragmentCallbacks); + + underTest.onActivityCreated(activity, null); + verify(manager).registerFragmentLifecycleCallbacks(fragmentCallbacks, true); + } + + @Test + void pre29CallbackIgnoresNonFragmentActivity() { + Activity activity = mock(Activity.class); + + Application.ActivityLifecycleCallbacks underTest = + RumFragmentActivityRegisterer.createPre29(fragmentCallbacks); + + underTest.onActivityCreated(activity, null); + } +}