fix: Android 4.2 兼容性修复 - 降级第三方库 + 移除 ExoPlayer + multidex 配置

1. OkHttp 3.14.9 -> 3.12.0(Android10Platform 引用 Android 10+ API 导致 VerifyError)
2. Gson 2.10.1 -> 2.8.6(ReflectionHelper 引用 Java 9+ API 导致 VerifyError)
3. Retrofit 2.9.0 -> 2.6.0(与客户环境一致,配合 OkHttp 3.12.0)
4. Coroutines 1.7.3 -> 1.6.4(1.7+ 要求 minSdk 21)
5. 移除 ExoPlayer 2.19.1,改用系统原生 MediaPlayer + SurfaceView
6. 主题 MaterialComponents -> AppCompat(避免 MaterialComponentsViewInflater multidex 问题)
7. multidex-config.pro 新增 kotlinx.coroutines.** 和 retrofit2.** 到主 dex
8. 新增 Android 4.2 兼容性改动文档
This commit is contained in:
zhaogd
2026-07-28 19:47:57 +08:00
parent eee8203a68
commit 37738504b9
7 changed files with 275 additions and 75 deletions
+18 -10
View File
@@ -72,23 +72,25 @@ dependencies {
// multidex
implementation 'androidx.multidex:multidex:2.0.1'
// Retrofit + OkHttp(3.12.x 兼容 API 16+)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.12'
// Retrofit 2.6.0 + OkHttp 3.12.0(与客户环境一致,兼容 Android 4.x)
// Retrofit 2.6.0 自然依赖 OkHttp 3.12.0(无 Android10Platform,不会 VerifyError)
// OkHttp 3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
// Gson
implementation 'com.google.code.gson:gson:2.10.1'
// Gson 2.8.6(2.9+ 引入 ReflectionHelper,引用 Java 9+ API,在 Android 4.x 上 VerifyError)
implementation 'com.google.code.gson:gson:2.8.6'
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// Coroutines 1.6.4(1.7+ 要求 minSdk 21,1.6.4 兼容 API 16+)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
// JetsenxSdk + NewTVSDK
implementation files('libs/JetsenxSdk-api-future-release-v1.23.aar')
implementation files('libs/NewTVSDK-v4.0.3-app-lite.jar')
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer:2.19.1'
// 视频播放使用系统原生 MediaPlayer + SurfaceView,无需 ExoPlayer
// commons-codec(SDK 依赖)
implementation 'commons-codec:commons-codec:1.15'
@@ -103,3 +105,9 @@ dependencies {
implementation 'net.jpountz.lz4:lz4:1.3.0'
implementation 'org.slf4j:slf4j-android:1.7.36'
}
// 强制 OkHttp 3.12.0(3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError)
// 其他依赖传递引入 3.12.13,需 force 锁定 3.12.0
configurations.all {
resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0'
}
+25 -3
View File
@@ -1,6 +1,28 @@
# Android 4.x Dalvik multidex 兼容配置
# InitializationProvider 已从 manifest 移除,仅需保留 App 自身启动类
#
# 核心原则:Dalvik multidex 下,只有以下两种场景无法加载 secondary dex 中的类
# 1. Android 框架级反射:LayoutInflater.createView 使用框架 ClassLoader
# 创建 View 时找不到 secondary dex 中的 AppCompat 控件类
# 2. 协程内部类加载:kotlinx.coroutines 内部加载 CoroutineExceptionHandlerImplKt
# 等核心类时,在 Dalvik multidex 下失败
# 注意:已将主题从 MaterialComponents 改为 AppCompat,避免 MaterialComponentsViewInflater
# 的 multidex 类加载问题,同时减少主 dex 方法数(material 包约 20000+ 方法)
# App 自身核心类
-keep class com.pukka.mediaapp.PartnerApp { *; }
-keep class com.pukka.mediaapp.ui.PartnerLaunchActivity { *; }
# === 项目自身所有类 ===
-keep class com.pukka.mediaapp.** { *; }
# === AppCompat 完整包(app + widget)===
# LayoutInflater.createViewFromTag 反射创建:FitWindowsLinearLayout、AppCompatButton 等
# AppCompatDelegateImpl.createSubDecor inflate 内部装饰布局
-keep class androidx.appcompat.app.** { *; }
-keep class androidx.appcompat.widget.** { *; }
# === Kotlin Coroutines ===
# 协程启动时内部加载 CoroutineExceptionHandlerImplKt(无法从 secondary dex 加载)
# 覆盖 flow(StateFlow)、android(Dispatchers)、internal 等
-keep class kotlinx.coroutines.** { *; }
# === Retrofit ===
# KotlinExtensions.await() 被 HttpServiceMethod 在运行时调用,suspend 函数支持
-keep class retrofit2.** { *; }
-1
View File
@@ -9,7 +9,6 @@
<!-- 覆盖依赖库的 minSdk 要求,兼容 Android 4.2(API 17) -->
<uses-sdk tools:overrideLibrary="
com.google.android.exoplayer2,
androidx.core,
androidx.appcompat,
com.google.android.material,
@@ -1,28 +1,27 @@
package com.pukka.mediaapp.ui
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.Util
import com.pukka.mediaapp.databinding.ActivityPlayerBinding
/**
* 视频播放页面 - 使用 ExoPlayer
* 视频播放页面 - 使用系统原生 MediaPlayer + SurfaceView
* 兼容 Android 4.2+,无需第三方播放器库
*/
class VideoPlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityPlayerBinding
private var player: ExoPlayer? = null
private var mediaPlayer: MediaPlayer? = null
private var hasError = false
private var videoUrl: String = ""
private var surfaceReady = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -38,7 +37,7 @@ class VideoPlayerActivity : AppCompatActivity() {
binding = ActivityPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
val videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL).orEmpty()
videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL).orEmpty()
val title = intent.getStringExtra(EXTRA_TITLE).orEmpty()
Log.d(TAG, "onCreate: videoUrl=$videoUrl title=$title")
@@ -50,55 +49,71 @@ class VideoPlayerActivity : AppCompatActivity() {
return
}
setupPlayer(videoUrl)
setupPlayer()
}
private fun setupPlayer(videoUrl: String) {
private fun setupPlayer() {
showLoading(true)
val userAgent = Util.getUserAgent(this, "PartnerTestApp")
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
.setUserAgent(userAgent)
.setConnectTimeoutMs(15000)
.setReadTimeoutMs(30000)
.setAllowCrossProtocolRedirects(true)
val dataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory)
player = ExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.build()
binding.playerView.player = player
binding.playerView.useController = true
binding.playerView.controllerAutoShow = true
binding.playerView.controllerHideOnTouch = true
binding.playerView.controllerShowTimeoutMs = 5000
val mediaItem = MediaItem.fromUri(Uri.parse(videoUrl))
player?.setMediaItem(mediaItem)
player?.prepare()
player?.playWhenReady = true
player?.volume = 1f
player?.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> showLoading(false)
Player.STATE_BUFFERING -> showLoading(true)
Player.STATE_ENDED -> showLoading(false)
Player.STATE_IDLE -> showLoading(false)
}
val surfaceView = binding.surfaceView
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG, "surfaceCreated")
surfaceReady = true
startPlayback()
}
override fun onPlayerError(error: com.google.android.exoplayer2.PlaybackException) {
Log.e(TAG, "onPlayerError", error)
showLoading(false)
showError("播放错误: ${error.message}")
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "surfaceDestroyed")
surfaceReady = false
mediaPlayer?.release()
mediaPlayer = null
}
})
}
private fun startPlayback() {
if (!surfaceReady) return
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(this@VideoPlayerActivity, Uri.parse(videoUrl))
setDisplay(binding.surfaceView.holder)
setOnPreparedListener { mp ->
Log.d(TAG, "onPrepared, duration=${mp.duration}")
showLoading(false)
mp.start()
mp.setVolume(1f, 1f)
}
setOnCompletionListener {
Log.d(TAG, "onCompletion")
showLoading(false)
}
setOnErrorListener { mp, what, extra ->
Log.e(TAG, "onError what=$what extra=$extra")
showLoading(false)
showError("播放错误: $what/$extra")
true
}
setOnBufferingUpdateListener { _, percent ->
Log.d(TAG, "onBufferingUpdate percent=$percent")
}
prepareAsync()
}
} catch (e: Exception) {
Log.e(TAG, "startPlayback failed", e)
showLoading(false)
showError("播放失败: ${e.message}")
}
}
private fun showLoading(show: Boolean) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
@@ -108,28 +123,30 @@ class VideoPlayerActivity : AppCompatActivity() {
showLoading(false)
binding.tvError.text = message
binding.tvError.visibility = View.VISIBLE
binding.playerView.visibility = View.GONE
binding.surfaceView.visibility = View.GONE
}
override fun onResume() {
super.onResume()
if (!hasError) player?.playWhenReady = true
if (surfaceReady && mediaPlayer != null && !hasError) {
mediaPlayer?.start()
}
}
override fun onPause() {
super.onPause()
player?.playWhenReady = false
mediaPlayer?.pause()
}
override fun onStop() {
super.onStop()
player?.stop()
mediaPlayer?.stop()
}
override fun onDestroy() {
super.onDestroy()
player?.release()
player = null
mediaPlayer?.release()
mediaPlayer = null
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
+3 -5
View File
@@ -1,15 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:resize_mode="fit" />
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/progressBar"
+1 -1
View File
@@ -1,5 +1,5 @@
<resources>
<style name="Theme.PartnerTest" parent="Theme.MaterialComponents.NoActionBar">
<style name="Theme.PartnerTest" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">#444466</item>
<item name="colorPrimaryDark">#1A1A2E</item>
<item name="colorAccent">#00FF88</item>