Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

recording: fix crash when calling view.isVisible #201

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Next

## 3.8.3 - 2024-10-25

- recording: fix crash when calling view.isVisible ([#201](https://github.com/PostHog/posthog-android/pull/201))
- recording: change debouncerDelayMs default from 500ms to 1000ms (1s) ([#201](https://github.com/PostHog/posthog-android/pull/201))

## 3.8.2 - 2024-10-14

- recording: session replay respect feature flag variants ([#197](https://github.com/PostHog/posthog-android/pull/197))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Point
import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.RectF
Expand All @@ -30,9 +31,7 @@ import android.view.MotionEvent
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import android.view.Window
import android.view.accessibility.AccessibilityNodeInfo
import android.webkit.WebView
import android.widget.Button
import android.widget.CheckBox
Expand Down Expand Up @@ -460,20 +459,45 @@ public class PostHogReplayIntegration(
status.lastSnapshot = wireframe
}

/**
* Adapted from https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/View.java;l=11620;bpv=0;bpt=1
*/
private fun View.isVisible(): Boolean {
// TODO: also check for getGlobalVisibleRect intersects the display
val visible = isShown && width >= 0 && height >= 0 && this !is ViewStub

// Between API 16 and API 29, this method may incorrectly return false when magnification
// is enabled. On other versions, a node is considered visible even if it is not on
// the screen because magnification is active.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
return visible
}
try {
if (isAttachedToWindow) {
Copy link
Member Author

@marandaneto marandaneto Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is vendoring the most important part of onInitializeAccessibilityNodeInfo
but we don't have control over the internal impl nor do I know if it requires the main thread or not since I cannot reproduce the original issue, so I copied and adapted and added a try-catch just in case

// Attached to invisible window means this view is not visible.
if (windowVisibility != View.VISIBLE) {
return false
}
// An invisible predecessor or one with alpha zero means
// that this view is not visible to the user.
var current: Any? = this
while (current is View) {
val view = current
val transitionAlpha = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) view.transitionAlpha else 1f
// We have attach info so this view is attached and there is no
// need to check whether we reach to ViewRootImpl on the way up.
if (view.alpha <= 0 || transitionAlpha <= 0 || view.visibility != View.VISIBLE) {
return false
}
current = view.parent
}
// Check if the view is entirely covered by its predecessors.
val visibleRect = Rect()
val offset = Point()

val nodeInfo = AccessibilityNodeInfo()
onInitializeAccessibilityNodeInfo(nodeInfo)
return visible && nodeInfo.isVisibleToUser
return getGlobalVisibleRect(visibleRect, offset)

// TODO: also check for getGlobalVisibleRect intersects the display
// if (boundInView != null) {
// visibleRect.offset(-offset.x, -offset.y)
// return boundInView.intersect(visibleRect)
// }
}
} catch (e: Throwable) {
config.logger.log("Session Replay isVisible failed: $e.")
}
return false
}

private fun Drawable.shouldMaskDrawable(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public class PostHogSessionReplayConfig
* Deboucer delay used to reduce the number of snapshots captured and reduce performance impact
* This is used for capturing the view as a wireframe or screenshot
* The lower the number more snapshots will be captured but higher the performance impact
* Defaults to 500ms
* Defaults to 1000ms = 1s
* Ps: it was 500ms by default until version 3.8.2
*/
@PostHogExperimental
public var debouncerDelayMs: Long = 500,
public var debouncerDelayMs: Long = 1000,
)
Loading