Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package com.example.androidobservability.masking

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.Toast
import android.graphics.Color
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.TextView
import androidx.activity.ComponentActivity
import android.app.AlertDialog
import androidx.core.graphics.toColorInt
import com.example.androidobservability.R

class XMLMaskingActivity : ComponentActivity() {
Expand Down Expand Up @@ -59,6 +68,62 @@ class XMLMaskingActivity : ComponentActivity() {
findViewById<Button>(R.id.button_toast).setOnClickListener {
Toast.makeText(this, "This is an example toast.", Toast.LENGTH_SHORT).show()
}
findViewById<Button>(R.id.button_floating_popup).setOnClickListener {
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

val params = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = window.decorView.applicationWindowToken
gravity = Gravity.CENTER
}

val popupView = FloatingPopupView(this, "Floating Popup").apply {
onSendClicked = {
Toast.makeText(this@XMLMaskingActivity, "Send clicked", Toast.LENGTH_SHORT).show()
}
onDismissRequested = {
try {
windowManager.removeView(this)
} catch (_: Exception) {
}
}
}

windowManager.addView(popupView, params)
}

findViewById<Button>(R.id.button_attached_dialog).setOnClickListener {
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

val params = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
// Attach to this window so it behaves like an attached dialog
token = window.decorView.windowToken
gravity = Gravity.CENTER
}

val popupView = FloatingPopupView(this, "Attached Dialog").apply {
onSendClicked = {
Toast.makeText(this@XMLMaskingActivity, "Send clicked", Toast.LENGTH_SHORT).show()
}
onDismissRequested = {
try {
windowManager.removeView(this)
} catch (_: Exception) {
}
}
}

windowManager.addView(popupView, params)
}

val firstField = findViewById<EditText>(R.id.input_first)
val secondField = findViewById<EditText>(R.id.input_second)
Expand All @@ -71,4 +136,75 @@ class XMLMaskingActivity : ComponentActivity() {
}
}

class FloatingPopupView(context: Context, private val headerTitle: String) : FrameLayout(context) {

var onSendClicked: (() -> Unit)? = null
var onDismissRequested: (() -> Unit)? = null

init {
setBackgroundColor("#80000000".toColorInt())
isClickable = true
isFocusable = true

// White card container with vertical layout to host header and row
val contentContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Color.WHITE)
elevation = 12f
setPadding(48, 32, 32, 32)
}

val header = TextView(context).apply {
text = headerTitle
setTextColor(Color.BLACK)
textSize = 16f
}

val row = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
}

val label = TextView(context).apply {
text = "UserName"
setTextColor(Color.BLACK)
}

val sendButton = ImageButton(context).apply {
setImageResource(android.R.drawable.ic_menu_send)
contentDescription = "Send"
background = null
}

row.addView(label)
row.addView(
sendButton,
LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
).apply { leftMargin = 24 }
)

contentContainer.addView(header)
contentContainer.addView(row)

addView(
contentContainer,
LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
)

// Outside tap dismiss
setOnClickListener { onDismissRequested?.invoke() }
// Consume inner content clicks
contentContainer.setOnClickListener { }
sendButton.setOnClickListener {
onSendClicked?.invoke()
onDismissRequested?.invoke()
}
}
}


22 changes: 22 additions & 0 deletions e2e/android/app/src/main/res/layout/activity_masking_bench.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Toast" />

<Space
android:layout_width="12dp"
android:layout_height="1dp" />

<Button
android:id="@+id/button_floating_popup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Floating Popup" />

<Space
android:layout_width="12dp"
android:layout_height="1dp" />

<Button
android:id="@+id/button_attached_dialog"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Attached Dialog" />
</LinearLayout>

<TextView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ class CaptureSource(

private fun pickBaseWindow(windowsEntries: List<WindowEntry>): WindowEntry? {
windowsEntries.firstOrNull {
(it.wmType == TYPE_APPLICATION || it.wmType == TYPE_BASE_APPLICATION)
val wmType = it.layoutParams?.type ?: 0
(wmType == TYPE_APPLICATION || wmType == TYPE_BASE_APPLICATION)
}?.let { return it }

windowsEntries.firstOrNull { it.type == WindowType.ACTIVITY }?.let { return it }
Expand All @@ -172,10 +173,13 @@ class CaptureSource(

private suspend fun captureViewBitmap(windowEntry: WindowEntry): Bitmap? {
val view = windowEntry.rootView
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && windowEntry.isPixelCopyCandidate()) {
val window = windowInspector.findWindow(view)
if (window != null) {
pixelCopy(window, view, windowEntry.rect())?.let { return it }
pixelCopy(window, view, windowEntry.rect())?.let {
return it
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.launchdarkly.observability.replay.capture

import android.graphics.Rect
import android.os.Build
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING


enum class WindowType {
Expand All @@ -13,7 +16,7 @@ enum class WindowType {
data class WindowEntry(
val rootView: View,
var type: WindowType,
val wmType: Int,
val layoutParams: WindowManager.LayoutParams?,
val width: Int,
val height: Int,
val screenLeft: Int,
Expand All @@ -22,6 +25,23 @@ data class WindowEntry(
fun rect(): Rect {
return Rect(0, 0, width, height)
}

fun isPixelCopyCandidate(): Boolean {
if (type != WindowType.ACTIVITY) {
return false
}

if (layoutParams?.type == TYPE_APPLICATION_STARTING) { // Starting/Splash screen
return false
}

if (((layoutParams?.flags ?: 0) and WindowManager.LayoutParams.FLAG_SECURE) != 0) {
// Secure window
return false
}

return true
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class WindowInspector(private val logger: LDLogger) {
WindowEntry(
rootView = view,
type = determineWindowType(wmType),
wmType = wmType,
layoutParams = layoutParams,
width = view.width,
height = view.height,
screenLeft = loc[0],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.launchdarkly.observability.replay.capture

import android.os.Build
import android.view.View
import android.view.WindowManager
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import io.mockk.mockk

class WindowEntryTest {
private fun createEntry(
type: WindowType = WindowType.ACTIVITY,
layoutParams: WindowManager.LayoutParams? = null,
width: Int = 100,
height: Int = 100,
screenLeft: Int = 0,
screenTop: Int = 0
): WindowEntry {
val rootView: View = mockk(relaxed = true)
return WindowEntry(
rootView = rootView,
type = type,
layoutParams = layoutParams,
width = width,
height = height,
screenLeft = screenLeft,
screenTop = screenTop
)
}

@Test
fun `non-activity windows are not candidates on O+`() {
val entry = createEntry(
type = WindowType.DIALOG,
layoutParams = WindowManager.LayoutParams()
)
assertFalse(entry.isPixelCopyCandidate())
}

@Test
fun `starting window is not a candidate on O+`() {
val lp = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_STARTING
}
val entry = createEntry(
type = WindowType.ACTIVITY,
layoutParams = lp
)
assertFalse(entry.isPixelCopyCandidate())
}

@Test
fun `secure window is not a candidate on O+`() {
val lp = WindowManager.LayoutParams().apply {
flags = flags or WindowManager.LayoutParams.FLAG_SECURE
}
val entry = createEntry(
type = WindowType.ACTIVITY,
layoutParams = lp
)
assertFalse(entry.isPixelCopyCandidate())
}

@Test
fun `activity non-secure non-starting window is a candidate on O+`() {
val lp = WindowManager.LayoutParams() // default: not TYPE_APPLICATION_STARTING, no FLAG_SECURE
val entry = createEntry(
type = WindowType.ACTIVITY,
layoutParams = lp
)
assertTrue(entry.isPixelCopyCandidate())
}
}


Loading