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 },