Skip to content

Commit

Permalink
Merge 343a3dd into b03d10f
Browse files Browse the repository at this point in the history
  • Loading branch information
markushi authored May 4, 2023
2 parents b03d10f + 343a3dd commit ea7fcbe
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
- Create `User` and `Breadcrumb` from map ([#2614](https://github.com/getsentry/sentry-java/pull/2614))
- Add `sent_at` to envelope header item ([#2638](https://github.com/getsentry/sentry-java/pull/2638))
- Add support for Sentry Kotlin Plugin ([#2695](https://github.com/getsentry/sentry-java/pull/2695))

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.compose.gestures.ComposeGestureTargetLocator;
import io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.Objects;
import java.io.BufferedInputStream;
Expand All @@ -42,9 +44,12 @@
@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
final class AndroidOptionsInitializer {

static final String SENTRY_COMPOSE_INTEGRATION_CLASS_NAME =
static final String SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME =
"io.sentry.compose.gestures.ComposeGestureTargetLocator";

static final String SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME =
"io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter";

static final String COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner";

/** private ctor */
Expand Down Expand Up @@ -148,22 +153,34 @@ static void initializeIntegrationsAndProcessors(

final boolean isAndroidXScrollViewAvailable =
loadClass.isClassAvailable("androidx.core.view.ScrollingView", options);
final boolean isComposeUpstreamAvailable =
loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);

if (options.getGestureTargetLocators().isEmpty()) {
final List<GestureTargetLocator> gestureTargetLocators = new ArrayList<>(2);
gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));

final boolean isComposeUpstreamAvailable =
loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);
final boolean isComposeAvailable =
(isComposeUpstreamAvailable
&& loadClass.isClassAvailable(SENTRY_COMPOSE_INTEGRATION_CLASS_NAME, options));
&& loadClass.isClassAvailable(
SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME, options));

if (isComposeAvailable) {
gestureTargetLocators.add(new ComposeGestureTargetLocator());
}
options.setGestureTargetLocators(gestureTargetLocators);
}

if (options.getViewHierarchyExporters().isEmpty()
&& isComposeUpstreamAvailable
&& loadClass.isClassAvailable(
SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME, options)) {

final List<ViewHierarchyExporter> viewHierarchyExporters = new ArrayList<>(1);
viewHierarchyExporters.add(new ComposeViewHierarchyExporter());
options.setViewHierarchyExporters(viewHierarchyExporters);
}

options.setMainThreadChecker(AndroidMainThreadChecker.getInstance());
if (options.getCollectors().isEmpty()) {
options.addCollector(new AndroidMemoryCollector());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ class AndroidOptionsInitializerTest {
fixture.initSutWithClassLoader(
classesToLoad = listOf(
AndroidOptionsInitializer.COMPOSE_CLASS_NAME,
AndroidOptionsInitializer.SENTRY_COMPOSE_INTEGRATION_CLASS_NAME
AndroidOptionsInitializer.SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME
)
)

Expand Down
10 changes: 10 additions & 0 deletions sentry-compose-helper/api/sentry-compose-helper.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
public class io/sentry/compose/SentryComposeUtil {
public fun <init> ()V
public static final fun getLayoutNodeXY (Landroidx/compose/ui/node/LayoutNode;)[I
}

public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator {
public fun <init> ()V
public fun locate (Ljava/lang/Object;FFLio/sentry/internal/gestures/UiElement$Type;)Lio/sentry/internal/gestures/UiElement;
Expand All @@ -10,3 +15,8 @@ public final class io/sentry/compose/helper/BuildConfig {
public static final field VERSION_NAME Ljava/lang/String;
}

public final class io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter : io/sentry/internal/viewhierarchy/ViewHierarchyExporter {
public fun <init> ()V
public fun export (Lio/sentry/protocol/ViewHierarchyNode;Ljava/lang/Object;)Z
}

10 changes: 10 additions & 0 deletions sentry-compose-helper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ kotlin {
compileOnly(compose.ui)
}
}
val jvmTest by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.ui)

implementation(Config.TestLibs.kotlinTestJunit)
implementation(Config.TestLibs.mockitoKotlin)
implementation(Config.TestLibs.mockitoInline)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.sentry.compose;

import androidx.compose.ui.layout.LayoutCoordinatesKt;
import androidx.compose.ui.node.LayoutNode;
import org.jetbrains.annotations.NotNull;

public class SentryComposeUtil {
public static final int[] getLayoutNodeXY(@NotNull final LayoutNode node) {
// Offset is a Kotlin value class, packing x/y into a long
// TODO find a way to use the existing APIs
final long nodePosition = LayoutCoordinatesKt.positionInWindow(node.getCoordinates());
final int nodeX = (int) Float.intBitsToFloat((int) (nodePosition >> 32));
final int nodeY = (int) Float.intBitsToFloat((int) (nodePosition));
return new int[] {nodeX, nodeY};
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.sentry.compose.gestures;

import androidx.compose.ui.layout.LayoutCoordinatesKt;
import androidx.compose.ui.layout.ModifierInfo;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.Owner;
import androidx.compose.ui.semantics.SemanticsConfiguration;
import androidx.compose.ui.semantics.SemanticsModifier;
import androidx.compose.ui.semantics.SemanticsPropertyKey;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.compose.SentryComposeUtil;
import io.sentry.compose.helper.BuildConfig;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.gestures.UiElement;
Expand Down Expand Up @@ -63,7 +63,7 @@ public ComposeGestureTargetLocator() {
isScrollable = true;
} else if ("OnClick".equals(key)) {
isClickable = true;
} else if ("TestTag".equals(key)) {
} else if ("SentryTag".equals(key) || "TestTag".equals(key)) {
if (entry.getValue() instanceof String) {
testTag = (String) entry.getValue();
}
Expand Down Expand Up @@ -96,11 +96,9 @@ private static boolean layoutNodeBoundsContain(
final int nodeHeight = node.getHeight();
final int nodeWidth = node.getWidth();

// Offset is a Kotlin value class, packing x/y into a long
// TODO find a way to use the existing APIs
final long nodePosition = LayoutCoordinatesKt.positionInWindow(node.getCoordinates());
final int nodeX = (int) Float.intBitsToFloat((int) (nodePosition >> 32));
final int nodeY = (int) Float.intBitsToFloat((int) (nodePosition));
final int[] xy = SentryComposeUtil.getLayoutNodeXY(node);
final int nodeX = xy[0];
final int nodeY = xy[1];

return x >= nodeX && x <= (nodeX + nodeWidth) && y >= nodeY && y <= (nodeY + nodeHeight);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.sentry.compose.viewhierarchy;

import androidx.compose.runtime.collection.MutableVector;
import androidx.compose.ui.layout.ModifierInfo;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.Owner;
import androidx.compose.ui.semantics.SemanticsConfiguration;
import androidx.compose.ui.semantics.SemanticsModifier;
import androidx.compose.ui.semantics.SemanticsPropertyKey;
import io.sentry.compose.SentryComposeUtil;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.protocol.ViewHierarchyNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@SuppressWarnings("KotlinInternalInJava")
public final class ComposeViewHierarchyExporter implements ViewHierarchyExporter {

@Override
public boolean export(@NotNull final ViewHierarchyNode parent, @NotNull final Object element) {

if (!(element instanceof Owner)) {
return false;
}

final @NotNull LayoutNode rootNode = ((Owner) element).getRoot();
addChild(parent, rootNode);
return true;
}

private static void addChild(
@NotNull final ViewHierarchyNode parent, @NotNull final LayoutNode node) {
if (node.isPlaced()) {
final ViewHierarchyNode vhNode = new ViewHierarchyNode();
setBounds(node, vhNode);
setTag(node, vhNode);
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(vhNode);

final MutableVector<LayoutNode> children = node.getZSortedChildren();
final int childrenCount = children.getSize();
for (int i = 0; i < childrenCount; i++) {
final LayoutNode child = children.get(i);
addChild(vhNode, child);
}
}
}

private static void setTag(
@NotNull final LayoutNode node, @NotNull final ViewHierarchyNode vhNode) {
final List<ModifierInfo> modifiers = node.getModifierInfo();
for (ModifierInfo modifierInfo : modifiers) {
if (modifierInfo.getModifier() instanceof SemanticsModifier) {
final SemanticsModifier semanticsModifierCore =
(SemanticsModifier) modifierInfo.getModifier();
final SemanticsConfiguration semanticsConfiguration =
semanticsModifierCore.getSemanticsConfiguration();
for (Map.Entry<? extends SemanticsPropertyKey<?>, ?> entry : semanticsConfiguration) {
final @Nullable String key = entry.getKey().getName();
if ("SentryTag".equals(key) || "TestTag".equals(key)) {
if (entry.getValue() instanceof String) {
vhNode.setTag((String) entry.getValue());
}
}
}
}
}
}

private static void setBounds(@NotNull LayoutNode node, @NotNull ViewHierarchyNode vhNode) {
final int nodeHeight = node.getHeight();
final int nodeWidth = node.getWidth();

final int[] xy = SentryComposeUtil.getLayoutNodeXY(node);

vhNode.setHeight(Double.valueOf(nodeHeight));
vhNode.setWidth(Double.valueOf(nodeWidth));
vhNode.setX(Double.valueOf(xy[0]));
vhNode.setY(Double.valueOf(xy[1]));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.sentry.compose.viewhierarchy;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import androidx.compose.runtime.collection.MutableVector;
import androidx.compose.ui.layout.LayoutCoordinates;
import androidx.compose.ui.layout.ModifierInfo;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.Owner;
import androidx.compose.ui.semantics.SemanticsConfiguration;
import androidx.compose.ui.semantics.SemanticsModifier;
import androidx.compose.ui.semantics.SemanticsPropertyKey;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.protocol.ViewHierarchyNode;
import java.util.ArrayList;
import java.util.List;
import kotlin.jvm.functions.Function2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import org.mockito.Mockito;

public class ComposeViewHierarchyExporterTest {

@Test
public void testComposeViewHierarchyExport() {
final ViewHierarchyNode rootVhNode = new ViewHierarchyNode();

final LayoutNode childA = mockLayoutNode(true, "childA", 10, 20);
final LayoutNode childB = mockLayoutNode(true, null, 10, 20);
final LayoutNode childC = mockLayoutNode(false, null, 10, 20);
final LayoutNode parent = mockLayoutNode(true, "root", 30, 40, childA, childB, childC);

final Owner node = Mockito.mock(Owner.class);
Mockito.when(node.getRoot()).thenReturn(parent);

final ViewHierarchyExporter exporter = new ComposeViewHierarchyExporter();
exporter.export(rootVhNode, node);

assertEquals(1, rootVhNode.getChildren().size());
final ViewHierarchyNode parentVhNode = rootVhNode.getChildren().get(0);

assertEquals("root", parentVhNode.getTag());
assertEquals(30.0, parentVhNode.getWidth().doubleValue(), 0.001);
assertEquals(40.0, parentVhNode.getHeight().doubleValue(), 0.001);

// ensure not placed elements (childC) are not part of the view hierarchy
assertEquals(2, parentVhNode.getChildren().size());

final ViewHierarchyNode childAVhNode = parentVhNode.getChildren().get(0);
assertEquals("childA", childAVhNode.getTag());
assertEquals(10.0, childAVhNode.getWidth().doubleValue(), 0.001);
assertEquals(20.0, childAVhNode.getHeight().doubleValue(), 0.001);
assertNull(childAVhNode.getChildren());

final ViewHierarchyNode childBVhNode = parentVhNode.getChildren().get(1);
assertNull(childBVhNode.getTag());
}

private static LayoutNode mockLayoutNode(
final boolean isPlaced,
final @Nullable String tag,
final int width,
final int height,
LayoutNode... children) {
final LayoutNode nodeA = Mockito.mock(LayoutNode.class);
Mockito.when(nodeA.isPlaced()).thenReturn(isPlaced);
Mockito.when((nodeA.getWidth())).thenReturn(width);
Mockito.when((nodeA.getHeight())).thenReturn(height);

final ModifierInfo modifierInfo = Mockito.mock(ModifierInfo.class);
Mockito.when(modifierInfo.getModifier())
.thenReturn(
new SemanticsModifier() {
@NotNull
@Override
public SemanticsConfiguration getSemanticsConfiguration() {
final SemanticsConfiguration config = new SemanticsConfiguration();
config.set(
new SemanticsPropertyKey<>(
"SentryTag",
new Function2<String, String, String>() {
@Override
public String invoke(String s, String s2) {
return s;
}
}),
tag);
return config;
}
});
final List<ModifierInfo> modifierInfoList = new ArrayList<>();
modifierInfoList.add(modifierInfo);
Mockito.when((nodeA.getModifierInfo())).thenReturn(modifierInfoList);

Mockito.when((nodeA.getZSortedChildren()))
.thenReturn(new MutableVector<>(children, children.length));

final LayoutCoordinates coordinates = Mockito.mock(LayoutCoordinates.class);
Mockito.when(nodeA.getCoordinates()).thenReturn(coordinates);
return nodeA;
}
}
7 changes: 7 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ public final class io/sentry/compose/SentryComposeTracingKt {
public static final fun SentryTraced (Ljava/lang/String;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class io/sentry/compose/SentryModifier {
public static final field $stable I
public static final field INSTANCE Lio/sentry/compose/SentryModifier;
public static final field TAG Ljava/lang/String;
public static final fun sentryTag (Landroidx/compose/ui/Modifier;Ljava/lang/String;)Landroidx/compose/ui/Modifier;
}

public final class io/sentry/compose/SentryNavigationIntegrationKt {
public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController;
}
Expand Down
2 changes: 2 additions & 0 deletions sentry-compose/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# The Android SDK checks at runtime if these classes are available via Class.forName
-keep class io.sentry.compose.gestures.ComposeGestureTargetLocator { <init>(...); }
-keep class io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter { <init>(...); }

-keepnames interface androidx.compose.ui.node.Owner

# R8 will warn about missing classes if people don't have androidx.compose-navigation on their
Expand Down
Loading

0 comments on commit ea7fcbe

Please sign in to comment.