From 5363db0b80387ffb87037f4e7cfe580bf7aa3031 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 29 Aug 2026 21:35:58 -0400 Subject: [PATCH] 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 --- app/build.gradle.kts | 8 ++++-- app/src/main/AndroidManifest.xml | 1 + .../wholphin/ui/setup/SwitchServerContent.kt | 28 ++++++++++++++++++- .../ui/setup/SwitchServerViewModel.kt | 15 ++++++++++ .../wholphin/services/IntentServiceTest.kt | 2 ++ .../ui/playback/PlaybackViewModelTests.kt | 2 ++ gradle/libs.versions.toml | 4 +++ wholphin-mpv-stub/build.gradle.kts | 6 ++-- 8 files changed, 60 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e8046a68..e005cb6f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,14 +65,16 @@ kotlin { } } +private fun Provider.getInt() = get().toInt() + configure { namespace = "com.github.damontecres.wholphin" - compileSdk = 36 + compileSdk = libs.versions.compileSdk.getInt() defaultConfig { applicationId = "com.github.damontecres.wholphin" - minSdk = 23 - targetSdk = 36 + minSdk = libs.versions.minSdk.getInt() + targetSdk = libs.versions.targetSdk.getInt() versionCode = gitTags.trim().lines().size versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" } testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6036dabd..582e000a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ android:installLocation="auto"> + diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt index 24a8e0ca..c25fe776 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt @@ -1,6 +1,10 @@ package com.github.damontecres.wholphin.ui.setup +import android.Manifest 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.slideInVertically 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.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState +import timber.log.Timber @Composable fun SwitchServerContent( @@ -294,10 +299,31 @@ private fun AddServerDialog( val state by viewModel.state.collectAsState() var showEnterAddress by remember { mutableStateOf(false) } + val permissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + viewModel.discoverServers() + } else { + showEnterAddress = true + } + } + LaunchedEffect(Unit) { viewModel.clearAddServerState() if (!showEnterAddress) { - viewModel.discoverServers() + if (viewModel.hasPermission) { + 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, + ) + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index 7098c118..53190665 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -1,7 +1,11 @@ package com.github.damontecres.wholphin.ui.setup +import android.Manifest import android.content.Context +import android.content.pm.PackageManager +import android.os.Build import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R @@ -46,6 +50,17 @@ class SwitchServerViewModel private val _state = MutableStateFlow(SwitchServerState()) val state: StateFlow = _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() { _state.update { it.copy(addServerState = LoadingState.Pending) } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt index 17c6a313..bb4904aa 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt @@ -25,8 +25,10 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [30]) class IntentServiceTest { private val api: ApiClient = mockk() private val serverRepository: ServerRepository = mockk() diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt index adb29999..cf7ee59f 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt @@ -77,10 +77,12 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [30]) class PlaybackViewModelTests { private val testDispatcher = StandardTestDispatcher() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2dbe114c..ff09b53d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,10 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" material = "1.14.0" +compileSdk = "37" +targetSdk = "37" +minSdk = "23" + [libraries] 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" } diff --git a/wholphin-mpv-stub/build.gradle.kts b/wholphin-mpv-stub/build.gradle.kts index 6002fba9..9017f416 100644 --- a/wholphin-mpv-stub/build.gradle.kts +++ b/wholphin-mpv-stub/build.gradle.kts @@ -2,14 +2,16 @@ plugins { alias(libs.plugins.android.library) } +private fun Provider.getInt() = get().toInt() + android { namespace = "com.github.damontecres.wholphin.mpv" compileSdk { - version = release(36) + version = release(libs.versions.compileSdk.getInt()) } defaultConfig { - minSdk = 23 + minSdk = libs.versions.minSdk.getInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro")