Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.sentry.android.core;

import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME;

import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.Build;
import io.sentry.core.EnvelopeReader;
import io.sentry.core.IEnvelopeReader;
import io.sentry.core.ILogger;
Expand All @@ -13,6 +16,7 @@
import io.sentry.core.util.Objects;
import java.io.File;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Android Options initializer, it reads configurations from AndroidManifest and sets to the
Expand Down Expand Up @@ -47,6 +51,40 @@ static void init(
final @NotNull SentryAndroidOptions options,
@NotNull Context context,
final @NotNull ILogger logger) {
init(options, context, logger, new BuildInfoProvider());
}

/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param context the Application context
* @param logger the ILogger interface
* @param buildInfoProvider the IBuildInfoProvider interface
*/
static void init(
final @NotNull SentryAndroidOptions options,
@NotNull Context context,
final @NotNull ILogger logger,
final @NotNull IBuildInfoProvider buildInfoProvider) {
init(options, context, logger, buildInfoProvider, new LoadClass());
}

/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param context the Application context
* @param logger the ILogger interface
* @param buildInfoProvider the IBuildInfoProvider interface
* @param loadClass the ILoadClass interface
*/
static void init(
final @NotNull SentryAndroidOptions options,
@NotNull Context context,
final @NotNull ILogger logger,
final @NotNull IBuildInfoProvider buildInfoProvider,
final @NotNull ILoadClass loadClass) {
Objects.requireNonNull(context, "The context is required.");

// it returns null if ContextImpl, so let's check for nullability
Expand All @@ -67,12 +105,12 @@ static void init(

final IEnvelopeReader envelopeReader = new EnvelopeReader();

installDefaultIntegrations(context, options, envelopeReader);
installDefaultIntegrations(context, options, envelopeReader, buildInfoProvider, loadClass);

readDefaultOptionValues(options, context);

options.addEventProcessor(
new DefaultAndroidEventProcessor(context, options, new BuildInfoProvider()));
new DefaultAndroidEventProcessor(context, options, buildInfoProvider));

options.setSerializer(new AndroidSerializer(options.getLogger(), envelopeReader));

Expand All @@ -82,11 +120,14 @@ static void init(
private static void installDefaultIntegrations(
final @NotNull Context context,
final @NotNull SentryOptions options,
final @NotNull IEnvelopeReader envelopeReader) {
final @NotNull IEnvelopeReader envelopeReader,
final @NotNull IBuildInfoProvider buildInfoProvider,
final @NotNull ILoadClass loadClass) {

// Integrations are registered in the same order. NDK before adding Watch outbox,
// because sentry-native move files around and we don't want to watch that.
options.addIntegration(new NdkIntegration());
final Class<?> sentryNdkClass = loadNdkIfAvailable(options, buildInfoProvider, loadClass);
options.addIntegration(new NdkIntegration(sentryNdkClass));
options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver(envelopeReader));

// Send cached envelopes from outbox path
Expand Down Expand Up @@ -183,4 +224,28 @@ private static void initializeCacheDirs(
options.getLogger().log(SentryLevel.WARNING, "No session dir path is defined in options.");
}
}

private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) {
return buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN;
}

private static @Nullable Class<?> loadNdkIfAvailable(
final @NotNull SentryOptions options,
final @NotNull IBuildInfoProvider buildInfoProvider,
final @NotNull ILoadClass loadClass) {
if (isNdkAvailable(buildInfoProvider)) {
try {
return loadClass.loadClass(SENTRY_NDK_CLASS_NAME);
} catch (ClassNotFoundException e) {
options.getLogger().log(SentryLevel.ERROR, "Failed to load SentryNdk.", e);
} catch (UnsatisfiedLinkError e) {
options
.getLogger()
.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) SentryNdk.", e);
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e);
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.sentry.android.core;

/** An Adapter for making Class.forName testable */
interface ILoadClass {

/**
* Try to load a class via reflection
*
* @param clazz the full class name
* @return a Class<?>
* @throws ClassNotFoundException if class is not found
*/
Class<?> loadClass(String clazz) throws ClassNotFoundException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.sentry.android.core;

import org.jetbrains.annotations.NotNull;

final class LoadClass implements ILoadClass {
@Override
public @NotNull Class<?> loadClass(@NotNull String clazz) throws ClassNotFoundException {
return Class.forName(clazz);
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
package io.sentry.android.core;

import android.os.Build;
import io.sentry.core.IHub;
import io.sentry.core.Integration;
import io.sentry.core.SentryLevel;
import io.sentry.core.SentryOptions;
import io.sentry.core.util.Objects;
import java.lang.reflect.Method;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/** Enables the NDK error reporting for Android */
public final class NdkIntegration implements Integration {
private boolean isNdkAvailable() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;

public static final String SENTRY_NDK_CLASS_NAME = "io.sentry.android.ndk.SentryNdk";

private final @Nullable Class<?> sentryNdkClass;

public NdkIntegration(final @Nullable Class<?> sentryNdkClass) {
this.sentryNdkClass = sentryNdkClass;
}

@Override
public final void register(IHub hub, SentryOptions options) {
boolean enabled = options.isEnableNdk() && isNdkAvailable();
public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) {
Objects.requireNonNull(hub, "Hub is required");
Objects.requireNonNull(options, "SentryOptions is required");

final boolean enabled = options.isEnableNdk();
options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration enabled: %s", enabled);

// Note: `hub` isn't used here because the NDK integration writes files to disk which are picked
// up by another integration (EnvelopeFileObserverIntegration).
if (enabled) {
if (enabled && sentryNdkClass != null) {
try {
Class<?> cls = Class.forName("io.sentry.android.ndk.SentryNdk");

Method method = cls.getMethod("init", SentryOptions.class);
Object[] args = new Object[1];
final Method method = sentryNdkClass.getMethod("init", SentryOptions.class);
final Object[] args = new Object[1];
args[0] = options;
method.invoke(null, args);

options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed.");
} catch (ClassNotFoundException e) {
options.setEnableNdk(false);
options.getLogger().log(SentryLevel.ERROR, "Failed to load SentryNdk.", e);
} catch (UnsatisfiedLinkError e) {
} catch (NoSuchMethodException e) {
options.setEnableNdk(false);
options
.getLogger()
.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) SentryNdk.", e);
.log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.init method.", e);
} catch (Throwable e) {
options.setEnableNdk(false);
options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e);
Expand All @@ -46,4 +52,10 @@ public final void register(IHub hub, SentryOptions options) {
options.setEnableNdk(false);
}
Comment thread
marandaneto marked this conversation as resolved.
}

@TestOnly
@Nullable
Class<?> getSentryNdkClass() {
return sentryNdkClass;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package io.sentry.android.core

import android.app.Application
import android.content.Context
import android.os.Bundle
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.sentry.core.ILogger
import io.sentry.core.MainEventProcessor
import io.sentry.core.SentryLevel
import io.sentry.core.SentryOptions
import java.io.File
import java.lang.RuntimeException
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -181,13 +189,85 @@ class AndroidOptionsInitializerTest {
}

@Test
fun `NdkIntegration added to integration list`() {
val sentryOptions = SentryAndroidOptions()
val mockContext = createMockContext()
fun `NdkIntegration will load SentryNdk class and add to the integration list`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
isDebug = true
}

AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMock())

AndroidOptionsInitializer.init(sentryOptions, mockContext)
val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration }
assertNotNull(actual)
assertNotNull((actual as NdkIntegration).sentryNdkClass)

verify(logger, never()).log(eq(SentryLevel.ERROR), any<String>(), any())
verify(logger, never()).log(eq(SentryLevel.FATAL), any<String>(), any())
}

@Test
fun `NdkIntegration won't be enabled because API is lower than 16`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
isDebug = true
}

AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(14), createClassMock())

val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration }
assertNull((actual as NdkIntegration).sentryNdkClass)

verify(logger, never()).log(eq(SentryLevel.ERROR), any<String>(), any())
verify(logger, never()).log(eq(SentryLevel.FATAL), any<String>(), any())
}

@Test
fun `NdkIntegration won't be enabled, it throws linkage error`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
isDebug = true
}

AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(UnsatisfiedLinkError()))

val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration }
assertNull((actual as NdkIntegration).sentryNdkClass)

verify(logger).log(eq(SentryLevel.ERROR), any<String>(), any())
}

@Test
fun `NdkIntegration won't be enabled, it throws class not found`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
isDebug = true
}

AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(ClassNotFoundException()))

val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration }
assertNull((actual as NdkIntegration).sentryNdkClass)

verify(logger).log(eq(SentryLevel.ERROR), any<String>(), any())
}

@Test
fun `NdkIntegration won't be enabled, it throws unknown error`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
isDebug = true
}

AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(RuntimeException()))

val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration }
assertNull((actual as NdkIntegration).sentryNdkClass)

verify(logger).log(eq(SentryLevel.ERROR), any<String>(), any())
}

@Test
Expand Down Expand Up @@ -247,4 +327,28 @@ class AndroidOptionsInitializerTest {
whenever(mockContext.cacheDir).thenReturn(file)
return mockContext
}

private fun createBundleWithDsn(): Bundle {
return Bundle().apply {
putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123")
}
}

private fun createBuildInfo(minApi: Int = 16): IBuildInfoProvider {
val buildInfo = mock<IBuildInfoProvider>()
whenever(buildInfo.sdkInfoVersion).thenReturn(minApi)
return buildInfo
}

private fun createClassMock(clazz: Class<*> = SentryNdk::class.java): ILoadClass {
val loadClassMock = mock<ILoadClass>()
whenever(loadClassMock.loadClass(any())).thenReturn(clazz)
return loadClassMock
}

private fun createClassMockThrows(ex: Throwable): ILoadClass {
val loadClassMock = mock<ILoadClass>()
whenever(loadClassMock.loadClass(any())).thenThrow(ex)
return loadClassMock
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import android.app.Application
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.os.Bundle
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import java.io.File
import java.io.FileNotFoundException

object ContextUtilsTest {
fun mockMetaData(mockContext: Context = createMockContext(), metaData: Bundle): Context {
val mockPackageManager: PackageManager = mock()
val mockApplicationInfo: ApplicationInfo = mock()
val assets: AssetManager = mock()

whenever(mockContext.packageName).thenReturn("io.sentry.sample.test")
whenever(mockContext.packageManager).thenReturn(mockPackageManager)
whenever(mockPackageManager.getApplicationInfo(mockContext.packageName, PackageManager.GET_META_DATA))
.thenReturn(mockApplicationInfo)
whenever(assets.open(any())).thenThrow(FileNotFoundException())
whenever(mockContext.assets).thenReturn(assets)

mockApplicationInfo.metaData = metaData
return mockContext
Expand Down
Loading