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

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