diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01461e21..951f7ba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,6 +176,10 @@ jobs: working-directory: android run: ./gradlew :app:testDebugUnitTest :saf_util:testDebugUnitTest :libass:testDebugUnitTest -x :app:compileFlutterBuildDebug --continue + - name: Check Android API compatibility + working-directory: android + run: ./gradlew :app:lintDebug + native-format: name: Native Formatting runs-on: ubuntu-latest diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 18a23892..35822b94 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -166,6 +166,12 @@ android { jniLibs.srcDir(File(mpvDir, "libcxx/jni")) } } + + lint { + // Enforce the app-owned minSdk boundary without auditing upstream AndroidX. + checkDependencies = false + checkOnly += setOf("NewApi") + } } flutter { diff --git a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt index f056fdf1..e67018f2 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt @@ -21,6 +21,7 @@ import android.view.WindowInsets import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout +import androidx.annotation.RequiresApi import com.edde746.plezy.exoplayer.ExoPlayerPlugin import com.edde746.plezy.mpv.MpvAudioPlayerPlugin import com.edde746.plezy.mpv.MpvPlayerPlugin @@ -676,6 +677,7 @@ class MainActivity : FlutterActivity() { } } + @RequiresApi(Build.VERSION_CODES.O) private fun isPipPermissionGranted(): Boolean { val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager return appOpsManager.checkOpNoThrow( @@ -685,6 +687,7 @@ class MainActivity : FlutterActivity() { ) == AppOpsManager.MODE_ALLOWED } + @RequiresApi(Build.VERSION_CODES.O) private fun buildPipParams(width: Int, height: Int, autoEnterEnabled: Boolean? = null): PictureInPictureParams { val (w, h) = if (width <= 0 || height <= 0) { Pair(16, 9) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/AssLatencyCalibrator.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/AssLatencyCalibrator.kt index 48e151e0..27445749 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/AssLatencyCalibrator.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/AssLatencyCalibrator.kt @@ -3,6 +3,7 @@ package com.edde746.plezy.exoplayer import android.os.Build import android.view.SurfaceControl import android.view.SurfaceView +import androidx.annotation.RequiresApi import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import kotlin.math.abs @@ -101,6 +102,7 @@ internal class AssLatencyCalibrator( /** Never called while holding [lock]: applyTransactionToFrame is a binder call and runs on the * GL swap path. The native slot allocation is atomic, so concurrent callers are safe. */ + @RequiresApi(34) private fun attach(surface: SurfaceView, releaseTimeNs: Long, source: Int) { try { val tx = SurfaceControl.Transaction() diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 517853fd..71854514 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.annotation.OptIn +import androidx.annotation.RequiresApi import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -2057,6 +2058,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { .build() } + @RequiresApi(Build.VERSION_CODES.Q) @Suppress("DEPRECATION") private fun isDirectPlaybackSupportedApi29( audioFormat: AudioFormat, diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 55fc218f..6411d12f 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -5,6 +5,7 @@ import android.content.Context import android.graphics.PixelFormat import android.media.AudioAttributes import android.media.ImageReader +import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log @@ -135,9 +136,15 @@ class MpvPlayerCore( Log.d(TAG, "Created MPV placeholder surface") } + @Suppress("DEPRECATION") private fun currentDisplayFpsOverride(): String? { if (audioOnly) return null - val refreshRate = activity.display?.mode?.refreshRate ?: return null + val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.display + } else { + activity.windowManager.defaultDisplay + } + val refreshRate = display?.mode?.refreshRate ?: return null if (refreshRate <= 0f) return null return refreshRate.toString() } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/shared/MediaCodecQuery.kt b/android/app/src/main/kotlin/com/edde746/plezy/shared/MediaCodecQuery.kt index 796a014a..f104a686 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/shared/MediaCodecQuery.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/shared/MediaCodecQuery.kt @@ -24,15 +24,14 @@ internal object MediaCodecQuery { } fun isHardwareAccelerated(info: MediaCodecInfo): Boolean { - val sdkInt = Build.VERSION.SDK_INT val name = info.name - return if (sdkInt >= Build.VERSION_CODES.Q) { - isHardwareAccelerated(sdkInt, info.isHardwareAccelerated, name) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + isHardwareAccelerated(Build.VERSION.SDK_INT, info.isHardwareAccelerated, name) } else { // MediaCodecInfo.isHardwareAccelerated() does not exist before API 29. // Keep the call inside the version gate so older Fire OS releases do not // fail with NoSuchMethodError while initializing playback. - isHardwareAccelerated(sdkInt, false, name) + isHardwareAccelerated(Build.VERSION.SDK_INT, false, name) } } diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 01337e8d..0e23f30d 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -3,8 +3,6 @@ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index c927cb19..339e3091 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -3,8 +3,6 @@ diff --git a/android/gradle.properties b/android/gradle.properties index 475a6280..ec8063fc 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,8 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# All bundled dependencies use AndroidX. Jetifier also cannot process modern +# JVM-only test artifacts when Android lint builds dependency models. +android.enableJetifier=false # This builtInKotlin flag was added automatically by Flutter migrator android.builtInKotlin=false # This newDsl flag was added automatically by Flutter migrator