Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Internal

- Avoid constructing an exception per view when resolving view ids during view-hierarchy and gesture capture ([#5631](https://github.com/getsentry/sentry-java/pull/5631))

## 8.45.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,10 @@ private static ViewHierarchyNode viewToNode(@NotNull final View view) {
node.setType(className);

try {
final String identifier = ViewUtils.getResourceId(view);
node.setIdentifier(identifier);
final @Nullable String identifier = ViewUtils.resolveResourceId(view);
if (identifier != null) {
node.setIdentifier(identifier);
}
} catch (Throwable e) {
// ignored
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.sentry.android.core.internal.gestures;

import android.content.res.Resources;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ScrollView;
Expand Down Expand Up @@ -42,13 +41,12 @@ && isViewScrollable(view, isAndroidXAvailable.getValue())) {
}

private UiElement createUiElement(final @NotNull View targetView) {
try {
final String resourceName = ViewUtils.getResourceId(targetView);
@Nullable String className = ClassUtil.getClassName(targetView);
return new UiElement(targetView, className, resourceName, null, ORIGIN);
} catch (Resources.NotFoundException ignored) {
final @Nullable String resourceName = ViewUtils.resolveResourceId(targetView);
if (resourceName == null) {
return null;
}
@Nullable String className = ClassUtil.getClassName(targetView);
return new UiElement(targetView, className, resourceName, null, ORIGIN);
}

private static boolean isViewTappable(final @NotNull View view) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,12 @@ private static boolean touchWithinBounds(
* @return human-readable view id
*/
static String getResourceIdWithFallback(final @NotNull View view) {
final int viewId = view.getId();
try {
return getResourceId(view);
} catch (Resources.NotFoundException e) {
final @Nullable String resourceId = resolveResourceId(view);
if (resourceId == null) {
// fall back to hex representation of the id
return "0x" + Integer.toString(viewId, 16);
return "0x" + Integer.toString(view.getId(), 16);
}
return resourceId;
}

/**
Expand All @@ -121,15 +120,36 @@ static String getResourceIdWithFallback(final @NotNull View view) {
* @throws Resources.NotFoundException in case the view id was not found
*/
public static String getResourceId(final @NotNull View view) throws Resources.NotFoundException {
final @Nullable String resourceId = resolveResourceId(view);
if (resourceId == null) {
throw new Resources.NotFoundException();
}
return resourceId;
}

/**
* Retrieves the human-readable view id based on {@code view.getContext().getResources()}, or
* {@code null} when the view has no resource-backed id. Unlike {@link #getResourceId(View)} this
* does not throw for unresolved ids, avoiding exception-driven control flow on hot, main-thread
* paths such as view-hierarchy snapshots and gesture target resolution.
*
* @param view - the view whose id is being retrieved
* @return human-readable view id, or {@code null} if it cannot be resolved
*/
public static @Nullable String resolveResourceId(final @NotNull View view) {
final int viewId = view.getId();
if (viewId == View.NO_ID || isViewIdGenerated(viewId)) {
throw new Resources.NotFoundException();
return null;
}
final Resources resources = view.getContext().getResources();
if (resources != null) {
if (resources == null) {
return "";
}
try {
return resources.getResourceEntryName(viewId);
} catch (Resources.NotFoundException e) {
return null;
}
return "";
}

private static boolean isViewIdGenerated(int id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.view.View
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
Expand Down Expand Up @@ -80,6 +81,65 @@ class ViewUtilsTest {
verify(context, never()).resources
}

@Test
fun `resolveResourceId returns resource name when available`() {
val view =
mock<View> {
whenever(it.id).doReturn(View.generateViewId())

val context = mock<Context>()
val resources = mock<Resources>()
whenever(resources.getResourceEntryName(it.id)).thenReturn("test_view")
whenever(context.resources).thenReturn(resources)
whenever(it.context).thenReturn(context)
}

assertEquals("test_view", ViewUtils.resolveResourceId(view))
}

@Test
fun `resolveResourceId returns null without throwing for generated id`() {
val context = mock<Context>()
val view =
mock<View> {
// View.generateViewId() starts with 1
whenever(it.id).doReturn(1)
whenever(it.context).thenReturn(context)
}

assertNull(ViewUtils.resolveResourceId(view))
verify(context, never()).resources
}

@Test
fun `resolveResourceId returns null without throwing when view has no id`() {
val context = mock<Context>()
val view =
mock<View> {
whenever(it.id).doReturn(View.NO_ID)
whenever(it.context).thenReturn(context)
}

assertNull(ViewUtils.resolveResourceId(view))
verify(context, never()).resources
}

@Test
fun `resolveResourceId returns null without throwing when resource not found`() {
val view =
mock<View> {
whenever(it.id).doReturn(1234)

val context = mock<Context>()
val resources = mock<Resources>()
whenever(resources.getResourceEntryName(it.id)).thenThrow(Resources.NotFoundException())
whenever(context.resources).thenReturn(resources)
whenever(it.context).thenReturn(context)
}

assertNull(ViewUtils.resolveResourceId(view))
}

@Test
fun `getResourceIdWithFallback falls back to hexadecimal id when resource not found`() {
val view =
Expand Down
Loading