diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8c783e..b38fe017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- recording: fix observation on multiple threads in layout/draw is not supported for compose ([#198](https://github.com/PostHog/posthog-android/pull/198)) + ## 3.9.0 - 2024-10-30 - recording: add replay masking to jetpack compose views ([#198](https://github.com/PostHog/posthog-android/pull/198)) diff --git a/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt b/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt index 2d803c85..4d75f111 100644 --- a/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt +++ b/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt @@ -607,43 +607,59 @@ public class PostHogReplayIntegration( view: View, maskableWidgets: MutableList, ) { - try { - val semanticsOwner = - (view as? RootForTest)?.semanticsOwner ?: run { - config.logger.log("View is not a RootForTest: $view") - return - } - val semanticsNodes = semanticsOwner.getAllSemanticsNodes(true) + val latch = CountDownLatch(1) - semanticsNodes.forEach { node -> - val hasText = node.config.contains(SemanticsProperties.Text) - val hasEditableText = node.config.contains(SemanticsProperties.EditableText) - val hasPassword = node.config.contains(SemanticsProperties.Password) - val hasImage = node.config.contains(SemanticsProperties.ContentDescription) + // compose requires the handler to be on the main thread + // see https://github.com/PostHog/posthog-android/issues/203 + mainHandler.handler.post { + try { + val semanticsOwner = + (view as? RootForTest)?.semanticsOwner ?: run { + config.logger.log("View is not a RootForTest: $view") + return@post + } + val semanticsNodes = semanticsOwner.getAllSemanticsNodes(true) - val hasMaskModifier = node.config.contains(PostHogReplayMask) - val isNoCapture = hasMaskModifier && node.config[PostHogReplayMask] + semanticsNodes.forEach { node -> + val hasText = node.config.contains(SemanticsProperties.Text) + val hasEditableText = node.config.contains(SemanticsProperties.EditableText) + val hasPassword = node.config.contains(SemanticsProperties.Password) + val hasImage = node.config.contains(SemanticsProperties.ContentDescription) - when { - isNoCapture -> { - maskableWidgets.add(node.boundsInWindow.toRect()) - } + val hasMaskModifier = node.config.contains(PostHogReplayMask) + val isNoCapture = hasMaskModifier && node.config[PostHogReplayMask] - !hasMaskModifier -> { - when { - (hasText || hasEditableText) && (config.sessionReplayConfig.maskAllTextInputs || hasPassword) -> { - maskableWidgets.add(node.boundsInWindow.toRect()) - } + when { + isNoCapture -> { + maskableWidgets.add(node.boundsInWindow.toRect()) + } - hasImage && config.sessionReplayConfig.maskAllImages -> { - maskableWidgets.add(node.boundsInWindow.toRect()) + !hasMaskModifier -> { + when { + (hasText || hasEditableText) && (config.sessionReplayConfig.maskAllTextInputs || hasPassword) -> { + maskableWidgets.add(node.boundsInWindow.toRect()) + } + + hasImage && config.sessionReplayConfig.maskAllImages -> { + maskableWidgets.add(node.boundsInWindow.toRect()) + } } } } } + } catch (e: Throwable) { + // swallow possible errors due to compose versioning, etc + config.logger.log("Session Replay findMaskableComposeWidgets (main thread) failed: $e") + } finally { + latch.countDown() } + } + + try { + // await for 1s max + latch.await(1000, TimeUnit.MILLISECONDS) + config.logger.log("test") } catch (e: Throwable) { - // swallow possible errors due to compose versioning, etc config.logger.log("Session Replay findMaskableComposeWidgets failed: $e") } }