diff --git a/app/src/main/java/com/xyoye/dandanplay/ui/main/MainActivity.kt b/app/src/main/java/com/xyoye/dandanplay/ui/main/MainActivity.kt index 0702ff52d..08e2bc9e5 100644 --- a/app/src/main/java/com/xyoye/dandanplay/ui/main/MainActivity.kt +++ b/app/src/main/java/com/xyoye/dandanplay/ui/main/MainActivity.kt @@ -1,6 +1,8 @@ package com.xyoye.dandanplay.ui.main import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.MutableLiveData @@ -22,6 +24,7 @@ import com.xyoye.dandanplay.BR import com.xyoye.dandanplay.R import com.xyoye.dandanplay.databinding.ActivityMainBinding import com.xyoye.data_component.data.LoginData +import com.xyoye.user_component.ui.weight.DeveloperMenus import kotlin.random.Random import kotlin.system.exitProcess @@ -47,6 +50,9 @@ class MainActivity : BaseActivity(), private var fragmentTag = "" private var touchTime = 0L + // 标题栏菜单管理器 + private lateinit var mMenus: DeveloperMenus + override fun initViewModel() = ViewModelInit( BR.viewModel, @@ -86,10 +92,12 @@ class MainActivity : BaseActivity(), title = "弹弹play" switchFragment(TAG_FRAGMENT_HOME) } + R.id.navigation_media -> { title = "媒体库" switchFragment(TAG_FRAGMENT_MEDIA) } + R.id.navigation_personal -> { title = "个人中心" switchFragment(TAG_FRAGMENT_PERSONAL) @@ -123,6 +131,16 @@ class MainActivity : BaseActivity(), return super.onKeyDown(keyCode, event) } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + mMenus = DeveloperMenus.inflater(this, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + mMenus.onOptionsItemSelected(item) + return super.onOptionsItemSelected(item) + } + override fun getLoginLiveData(): MutableLiveData { return viewModel.reLoginLiveData } @@ -158,6 +176,7 @@ class MainActivity : BaseActivity(), fragmentTag = tag } } + TAG_FRAGMENT_MEDIA -> { val fragment = supportFragmentManager.findFragmentByTag(TAG_FRAGMENT_MEDIA) if (fragment == null) { @@ -174,6 +193,7 @@ class MainActivity : BaseActivity(), fragmentTag = tag } } + TAG_FRAGMENT_PERSONAL -> { val fragment = supportFragmentManager.findFragmentByTag(TAG_FRAGMENT_PERSONAL) if (fragment == null) { @@ -190,6 +210,7 @@ class MainActivity : BaseActivity(), fragmentTag = tag } } + else -> { throw RuntimeException("no match fragment") } diff --git a/common_component/libs/arm64-v8a/libsecurity.so b/common_component/libs/arm64-v8a/libsecurity.so index 56b34fc37..87b5b601d 100755 Binary files a/common_component/libs/arm64-v8a/libsecurity.so and b/common_component/libs/arm64-v8a/libsecurity.so differ diff --git a/common_component/libs/armeabi-v7a/libsecurity.so b/common_component/libs/armeabi-v7a/libsecurity.so index 86139480a..dd1278d51 100755 Binary files a/common_component/libs/armeabi-v7a/libsecurity.so and b/common_component/libs/armeabi-v7a/libsecurity.so differ diff --git a/common_component/src/main/java/com/xyoye/common_component/adapter/BaseAdapter.kt b/common_component/src/main/java/com/xyoye/common_component/adapter/BaseAdapter.kt index 90dc6568b..ba4544b6e 100644 --- a/common_component/src/main/java/com/xyoye/common_component/adapter/BaseAdapter.kt +++ b/common_component/src/main/java/com/xyoye/common_component/adapter/BaseAdapter.kt @@ -106,7 +106,13 @@ class BaseAdapter : AnimatedAdapter() { super.setData(data) if (diffCreator != null) { - setDiffData(data, diffCreator!!) + // [Bugly] #2529539 + // TODO: 临时的解决方案,需要复现与排查 + try { + setDiffData(data, diffCreator!!) + } catch (e: Exception) { + setNotifyData(data) + } } else { setNotifyData(data) } diff --git a/common_component/src/main/java/com/xyoye/common_component/config/DevelopConfigTable.kt b/common_component/src/main/java/com/xyoye/common_component/config/DevelopConfigTable.kt new file mode 100644 index 000000000..a663744aa --- /dev/null +++ b/common_component/src/main/java/com/xyoye/common_component/config/DevelopConfigTable.kt @@ -0,0 +1,26 @@ +package com.xyoye.common_component.config + +import com.xyoye.mmkv_annotation.MMKVFiled +import com.xyoye.mmkv_annotation.MMKVKotlinClass + +/** + * author: xyoye1997@outlook.com + * time : 2025/1/22 + * desc : 开发者配置表 + */ + +@MMKVKotlinClass(className = "DevelopConfig") +object DevelopConfigTable { + + // AppId + @MMKVFiled + const val appId = "" + + // App Secret + @MMKVFiled + const val appSecret = "" + + // 是否已自动显示认证弹窗 + @MMKVFiled + const val isAutoShowAuthDialog = false +} \ No newline at end of file diff --git a/common_component/src/main/java/com/xyoye/common_component/network/Retrofit.kt b/common_component/src/main/java/com/xyoye/common_component/network/Retrofit.kt index 16f796c33..f2f74796b 100644 --- a/common_component/src/main/java/com/xyoye/common_component/network/Retrofit.kt +++ b/common_component/src/main/java/com/xyoye/common_component/network/Retrofit.kt @@ -5,8 +5,10 @@ import com.xyoye.common_component.network.helper.AgentInterceptor import com.xyoye.common_component.network.helper.AuthInterceptor import com.xyoye.common_component.network.helper.BackupDomainInterceptor import com.xyoye.common_component.network.helper.DecompressInterceptor +import com.xyoye.common_component.network.helper.DeveloperCertificateInterceptor import com.xyoye.common_component.network.helper.DynamicBaseUrlInterceptor import com.xyoye.common_component.network.helper.LoggerInterceptor +import com.xyoye.common_component.network.helper.SignatureInterceptor import com.xyoye.common_component.network.service.AlistService import com.xyoye.common_component.network.service.DanDanService import com.xyoye.common_component.network.service.ExtendedService @@ -43,6 +45,8 @@ class Retrofit private constructor() { .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(4, TimeUnit.SECONDS) .hostnameVerifier { _, _ -> true } + .addInterceptor(SignatureInterceptor()) + .addInterceptor(DeveloperCertificateInterceptor()) .addInterceptor(AgentInterceptor()) .addInterceptor(AuthInterceptor()) .addInterceptor(DecompressInterceptor()) diff --git a/common_component/src/main/java/com/xyoye/common_component/network/helper/DeveloperCertificateInterceptor.kt b/common_component/src/main/java/com/xyoye/common_component/network/helper/DeveloperCertificateInterceptor.kt new file mode 100644 index 000000000..b15846cf9 --- /dev/null +++ b/common_component/src/main/java/com/xyoye/common_component/network/helper/DeveloperCertificateInterceptor.kt @@ -0,0 +1,49 @@ +package com.xyoye.common_component.network.helper + +import com.xyoye.common_component.config.DevelopConfig +import com.xyoye.common_component.utils.SecurityHelper +import okhttp3.Interceptor +import okhttp3.Response + +/** + * author: xyoye1997@outlook.com + * time : 2025/1/22 + * desc : 开发者凭证拦截器 + */ +class DeveloperCertificateInterceptor : Interceptor { + companion object { + const val HEADER_APP_ID = "X-AppId" + const val HEADER_APP_SECRET = "X-AppSecret" + } + + + override fun intercept(chain: Interceptor.Chain): Response { + val oldRequest = chain.request() + + // 官方应用,不做处理 + if (SecurityHelper.getInstance().isOfficialApplication) { + return chain.proceed(oldRequest) + } + + // 请求自带凭证,不做处理 + val requestAppId = oldRequest.header(HEADER_APP_ID) + val requestAppSecret = oldRequest.header(HEADER_APP_SECRET) + if (requestAppId?.isNotEmpty() == true && requestAppSecret?.isNotEmpty() == true) { + return chain.proceed(oldRequest) + } + + // 未配置凭证,不做处理 + val appId = DevelopConfig.getAppId() + val appSecret = DevelopConfig.getAppSecret() + if (appId.isNullOrEmpty() || appSecret.isNullOrEmpty()) { + return chain.proceed(oldRequest) + } + + // 添加凭证 + return chain.proceed( + oldRequest.newBuilder() + .header(HEADER_APP_ID, appId) + .header(HEADER_APP_SECRET, appSecret).build() + ) + } +} \ No newline at end of file diff --git a/common_component/src/main/java/com/xyoye/common_component/network/helper/SignatureInterceptor.kt b/common_component/src/main/java/com/xyoye/common_component/network/helper/SignatureInterceptor.kt new file mode 100644 index 000000000..16acf5487 --- /dev/null +++ b/common_component/src/main/java/com/xyoye/common_component/network/helper/SignatureInterceptor.kt @@ -0,0 +1,29 @@ +package com.xyoye.common_component.network.helper + +import com.xyoye.common_component.base.app.BaseApplication +import com.xyoye.common_component.utils.SecurityHelper +import okhttp3.Interceptor +import okhttp3.Response + + +/** + * author: xyoye1997@outlook.com + * time : 2025/1/21 + * desc : 签名验证拦截器 + */ +class SignatureInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val oldRequest = chain.request() + val newRequest = oldRequest.newBuilder() + + SecurityHelper.getInstance().getSignatureMap( + oldRequest.url.encodedPath, + BaseApplication.getAppContext() + ).forEach { + newRequest.addHeader(it.key, it.value ?: "") + } + + return chain.proceed(newRequest.build()) + } +} \ No newline at end of file diff --git a/common_component/src/main/java/com/xyoye/common_component/network/repository/UserRepository.kt b/common_component/src/main/java/com/xyoye/common_component/network/repository/UserRepository.kt index 1e9361f4a..f23648299 100644 --- a/common_component/src/main/java/com/xyoye/common_component/network/repository/UserRepository.kt +++ b/common_component/src/main/java/com/xyoye/common_component/network/repository/UserRepository.kt @@ -113,4 +113,12 @@ object UserRepository : BaseRepository() { .doPost { Retrofit.danDanService.updatePassword(it) } + + /** + * 校验凭证 + */ + suspend fun checkAuthenticate(appId: String, appSecret: String) = request() + .doGet { + Retrofit.danDanService.checkAuthenticate(appId, appSecret, 1) + } } \ No newline at end of file diff --git a/common_component/src/main/java/com/xyoye/common_component/network/service/DanDanService.kt b/common_component/src/main/java/com/xyoye/common_component/network/service/DanDanService.kt index 9fa662318..45323e7fa 100644 --- a/common_component/src/main/java/com/xyoye/common_component/network/service/DanDanService.kt +++ b/common_component/src/main/java/com/xyoye/common_component/network/service/DanDanService.kt @@ -1,5 +1,6 @@ package com.xyoye.common_component.network.service +import com.xyoye.common_component.network.helper.DeveloperCertificateInterceptor import com.xyoye.common_component.network.request.RequestParams import com.xyoye.data_component.data.AnimeDetailData import com.xyoye.data_component.data.AnimeTagData @@ -17,9 +18,11 @@ import com.xyoye.data_component.data.SearchAnimeData import com.xyoye.data_component.data.SendDanmuData import okhttp3.RequestBody import okhttp3.ResponseBody +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Path @@ -116,4 +119,11 @@ interface DanDanService { @POST("/api/v2/playhistory") suspend fun addPlayHistory(@Body body: RequestBody): CommonJsonData + + @GET("api/v2/login/renew") + suspend fun checkAuthenticate( + @Header(DeveloperCertificateInterceptor.HEADER_APP_ID) appId: String, + @Header(DeveloperCertificateInterceptor.HEADER_APP_SECRET) appSecret: String, + @Header("X-Auth") authMode: Int + ): Response } \ No newline at end of file diff --git a/common_component/src/main/java/com/xyoye/common_component/utils/SecurityHelper.java b/common_component/src/main/java/com/xyoye/common_component/utils/SecurityHelper.java index 793fc5b0a..bc6f0333b 100644 --- a/common_component/src/main/java/com/xyoye/common_component/utils/SecurityHelper.java +++ b/common_component/src/main/java/com/xyoye/common_component/utils/SecurityHelper.java @@ -4,6 +4,9 @@ import com.xyoye.common_component.base.app.BaseApplication; +import java.util.HashMap; +import java.util.Map; + /** * Created by xyoye on 2021/1/6. */ @@ -52,7 +55,32 @@ public Boolean isOfficialApplication() { return !ERROR_RESULT.equals(getAppId()); } + public Map getSignatureMap(String path, Context context) { + Object signature = getSignature(path, context); + if (signature == null) { + return null; + } + + if (signature instanceof Map) { + HashMap map = new HashMap<>(); + for (Map.Entry entry : ((Map) signature).entrySet()) { + Object key = entry.getKey(); + if (key instanceof String) { + Object value = entry.getValue(); + if (value instanceof String) { + map.put((String) key, (String) value); + } + } + } + return map; + } + + return null; + } + private static native String getKey(int position, Context context); private static native String buildHash(String hashInfo, Context context); + + private static native Object getSignature(String path, Context context); } diff --git a/user_component/src/main/java/com/xyoye/user_component/ui/dialog/DeveloperAuthenticateDialog.kt b/user_component/src/main/java/com/xyoye/user_component/ui/dialog/DeveloperAuthenticateDialog.kt new file mode 100644 index 000000000..43f58329e --- /dev/null +++ b/user_component/src/main/java/com/xyoye/user_component/ui/dialog/DeveloperAuthenticateDialog.kt @@ -0,0 +1,110 @@ +package com.xyoye.user_component.ui.dialog + +import android.app.Activity +import com.xyoye.common_component.base.BaseActivity +import com.xyoye.common_component.config.DevelopConfig +import com.xyoye.common_component.extension.startUrlActivity +import com.xyoye.common_component.network.repository.UserRepository +import com.xyoye.common_component.utils.SupervisorScope +import com.xyoye.common_component.weight.ToastCenter +import com.xyoye.common_component.weight.dialog.BaseBottomDialog +import com.xyoye.user_component.R +import com.xyoye.user_component.databinding.DialogDeveloperAuthenticateBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * author: xyoye1997@outlook.com + * time : 2025/1/22 + * desc : + */ + +class DeveloperAuthenticateDialog( + private val activity: Activity, + private val onAuthenticate: () -> Unit +) : BaseBottomDialog(activity) { + + private lateinit var binding: DialogDeveloperAuthenticateBinding + + override fun getChildLayoutId(): Int { + return R.layout.dialog_developer_authenticate + } + + override fun initView(binding: DialogDeveloperAuthenticateBinding) { + this.binding = binding + + setTitle("开发者认证") + + binding.inputAppId.setText(DevelopConfig.getAppId()) + binding.inputAppSecret.setText(DevelopConfig.getAppSecret()) + + setNegativeText("忽略") + setNegativeListener { + dismiss() + } + + setPositiveListener { + checkAuthenticate() + } + + binding.tvReadDocument.setOnClickListener { + activity.startUrlActivity("https://doc.dandanplay.com/open/") + } + } + + /** + * 开发者认证 + */ + private fun checkAuthenticate() { + val appId = binding.inputAppId.text.toString() + val appSecret = binding.inputAppSecret.text.toString() + + if (appId.isEmpty() || appSecret.isEmpty()) { + ToastCenter.showWarning("请输入AppId和AppSecret") + return + } + + SupervisorScope.IO.launch { + loading(true) + val result = UserRepository.checkAuthenticate(appId, appSecret).getOrNull() + loading(false) + if (result != null && result.code() == 200) { + authenticateSuccess(appId, appSecret) + return@launch + } + + if (result?.code() == 403) { + ToastCenter.showError("认证失败,凭证不被允许访问") + } else { + ToastCenter.showError("认证过程中发生意外,请稍后重试") + } + } + } + + /** + * 显示/隐藏加载框 + */ + private suspend fun loading(show: Boolean) { + if (activity is BaseActivity<*, *>) { + withContext(Dispatchers.Main) { + if (show) { + activity.showLoading() + } else { + activity.hideLoading() + } + } + } + } + + /** + * 认证成功 + */ + private fun authenticateSuccess(appId: String, appSecret: String) { + DevelopConfig.putAppId(appId) + DevelopConfig.putAppSecret(appSecret) + ToastCenter.showSuccess("认证成功") + onAuthenticate.invoke() + dismiss() + } +} \ No newline at end of file diff --git a/user_component/src/main/java/com/xyoye/user_component/ui/weight/DeveloperMenus.kt b/user_component/src/main/java/com/xyoye/user_component/ui/weight/DeveloperMenus.kt new file mode 100644 index 000000000..843648db1 --- /dev/null +++ b/user_component/src/main/java/com/xyoye/user_component/ui/weight/DeveloperMenus.kt @@ -0,0 +1,111 @@ +package com.xyoye.user_component.ui.weight + +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import com.xyoye.common_component.config.DevelopConfig +import com.xyoye.common_component.extension.toResDrawable +import com.xyoye.common_component.utils.SecurityHelper +import com.xyoye.common_component.utils.SupervisorScope +import com.xyoye.user_component.R +import com.xyoye.user_component.ui.dialog.DeveloperAuthenticateDialog +import kotlinx.coroutines.launch + +/** + * author: xyoye1997@outlook.com + * time : 2025/1/22 + * desc : + */ +class DeveloperMenus private constructor( + private val activity: AppCompatActivity, + menu: Menu +) { + + companion object { + fun inflater(activity: AppCompatActivity, menu: Menu): DeveloperMenus { + activity.menuInflater.inflate(R.menu.menu_developer, menu) + return DeveloperMenus(activity, menu) + } + } + + // 菜单项 + private val item = menu.findItem(R.id.item_source_authenticate) + + // 验证弹窗 + private var authenticateDialog: DeveloperAuthenticateDialog? = null + + private val isDeveloperAuthenticate: Boolean + get() = DevelopConfig.getAppId()?.isNotEmpty() == true + && DevelopConfig.getAppSecret()?.isNotEmpty() == true + + init { + updateItem() + + // 考虑自动显示认证弹窗 + SupervisorScope.Main.launch { considerShowAuthenticateDialog() } + } + + fun onOptionsItemSelected(item: MenuItem) { + if (item.itemId == R.id.item_source_authenticate) { + showAuthenticateDialog() + return + } + } + + /** + * 显示认证弹窗 + */ + private fun showAuthenticateDialog() { + authenticateDialog?.dismiss() + authenticateDialog = DeveloperAuthenticateDialog(activity) { + SupervisorScope.Main.launch { updateItem() } + } + authenticateDialog?.show() + } + + /** + * 考虑显示认证弹窗 + */ + private fun considerShowAuthenticateDialog() { + // 官方应用,不做处理 + if (SecurityHelper.getInstance().isOfficialApplication) { + return + } + + // 已认证,不做处理 + if (isDeveloperAuthenticate) { + return + } + + // 已自动提示认证弹窗 + if (DevelopConfig.isIsAutoShowAuthDialog()) { + return + } + + // 只自动提示一次 + DevelopConfig.putIsAutoShowAuthDialog(true) + + // 显示认证弹窗 + showAuthenticateDialog() + } + + /** + * 更新菜单项 + */ + private fun updateItem() { + if (SecurityHelper.getInstance().isOfficialApplication) { + item.isVisible = false + return + } + item.isVisible = true + + val (title, iconRes) = if (isDeveloperAuthenticate) { + "已认证" to R.drawable.ic_developer_authenticated + } else { + "未认证" to R.drawable.ic_developer_unauthenticated + } + + item.title = title + item.icon = iconRes.toResDrawable(activity) + } +} \ No newline at end of file diff --git a/user_component/src/main/res/drawable/ic_developer_authenticated.xml b/user_component/src/main/res/drawable/ic_developer_authenticated.xml new file mode 100644 index 000000000..d9b64e4c4 --- /dev/null +++ b/user_component/src/main/res/drawable/ic_developer_authenticated.xml @@ -0,0 +1,9 @@ + + + diff --git a/user_component/src/main/res/drawable/ic_developer_unauthenticated.xml b/user_component/src/main/res/drawable/ic_developer_unauthenticated.xml new file mode 100644 index 000000000..eba9e257f --- /dev/null +++ b/user_component/src/main/res/drawable/ic_developer_unauthenticated.xml @@ -0,0 +1,9 @@ + + + diff --git a/user_component/src/main/res/layout/dialog_developer_authenticate.xml b/user_component/src/main/res/layout/dialog_developer_authenticate.xml new file mode 100644 index 000000000..4a8048bc5 --- /dev/null +++ b/user_component/src/main/res/layout/dialog_developer_authenticate.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/user_component/src/main/res/menu/menu_developer.xml b/user_component/src/main/res/menu/menu_developer.xml new file mode 100644 index 000000000..d69bedf98 --- /dev/null +++ b/user_component/src/main/res/menu/menu_developer.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/user_component/src/main/res/values/strings.xml b/user_component/src/main/res/values/strings.xml index ac803990d..fccfb2b31 100644 --- a/user_component/src/main/res/values/strings.xml +++ b/user_component/src/main/res/values/strings.xml @@ -77,6 +77,12 @@ Tips:扩展名应以,分隔,如:mp4,rmvb,mkv 重置为默认支持的扩展名 + App Id + App Secret + 应弹弹play开放平台要求,请开发者完善AppId与AppSecret,没有相关凭证的应用在使用弹弹play API相关功能时可能受到限制,如番剧列表、弹幕匹配、用户信息等。 + * 此配置仅用于弹弹play API相关功能,不影响其他本地功能的使用,媒体库视频播放、弹/字幕文件加载仍可正常使用 + 详细信息请查看弹弹play开放平台文档 + @drawable/ic_bailitusu @drawable/ic_baoqingtian