From 31465051ffca918f4199ead3df3614a82a98d608 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:24:20 -0400 Subject: [PATCH] Dev: use build product flavors for app store release (#1205) ## Description Instead of applying a code patch, this PR uses build time [product flavors](https://developer.android.com/build/build-variants#product-flavors) to define the existing default and an app store variant. This is more robust than maintaining a patch file and making sure it is applied before building. The app store variant applies the same code changes (removing install permission, requiring leanback, & setting `UpdateChecker.ACTIVE`), but via manifest placeholders/merging and `BuildConfig` entries. There are no user facing changes. ### Related issues None ### Testing Built the variants and verified manifest changes via aapt ## Screenshots N/A ## AI or LLM usage None --- .github/workflows/main.yml | 4 +- .github/workflows/pr.yml | 15 +-- .github/workflows/release.yml | 23 ++--- app/build.gradle.kts | 16 ++++ app/src/appstore/AndroidManifest.xml | 7 ++ app/src/debug/AndroidManifest.xml | 94 ------------------- app/src/main/AndroidManifest.xml | 2 +- .../wholphin/services/UpdateChecker.kt | 2 +- .../wholphin/ui/detail/DebugPage.kt | 35 +++++++ app/src/patches/play_store.patch | 36 ------- .../damontecres/wholphin/ui/BasicUiTests.kt | 22 +++++ 11 files changed, 91 insertions(+), 165 deletions(-) create mode 100644 app/src/appstore/AndroidManifest.xml delete mode 100644 app/src/debug/AndroidManifest.xml delete mode 100644 app/src/patches/play_store.patch diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 390b1232..7cbde21f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,7 +54,7 @@ jobs: ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}" ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}" run: | - ./gradlew clean assembleRelease assembleDebug --no-daemon + ./gradlew clean assembleDefaultRelease assembleDefaultDebug --no-daemon - name: Verify signatures run: | @@ -67,7 +67,7 @@ jobs: find app/build/outputs/apk -name '*.apk' -print0 | while IFS= read -r -d '' line; do # Wholphin-release-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-arm64-v8a.apk - short_name="$(echo $line | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')" + short_name="$(echo $line | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//' | sed -E 's/-default//')" echo "$line => $short_name" cp "$line" "$short_name" done diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dc518c87..bde4e1ef 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -39,19 +39,6 @@ jobs: - name: Build app id: buildapp run: | - ./gradlew clean assembleDebug testDebugUnitTest --no-daemon + ./gradlew clean assembleDefaultDebug testDefaultDebugUnitTest --no-daemon apks=$(find app/build/outputs \( -name '*.apk' -or -name '*.aab' \) -print0 | tr '\0' ',' | sed 's/,$//') echo "apks=$apks" >> "$GITHUB_OUTPUT" - - test-patch: - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Checkout the code - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - name: Test applying patch - run: | - git apply app/src/patches/play_store.patch - git diff diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 466aa8b1..d47134ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,22 +36,8 @@ jobs: ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}" ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}" run: | - ./gradlew clean assembleRelease --no-daemon + ./gradlew clean assembleDefaultRelease bundleAppstoreRelease --no-daemon - - name: Build app - id: buildaab - env: - KEY_ALIAS: "${{ secrets.KEY_ALIAS }}" - KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}" - KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}" - SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" - ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}" - ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}" - run: | - git apply app/src/patches/play_store.patch - ./gradlew bundleRelease --no-daemon - aab=$(find app/build/outputs -name '*.aab') - echo "aab=$aab" >> "$GITHUB_OUTPUT" - name: Verify signatures run: | echo "Verify APK signatures" @@ -62,13 +48,16 @@ jobs: run: | find app/build/outputs/apk -name '*.apk' -print0 | while IFS= read -r -d '' line; do - # Wholphin-release-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-arm64-v8a.apk - short_name="$(echo $line | sed -E 's/-release-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')" + # Wholphin-default-release-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-arm64-v8a.apk + short_name="$(echo $line | sed -E 's/-default-release-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')" echo "$line => $short_name" cp "$line" "$short_name" done apks=$(find app/build/outputs/apk -name '*.apk' -print0 | tr '\0' ',' | sed 's/,$//') echo "apks=$apks" >> "$GITHUB_OUTPUT" + + aab=$(find app/build/outputs -name '*.aab') + echo "aab=$aab" >> "$GITHUB_OUTPUT" - name: Checksums run: | echo "SHA256 checksums:" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbda6b36..80a6b2e4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,6 +47,8 @@ android { versionCode = gitTags.trim().lines().size versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" } testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner" + + buildConfigField("long", "BUILD_TIME", System.currentTimeMillis().toString()) } signingConfigs { @@ -108,6 +110,20 @@ android { } } } + flavorDimensions += "version" + productFlavors { + create("default") { + dimension = "version" + isDefault = true + manifestPlaceholders += mapOf("leanback" to false) + buildConfigField("boolean", "UPDATING_ENABLED", "true") + } + create("appstore") { + dimension = "version" + manifestPlaceholders += mapOf("leanback" to true) + buildConfigField("boolean", "UPDATING_ENABLED", "false") + } + } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/app/src/appstore/AndroidManifest.xml b/app/src/appstore/AndroidManifest.xml new file mode 100644 index 00000000..8a762a28 --- /dev/null +++ b/app/src/appstore/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 29b2eb75..00000000 --- a/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4479ae86..8dd32ce9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ android:required="false" /> + android:required="${leanback}" /> diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index f1701416..40a04218 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -65,7 +65,7 @@ class UpdateChecker private val NOTE_REGEX = Regex("") - val ACTIVE = true + const val ACTIVE = BuildConfig.UPDATING_ENABLED } /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt index cfae65ba..543d8e2c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt @@ -54,6 +54,7 @@ import org.jellyfin.sdk.model.DeviceInfo import timber.log.Timber import java.io.BufferedReader import java.io.InputStreamReader +import java.util.Date import javax.inject.Inject @HiltViewModel @@ -75,6 +76,34 @@ class DebugViewModel display.supportedModes.orEmpty() } + val av1Included by lazy { + try { + Class.forName("androidx.media3.decoder.av1.Libdav1dVideoRenderer") + true + } catch (_: ClassNotFoundException) { + false + } + } + + val ffmpegIncluded by lazy { + try { + Class.forName("androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer") + true + } catch (_: ClassNotFoundException) { + false + } + } + + val libMpvLoaded by lazy { + try { + System.loadLibrary("player") + System.loadLibrary("mpv") + true + } catch (_: Exception) { + false + } + } + init { viewModelScope.launchIO { serverRepository.currentUser.value?.rowId?.let { @@ -218,6 +247,7 @@ fun DebugPage( style = MaterialTheme.typography.displaySmall, color = MaterialTheme.colorScheme.onSurface, ) + val buildTime = Date(BuildConfig.BUILD_TIME) val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0) val installInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -239,6 +269,11 @@ fun DebugPage( "Version Code: ${pkgInfo.versionCodeLong}", "ClientInfo: ${viewModel.clientInfo}", "Build type: ${BuildConfig.BUILD_TYPE}", + "Build flavor: ${BuildConfig.FLAVOR}", + "Build time: $buildTime", + "FFMPEG included: ${viewModel.ffmpegIncluded}", + "AV1 included: ${viewModel.av1Included}", + "libmpv loaded: ${viewModel.libMpvLoaded}", "Debug enabled: ${BuildConfig.DEBUG}", "ABIs: ${Build.SUPPORTED_ABIS.toList()}", ) + installInfo diff --git a/app/src/patches/play_store.patch b/app/src/patches/play_store.patch deleted file mode 100644 index c1214f0e..00000000 --- a/app/src/patches/play_store.patch +++ /dev/null @@ -1,36 +0,0 @@ -# This patches Wholphin for releasing on app stores where self updating is not permitted - -diff --git i/app/src/main/AndroidManifest.xml w/app/src/main/AndroidManifest.xml -index 4479ae86..80596698 100644 ---- i/app/src/main/AndroidManifest.xml -+++ w/app/src/main/AndroidManifest.xml -@@ -6,7 +6,6 @@ - - - -- - -@@ -20,7 +19,7 @@ - android:required="false" /> - -+ android:required="true" /> - -diff --git i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -index 9e0665dd..bc1e1c16 100644 ---- i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -+++ w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -@@ -65,7 +65,7 @@ class UpdateChecker - - private val NOTE_REGEX = Regex("") - -- val ACTIVE = true -+ val ACTIVE = false - } - - /** diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/BasicUiTests.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/BasicUiTests.kt index 66178a4e..25121739 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/BasicUiTests.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/BasicUiTests.kt @@ -1,5 +1,7 @@ package com.github.damontecres.wholphin.ui +import android.app.Application +import android.content.pm.ActivityInfo import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key @@ -14,6 +16,7 @@ import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.requestFocus import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.test.core.app.ApplicationProvider import com.github.damontecres.wholphin.services.ScreensaverService import com.github.damontecres.wholphin.services.ScreensaverState import com.github.damontecres.wholphin.services.SetupDestination @@ -49,8 +52,11 @@ import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import javax.inject.Inject @@ -61,7 +67,23 @@ class BasicUiTests { @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) + // From https://github.com/robolectric/robolectric/pull/4736#issuecomment-1831034882 @get:Rule(order = 1) + val addActivityToRobolectricRule = + object : TestWatcher() { + override fun starting(description: Description?) { + super.starting(description) + val appContext: Application = ApplicationProvider.getApplicationContext() + val activityInfo = + ActivityInfo().apply { + name = TestActivity::class.java.name + packageName = appContext.packageName + } + shadowOf(appContext.packageManager).addOrUpdateActivity(activityInfo) + } + } + + @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() @Inject