feat: Android 4.2 兼容改造
- minSdk 19 -> 17(config.gradle) - OkHttp 4.12 -> 3.12.0(app build.gradle 新老双栈统一锁定) - Retrofit 2.11 -> 2.6.0 + Coroutines 1.8.1 -> 1.6.4 - SignRequestPolicy: toHttpUrlOrNull -> HttpUrl.parse(3.12 API 适配) - HeaderInterceptor/SignAuthenticator: 属性访问改方法调用(url()/request()/priorResponse()) - VideoViewTv: SDK_INT<19 强制切 AndroidMediaPlayer 内核 + instanceof 短路保护 - AndroidManifest: overrideLibrary OAID 4.1.3(minSdk 19) - 全局 resolutionStrategy.force 锁定 OkHttp/Retrofit 版本
This commit is contained in:
@@ -264,7 +264,8 @@ dependencies {
|
|||||||
|
|
||||||
// implementation 'com.sensorsdata.analytics.android:SensorsAnalyticsSDK:6.8.3'
|
// implementation 'com.sensorsdata.analytics.android:SensorsAnalyticsSDK:6.8.3'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.12.13'
|
// 3.12.0:3.12.1+(含 3.12.13)引入 Android10Platform 触发 Dalvik VerifyError,必须精确锁 3.12.0
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
implementation 'com.google.code.gson:gson:2.8.9'
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.1.3'
|
implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.1.3'
|
||||||
implementation('io.github.volcengine-tls:tls-android-producer:2.0.2-api16') {
|
implementation('io.github.volcengine-tls:tls-android-producer:2.0.2-api16') {
|
||||||
@@ -278,22 +279,28 @@ dependencies {
|
|||||||
implementation 'com.google.guava:guava:31.1-android'
|
implementation 'com.google.guava:guava:31.1-android'
|
||||||
|
|
||||||
// ===== 网络层(脚手架新引入,与既有 HttpUtils 双栈共存) =====
|
// ===== 网络层(脚手架新引入,与既有 HttpUtils 双栈共存) =====
|
||||||
// 为什么显式指定版本:app 模块此前未声明 retrofit/okhttp/moshi/coroutines,避免被根 config.gradle 隐式拉入不同版本。
|
// ===== Android 4.2 兼容(对齐 mediaapp 已验证组合,见 Android4.2兼容性改动文档)=====
|
||||||
// Kotlin 1.9.22 -> KSP 1.9.22-1.0.17 已对齐。
|
// - OkHttp 4.x 官方要求 API 21+;3.13+ 含 Android10Platform(API 10 SSL API)触发 Dalvik VerifyError。
|
||||||
// 为什么用 `platform(enforcedPlatform)` 而非 `implementation`:okhttp-bom 是 Bill-of-Materials,本身仅提供版本对齐,
|
// 3.12.0 是支持 API 9-20 的长期分支(与 libcore/libdata 老栈版本一致),配合下方 force 全局锁定。
|
||||||
// 不能被当作运行时产物拉到 runtime classpath(这正是上一个编译报错的根因)。
|
// - Retrofit 2.6.0:suspend 函数 2.6.0 起支持;2.9+ 传递依赖 OkHttp 3.14.9(含 Android10Platform)。
|
||||||
implementation platform('com.squareup.okhttp3:okhttp-bom:4.12.0')
|
// - Coroutines 1.6.4:1.7+ 要求 minSdk 21,1.6.4 兼容 API 16+。
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
implementation 'com.squareup.retrofit2:converter-moshi:2.11.0'
|
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp'
|
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor'
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
|
||||||
implementation 'com.squareup.moshi:moshi:1.15.1'
|
implementation 'com.squareup.moshi:moshi:1.15.1'
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
|
implementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
|
||||||
ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
|
ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||||
|
|
||||||
|
// 防止其他依赖(tls-producer/libdata 等)传递引入更高版本 OkHttp/Retrofit 破坏 4.2 兼容
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
|
resolutionStrategy.force 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
|
}
|
||||||
|
|
||||||
implementation 'net.jpountz.lz4:lz4:1.3.0'
|
implementation 'net.jpountz.lz4:lz4:1.3.0'
|
||||||
implementation 'org.slf4j:slf4j-android:1.7.36'
|
implementation 'org.slf4j:slf4j-android:1.7.36'
|
||||||
//腾讯buggly
|
//腾讯buggly
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
package="com.pukka.jiecheng"
|
package="com.pukka.jiecheng"
|
||||||
tools:ignore="MissingLeanbackLauncher">
|
tools:ignore="MissingLeanbackLauncher">
|
||||||
|
|
||||||
|
<!-- Android 4.2 兼容(minSdk 17):覆盖 minSdk>17 的依赖库声明,运行时兼容性靠
|
||||||
|
降级依赖版本 + 不触碰高版本 API 路径保证。清单随探测编译逐步补充。 -->
|
||||||
|
<uses-sdk tools:overrideLibrary="com.github.gzuliyujiang.oaid" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.telephony"
|
android:name="android.hardware.telephony"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class HeaderInterceptor : Interceptor {
|
|||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val original = chain.request()
|
val original = chain.request()
|
||||||
val path = SignRequestPolicy.relativePath(original.url.toString())
|
// OkHttp 3.12 兼容:Java 版字段访问需用方法调用(url()),4.x 的 Kotlin val 语法不可用
|
||||||
|
val path = SignRequestPolicy.relativePath(original.url().toString())
|
||||||
|
|
||||||
val builder = original.newBuilder()
|
val builder = original.newBuilder()
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ class SignAuthenticator : Authenticator {
|
|||||||
private var credentialBroken: Boolean = false
|
private var credentialBroken: Boolean = false
|
||||||
|
|
||||||
override fun authenticate(route: Route?, response: Response): Request? {
|
override fun authenticate(route: Route?, response: Response): Request? {
|
||||||
val path = SignRequestPolicy.relativePath(response.request.url.toString())
|
// OkHttp 3.12 兼容:Java 版 request()/priorResponse() 方法调用(4.x Kotlin val 语法不可用)
|
||||||
|
val path = SignRequestPolicy.relativePath(response.request().url().toString())
|
||||||
|
|
||||||
// 保护 1:首签/刷新接口 401 直接放行
|
// 保护 1:首签/刷新接口 401 直接放行
|
||||||
if (!SignRequestPolicy.shouldTriggerRefresh(path)) {
|
if (!SignRequestPolicy.shouldTriggerRefresh(path)) {
|
||||||
@@ -63,12 +64,12 @@ class SignAuthenticator : Authenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保护 2:刷新调用本身带 X-Refresh-Call=1 标记,Authenticator 见到放行
|
// 保护 2:刷新调用本身带 X-Refresh-Call=1 标记,Authenticator 见到放行
|
||||||
if (response.request.header(HEADER_REFRESH_CALL) == "1") {
|
if (response.request().header(HEADER_REFRESH_CALL) == "1") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保护 3:priorResponse 链上已有 X-Refresh-Marker,说明已刷新过一次仍 401,放弃
|
// 保护 3:priorResponse 链上已有 X-Refresh-Marker,说明已刷新过一次仍 401,放弃
|
||||||
val prior = response.priorResponse
|
val prior = response.priorResponse()
|
||||||
if (prior != null && prior.header(HEADER_REFRESH_MARKER) != null) {
|
if (prior != null && prior.header(HEADER_REFRESH_MARKER) != null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ class SignAuthenticator : Authenticator {
|
|||||||
Network.tokenStore.refreshAndPersist(newSign)
|
Network.tokenStore.refreshAndPersist(newSign)
|
||||||
|
|
||||||
// 重建原 Request:保留所有原有 headers,注入新 sign 头 + X-Refresh-Marker 标记
|
// 重建原 Request:保留所有原有 headers,注入新 sign 头 + X-Refresh-Marker 标记
|
||||||
response.request.newBuilder()
|
response.request().newBuilder()
|
||||||
.header("sign", newSign)
|
.header("sign", newSign)
|
||||||
.header(HEADER_REFRESH_MARKER, "1")
|
.header(HEADER_REFRESH_MARKER, "1")
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.pukka.jiecheng.network.auth
|
package com.pukka.jiecheng.network.auth
|
||||||
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sign 头注入 / 401 刷新触发策略。
|
* sign 头注入 / 401 刷新触发策略。
|
||||||
@@ -36,11 +36,14 @@ object SignRequestPolicy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取相对 baseUrl 的 path(去掉 query/fragment),用于上述匹配。
|
* 提取相对 baseUrl 的 path(去掉 query/fragment),用于上述匹配。
|
||||||
|
*
|
||||||
|
* 注意:HttpUrl.parse 是 OkHttp 3.12 的 API(4.x 才有 toHttpUrlOrNull 扩展),
|
||||||
|
* 项目已为 Android 4.2 兼容全局锁定 OkHttp 3.12.0,勿改回扩展函数。
|
||||||
*/
|
*/
|
||||||
fun relativePath(absoluteUrl: String): String {
|
fun relativePath(absoluteUrl: String): String {
|
||||||
val url = absoluteUrl.toHttpUrlOrNull() ?: return absoluteUrl
|
val url = HttpUrl.parse(absoluteUrl) ?: return absoluteUrl
|
||||||
// pathSegments.joinToString("/") 自动处理首尾斜杠
|
// pathSegments.joinToString("/") 自动处理首尾斜杠
|
||||||
return url.encodedPath.trimStart('/').let {
|
return url.encodedPath().trimStart('/').let {
|
||||||
// 去掉 /api/ 前缀,匹配 Set<String> 中的相对路径
|
// 去掉 /api/ 前缀,匹配 Set<String> 中的相对路径
|
||||||
if (it.startsWith("api/")) it.removePrefix("api/") else it
|
if (it.startsWith("api/")) it.removePrefix("api/") else it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
app_android = [
|
app_android = [
|
||||||
compileSdkVersion: 34,
|
compileSdkVersion: 34,
|
||||||
minSdkVersion : 19,
|
minSdkVersion : 17, // Android 4.2 兼容(探测编译,待验证)
|
||||||
targetSdkVersion : 29,
|
targetSdkVersion : 29,
|
||||||
multiDexEnabled : true
|
multiDexEnabled : true
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -328,6 +328,11 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initVideoView() {
|
private void initVideoView() {
|
||||||
|
// Android 4.2 兼容:ExoPlayer 2.19 官方支持 API 19+,低版本强制切系统 MediaPlayer 内核,
|
||||||
|
// 避免 PkExoMediaPlayer/ExoPlayer 类在 Dalvik 上触发 VerifyError
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
terminalType = 0;
|
||||||
|
}
|
||||||
if (!checkAudioCapability()){
|
if (!checkAudioCapability()){
|
||||||
terminalType = 0;
|
terminalType = 0;
|
||||||
}
|
}
|
||||||
@@ -562,7 +567,9 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
|
|||||||
reset();
|
reset();
|
||||||
mMediaPlayer.release();
|
mMediaPlayer.release();
|
||||||
if(mMediaPlayer != null){
|
if(mMediaPlayer != null){
|
||||||
if(mMediaPlayer instanceof PkExoMediaPlayer){
|
// terminalType 短路:非 Exo 内核时跳过 instanceof,避免 Dalvik 解析
|
||||||
|
// PkExoMediaPlayer 引发的 ExoPlayer 类加载 VerifyError(Android 4.2)
|
||||||
|
if(terminalType == 2 && mMediaPlayer instanceof PkExoMediaPlayer){
|
||||||
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
|
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
|
||||||
pkExoMediaPlayer.resetListeners();
|
pkExoMediaPlayer.resetListeners();
|
||||||
}
|
}
|
||||||
@@ -1491,7 +1498,8 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
|
|||||||
onPauseListener = null;
|
onPauseListener = null;
|
||||||
onResumeListener = null;
|
onResumeListener = null;
|
||||||
if(mMediaPlayer != null){
|
if(mMediaPlayer != null){
|
||||||
if(mMediaPlayer instanceof PkExoMediaPlayer){
|
// terminalType 短路:同上,避免非 Exo 内核时 Dalvik 触发 PkExoMediaPlayer 类解析
|
||||||
|
if(terminalType == 2 && mMediaPlayer instanceof PkExoMediaPlayer){
|
||||||
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
|
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
|
||||||
pkExoMediaPlayer.resetListeners();
|
pkExoMediaPlayer.resetListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user