From ebe1fc846472e9ce35b59bcdc4096bc3973ea307 Mon Sep 17 00:00:00 2001 From: Kayos Date: Tue, 26 May 2026 08:43:06 -0700 Subject: [PATCH] vc=52: R8 enabled + surface-handoff polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R8 (minify + resource-shrink) flipped on for BOTH debug AND release variants — we publish the debug APK to fdroid (per existing pipeline), and the audit-flagged Log.d strip discipline required R8 to actually run on the variant we ship. New strawApp/proguard-rules.pro covers: * UniFFI bindings (uniffi.strawcore.*) — reflective FFI dispatch from Rust side, must survive minification * JNA — Library subclasses reflectively loaded by name * kotlinx-serialization @Serializable — generated $$serializer companions, kept via both the package-anchored rule and the annotation-wildcard rule for belt + suspenders * Media3 session Parcelables (cross-process via Binder) * Compose runtime + Strawcore exception hierarchy Surface-handoff polish on inline ↔ fullscreen transitions: setKeepContentOnPlayerReset(true) on both PlayerViews (inline in VideoDetail + fullscreen Player). When the detaching view's player is nulled on dispose, it holds the last rendered frame instead of flashing black. The receiving view's surface takeover then renders the next frame without the ~1-frame black gap. Round-4 audit HIGH-5 was the closest writeup. Expected APK-size win from R8: ~30-40%. Need real-device verification post-install — the keep rules are best-effort and a missing rule manifests as runtime ClassNotFoundException or silently-broken kotlinx-serialization decoding. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 4 +- strawApp/build.gradle.kts | 19 ++++- strawApp/proguard-rules.pro | 81 +++++++++++++++++++ .../straw/feature/detail/VideoDetailScreen.kt | 6 ++ .../straw/feature/player/PlayerScreen.kt | 8 ++ 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 strawApp/proguard-rules.pro diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 73ed00783..9eff49449 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 51 -const val STRAW_VERSION_NAME = "0.1.0-BK" +const val STRAW_VERSION_CODE = 52 +const val STRAW_VERSION_NAME = "0.1.0-BL" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/build.gradle.kts b/strawApp/build.gradle.kts index bb98352e7..b62f16540 100644 --- a/strawApp/build.gradle.kts +++ b/strawApp/build.gradle.kts @@ -39,13 +39,30 @@ configure { } buildTypes { + // R8 enabled on BOTH variants — we publish the debug APK to + // fdroid (com.sulkta.straw.debug) per the existing pipeline, + // and audit-flagged Log.d strips depended on R8 actually + // running on the variant we ship. Keep rules in + // strawApp/proguard-rules.pro cover UniFFI + JNA + + // kotlinx-serialization companions. debug { isDebuggable = true applicationIdSuffix = ".debug" resValue("string", "app_name", "Straw debug") + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } diff --git a/strawApp/proguard-rules.pro b/strawApp/proguard-rules.pro new file mode 100644 index 000000000..2a81fe15c --- /dev/null +++ b/strawApp/proguard-rules.pro @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: 2026 Sulkta-Coop +# SPDX-License-Identifier: GPL-3.0-or-later +# +# R8 keep rules for the Straw app module. The legacy `app/proguard-rules.pro` +# is for the upstream NewPipe module — different namespaces, different +# rules. This file is OURS. +# +# AGP's getDefaultProguardFile("proguard-android-optimize.txt") handles +# the Android framework + AndroidX + Compose runtime defaults via +# consumer rules shipped with each library. We only need to spell out +# what those defaults can't see: +# +# * UniFFI bindings — reflective FFI dispatch from generated code. +# * JNA — reflects on every class extending com.sun.jna.Library +# (that's how the loadLibrary glue works). +# * Our kotlinx-serialization @Serializable types — their generated +# $$serializer companions get tree-shaken without explicit keeps. +# * Media3 session metadata Parcelables. + +# -- UniFFI ------------------------------------------------------------- +# Generated bindings live under uniffi.strawcore.*. The Rust side calls +# them via JNI symbol name; if R8 renames the class or methods, every +# extractor call NPEs. +-keep class uniffi.strawcore.** { *; } +-keep class uniffi.** { *; } + +# -- JNA --------------------------------------------------------------- +# JNA looks up Library subclasses by Class.forName + reflection at +# load time. Anything that extends Library or has @FieldOrder must +# survive. +-keep class * extends com.sun.jna.Library { *; } +-keep class com.sun.jna.** { *; } +-dontwarn com.sun.jna.** + +# -- kotlinx-serialization --------------------------------------------- +# Every @Serializable type gets a synthetic Companion + $$serializer +# class. R8 will strip the $$serializer if nothing visibly calls it +# (the lookup goes through reflection on the Companion). +-keepattributes *Annotation*, InnerClasses +-dontwarn kotlinx.serialization.** + +-keep,includedescriptorclasses class com.sulkta.straw.**$$serializer { *; } +-keepclassmembers class com.sulkta.straw.** { + *** Companion; +} +-keepclasseswithmembers class com.sulkta.straw.** { + kotlinx.serialization.KSerializer serializer(...); +} + +# Same dance for our top-level @Serializable types defined outside +# `com.sulkta.straw.**` (Rust DTOs, etc.). Belt + suspenders. +-keepclassmembers @kotlinx.serialization.Serializable class * { + static **$Companion Companion; + public static <1>$Companion Companion; +} +-keepclasseswithmembers @kotlinx.serialization.Serializable class * { + kotlinx.serialization.KSerializer serializer(...); +} +-keep class **$$serializer { *; } + +# -- Media3 / ExoPlayer ------------------------------------------------ +# Most of Media3 ships consumer rules but session-related Parcelables +# are reflectively reconstructed across process boundaries (the +# MediaController talks to PlaybackService via Binder). Keep their +# field names. +-keep class androidx.media3.session.** { *; } +-keep class androidx.media3.common.MediaItem { *; } +-keep class androidx.media3.common.MediaItem$* { *; } +-keep class androidx.media3.common.MediaMetadata { *; } + +# -- Strawcore exceptions / DTOs reflected by UniFFI -------------------- +# StrawcoreError is a sealed Throwable hierarchy exposed via UniFFI. +# Keep all subclasses + their fields so the Kotlin pattern-match works +# after minification. +-keep class com.sulkta.straw.feature.player.** { *; } + +# -- Reflection-via-Class.forName paths from Compose -------------------- +# Compose's runtime does some Class.forName for its own bootstrap; the +# AGP consumer rules cover this, but documenting the dependency here +# so a future bump doesn't surprise us. +-keep class androidx.compose.runtime.** { *; } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt index 3dd48fb88..04d79017d 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt @@ -799,6 +799,12 @@ private fun InlinePlayer( PlayerView(ctx).apply { player = controller useController = true + // Same surface-handoff polish as the + // fullscreen PlayerView — hold the last + // frame on dispose so the inline ↔ + // fullscreen transition doesn't flash + // black between detach + reattach. + setKeepContentOnPlayerReset(true) } }, update = { it.player = controller }, diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt index fb466eebc..02479cf99 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt @@ -178,6 +178,14 @@ fun PlayerScreen( PlayerView(ctx).apply { player = controller useController = true + // Keep the last frame on screen when this + // view's player is reset (fullscreen → + // inline transition). Without this, the + // detaching PlayerView flashes black for + // ~1 frame before the receiving view takes + // over the surface. + controllerHideOnTouch = true + setKeepContentOnPlayerReset(true) } }, update = { it.player = controller },