From 48b8c413a2e06081761a7ee4bee4d501c3d7522b Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 29 Sep 2026 17:21:44 -0400 Subject: [PATCH 1/7] feat(android): prototype screenshot-mode window composition --- .changeset/sixty-chefs-hug.md | 5 + .../replay/PostHogReplayIntegration.kt | 288 ++++++++++++++++- .../replay/PostHogReplayIntegrationTest.kt | 21 ++ .../posthog-android-sample/build.gradle.kts | 1 + .../posthog-android-sample/gradle.lockfile | 42 ++- .../src/debug/AndroidManifest.xml | 7 +- .../posthog/android/sample/CheckoutProof.kt | 240 ++++++++++++++ .../sample/WindowCompositionProofActivity.kt | 304 ++++++++++++++++++ .../src/debug/res/values/themes.xml | 4 + 9 files changed, 891 insertions(+), 21 deletions(-) create mode 100644 .changeset/sixty-chefs-hug.md create mode 100644 posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/CheckoutProof.kt create mode 100644 posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/WindowCompositionProofActivity.kt create mode 100644 posthog-samples/posthog-android-sample/src/debug/res/values/themes.xml diff --git a/.changeset/sixty-chefs-hug.md b/.changeset/sixty-chefs-hug.md new file mode 100644 index 000000000..2c31dc5fc --- /dev/null +++ b/.changeset/sixty-chefs-hug.md @@ -0,0 +1,5 @@ +--- +"posthog-android": minor +--- + +Compose Android activity and dialog layers in screenshot-mode session replay 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 c0d545ca0..78508ce9f 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 @@ -1,7 +1,9 @@ package com.posthog.android.replay import android.annotation.SuppressLint +import android.app.Activity import android.content.Context +import android.content.ContextWrapper import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas @@ -23,6 +25,7 @@ import android.graphics.drawable.VectorDrawable import android.os.Build import android.os.Handler import android.os.HandlerThread +import android.os.IBinder import android.os.Looper import android.text.InputType import android.util.TypedValue @@ -103,6 +106,7 @@ import curtains.touchEventInterceptors import curtains.windowAttachCount import java.lang.ref.WeakReference import java.util.Collections +import java.util.IdentityHashMap import java.util.WeakHashMap import java.util.concurrent.CountDownLatch import java.util.concurrent.ExecutorService @@ -128,6 +132,27 @@ public class PostHogReplayIntegration( // session must invalidate work too, so session identity alone is not a sufficient lease. private var snapshotGeneration: Long = 0 + // Scene state is committed under decorViews, like the existing per-root snapshot history. + private val sceneImages = WeakHashMap() + private val sceneDirtyViews = Collections.newSetFromMap(IdentityHashMap()) + private val sceneIds = WeakHashMap() + private var nextSceneId = 10_000_001 + private var lastScene: List? = null + private var sceneViewport: Pair? = null + private var sceneFollowUp = false + private var sceneRevision = 0L + + private data class SceneLayer( + val view: View, + val window: Window, + val status: ViewTreeSnapshotStatus, + val x: Int, + val y: Int, + val width: Int, + val height: Int, + val dimAmount: Float, + ) + private val passwordInputTypes = setOf( InputType.TYPE_TEXT_VARIATION_PASSWORD, @@ -348,24 +373,36 @@ public class PostHogReplayIntegration( return@onNextDraw } - submitCapture(drawState) { - try { - if (decorViews[decorView]?.drawState !== drawState) { - return@submitCapture + if (config.sessionReplayConfig.screenshot) { + requestSceneCapture(decorView) + } else { + submitCapture(drawState) { + try { + if (decorViews[decorView]?.drawState !== drawState) { + return@submitCapture + } + generateSnapshot(WeakReference(decorView), WeakReference(window)) + } catch (e: Throwable) { + config.logger.log("Session Replay generateSnapshot failed: $e.") } - generateSnapshot(WeakReference(decorView), WeakReference(window)) - } catch (e: Throwable) { - config.logger.log("Session Replay generateSnapshot failed: $e.") } } } val layoutListener = - ViewTreeObserver.OnGlobalLayoutListener { drawState.recordLayout() } + ViewTreeObserver.OnGlobalLayoutListener { + drawState.recordLayout() + if (isActive() && isNativeSdk && config.sessionReplayConfig.screenshot) { + requestSceneCapture(decorView) + } + } decorView.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) val status = ViewTreeSnapshotStatus(listener, layoutListener, drawState = drawState) decorViews[decorView] = status + if (isActive() && isNativeSdk && config.sessionReplayConfig.screenshot) { + mainHandler.handler.post { requestSceneCapture(decorView) } + } } catch (e: Throwable) { config.logger.log("Session Replay onDecorViewReady failed: $e.") } @@ -413,9 +450,221 @@ public class PostHogReplayIntegration( return true } + private fun requestSceneCapture(changedView: View? = null) { + if (!isActive() || !isNativeSdk || !config.sessionReplayConfig.screenshot) return + synchronized(decorViews) { + changedView?.let { sceneDirtyViews.add(it) } + sceneRevision++ + } + val layers = selectSceneLayers() ?: return + val base = layers.first() + val accepted = + submitCapture(base.status.drawState) { + try { + generateSceneSnapshot(layers) + } catch (e: Throwable) { + config.logger.log("Session Replay scene capture failed: $e.") + } finally { + mainHandler.handler.post { + val needsFollowUp = synchronized(decorViews) { sceneFollowUp } + if (needsFollowUp) { + synchronized(decorViews) { sceneFollowUp = false } + requestSceneCapture() + } + } + } + } + if (!accepted) { + synchronized(decorViews) { sceneFollowUp = true } + } + } + + private fun Context.activityOwner(): Activity? { + var current: Context = this + repeat(10) { + if (current is Activity) return current + current = (current as? ContextWrapper)?.baseContext ?: return null + } + return null + } + + internal fun isSceneWindow( + baseToken: IBinder, + baseOwner: Activity?, + params: WindowManager.LayoutParams, + window: Window, + ): Boolean = params.token == baseToken || (baseOwner != null && window.context.activityOwner() === baseOwner) + + // Curtains' root list is read on main, after its attachment callback completes. + // A dialog can attach before its activity decor. On older Android versions its + // public window token can differ from the activity's, so use the owning Activity + // as well as the window token to associate ordinary PhoneWindow dialogs. + private fun selectSceneLayers(): List? { + val roots = Curtains.rootViews + val baseIndex = + roots.indexOfLast { + it.isAliveAndAttachedToWindow() && it.isShown && + (it.layoutParams as? WindowManager.LayoutParams)?.type == WindowManager.LayoutParams.TYPE_BASE_APPLICATION + } + if (baseIndex < 0) return null + val base = roots[baseIndex] + val token = (base.layoutParams as? WindowManager.LayoutParams)?.token ?: return null + val displayId = base.display?.displayId + val owner = base.phoneWindow?.context?.activityOwner() + val layers = mutableListOf() + for (view in listOf(base) + roots.filter { it !== base }) { + if (!view.isAliveAndAttachedToWindow() || !view.isShown || view.display?.displayId != displayId) continue + val params = view.layoutParams as? WindowManager.LayoutParams ?: continue + if (view !== base && params.type != WindowManager.LayoutParams.TYPE_APPLICATION) continue + val window = view.phoneWindow ?: continue + if (view !== base && !isSceneWindow(token, owner, params, window)) { + continue + } + val status = decorViews[view] ?: return null + if (view.width <= 0 || view.height <= 0) return null + val position = IntArray(2) + view.getLocationOnScreen(position) + val attributes = window.attributes + layers.add( + SceneLayer( + view, + window, + status, + position[0], + position[1], + view.width, + view.height, + if (attributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND != 0) attributes.dimAmount else 0f, + ), + ) + } + return layers.takeIf { it.firstOrNull()?.view === base } + } + + private fun generateSceneSnapshot(layers: List): Boolean { + val postHog = postHog ?: return false + val sessionId = PostHogSessionManager.getActiveSessionId()?.toString() ?: return false + val (generation, revision) = + synchronized(decorViews) { + if (!isActive() || replaySessionId != sessionId) return false + snapshotGeneration to sceneRevision + } + val timestamp = config.dateProvider.currentTimeMillis() + val images = mutableListOf() + val refreshed = mutableMapOf() + for (layer in layers) { + val (id, cached, dirty) = + synchronized(decorViews) { + val id = sceneIds.getOrPut(layer.view) { nextSceneId++ } + Triple(id, sceneImages[layer.view], sceneDirtyViews.remove(layer.view)) + } + val image = + if (!dirty && cached != null && + cached.width == layer.width.densityValue(screenDensity) && + cached.height == layer.height.densityValue(screenDensity) + ) { + cached + } else { + layer.view.toScreenshotWireframe(layer.window, layer.status.drawState, forceAlpha = true) + ?: run { + synchronized(decorViews) { + sceneDirtyViews.addAll(refreshed.keys) + sceneDirtyViews.add(layer.view) + } + return false + } + } + val positioned = + image.copy( + id = id, + x = layer.x.densityValue(screenDensity), + y = layer.y.densityValue(screenDensity), + width = layer.width.densityValue(screenDensity), + height = layer.height.densityValue(screenDensity), + ) + refreshed[layer.view] = positioned + images.add(positioned) + } + + // Android applies the topmost requesting window's dim to the scene beneath it. + val dimIndex = layers.indexOfLast { it.dimAmount > 0f } + val scene = mutableListOf() + images.forEachIndexed { index, image -> + if (index == dimIndex) { + val base = images.first() + scene.add( + RRWireframe( + id = 10_000_000, + x = base.x, + y = base.y, + width = base.width, + height = base.height, + type = "rectangle", + style = RRStyle(backgroundColor = "rgba(0,0,0,${layers[index].dimAmount})"), + ), + ) + } + scene.add(image) + } + val screenSize = + layers.first().view.context.screenSize() + ?: run { + synchronized(decorViews) { sceneDirtyViews.addAll(refreshed.keys) } + return false + } + val viewport = screenSize.width to screenSize.height + val events = + synchronized(decorViews) { + if (!isActive() || snapshotGeneration != generation || sceneRevision != revision || + PostHogSessionManager.peekSessionId()?.toString() != sessionId || + layers.any { layer -> + decorViews[layer.view] !== layer.status || !layer.view.isShown || + layer.view.width != layer.width || layer.view.height != layer.height + } + ) { + sceneDirtyViews.addAll(refreshed.keys) + return false + } + val result = mutableListOf() + if (sceneViewport != viewport) { + val title = layers.first().window.attributes.title?.toString()?.substringAfter("/") ?: "" + result.add(RRMetaEvent(width = viewport.first, height = viewport.second, timestamp = timestamp, href = title)) + sceneViewport = viewport + } + if (lastScene != scene) { + result.add(RRFullSnapshotEvent(scene, 0, 0, timestamp)) + lastScene = scene + } + sceneImages.clear() + sceneImages.putAll(refreshed) + val base = layers.first() + val (visible, keyboardEvent) = detectKeyboardVisibility(base.view, base.status.keyboardVisible) + base.status.keyboardVisible = visible + keyboardEvent?.let { result.add(it) } + result + } + if (events.isNotEmpty()) { + postHog.capture( + PostHogEventName.SNAPSHOT.event, + properties = + mapOf( + "\$snapshot_data" to events, + "\$snapshot_source" to "mobile", + "\$session_id" to sessionId, + "\$window_id" to sessionId, + ), + ) + } + return true + } + private val onRootViewsChangedListener = OnRootViewsChangedListener { view, added -> addView(view, added) + if (isActive() && isNativeSdk && config.sessionReplayConfig.screenshot) { + // Curtains notifies before it updates rootViews. Read the completed topology later. + mainHandler.handler.post { requestSceneCapture() } + } } private fun detectKeyboardVisibility( @@ -559,6 +808,10 @@ public class PostHogReplayIntegration( } decorViews.remove(view) + synchronized(decorViews) { + sceneImages.remove(view) + sceneDirtyViews.remove(view) + } } @Synchronized @@ -1740,6 +1993,7 @@ public class PostHogReplayIntegration( private fun View.toScreenshotWireframe( window: Window, drawState: WindowDrawState, + forceAlpha: Boolean = false, ): RRWireframe? { val view = this if (!view.isVisible()) { @@ -1765,7 +2019,7 @@ public class PostHogReplayIntegration( val screenshotScale = config.sessionReplayConfig.screenshotScale val compressionQuality = config.sessionReplayConfig.screenshotCompressionQuality val bitmapConfig = - when (config.sessionReplayConfig.screenshotColorMode) { + when (if (forceAlpha) PostHogScreenshotColorMode.ARGB_8888 else config.sessionReplayConfig.screenshotColorMode) { PostHogScreenshotColorMode.ARGB_8888 -> Bitmap.Config.ARGB_8888 PostHogScreenshotColorMode.RGB_565 -> Bitmap.Config.RGB_565 } @@ -2501,6 +2755,10 @@ public class PostHogReplayIntegration( } } + if (isNativeSdk && config.sessionReplayConfig.screenshot) { + mainHandler.handler.post { requestSceneCapture() } + } + if (!resumeCurrent) { // Without this, on a static UI the first user-driven onDraw can be tens of seconds // away — and incremental events (type:3) would ship under the new session before @@ -2517,6 +2775,13 @@ public class PostHogReplayIntegration( // clear state so it starts with a full snapshot again synchronized(decorViews) { snapshotGeneration++ + sceneImages.clear() + sceneDirtyViews.clear() + sceneIds.clear() + nextSceneId = 10_000_001 + lastScene = null + sceneViewport = null + sceneFollowUp = false decorViews.entries.forEach { resetViewSnapshotStates(it.value) } @@ -2536,6 +2801,11 @@ public class PostHogReplayIntegration( isSessionReplayActive = false snapshotGeneration++ pixelCopyBitmapBuffer.close() + sceneImages.clear() + sceneDirtyViews.clear() + lastScene = null + sceneViewport = null + sceneFollowUp = false decorViews.values.forEach { it.drawState.invalidateMaskCapture() } } } diff --git a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt index 11918b245..f634b75e2 100644 --- a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt +++ b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt @@ -1,6 +1,7 @@ package com.posthog.android.replay import android.app.Activity +import android.app.Dialog import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -9,6 +10,7 @@ import android.graphics.Point import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.BitmapDrawable +import android.os.Binder import android.os.Handler import android.os.Looper import android.util.Base64 @@ -315,6 +317,25 @@ internal class PostHogReplayIntegrationTest { } } + @Test + fun `dialog belongs to its activity scene even when window tokens differ`() { + val first = Robolectric.buildActivity(Activity::class.java).setup() + val second = Robolectric.buildActivity(Activity::class.java).setup() + try { + val sut = getSut() + val baseToken = Binder() + val dialogParams = WindowManager.LayoutParams().apply { token = Binder() } + assertTrue(sut.isSceneWindow(baseToken, first.get(), dialogParams, Dialog(first.get()).window!!)) + assertFalse(sut.isSceneWindow(baseToken, first.get(), dialogParams, Dialog(second.get()).window!!)) + assertFalse(sut.isSceneWindow(baseToken, null, dialogParams, Dialog(first.get()).window!!)) + dialogParams.token = baseToken + assertTrue(sut.isSceneWindow(baseToken, null, dialogParams, Dialog(second.get()).window!!)) + } finally { + second.pause().stop().destroy() + first.pause().stop().destroy() + } + } + @Test fun `running capture blocks duplicates but not other windows`() { val executor = createReplayExecutor() diff --git a/posthog-samples/posthog-android-sample/build.gradle.kts b/posthog-samples/posthog-android-sample/build.gradle.kts index dd9d384e1..c4514454c 100644 --- a/posthog-samples/posthog-android-sample/build.gradle.kts +++ b/posthog-samples/posthog-android-sample/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { implementation(platform("com.squareup.okhttp3:okhttp-bom:${PosthogBuildConfig.Dependencies.OKHTTP}")) implementation("com.squareup.okhttp3:okhttp") debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("com.google.android.material:material:1.12.0") debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14") } diff --git a/posthog-samples/posthog-android-sample/gradle.lockfile b/posthog-samples/posthog-android-sample/gradle.lockfile index 5c7de298c..f80843f21 100644 --- a/posthog-samples/posthog-android-sample/gradle.lockfile +++ b/posthog-samples/posthog-android-sample/gradle.lockfile @@ -8,9 +8,12 @@ androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugCompileC androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.appcompat:appcompat-resources:1.6.1=debugCompileClasspath,debugRuntimeClasspath +androidx.appcompat:appcompat:1.6.1=debugCompileClasspath,debugRuntimeClasspath androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.arch.core:core-runtime:2.2.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.arch.core:core-runtime:2.2.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.autofill:autofill:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.cardview:cardview:1.0.0=debugCompileClasspath,debugRuntimeClasspath androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection-ktx:1.5.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -57,22 +60,30 @@ androidx.compose.ui:ui-util:1.11.4=debugAndroidTestCompileClasspath,debugCompile androidx.compose.ui:ui:1.11.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose:compose-bom:2026.06.01=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.concurrent:concurrent-futures:1.1.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.constraintlayout:constraintlayout-solver:2.0.1=debugCompileClasspath,debugRuntimeClasspath +androidx.constraintlayout:constraintlayout:2.0.1=debugCompileClasspath,debugRuntimeClasspath +androidx.coordinatorlayout:coordinatorlayout:1.1.0=debugCompileClasspath,debugRuntimeClasspath androidx.core:core-ktx:1.18.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.core:core:1.18.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.cursoradapter:cursoradapter:1.0.0=debugCompileClasspath,debugRuntimeClasspath androidx.customview:customview-poolingcontainer:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.documentfile:documentfile:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.dynamicanimation:dynamicanimation:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.customview:customview:1.1.0=debugCompileClasspath,debugRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.drawerlayout:drawerlayout:1.1.1=debugCompileClasspath,debugRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.emoji2:emoji2-views-helper:1.4.0=debugRuntimeClasspath androidx.emoji2:emoji2:1.4.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.fragment:fragment:1.3.6=debugCompileClasspath,debugRuntimeClasspath androidx.graphics:graphics-path:1.0.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.interpolator:interpolator:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.legacy:legacy-support-core-utils:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-common-java8:2.9.4=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-common-jvm:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-common:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-livedata-core:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-livedata:2.9.4=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.9.4=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-process:2.9.4=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-runtime-android:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-runtime-compose-android:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -85,14 +96,16 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4=debugRuntimeClasspath,debugUnit androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel:2.9.4=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.loader:loader:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.loader:loader:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.print:print:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.print:print:1.0.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.profileinstaller:profileinstaller:1.4.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.recyclerview:recyclerview:1.1.0=debugCompileClasspath,debugRuntimeClasspath +androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugCompileClasspath,debugRuntimeClasspath androidx.savedstate:savedstate-android:1.3.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate-compose-android:1.3.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate-compose:1.3.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -100,8 +113,12 @@ androidx.savedstate:savedstate-ktx:1.3.2=debugRuntimeClasspath,debugUnitTestRunt androidx.savedstate:savedstate:1.3.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.transition:transition:1.6.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.transition:transition:1.6.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.vectordrawable:vectordrawable-animated:1.1.0=debugCompileClasspath,debugRuntimeClasspath +androidx.vectordrawable:vectordrawable:1.1.0=debugCompileClasspath,debugRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.viewpager2:viewpager2:1.0.0=debugCompileClasspath,debugRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugCompileClasspath,debugRuntimeClasspath androidx.window:window-core-android:1.5.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.window:window-core:1.5.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.window:window:1.5.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath @@ -140,6 +157,7 @@ com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.7=dokkaGfmPart com.fasterxml.jackson.module:jackson-module-kotlin:2.12.7=dokkaGfmPartialPlugin,dokkaGfmPartialRuntime,dokkaGfmPlugin,dokkaGfmRuntime,dokkaHtmlPartialPlugin,dokkaHtmlPartialRuntime,dokkaHtmlPlugin,dokkaHtmlRuntime,dokkaJavadocPartialPlugin,dokkaJavadocPartialRuntime,dokkaJavadocPlugin,dokkaJavadocRuntime,dokkaJekyllPartialPlugin,dokkaJekyllPartialRuntime,dokkaJekyllPlugin,dokkaJekyllRuntime com.fasterxml.jackson:jackson-bom:2.12.7=dokkaGfmPartialPlugin,dokkaGfmPartialRuntime,dokkaGfmPlugin,dokkaGfmRuntime,dokkaHtmlPartialPlugin,dokkaHtmlPartialRuntime,dokkaHtmlPlugin,dokkaHtmlRuntime,dokkaJavadocPartialPlugin,dokkaJavadocPartialRuntime,dokkaJavadocPlugin,dokkaJavadocRuntime,dokkaJekyllPartialPlugin,dokkaJekyllPartialRuntime,dokkaJekyllPlugin,dokkaJekyllRuntime com.fasterxml.woodstox:woodstox-core:6.2.4=dokkaGfmPartialRuntime,dokkaGfmRuntime,dokkaHtmlPartialRuntime,dokkaHtmlRuntime,dokkaJavadocPartialRuntime,dokkaJavadocRuntime,dokkaJekyllPartialRuntime,dokkaJekyllRuntime +com.google.android.material:material:1.12.0=debugCompileClasspath,debugRuntimeClasspath com.google.android:annotations:4.1.1.4=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core com.google.api.grpc:proto-google-common-protos:2.17.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core com.google.auto.service:auto-service-annotations:1.1.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle @@ -150,6 +168,7 @@ com.google.code.gson:gson:2.10.1=_internal-unified-test-platform-android-test-pl com.google.code.gson:gson:2.8.9=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-launcher com.google.crypto.tink:tink:1.7.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control com.google.dagger:dagger:2.48=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core +com.google.errorprone:error_prone_annotations:2.15.0=debugRuntimeClasspath com.google.errorprone:error_prone_annotations:2.23.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher com.google.guava:failureaccess:1.0.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher com.google.guava:guava:32.0.1-jre=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher @@ -226,6 +245,7 @@ org.jetbrains.dokka:analysis-markdown:1.9.20=dokkaGfmPartialPlugin,dokkaGfmPlugi org.jetbrains.dokka:dokka-base:1.9.20=dokkaGfmPartialPlugin,dokkaGfmPlugin,dokkaHtmlPartialPlugin,dokkaHtmlPlugin,dokkaJavadocPartialPlugin,dokkaJavadocPlugin,dokkaJekyllPartialPlugin,dokkaJekyllPlugin org.jetbrains.dokka:dokka-core:1.9.20=dokkaGfmPartialRuntime,dokkaGfmRuntime,dokkaHtmlPartialRuntime,dokkaHtmlRuntime,dokkaJavadocPartialRuntime,dokkaJavadocRuntime,dokkaJekyllPartialRuntime,dokkaJekyllRuntime org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-bom:1.8.22=debugRuntimeClasspath org.jetbrains.kotlin:kotlin-build-tools-api:2.1.21=kotlinBuildToolsApiClasspath org.jetbrains.kotlin:kotlin-build-tools-impl:2.1.21=kotlinBuildToolsApiClasspath org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath diff --git a/posthog-samples/posthog-android-sample/src/debug/AndroidManifest.xml b/posthog-samples/posthog-android-sample/src/debug/AndroidManifest.xml index b6eb7f426..3be41d170 100644 --- a/posthog-samples/posthog-android-sample/src/debug/AndroidManifest.xml +++ b/posthog-samples/posthog-android-sample/src/debug/AndroidManifest.xml @@ -4,6 +4,11 @@ + tools:replace="android:usesCleartextTraffic"> + + diff --git a/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/CheckoutProof.kt b/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/CheckoutProof.kt new file mode 100644 index 000000000..2d057f232 --- /dev/null +++ b/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/CheckoutProof.kt @@ -0,0 +1,240 @@ +package com.posthog.android.sample + +import android.app.Activity +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import com.google.android.material.bottomsheet.BottomSheetDialog + +/** A recognizable, synthetic checkout for inspecting screenshot-scene recordings. */ +internal class CheckoutProof(private val activity: Activity) { + private val handler = Handler(Looper.getMainLooper()) + private val ink = Color.rgb(26, 37, 54) + private val muted = Color.rgb(103, 116, 135) + private val navy = Color.rgb(29, 47, 79) + private val accent = Color.rgb(55, 108, 177) + private val surface = Color.WHITE + + fun show() { + val screen = + LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setBackgroundColor(Color.rgb(245, 247, 251)) + } + val header = + LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(24), dp(26), dp(24), dp(28)) + setBackgroundColor(navy) + } + header.stack(label("NORTHSTAR MARKET", 12f, Color.rgb(170, 205, 246), true)) + header.stack(label("Checkout", 30f, surface, true), top = 7) + header.stack(label("Your order is almost on its way", 14f, Color.rgb(214, 225, 239)), top = 4) + screen.stack(header) + + val body = + LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(20), dp(20), dp(20), dp(16)) + } + screen.addView(body, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)) + body.stack(label("Order details", 20f, ink, true)) + + val delivery = panel() + delivery.stack(label("DELIVERY", 11f, accent, true)) + val deliveryName = label("Standard delivery", 18f, ink, true) + delivery.stack(deliveryName, top = 6) + val deliveryEta = label("Arrives in 3–5 business days", 13f, muted) + delivery.stack(deliveryEta, top = 3) + body.stack(delivery, top = 16) + + val item = panel() + item.stack(label("YOUR ITEM", 11f, muted, true)) + val itemRow = + LinearLayout(activity).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + } + val productImage = + FrameLayout(activity).apply { + background = rounded(Color.rgb(232, 238, 248), 12) + addView(label("🎧", 39f, ink).apply { gravity = Gravity.CENTER }, FrameLayout.LayoutParams(-1, -1)) + } + itemRow.addView(productImage, LinearLayout.LayoutParams(dp(72), dp(72))) + val productText = LinearLayout(activity).apply { orientation = LinearLayout.VERTICAL } + productText.stack(label("Wireless headphones", 15f, ink, true)) + productText.stack(label("Midnight blue · Qty 1", 12f, muted), top = 4) + productText.stack(label("$89.00", 15f, ink, true), top = 7) + itemRow.addView(productText, LinearLayout.LayoutParams(0, -2, 1f).apply { leftMargin = dp(15) }) + item.stack(itemRow, top = 12) + body.stack(item, top = 14) + + val address = panel() + address.stack(label("DELIVER TO", 11f, muted, true)) + address.stack( + label("Alex Example · 123 Demo Street", 14f, ink).apply { tag = "ph-no-capture" }, + top = 8, + ) + address.stack(label("San Francisco, CA", 13f, muted), top = 3) + body.stack(address, top = 14) + + body.stack(label("Payment summary", 17f, ink, true), top = 23) + body.stack(summaryRow("Subtotal", "$89.00"), top = 10) + val shippingAmount = label("FREE", 14f, accent, true) + body.stack(summaryRow("Shipping", shippingAmount), top = 9) + val totalAmount = label("$89.00", 18f, ink, true) + body.stack(summaryRow("Total", totalAmount), top = 15) + body.addView(View(activity), LinearLayout.LayoutParams(1, 0, 1f)) + + val sheet = + LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(24), dp(14), dp(24), dp(28)) + background = rounded(surface, 24) + } + val handle = View(activity).apply { background = rounded(Color.rgb(198, 206, 216), 4) } + sheet.addView( + LinearLayout(activity).apply { + gravity = Gravity.CENTER + addView(handle, LinearLayout.LayoutParams(dp(38), dp(4))) + }, + LinearLayout.LayoutParams(-1, dp(18)), + ) + sheet.stack(label("Choose delivery", 24f, ink, true), top = 10) + sheet.stack(label("When should we bring your order?", 14f, muted), top = 5) + sheet.stack(deliveryOption("Standard", "3–5 business days", "FREE", false), top = 21) + sheet.stack(deliveryOption("Express", "Tomorrow, by 8 pm", "$10.00", true), top = 10) + sheet.stack(label("DELIVERING TO", 11f, muted, true), top = 21) + sheet.stack( + label("Alex Example · 123 Demo Street", 13f, ink).apply { tag = "ph-no-capture" }, + top = 7, + ) + + val dialog = + BottomSheetDialog(activity).apply { + setContentView(sheet) + window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + window?.setDimAmount(0.48f) + } + Log.i("WindowCompositionProof", "checkout windowClass=${dialog.javaClass.name}") + + fun selectExpress() { + deliveryName.text = "Express delivery" + deliveryEta.text = "Arrives tomorrow, by 8 pm" + shippingAmount.text = "$10.00" + totalAmount.text = "$99.00" + dialog.dismiss() + } + sheet.stack(action("Use express delivery") { selectExpress() }, top = 22) + body.stack(action("Change delivery option") { dialog.show() }, top = 20) + body.stack(label("Secure checkout · Demo order", 12f, muted).apply { gravity = Gravity.CENTER }, top = 13) + activity.setContentView(screen) + + handler.postDelayed({ if (!activity.isFinishing && !dialog.isShowing) dialog.show() }, 4000) + handler.postDelayed({ if (!activity.isFinishing && dialog.isShowing) selectExpress() }, 10000) + } + + private fun deliveryOption( + title: String, + subtitle: String, + price: String, + selected: Boolean, + ): View { + val row = + LinearLayout(activity).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(dp(16), dp(13), dp(16), dp(13)) + background = + rounded( + if (selected) Color.rgb(238, 245, 255) else surface, + 13, + if (selected) accent else Color.rgb(218, 224, 232), + ) + } + row.addView(label(if (selected) "◉" else "○", 24f, accent), LinearLayout.LayoutParams(dp(32), -2)) + val copy = LinearLayout(activity).apply { orientation = LinearLayout.VERTICAL } + copy.stack(label(title, 15f, ink, true)) + copy.stack(label(subtitle, 12f, muted), top = 3) + row.addView(copy, LinearLayout.LayoutParams(0, -2, 1f)) + row.addView(label(price, 13f, ink, true)) + return row + } + + private fun summaryRow( + name: String, + amount: String, + ): View = summaryRow(name, label(amount, 14f, ink)) + + private fun summaryRow( + name: String, + amount: TextView, + ): View = + LinearLayout(activity).apply { + orientation = LinearLayout.HORIZONTAL + addView(label(name, 14f, muted), LinearLayout.LayoutParams(0, -2, 1f)) + addView(amount) + } + + private fun panel(): LinearLayout = + LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(17), dp(16), dp(17), dp(16)) + background = rounded(surface, 15) + elevation = dp(2).toFloat() + } + + private fun action( + title: String, + onClick: () -> Unit, + ): TextView = + label(title, 15f, surface, true).apply { + gravity = Gravity.CENTER + background = rounded(navy, 12) + setOnClickListener { onClick() } + minimumHeight = dp(53) + } + + private fun label( + value: String, + size: Float, + color: Int, + bold: Boolean = false, + ): TextView = + TextView(activity).apply { + text = value + textSize = size + setTextColor(color) + if (bold) typeface = Typeface.DEFAULT_BOLD + } + + private fun rounded( + color: Int, + radius: Int, + border: Int? = null, + ): GradientDrawable = + GradientDrawable().apply { + setColor(color) + cornerRadius = dp(radius).toFloat() + if (border != null) setStroke(dp(1), border) + } + + private fun dp(value: Int): Int = (value * activity.resources.displayMetrics.density + 0.5f).toInt() + + private fun LinearLayout.stack( + child: View, + top: Int = 0, + ) { + addView(child, LinearLayout.LayoutParams(-1, -2).apply { topMargin = dp(top) }) + } +} diff --git a/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/WindowCompositionProofActivity.kt b/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/WindowCompositionProofActivity.kt new file mode 100644 index 000000000..a852b5d0c --- /dev/null +++ b/posthog-samples/posthog-android-sample/src/debug/java/com/posthog/android/sample/WindowCompositionProofActivity.kt @@ -0,0 +1,304 @@ +package com.posthog.android.sample + +import android.app.Activity +import android.app.Dialog +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.PixelCopy +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.TextView +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class WindowCompositionProofActivity : Activity() { + private val mainHandler = Handler(Looper.getMainLooper()) + private lateinit var overlay: Dialog + private var nestedOverlay: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent.getStringExtra("scene") == "bottom-sheet") { + CheckoutProof(this).show() + return + } + val root = FrameLayout(this).apply { setBackgroundColor(Color.rgb(20, 70, 180)) } + root.addView( + TextView(this).apply { + text = "PROOF PRIVATE ACTIVITY" + tag = "ph-no-capture" + textSize = 21f + setTextColor(Color.YELLOW) + setBackgroundColor(Color.rgb(120, 30, 100)) + }, + FrameLayout.LayoutParams(450, 100).apply { + leftMargin = 60 + topMargin = 340 + }, + ) + setContentView(root) + val scene = intent.getStringExtra("scene") ?: "transparent" + if (scene == "memory-single") return + val wrap = scene in setOf("wrap", "nested", "memory-nested", "visibility", "nonfocus", "windowalpha") + overlay = + Dialog(this).apply { + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + val content = FrameLayout(this@WindowCompositionProofActivity) + val card = FrameLayout(this@WindowCompositionProofActivity) + card.addView( + object : View(this@WindowCompositionProofActivity) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + override fun onDraw(canvas: Canvas) { + paint.color = Color.argb(128, 255, 0, 0) + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + paint.color = Color.WHITE + canvas.drawCircle(width / 2f, height / 2f, width / 4f, paint) + } + }, + FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), + ) + card.addView( + TextView(this@WindowCompositionProofActivity).apply { + text = "PROOF PRIVATE DIALOG" + tag = "ph-no-capture" + textSize = 15f + setTextColor(Color.YELLOW) + setBackgroundColor(Color.rgb(40, 120, 40)) + }, + FrameLayout.LayoutParams(320, 70, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL), + ) + content.addView(card, FrameLayout.LayoutParams(360, 360, Gravity.CENTER)) + setContentView(content) + window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + if (scene == "nonfocus") window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + window?.setDimAmount(0.35f) + window?.setLayout( + if (wrap) WindowManager.LayoutParams.WRAP_CONTENT else WindowManager.LayoutParams.MATCH_PARENT, + if (wrap) WindowManager.LayoutParams.WRAP_CONTENT else WindowManager.LayoutParams.MATCH_PARENT, + ) + show() + if (scene == "windowalpha") { + window?.let { it.attributes = it.attributes.apply { alpha = 0.5f } } + } + window?.setLayout( + if (wrap) WindowManager.LayoutParams.WRAP_CONTENT else WindowManager.LayoutParams.MATCH_PARENT, + if (wrap) WindowManager.LayoutParams.WRAP_CONTENT else WindowManager.LayoutParams.MATCH_PARENT, + ) + } + if (scene == "nested" || scene == "memory-nested") { + nestedOverlay = + Dialog(this).apply { + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setContentView(View(this@WindowCompositionProofActivity).apply { setBackgroundColor(Color.GREEN) }) + window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + window?.setDimAmount(0.60f) + show() + window?.setLayout(280, 280) + } + } + if (scene != "memory-nested") mainHandler.postDelayed({ capture(scene) }, 1200) + if (scene == "visibility") { + mainHandler.postDelayed({ + overlay.hide() + logVisibility("hidden") + }, 5000) + mainHandler.postDelayed({ + overlay.show() + logVisibility("shown") + }, 8000) + mainHandler.postDelayed({ + overlay.dismiss() + logVisibility("dismissed") + }, 11000) + } + } + + private fun logVisibility(state: String) { + val decorView = overlay.window?.decorView + Log.i( + TAG, + "visibility $state attached=${decorView?.isAttachedToWindow} shown=${decorView?.isShown} visibility=${decorView?.visibility}", + ) + } + + private fun capture(scene: String) { + val activityWindow = window + val dialogWindow = overlay.window ?: return + val activityView = activityWindow.decorView + val dialogView = dialogWindow.decorView + val activityLocation = IntArray(2).also(activityView::getLocationOnScreen) + val dialogLocation = IntArray(2).also(dialogView::getLocationOnScreen) + val dialogWindowLocation = IntArray(2).also(dialogView::getLocationInWindow) + val dialogSurfaceLocation = IntArray(2).also(dialogView::getLocationInSurface) + val nestedView = nestedOverlay?.window?.decorView + val nestedLocation = nestedView?.let { IntArray(2).also(it::getLocationOnScreen).contentToString() } + Log.i( + TAG, + "scene=$scene windowClass=${overlay.javaClass.name} " + + "activity=${activityView.width}x${activityView.height}@${activityLocation.contentToString()} " + + "dialog=${dialogView.width}x${dialogView.height}@${dialogLocation.contentToString()} " + + "inWindow=${dialogWindowLocation.contentToString()} inSurface=${dialogSurfaceLocation.contentToString()} " + + "dim=${dialogWindow.attributes.dimAmount} dialogFocus=${dialogView.hasWindowFocus()} " + + "opacity=${dialogWindow.attributes.alpha} nested=${nestedView?.width}x${nestedView?.height}@$nestedLocation " + + "nestedDim=${nestedOverlay?.window?.attributes?.dimAmount}", + ) + Thread { + val handlerThread = HandlerThread("proof-pixel-copy").apply { start() } + try { + copy(scene, "activity", activityWindow, activityView.width, activityView.height, Bitmap.Config.ARGB_8888, handlerThread) + copy(scene, "dialog-argb", dialogWindow, dialogView.width, dialogView.height, Bitmap.Config.ARGB_8888, handlerThread) + copy( + scene, + "dialog-decor-crop", + dialogWindow, + dialogView.width, + dialogView.height, + Bitmap.Config.ARGB_8888, + handlerThread, + Rect(0, 0, dialogView.width, dialogView.height), + ) + if (Build.VERSION.SDK_INT == 26 && scene == "wrap") { + copy( + scene, + "dialog-surface-crop", + dialogWindow, + dialogView.width, + dialogView.height, + Bitmap.Config.ARGB_8888, + handlerThread, + Rect( + dialogSurfaceLocation[0], + dialogSurfaceLocation[1], + dialogView.width + dialogSurfaceLocation[0], + dialogView.height + dialogSurfaceLocation[1], + ), + ) + } + copy(scene, "dialog-rgb565", dialogWindow, dialogView.width, dialogView.height, Bitmap.Config.RGB_565, handlerThread) + val width = dialogView.width + val height = dialogView.height + if (width > 400 && height > 400) { + val crop = Rect(width / 2 - 200, height / 2 - 200, width / 2 + 200, height / 2 + 200) + copy(scene, "dialog-crop", dialogWindow, 200, 200, Bitmap.Config.ARGB_8888, handlerThread, crop) + } + copyAcrossFormats(scene, dialogWindow, width, height, activityView.width, activityView.height, handlerThread) + nestedOverlay?.window?.let { nestedWindow -> + nestedView?.let { + copy(scene, "nested-argb", nestedWindow, it.width, it.height, Bitmap.Config.ARGB_8888, handlerThread) + } + } + } finally { + handlerThread.quitSafely() + } + }.start() + } + + private fun copy( + scene: String, + name: String, + source: Window, + width: Int, + height: Int, + config: Bitmap.Config, + handlerThread: HandlerThread, + crop: Rect? = null, + ) { + if (width <= 0 || height <= 0) return + val bitmap = Bitmap.createBitmap(width, height, config) + bitmap.eraseColor(Color.MAGENTA) + val latch = CountDownLatch(1) + var result = -1 + val handler = Handler(handlerThread.looper) + PixelCopy.request(source, crop, bitmap, { + result = it + latch.countDown() + }, handler) + val finished = latch.await(5, TimeUnit.SECONDS) + if (!finished || result != PixelCopy.SUCCESS) { + Log.i(TAG, "$scene/$name PixelCopy finished=$finished result=$result") + bitmap.recycle() + return + } + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + val transparent = pixels.count { Color.alpha(it) == 0 } + val partial = pixels.count { Color.alpha(it) in 1..254 } + val opaque = pixels.size - transparent - partial + val encoded = ByteArrayOutputStream().also { bitmap.compress(Bitmap.CompressFormat.WEBP, 80, it) }.toByteArray() + val decoded = android.graphics.BitmapFactory.decodeByteArray(encoded, 0, encoded.size) + val center = decoded.getPixel(width / 2, height / 2) + val corner = decoded.getPixel(0, 0) + val output = File(getExternalFilesDir(null), "$scene-$name.webp") + output.writeBytes(encoded) + Log.i( + TAG, + "$scene/$name config=$config allocation=${bitmap.allocationByteCount} " + + "alpha=[$transparent,$partial,$opaque] decodedCenter=${Integer.toHexString(center)} " + + "decodedCorner=${Integer.toHexString(corner)} webp=${encoded.size} output=$output", + ) + decoded.recycle() + bitmap.recycle() + } + + private fun copyAcrossFormats( + scene: String, + source: Window, + width: Int, + height: Int, + maximumWidth: Int, + maximumHeight: Int, + handlerThread: HandlerThread, + ) { + val scratch = Bitmap.createBitmap(maximumWidth, maximumHeight, Bitmap.Config.ARGB_8888) + val allocation = scratch.allocationByteCount + try { + for (config in listOf(Bitmap.Config.RGB_565, Bitmap.Config.ARGB_8888)) { + scratch.reconfigure(width, height, config) + val beforeAlpha = scratch.hasAlpha() + val latch = CountDownLatch(1) + var result = -1 + PixelCopy.request(source, scratch, { + result = it + latch.countDown() + }, Handler(handlerThread.looper)) + val finished = latch.await(5, TimeUnit.SECONDS) + val pixels = IntArray(width * height) + if (finished && result == PixelCopy.SUCCESS) { + scratch.getPixels(pixels, 0, width, 0, 0, width, height) + } + val transparent = pixels.count { Color.alpha(it) == 0 } + val partial = pixels.count { Color.alpha(it) in 1..254 } + Log.i( + TAG, + "$scene/reconfigured $config finished=$finished result=$result " + + "beforeAlpha=$beforeAlpha afterAlpha=${scratch.hasAlpha()} " + + "allocation=${scratch.allocationByteCount} initialAllocation=$allocation " + + "transparent=$transparent partial=$partial corner=${Integer.toHexString(scratch.getPixel(0, 0))}", + ) + } + } finally { + scratch.recycle() + } + } + + companion object { + private const val TAG = "WindowCompositionProof" + } +} diff --git a/posthog-samples/posthog-android-sample/src/debug/res/values/themes.xml b/posthog-samples/posthog-android-sample/src/debug/res/values/themes.xml new file mode 100644 index 000000000..5620c1fd7 --- /dev/null +++ b/posthog-samples/posthog-android-sample/src/debug/res/values/themes.xml @@ -0,0 +1,4 @@ + + +