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
This commit is contained in:
Ray 2026-04-06 15:24:20 -04:00 committed by GitHub
parent 9a3af225a4
commit 31465051ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 91 additions and 165 deletions

View file

@ -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

View file

@ -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

View file

@ -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:"

View file

@ -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

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
</manifest>

View file

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<!-- Required for Android 11+ to query voice recognition availability -->
<queries>
<intent>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
</intent>
</queries>
<application
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Wholphin"
android:name=".WholphinApplication"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".test.TestActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- <service-->
<!-- android:name=".WholphinDreamService"-->
<!-- android:exported="true"-->
<!-- android:label="@string/app_name"-->
<!-- android:permission="android.permission.BIND_DREAM_SERVICE">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.service.dreams.DreamService" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- </intent-filter>-->
<!-- </service>-->
</application>
</manifest>

View file

@ -20,7 +20,7 @@
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
android:required="${leanback}" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />

View file

@ -65,7 +65,7 @@ class UpdateChecker
private val NOTE_REGEX = Regex("<!-- app-note:(.+) -->")
val ACTIVE = true
const val ACTIVE = BuildConfig.UPDATING_ENABLED
}
/**

View file

@ -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

View file

@ -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 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
@@ -20,7 +19,7 @@
android:required="false" />
<uses-feature
android:name="android.software.leanback"
- android:required="false" />
+ android:required="true" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
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("<!-- app-note:(.+) -->")
- val ACTIVE = true
+ val ACTIVE = false
}
/**

View file

@ -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<TestActivity>()
@Inject