From 2016b518ea2c62a8808043f7e46126c6729b4c5f Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jul 2024 13:18:08 +0200 Subject: [PATCH 1/5] chore: allow masking views with contentDescription --- USAGE.md | 2 +- posthog-android/api/posthog-android.api | 1 + .../replay/PostHogReplayIntegration.kt | 57 ++++++++++++------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/USAGE.md b/USAGE.md index 5af9f7f4..d04c5c03 100644 --- a/USAGE.md +++ b/USAGE.md @@ -219,7 +219,7 @@ val config = PostHogAndroidConfig(apiKey).apply { } ``` -If you don't want to mask everything, you can disable the mask config above and mask specific views using the `ph-no-capture` tag. +If you don't want to mask everything, you can disable the mask config above and mask specific views using the `ph-no-capture` value in the [android:tag](https://developer.android.com/reference/android/view/View#attr_android:tag) or [android:contentDescription](https://developer.android.com/reference/android/view/View#attr_android:contentDescription).. ```xml (Landroid/content/Context;Lcom/posthog/android/PostHogAndroidConfig;Lcom/posthog/android/internal/MainHandler;)V public fun install ()V public fun uninstall ()V 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 ade79c5d..7be66f28 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 @@ -460,17 +460,23 @@ public class PostHogReplayIntegration( return rect } + private fun View.isTextInputSensitive(): Boolean { + return isNoCapture(config.sessionReplayConfig.maskAllTextInputs) + } + + private fun View.isAnyInputSensitive(): Boolean { + return this.isTextInputSensitive() || config.sessionReplayConfig.maskAllImages + } + private fun TextView.shouldMaskTextView(): Boolean { // inputType is 0-based - return isNoCapture(config.sessionReplayConfig.maskAllTextInputs) || passwordInputTypes.contains(inputType - 1) + return this.isTextInputSensitive() || passwordInputTypes.contains(inputType - 1) } private fun findMaskableWidgets( view: View, maskableWidgets: MutableList, ) { - var parentMasked = false - if (view is TextView) { val viewText = view.text?.toString() var maskIt = false @@ -488,7 +494,7 @@ public class PostHogReplayIntegration( if (maskIt) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) - parentMasked = true + return } } @@ -498,7 +504,7 @@ public class PostHogReplayIntegration( if (maskIt) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) - parentMasked = true + return } } @@ -508,28 +514,36 @@ public class PostHogReplayIntegration( if (maskIt) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) - parentMasked = true + return + } + } + + if (view is WebView) { + val maskIt = view.isAnyInputSensitive() + + if (maskIt) { + val rect = view.globalVisibleRect() + maskableWidgets.add(rect) + return } } // if a view parent of any type is tagged as non masking, mask it - if (!parentMasked && view.isNoCapture()) { + if (view.isNoCapture()) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) - parentMasked = true + return } - if (!parentMasked) { - if (view is ViewGroup && view.childCount > 0) { - for (i in 0 until view.childCount) { - val viewChild = view.getChildAt(i) ?: continue - - if (!viewChild.isVisible()) { - continue - } + if (view is ViewGroup && view.childCount > 0) { + for (i in 0 until view.childCount) { + val viewChild = view.getChildAt(i) ?: continue - findMaskableWidgets(viewChild, maskableWidgets) + if (!viewChild.isVisible()) { + continue } + + findMaskableWidgets(viewChild, maskableWidgets) } } } @@ -609,7 +623,7 @@ public class PostHogReplayIntegration( } private fun Spinner.shouldMaskSpinner(): Boolean { - return isNoCapture(config.sessionReplayConfig.maskAllTextInputs) + return this.isTextInputSensitive() } private fun View.toWireframe(parentId: Int? = null): RRWireframe? { @@ -1117,7 +1131,8 @@ public class PostHogReplayIntegration( } private fun View.isNoCapture(maskInput: Boolean = false): Boolean { - return (tag as? String)?.lowercase()?.contains("ph-no-capture") == true || maskInput + return maskInput || (tag as? String)?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true || + contentDescription?.toString()?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true } private fun Drawable.copy(): Drawable? { @@ -1132,4 +1147,8 @@ public class PostHogReplayIntegration( private fun isSupported(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O } + + private companion object { + const val PH_NO_CAPTURE_LABEL = "ph-no-capture" + } } From 3ad4abd35e8ae85ce34c7adaa1be3534fe27fa04 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jul 2024 13:22:03 +0200 Subject: [PATCH 2/5] pr id --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b88ea57..7c1bf397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- recording: mask views with `contentDescription` setting and mask `WebView` if any masking is enabled ([#149](https://github.com/PostHog/posthog-android/pull/149)) + ## 3.4.2 - 2024-06-28 - chore: create ctor overloads for better Java DX ([#148](https://github.com/PostHog/posthog-android/pull/148)) From ed6d26c6a086e34b6d04c521bf94ee06dd4797c6 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jul 2024 13:22:58 +0200 Subject: [PATCH 3/5] fix --- posthog-android/api/posthog-android.api | 1 - .../java/com/posthog/android/replay/PostHogReplayIntegration.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/posthog-android/api/posthog-android.api b/posthog-android/api/posthog-android.api index dd01e78f..71c994df 100644 --- a/posthog-android/api/posthog-android.api +++ b/posthog-android/api/posthog-android.api @@ -46,7 +46,6 @@ public abstract interface class com/posthog/android/replay/PostHogDrawableConver } public final class com/posthog/android/replay/PostHogReplayIntegration : com/posthog/PostHogIntegration { - public static final field PH_NO_CAPTURE_LABEL Ljava/lang/String; public fun (Landroid/content/Context;Lcom/posthog/android/PostHogAndroidConfig;Lcom/posthog/android/internal/MainHandler;)V public fun install ()V public fun uninstall ()V 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 7be66f28..12004b32 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 @@ -1149,6 +1149,6 @@ public class PostHogReplayIntegration( } private companion object { - const val PH_NO_CAPTURE_LABEL = "ph-no-capture" + private const val PH_NO_CAPTURE_LABEL = "ph-no-capture" } } From 7173a05135a8f298bff4de56e83bef89103f0b3d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jul 2024 13:25:45 +0200 Subject: [PATCH 4/5] fix --- .../android/internal/PostHogLifecycleObserverIntegration.kt | 2 +- .../com/posthog/android/internal/PostHogSharedPreferences.kt | 2 +- .../com/posthog/android/replay/internal/NextDrawListener.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/posthog-android/src/main/java/com/posthog/android/internal/PostHogLifecycleObserverIntegration.kt b/posthog-android/src/main/java/com/posthog/android/internal/PostHogLifecycleObserverIntegration.kt index 8c6ac103..d9c3a771 100644 --- a/posthog-android/src/main/java/com/posthog/android/internal/PostHogLifecycleObserverIntegration.kt +++ b/posthog-android/src/main/java/com/posthog/android/internal/PostHogLifecycleObserverIntegration.kt @@ -30,7 +30,7 @@ internal class PostHogLifecycleObserverIntegration( private val lastUpdatedSession = AtomicLong(0L) private val sessionMaxInterval = (1000 * 60 * 30).toLong() // 30 minutes - companion object { + private companion object { // in case there are multiple instances or the SDK is closed/setup again // the value is still cached @JvmStatic diff --git a/posthog-android/src/main/java/com/posthog/android/internal/PostHogSharedPreferences.kt b/posthog-android/src/main/java/com/posthog/android/internal/PostHogSharedPreferences.kt index 351611a6..e8069a41 100644 --- a/posthog-android/src/main/java/com/posthog/android/internal/PostHogSharedPreferences.kt +++ b/posthog-android/src/main/java/com/posthog/android/internal/PostHogSharedPreferences.kt @@ -211,7 +211,7 @@ internal class PostHogSharedPreferences( return preferences } - companion object { + private companion object { private val SPECIAL_KEYS = listOf(GROUPS) } } diff --git a/posthog-android/src/main/java/com/posthog/android/replay/internal/NextDrawListener.kt b/posthog-android/src/main/java/com/posthog/android/replay/internal/NextDrawListener.kt index 4faa8a8d..bdde2b57 100644 --- a/posthog-android/src/main/java/com/posthog/android/replay/internal/NextDrawListener.kt +++ b/posthog-android/src/main/java/com/posthog/android/replay/internal/NextDrawListener.kt @@ -28,7 +28,7 @@ internal class NextDrawListener( } } - companion object { + internal companion object { // only call if onDecorViewReady internal fun View.onNextDraw( mainHandler: MainHandler, From ffbd669b5f95df5bfe5048df62fbdb29e676c056 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jul 2024 13:58:03 +0200 Subject: [PATCH 5/5] inline mask it --- .../android/replay/PostHogReplayIntegration.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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 12004b32..fbc10471 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 @@ -499,9 +499,7 @@ public class PostHogReplayIntegration( } if (view is Spinner) { - val maskIt = view.shouldMaskSpinner() - - if (maskIt) { + if (view.shouldMaskSpinner()) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) return @@ -509,9 +507,7 @@ public class PostHogReplayIntegration( } if (view is ImageView) { - val maskIt = view.shouldMaskImage() - - if (maskIt) { + if (view.shouldMaskImage()) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) return @@ -519,9 +515,7 @@ public class PostHogReplayIntegration( } if (view is WebView) { - val maskIt = view.isAnyInputSensitive() - - if (maskIt) { + if (view.isAnyInputSensitive()) { val rect = view.globalVisibleRect() maskableWidgets.add(rect) return