Skip to content
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
61069a5
add floating button
abelonogov-ld Nov 30, 2025
69799b5
sample of adding floating views
abelonogov-ld Nov 30, 2025
81c369f
consolidate in one view
abelonogov-ld Nov 30, 2025
8b0c650
use Canvas for certain types of windows
abelonogov-ld Nov 30, 2025
6dafcb3
space
abelonogov-ld Nov 30, 2025
04b4c75
Attached dialog sample
abelonogov-ld Nov 30, 2025
55b00a7
no message
abelonogov-ld Dec 1, 2025
8ddba24
fix size bug
abelonogov-ld Dec 1, 2025
008a414
Recycle bitmap early
abelonogov-ld Dec 1, 2025
4d5536b
Fix feedback
abelonogov-ld Dec 1, 2025
ce45b42
address feedback
abelonogov-ld Dec 1, 2025
50aee55
Volatile
abelonogov-ld Dec 1, 2025
04c252e
Unit tests
abelonogov-ld Dec 1, 2025
54f08de
create Mask
abelonogov-ld Dec 1, 2025
d6fd378
add floating button
abelonogov-ld Nov 30, 2025
59c2f6d
sample of adding floating views
abelonogov-ld Nov 30, 2025
3ff3e81
consolidate in one view
abelonogov-ld Nov 30, 2025
41e2645
use Canvas for certain types of windows
abelonogov-ld Nov 30, 2025
ac99967
space
abelonogov-ld Nov 30, 2025
801ae96
Attached dialog sample
abelonogov-ld Nov 30, 2025
e618524
Matrix
abelonogov-ld Dec 1, 2025
a92e869
Merge branch 'andrey/nonstandard-views' into andrey/delayed-mask2
abelonogov-ld Dec 1, 2025
d86bd09
Merge branch 'andrey/do-not-send-duplicate-windows' into andrey/delay…
abelonogov-ld Dec 1, 2025
1ece408
renaming to MaskCollector
abelonogov-ld Dec 1, 2025
3e570b4
add floating button
abelonogov-ld Nov 30, 2025
aef3154
sample of adding floating views
abelonogov-ld Nov 30, 2025
12ed36c
consolidate in one view
abelonogov-ld Nov 30, 2025
343690e
use Canvas for certain types of windows
abelonogov-ld Nov 30, 2025
a4cf491
space
abelonogov-ld Nov 30, 2025
b117d79
Attached dialog sample
abelonogov-ld Nov 30, 2025
55e29da
Filter AndroidComposeView
abelonogov-ld Dec 2, 2025
4c0db23
fix mask warning
abelonogov-ld Dec 2, 2025
0144cef
using root.locationOnScreen
abelonogov-ld Dec 2, 2025
31bb519
address feadback
abelonogov-ld Dec 2, 2025
4daa13f
Merge branch 'andrey/nonstandard-views' into andrey/delayed-mask2
abelonogov-ld Dec 2, 2025
4558bbe
Fix warnings
abelonogov-ld Dec 2, 2025
7cb314b
fix warnings
abelonogov-ld Dec 2, 2025
15825ce
Merge branch 'main' into andrey/mask-collector
abelonogov-ld Dec 2, 2025
89ba6c5
remove ressurectued import
abelonogov-ld Dec 2, 2025
0079a42
address feedback
abelonogov-ld Dec 3, 2025
1180d8b
delete renamed
abelonogov-ld Dec 3, 2025
04d2537
uncomment
abelonogov-ld Dec 2, 2025
a632d0e
works for Native views only
abelonogov-ld Dec 2, 2025
7584f01
rotation for XML Views
abelonogov-ld Dec 2, 2025
483930b
transformed coordinates for compose
abelonogov-ld Dec 3, 2025
3052849
reorder calls
abelonogov-ld Dec 3, 2025
50033e8
rebase fix
abelonogov-ld Dec 3, 2025
7a21080
Merge branch 'main' into andrey/transformed-coordinates
abelonogov-ld Dec 3, 2025
934607b
remove import
abelonogov-ld Dec 3, 2025
87c3415
cursor review
abelonogov-ld Dec 3, 2025
16a967c
address feedback
abelonogov-ld Dec 3, 2025
ca3fbbf
Merge branch 'main' into andrey/transformed-coordinates
abelonogov-ld Dec 4, 2025
436b494
address feedback
abelonogov-ld Dec 4, 2025
b6e9440
remove unused method
abelonogov-ld Dec 4, 2025
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.example.androidobservability.masking

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.os.Bundle
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
Expand All @@ -13,6 +17,13 @@ class XMLUserFormActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_form)

val creditCardSection = findViewById<View>(R.id.credit_card_section)
ObjectAnimator.ofFloat(creditCardSection, "rotationY", 0f, 360f).apply {
duration = 2000
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
}.start()

val inputCardholderName = findViewById<EditText>(R.id.input_cardholder_name)
val inputCardNumber = findViewById<EditText>(R.id.input_card_number)
val inputExpiry = findViewById<EditText>(R.id.input_expiry)
Expand Down
163 changes: 85 additions & 78 deletions e2e/android/app/src/main/res/layout/activity_user_form.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,101 +90,108 @@
</LinearLayout>

<!-- Credit Card Section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Credit Card Information"
android:textStyle="bold"
android:paddingBottom="8dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cardholder Name" />

<EditText
android:id="@+id/input_cardholder_name"
<LinearLayout
android:id="@+id/credit_card_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g., Jane Appleseed"
android:inputType="textPersonName"
android:paddingBottom="8dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Card Number" />
android:orientation="vertical">

<EditText
android:id="@+id/input_card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="1234 5678 9012 3456"
android:inputType="number"
android:maxLength="19"
android:paddingBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Credit Card Information"
android:textStyle="bold"
android:paddingBottom="8dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cardholder Name" />

<LinearLayout
android:layout_width="0dp"
<EditText
android:id="@+id/input_cardholder_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingRight="8dp">
android:hint="e.g., Jane Appleseed"
android:inputType="textPersonName"
android:paddingBottom="8dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Expiry (MM/YY)" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Card Number" />

<EditText
android:id="@+id/input_expiry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="MM/YY"
android:inputType="number" />
</LinearLayout>
<EditText
android:id="@+id/input_card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="1234 5678 9012 3456"
android:inputType="number"
android:maxLength="19"
android:paddingBottom="8dp" />

<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingLeft="8dp">
android:orientation="horizontal"
android:weightSum="2"
android:paddingBottom="8dp">

<TextView
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="CVV" />

<EditText
android:id="@+id/input_cvv"
android:layout_width="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:paddingRight="8dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Expiry (MM/YY)" />

<EditText
android:id="@+id/input_expiry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="MM/YY"
android:inputType="number" />
</LinearLayout>

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="123"
android:inputType="numberPassword"
android:maxLength="4" />
android:layout_weight="1"
android:orientation="vertical"
android:paddingLeft="8dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CVV" />

<EditText
android:id="@+id/input_cvv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="123"
android:inputType="numberPassword"
android:maxLength="4" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ZIP / Postal Code" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ZIP / Postal Code" />

<EditText
android:id="@+id/input_zip_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g., 94105"
android:inputType="textPostalAddress"
android:paddingBottom="8dp" />
<EditText
android:id="@+id/input_zip_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g., 94105"
android:inputType="textPostalAddress"
android:paddingBottom="8dp" />
</LinearLayout>

<Button
android:id="@+id/button_save_card"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import android.view.Window
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION
import android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION
import androidx.annotation.RequiresApi
import android.graphics.Path
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
import com.launchdarkly.observability.replay.masking.MaskMatcher
Expand All @@ -32,6 +33,7 @@ import kotlin.coroutines.resume
import androidx.core.graphics.withTranslation
import com.launchdarkly.observability.replay.masking.Mask
import androidx.core.graphics.createBitmap
import com.launchdarkly.observability.replay.masking.draw

/**
* A source of [CaptureEvent]s taken from the lowest visible window. Captures
Expand Down Expand Up @@ -86,7 +88,10 @@ class CaptureSource(
}
}
}


val timestamp = System.currentTimeMillis()
val session = sessionManager.getSessionId()

val windowsEntries = windowInspector.appWindows()
if (windowsEntries.isEmpty()) {
return@withContext null
Expand All @@ -104,8 +109,6 @@ class CaptureSource(
// TODO: O11Y-625 - see if holding bitmap is more efficient than base64 encoding immediately after compression
// TODO: O11Y-628 - use captureQuality option for scaling and adjust this bitmap accordingly, may need to investigate power of 2 rounding for performance
// Create a bitmap with the window dimensions
val timestamp = System.currentTimeMillis()
val session = sessionManager.getSessionId()
val baseResult = captureViewResult(baseWindowEntry) ?: return@withContext null

// capture rest of views on top of base
Expand Down Expand Up @@ -169,6 +172,7 @@ class CaptureSource(
private suspend fun captureViewResult(windowEntry: WindowEntry): CaptureResult? {
val bitmap = captureViewBitmap(windowEntry) ?: return null
val masks = maskCollector.collectMasks(windowEntry.rootView, maskMatchers)

return CaptureResult(windowEntry, bitmap, masks)
}

Expand Down Expand Up @@ -281,14 +285,31 @@ class CaptureSource(
* @param masks areas that will be masked
*/
private fun drawMasks(canvas: Canvas, masks: List<Mask>) {
val path = Path()
masks.forEach { mask ->
val integerRect = Rect(
mask.rect.left.toInt(),
mask.rect.top.toInt(),
mask.rect.right.toInt(),
mask.rect.bottom.toInt()
)
canvas.drawRect(integerRect, maskPaint)
mask.draw(path, canvas, maskPaint)
}
}

val maskIntRect = Rect()
fun drawMask(mask: Mask, path: Path, canvas: Canvas, paint: Paint) {
if (mask.points != null) {
val pts = mask.points

path.reset()
path.moveTo(pts[0], pts[1])
path.lineTo(pts[2], pts[3])
path.lineTo(pts[4], pts[5])
path.lineTo(pts[6], pts[7])
path.close()

canvas.drawPath(path, paint)
} else {
maskIntRect.left = mask.rect.left.toInt()
maskIntRect.top = mask.rect.top.toInt()
maskIntRect.right = mask.rect.right.toInt()
maskIntRect.bottom = mask.rect.bottom.toInt()
canvas.drawRect(maskIntRect, paint)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.launchdarkly.observability.replay.masking

import android.os.Build
import android.view.View
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.toAndroidRectF
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.semantics.SemanticsNode
Expand Down Expand Up @@ -73,12 +75,14 @@ data class ComposeMaskTarget(
return config.contains(SemanticsProperties.Text)
}

override fun mask(): Mask? {
override fun mask(context: MaskContext): Mask? {
val rect = boundsInWindow.toAndroidRectF()
if (rect.width() <= 0f || rect.height() <= 0f) {
return null
}
return Mask(boundsInWindow.toAndroidRectF(), view.id)

val points: FloatArray? = points(context)
return Mask(boundsInWindow.toAndroidRectF(), view.id, points)
}

override fun hasLDMask(): Boolean {
Expand All @@ -103,6 +107,41 @@ data class ComposeMaskTarget(
return hasSensitiveDescription
}

// return 4 points of polygon under transformations
private fun points(context: MaskContext): FloatArray? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return null
}

val coordinates = rootNode.layoutInfo.coordinates
if (!coordinates.isAttached) {
return null
}

val size = coordinates.size
if (size.width <= 0 || size.height <= 0) {
return null
}

val t1 = coordinates.localToScreen(Offset(0f, 0f))
val t2 = coordinates.localToScreen(Offset(size.width.toFloat(), 0f))
val t3 = coordinates.localToScreen(Offset(size.width.toFloat(), size.height.toFloat()))
val t4 = coordinates.localToScreen(Offset(0f, size.height.toFloat()))

val pts = floatArrayOf(
t1.x, t1.y,
t2.x, t2.y,
t3.x, t3.y,
t4.x, t4.y
)

for (i in pts.indices step 2) {
pts[i] -= context.rootX
pts[i + 1] -= context.rootY
}

return pts
}
}


Loading
Loading