Update API level to Android 17 (#1908)

## Description
Updates the compile & target SDKs to 37 (Android 17).

The only required change is to support the new `ACCESS_LOCAL_NETWORK`
permission. This is required for Android 17 devices to auto discover
Jellyfin servers on the local network. Wholphin will prompt for
permission and if granted, use the discover servers feature. If
permission is not granted, the user must enter the server URL manually.

### Related issues
N/A, but several dependency renovate PRs are blocked by this

### Testing
I tested with a regular Android 17 emulator. There are no Android TV 17
images (or probably even devices) available yet.

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-08-29 21:35:58 -04:00 • committed by GitHub
parent cc2e7dfdda
commit 5363db0b80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 60 additions and 6 deletions

View file

@ -65,14 +65,16 @@ kotlin {
} }
} }
private fun Provider<String>.getInt() = get().toInt()
configure<ApplicationExtension> { configure<ApplicationExtension> {
namespace = "com.github.damontecres.wholphin" namespace = "com.github.damontecres.wholphin"
compileSdk = 36 compileSdk = libs.versions.compileSdk.getInt()
defaultConfig { defaultConfig {
applicationId = "com.github.damontecres.wholphin" applicationId = "com.github.damontecres.wholphin"
minSdk = 23 minSdk = libs.versions.minSdk.getInt()
targetSdk = 36 targetSdk = libs.versions.targetSdk.getInt()
versionCode = gitTags.trim().lines().size versionCode = gitTags.trim().lines().size
versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" } versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" }
testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner" testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner"

View file

@ -4,6 +4,7 @@
android:installLocation="auto"> android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

View file

@ -1,6 +1,10 @@
package com.github.damontecres.wholphin.ui.setup package com.github.damontecres.wholphin.ui.setup
import android.Manifest
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
@ -67,6 +71,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import timber.log.Timber
@Composable @Composable
fun SwitchServerContent( fun SwitchServerContent(
@ -294,10 +299,31 @@ private fun AddServerDialog(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var showEnterAddress by remember { mutableStateOf(false) } var showEnterAddress by remember { mutableStateOf(false) }
val permissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (isGranted) {
viewModel.discoverServers()
} else {
showEnterAddress = true
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.clearAddServerState() viewModel.clearAddServerState()
if (!showEnterAddress) { if (!showEnterAddress) {
if (viewModel.hasPermission) {
viewModel.discoverServers() viewModel.discoverServers()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
permissionLauncher.launch(Manifest.permission.ACCESS_LOCAL_NETWORK)
} else {
Timber.w(
"No ACCESS_LOCAL_NETWORK permission, but API <%s: %s",
Build.VERSION_CODES.CINNAMON_BUN,
Build.VERSION.SDK_INT,
)
}
} }
} }

View file

@ -1,7 +1,11 @@
package com.github.damontecres.wholphin.ui.setup package com.github.damontecres.wholphin.ui.setup
import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
@ -46,6 +50,17 @@ class SwitchServerViewModel
private val _state = MutableStateFlow(SwitchServerState()) private val _state = MutableStateFlow(SwitchServerState())
val state: StateFlow<SwitchServerState> = _state val state: StateFlow<SwitchServerState> = _state
val hasPermission: Boolean
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_LOCAL_NETWORK,
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
fun clearAddServerState() { fun clearAddServerState() {
_state.update { it.copy(addServerState = LoadingState.Pending) } _state.update { it.copy(addServerState = LoadingState.Pending) }
} }

View file

@ -25,8 +25,10 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [30])
class IntentServiceTest { class IntentServiceTest {
private val api: ApiClient = mockk() private val api: ApiClient = mockk()
private val serverRepository: ServerRepository = mockk() private val serverRepository: ServerRepository = mockk()

View file

@ -77,10 +77,12 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [30])
class PlaybackViewModelTests { class PlaybackViewModelTests {
private val testDispatcher = StandardTestDispatcher() private val testDispatcher = StandardTestDispatcher()

View file

@ -51,6 +51,10 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
material = "1.14.0" material = "1.14.0"
compileSdk = "37"
targetSdk = "37"
minSdk = "23"
[libraries] [libraries]
wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" } wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" }
wholphin-extensions-ffmpeg = { module = "com.github.damontecres.wholphin.media3:decoder-ffmpeg", version.ref = "wholphin-extensions" } wholphin-extensions-ffmpeg = { module = "com.github.damontecres.wholphin.media3:decoder-ffmpeg", version.ref = "wholphin-extensions" }

View file

@ -2,14 +2,16 @@ plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
} }
private fun Provider<String>.getInt() = get().toInt()
android { android {
namespace = "com.github.damontecres.wholphin.mpv" namespace = "com.github.damontecres.wholphin.mpv"
compileSdk { compileSdk {
version = release(36) version = release(libs.versions.compileSdk.getInt())
} }
defaultConfig { defaultConfig {
minSdk = 23 minSdk = libs.versions.minSdk.getInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")