From 3d7cafd8c12f9a8fb40c6f7601d5e8040d32d02f Mon Sep 17 00:00:00 2001 From: Madray Haven Date: Tue, 20 Aug 2024 14:06:48 +0800 Subject: [PATCH] fix: login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 转为扫码登陆 --- .gitignore | 1 + app/build.gradle.kts | 47 ++-- app/src/main/AndroidManifest.xml | 12 + app/src/main/assets/js/ajax_interception.js | 13 +- .../bilidownload/app/activity/LoginPwd.kt | 47 ++-- .../bilidownload/app/activity/LoginQrcode.kt | 215 +++++++++++++++++ .../bilidownload/app/activity/LoginSms.kt | 224 ++++++++++++++++++ .../app/activity/LoginValidateAccount.kt | 18 +- .../bilidownload/app/activity/Welcome.kt | 2 +- .../app/ui/list/CountrySpinnerAdapter.kt | 44 ++++ .../bilidownload/app/viewmodel/FollowModel.kt | 4 +- .../bilidownload/app/viewmodel/HomeModel.kt | 8 +- .../app/viewmodel/LoginPwdModel.kt | 25 +- .../app/viewmodel/LoginQrcodeModel.kt | 119 ++++++++++ .../app/viewmodel/LoginSmsModel.kt | 167 +++++++++++++ .../app/viewmodel/OnlinePlayerModel.kt | 8 +- .../bilidownload/base/app/BaseViewModel.kt | 4 +- .../core/exsp/TokenPreference.java | 6 + .../core/forest/client/PassportClient.kt | 70 ++++-- .../core/forest/data/CaptchaResp.java | 2 - .../core/forest/data/CountryResp.java | 22 ++ .../core/forest/data/LoginResp.java | 24 +- .../core/forest/data/QrcodePollResp.java | 15 ++ .../core/forest/data/QrcodeResp.java | 15 ++ .../core/forest/data/SmsSendResp.java | 14 ++ .../core/forest/data/common/CookieInfo.java | 17 ++ .../core/forest/data/common/TokenInfo.java | 11 + .../sgpublic/bilidownload/core/util/_Bili.kt | 23 ++ .../sgpublic/bilidownload/core/util/_File.kt | 16 +- .../bilidownload/core/util/_Network.kt | 10 +- .../sgpublic/bilidownload/core/util/_Url.kt | 2 - .../main/res/layout/activity_login_qrcode.xml | 45 ++-- .../main/res/layout/activity_login_sms.xml | 118 +++++++++ app/src/main/res/layout/item_country_list.xml | 11 + .../main/res/layout/item_country_selected.xml | 11 + app/src/main/res/values/strings.xml | 12 + build.gradle.kts | 10 +- .../kotlin/io/github/sgpublic/gradle/Dep.kt | 8 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 3 + 41 files changed, 1258 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginQrcode.kt create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginSms.kt create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/app/ui/list/CountrySpinnerAdapter.kt create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginQrcodeModel.kt create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginSmsModel.kt create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CountryResp.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodePollResp.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodeResp.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/SmsSendResp.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/CookieInfo.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/TokenInfo.java create mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Bili.kt delete mode 100644 app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Url.kt create mode 100644 app/src/main/res/layout/activity_login_sms.xml create mode 100644 app/src/main/res/layout/item_country_list.xml create mode 100644 app/src/main/res/layout/item_country_selected.xml diff --git a/.gitignore b/.gitignore index 49f583a..6d8f764 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ local.properties version.properties buildSrc/build/ +.kotlin/ \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58b9cd4..9d804ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,8 +32,7 @@ fun VariantDimension.buildConfigField(name: String, value: Int) { } android { - compileSdk = 33 - buildToolsVersion = "33.0.0" + compileSdk = 34 namespace = "io.github.sgpublic.bilidownload" val properties = file("./sign/sign.properties") @@ -75,8 +74,8 @@ android { defaultConfig { applicationId = "io.github.sgpublic.bilidownload" - minSdk = 26 - targetSdk = 33 + minSdk = 29 + targetSdk = 34 versionCode = VersionGen.COMMIT_VERSION versionName = "3.5.0".also { buildConfigField("ORIGIN_VERSION_NAME", it) @@ -147,7 +146,11 @@ android { kotlinOptions { jvmTarget = "11" } - packagingOptions { + buildFeatures { + buildConfig = true + viewBinding = true + } + packaging { resources.excludes.addAll(listOf( "META-INF/DEPENDENCIES", "META-INF/NOTICE", @@ -188,27 +191,27 @@ kapt { } dependencies { - implementation("androidx.test.ext:junit-ktx:1.1.4") + implementation("androidx.test.ext:junit-ktx:1.2.1") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.4") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") - androidTestImplementation("androidx.test:runner:1.5.1") - androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") implementation(kotlin("reflect")) - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.appcompat:appcompat:1.5.1") - implementation("com.google.android.material:material:1.7.0") + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") /* https://github.com/zhpanvip/BannerViewPager */ implementation("com.github.zhpanvip:BannerViewPager:3.5.7") /* https://github.com/yanzhenjie/Sofia */ implementation("com.yanzhenjie:sofia:1.0.5") /* https://github.com/scwang90/MultiWaveHeader */ - implementation("com.scwang.wave:MultiWaveHeader:1.0.0") + implementation("io.github.sgpublic:MultiWaveHeader:1.0.2") /* https://github.com/li-xiaojun/XPopup */ implementation("com.github.li-xiaojun:XPopup:2.9.1") /* https://github.com/zxing/zxing qrcode */ @@ -216,7 +219,7 @@ dependencies { // /* https://github.com/KwaiAppTeam/AkDanmaku */ // implementation("com.kuaishou:akdanmaku:1.0.3") /* https://docs.geetest.com/sensebot/deploy/client/android */ - implementation("com.geetest.sensebot:sensebot:4.3.7") + implementation("com.geetest.sensebot:sensebot:4.4.2.1") /* https://github.com/sgpublic/ExSharedPreference */ implementation("io.github.sgpublic:exsp-runtime:${Dep.EXSP}") @@ -236,8 +239,8 @@ dependencies { implementation("com.google.protobuf:protobuf-java:${Dep.Proto}") // 阿b用的 cronet,如果用 okhttp 会导致 io.grpc.StatusRuntimeException: INTERNAL: Received unexpected EOS on DATA frame from server. implementation("io.grpc:grpc-cronet:${Dep.GrpcJava}") - implementation("com.google.android.gms:play-services-cronet:18.0.1") - implementation("org.chromium.net:cronet-fallback:106.5249.126") + implementation("com.google.android.gms:play-services-cronet:18.1.0") + implementation("org.chromium.net:cronet-fallback:119.6045.31") implementation("io.grpc:grpc-android:${Dep.GrpcJava}") implementation("io.grpc:grpc-protobuf:${Dep.GrpcJava}") implementation("io.grpc:grpc-stub:${Dep.GrpcJava}") @@ -258,13 +261,13 @@ dependencies { kapt("me.laoyuyu.aria:compiler:${Dep.Aria}") /* https://github.com/tony19/logback-android */ - implementation("com.github.tony19:logback-android:2.0.0") - implementation("org.slf4j:slf4j-api:1.7.36") + implementation("com.github.tony19:logback-android:3.0.0") + implementation("org.slf4j:slf4j-api:2.0.13") /* https://github.com/dromara/forest */ implementation("com.dtflys.forest:forest-core:1.5.26") - implementation("com.google.code.gson:gson:2.9.1") - implementation("com.squareup.okhttp3:okhttp:4.10.0") + implementation("com.google.code.gson:gson:2.10.1") + implementation("com.squareup.okhttp3:okhttp:4.12.0") } /** 自动修改输出文件名并定位文件 */ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eaef4c7..f56236d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,14 @@ android:name=".app.activity.LoginPwd" android:launchMode="singleTop" android:windowSoftInputMode="adjustResize"/> + + @@ -59,4 +67,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/assets/js/ajax_interception.js b/app/src/main/assets/js/ajax_interception.js index 0bae7c0..f478b49 100644 --- a/app/src/main/assets/js/ajax_interception.js +++ b/app/src/main/assets/js/ajax_interception.js @@ -1,13 +1,12 @@ -let verify = false; -XMLHttpRequest.prototype.reallyOpen = XMLHttpRequest.prototype.open; +let reallyOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { - verify = (url.toString() === "https://passport.bilibili.com/x/safecenter/login/tel/verify"); - this.reallyOpen(method, url, async, user, password); + Injection.logOnOpen(url.toString()); + reallyOpen(method, url, async, user, password); }; -XMLHttpRequest.prototype.reallySend = XMLHttpRequest.prototype.send; +let reallySend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(body) { if (body != null) { Injection.setVerifyBody(body.toString()); } - this.reallySend(body); -}; \ No newline at end of file + reallySend(body); +}; diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginPwd.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginPwd.kt index 18535c6..728ee22 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginPwd.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginPwd.kt @@ -28,7 +28,9 @@ import io.github.sgpublic.exsp.ExPreference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.json.JSONObject +@Deprecated("Use sms sending instead.") class LoginPwd: BaseViewModelActivity() { private val Token: TokenPreference by lazy { ExPreference.get() } private val User: UserPreference by lazy { ExPreference.get() } @@ -54,6 +56,7 @@ class LoginPwd: BaseViewModelActivity() } private val geetestBean: GT3ConfigBean by lazy { return@lazy GT3ConfigBean().also { + it.isCanceledOnTouchOutside = false it.listener = object : GT3Listener() { override fun onReceiveCaptchaCode(p0: Int) { } override fun onStatistics(p0: String?) { } @@ -95,26 +98,27 @@ class LoginPwd: BaseViewModelActivity() } ViewModel.CaptchaData.observe(this) { data -> ViewModel.Loading.postValue(false) -// geetestBean.api1Json = JSONObject().also { -// it.put("success", 1) -// it.put("challenge", data.geetest.challenge) -// it.put("gt", data.geetest.gt) -// it.put("token", data.token) -// } -// geetest.startCustomFlow() - XPopup.Builder(this) - .asCustom(GeetestDialog(this, data.url, { - ViewModel.Loading.postValue(false) - }, { validate -> - ViewModel.startGeetestAction( - data.token, data.geetest.challenge, - validate, "$validate|jordan", - ViewBinding.loginUsername.editText!!.text.takeOr(""), - ViewBinding.loginPassword.editText!!.text.takeOr(""), - ::validatePhone - ) - })) - .show() + log.debug("CaptchaData: $data") + geetestBean.api1Json = JSONObject().also { + it.put("success", 1) + it.put("challenge", data.geetest.challenge) + it.put("gt", data.geetest.gt) + it.put("token", data.token) + } + geetest.startCustomFlow() +// XPopup.Builder(this) +// .asCustom(GeetestDialog(this, data.url, { +// ViewModel.Loading.postValue(false) +// }, { validate -> +// ViewModel.startGeetestAction( +// data.token, data.geetest.challenge, +// validate, "$validate|jordan", +// ViewBinding.loginUsername.editText!!.text.takeOr(""), +// ViewBinding.loginPassword.editText!!.text.takeOr(""), +// ::validatePhone +// ) +// })) +// .show() } ViewModel.LoginData.observe(this) { data -> Token.accessToken = data.tokenInfo.accessToken @@ -165,7 +169,8 @@ class LoginPwd: BaseViewModelActivity() // } // else -> url // } - phoneValidate.launch(url) + ViewModel.getCaptcha() +// phoneValidate.launch(url) } override fun onViewSetup() { diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginQrcode.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginQrcode.kt new file mode 100644 index 0000000..3e2d2df --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginQrcode.kt @@ -0,0 +1,215 @@ +package io.github.sgpublic.bilidownload.app.activity + +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import android.view.View +import android.view.View.OnFocusChangeListener +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import com.geetest.sdk.GT3ConfigBean +import com.geetest.sdk.GT3ErrorBean +import com.geetest.sdk.GT3GeetestUtils +import com.geetest.sdk.GT3Listener +import com.google.gson.JsonObject +import com.lxj.xpopup.XPopup +import io.github.sgpublic.bilidownload.Application +import io.github.sgpublic.bilidownload.BuildConfig +import io.github.sgpublic.bilidownload.R +import io.github.sgpublic.bilidownload.app.ui.list.CountrySpinnerAdapter +import io.github.sgpublic.bilidownload.app.viewmodel.LoginQrcodeModel +import io.github.sgpublic.bilidownload.app.viewmodel.LoginSmsModel +import io.github.sgpublic.bilidownload.base.app.BaseViewModelActivity +import io.github.sgpublic.bilidownload.core.exsp.TokenPreference +import io.github.sgpublic.bilidownload.core.exsp.UserPreference +import io.github.sgpublic.bilidownload.core.util.createQRCodeBitmap +import io.github.sgpublic.bilidownload.core.util.fromGson +import io.github.sgpublic.bilidownload.core.util.genBuvid +import io.github.sgpublic.bilidownload.core.util.log +import io.github.sgpublic.bilidownload.core.util.take +import io.github.sgpublic.bilidownload.core.util.takeOr +import io.github.sgpublic.bilidownload.databinding.ActivityLoginQrcodeBinding +import io.github.sgpublic.bilidownload.databinding.ActivityLoginSmsBinding +import io.github.sgpublic.exsp.ExPreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.io.File +import java.util.UUID +import kotlin.math.log + +class LoginQrcode: BaseViewModelActivity() { + private val Token: TokenPreference by lazy { ExPreference.get() } + private val User: UserPreference by lazy { ExPreference.get() } + + override fun onActivityCreated(hasSavedInstanceState: Boolean) { + Token.isLogin = false + + lifecycleScope.launch(Dispatchers.Default) { + delay(500) + withContext(Dispatchers.Main) { + ViewModel.getQrcode( + ViewBinding.loginQrcodeImage.width, + ViewBinding.loginQrcodeImage.height, + ) + } + } + } + + override fun onViewModelSetup() { + ViewModel.Loading.observe(this) { + ViewBinding.loginQrcodeLoading.visibility = if (it) View.VISIBLE else View.GONE + } + ViewModel.Exception.observe(this) { + ViewModel.Loading.postValue(false) + Application.onToast(this, R.string.text_login_failed, it.message, it.code) + } + ViewModel.Qrcode.observe(this) { + ViewModel.Loading.postValue(false) + ViewBinding.loginQrcodeImage.setImageBitmap(it) + } + ViewModel.QrcodeState.observe(this) { state -> + when (state) { + LoginQrcodeModel.QrcodeStateEnum.Success -> { + ViewBinding.loginQrcodeLoadingBase.visibility = View.VISIBLE + ViewModel.Loading.postValue(true) + ViewBinding.loginQrcodeNotice.setText(R.string.text_login_action_doing) + } + LoginQrcodeModel.QrcodeStateEnum.Error -> { + ViewBinding.loginQrcodeLoadingBase.visibility = View.VISIBLE + ViewModel.Loading.postValue(false) + ViewBinding.loginQrcodeNotice.setText(R.string.text_login_qrcode_expire) + } + LoginQrcodeModel.QrcodeStateEnum.Waiting -> { + ViewModel.Loading.postValue(false) + ViewBinding.loginQrcodeLoadingBase.visibility = View.GONE + } + LoginQrcodeModel.QrcodeStateEnum.Scanned -> { + ViewBinding.loginQrcodeLoadingBase.visibility = View.VISIBLE + ViewModel.Loading.postValue(false) + ViewBinding.loginQrcodeNotice.setText(R.string.text_login_qrcode_confirm) + } + LoginQrcodeModel.QrcodeStateEnum.Expired -> { + ViewBinding.loginQrcodeLoadingBase.visibility = View.VISIBLE + ViewModel.Loading.postValue(false) + ViewBinding.loginQrcodeNotice.setText(R.string.text_login_qrcode_expire) + } + } + } + ViewModel.LoginData.observe(this) { data -> + Token.accessToken = data.tokenInfo.accessToken + Token.refreshToken = data.tokenInfo.refreshToken + for (cookie in data.cookieInfo.cookies) { + when (cookie.name) { + "bili_jct" -> Token.cookieBiliJct = cookie.value + "DedeUserID" -> Token.cookieDedeUserID = cookie.value + "DedeUserID__ckMd5" -> Token.cookieDedeUserID_ckMd5 = cookie.value + "sid" -> Token.cookieSid = cookie.value + "SESSDATA" -> Token.cookieSESSDATA = cookie.value + } + } + ViewModel.getUserInfo(Token.accessToken) + } + ViewModel.UserInfo.observe(this) { data -> + ViewModel.viewModelScope.launch { + withContext(Dispatchers.IO) { + User.mid = data.mid + User.name = data.name + User.sign = data.sign + User.face = data.face + User.sex = data.sex + User.level = data.level + User.vipStatus = data.vip.status + User.vipType = data.vip.type + User.vipLabel = data.vip.label.text + + Token.isLogin = true + } + Application.onToast(this@LoginQrcode, R.string.text_login_success) + Application.onToast(this@LoginQrcode, User.name) + Home.startActivity(this@LoginQrcode) + finish() + } + } + } + + override fun onViewSetup() { + ViewBinding.loginQrcodeStart.setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + val intent = packageManager.getLaunchIntentForPackage("tv.danmaku.bili") + if (intent != null) { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + withContext(Dispatchers.Main) { + startActivity(intent) + } + } else { + Application.onToast(this@LoginQrcode, R.string.text_login_qrcode_save_failed) + } + if (!writeQrcodeToDownload()) { + Application.onToast(this@LoginQrcode, R.string.text_login_qrcode_save_failed) + } else { + Application.onToast(this@LoginQrcode, R.string.text_login_qrcode_save_success) + } + } + } + } + + override fun onDestroy() { + ViewModel.Loading.postValue(false) + super.onDestroy() + } + + private fun writeQrcodeToDownload(): Boolean { + val bitmap = ViewModel.Qrcode.value + if (bitmap == null) { + log.warn("no qrcode to save.") + return false + } + + val filename = "login-qrcode-${System.currentTimeMillis()}.png" + val subdirectory = BuildConfig.PROJECT_NAME + + val contentValues = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, filename) + put(MediaStore.Downloads.MIME_TYPE, "image/png") + put(MediaStore.Downloads.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/$subdirectory") + } + + val url = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + + if (url == null) { + log.warn("contentResolver returns null when insert qrcode to download.") + return false + } + contentResolver.openOutputStream(url)?.use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + return true + } + log.warn("contentResolver returns null when openOutputStream.") + return false + } + + override val ViewBinding: ActivityLoginQrcodeBinding by viewBinding() + override val ViewModel: LoginQrcodeModel by viewModels() + override fun isActivityAtBottom(): Boolean = true + companion object { + fun startActivity(context: Context){ + val intent = Intent().run { + setClass(context, LoginQrcode::class.java) + } + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginSms.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginSms.kt new file mode 100644 index 0000000..74f2ac4 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginSms.kt @@ -0,0 +1,224 @@ +package io.github.sgpublic.bilidownload.app.activity + +import android.content.Context +import android.content.Intent +import android.view.View +import android.view.View.OnFocusChangeListener +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.viewModels +import androidx.lifecycle.viewModelScope +import com.geetest.sdk.GT3ConfigBean +import com.geetest.sdk.GT3ErrorBean +import com.geetest.sdk.GT3GeetestUtils +import com.geetest.sdk.GT3Listener +import com.google.gson.JsonObject +import com.lxj.xpopup.XPopup +import io.github.sgpublic.bilidownload.Application +import io.github.sgpublic.bilidownload.R +import io.github.sgpublic.bilidownload.app.dialog.GeetestDialog +import io.github.sgpublic.bilidownload.app.ui.list.CountrySpinnerAdapter +import io.github.sgpublic.bilidownload.app.viewmodel.LoginPwdModel +import io.github.sgpublic.bilidownload.app.viewmodel.LoginSmsModel +import io.github.sgpublic.bilidownload.base.app.BaseViewModelActivity +import io.github.sgpublic.bilidownload.core.exsp.TokenPreference +import io.github.sgpublic.bilidownload.core.exsp.UserPreference +import io.github.sgpublic.bilidownload.core.util.fromGson +import io.github.sgpublic.bilidownload.core.util.genBuvid +import io.github.sgpublic.bilidownload.core.util.log +import io.github.sgpublic.bilidownload.core.util.take +import io.github.sgpublic.bilidownload.core.util.takeOr +import io.github.sgpublic.bilidownload.databinding.ActivityLoginPwdBinding +import io.github.sgpublic.bilidownload.databinding.ActivityLoginSmsBinding +import io.github.sgpublic.exsp.ExPreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.util.UUID + +class LoginSms: BaseViewModelActivity() { + private val Token: TokenPreference by lazy { ExPreference.get() } + private val User: UserPreference by lazy { ExPreference.get() } + + override fun onActivityCreated(hasSavedInstanceState: Boolean) { + Token.isLogin = false + } + + private val geetest: GT3GeetestUtils by lazy { + return@lazy GT3GeetestUtils(this).also { + it.init(geetestBean) + } + } + private val geetestBean: GT3ConfigBean by lazy { + return@lazy GT3ConfigBean().also { + it.isCanceledOnTouchOutside = false + it.listener = object : GT3Listener() { + override fun onReceiveCaptchaCode(p0: Int) { } + override fun onStatistics(p0: String?) { } + override fun onClosed(p0: Int) { } + override fun onSuccess(p0: String?) { } + override fun onFailed(p0: GT3ErrorBean?) { + ViewModel.Loading.postValue(false) + } + override fun onDialogResult(result: String) { + geetest.dismissGeetestDialog() + ViewModel.Loading.postValue(true) + val buvid = genBuvid() + val loginSessionId = UUID.randomUUID().toString().replace("-", "") + Token.buvid = buvid + Token.loginSessionId = loginSessionId + val obj = JsonObject::class.java.fromGson(result) + ViewModel.sendSms( + cid = ViewModel.CountrySelected, + tel = ViewBinding.loginPhone.editText!!.text.takeOr("").toLong(), + buvid = buvid, + loginSessionId = loginSessionId, + token = geetestBean.api1Json.getString("token"), + challenge = obj.get("geetest_challenge").asString, + validate = obj.get("geetest_validate").asString, + seccode = obj.get("geetest_seccode").asString, + ) { + ViewModel.Loading.postValue(false) + } + } + override fun onButtonClick() { + geetest.getGeetest() + } + } + } + } + override fun onViewModelSetup() { + val asLoading = XPopup.Builder(this).asLoading( + Application.getString(R.string.text_login_action_doing) + ) + ViewModel.CodeCd.observe(this) { time -> + when { + time < 0 -> { + ViewBinding.loginGetCode.setText(R.string.text_login_get_code) + ViewBinding.loginGetCode.isEnabled = true + } + time == 0 -> { + ViewBinding.loginGetCode.setText(R.string.text_login_get_code_retry) + ViewBinding.loginGetCode.isEnabled = true + } + else -> { + ViewBinding.loginGetCode.text = getString(R.string.text_login_get_code_try_later, time) + ViewBinding.loginGetCode.isEnabled = false + } + } + } + ViewModel.CountryData.observe(this) { country -> + ViewBinding.loginCountry.adapter = CountrySpinnerAdapter(ArrayList(country.entries)) + ViewBinding.loginCountry.setSelection(0) + ViewBinding.loginCountry.visibility = View.VISIBLE + } + ViewModel.Loading.observe(this) { + it.take({asLoading.show()}, {asLoading.dismiss()}) + } + ViewModel.Exception.observe(this) { + ViewModel.Loading.postValue(false) + Application.onToast(this, R.string.text_login_failed, it.message, it.code) + } + ViewModel.CaptchaData.observe(this) { data -> + ViewModel.Loading.postValue(false) + log.debug("CaptchaData: $data") + geetestBean.api1Json = JSONObject().also { + it.put("success", 1) + it.put("challenge", data.geetest.challenge) + it.put("gt", data.geetest.gt) + it.put("token", data.token) + } + geetest.startCustomFlow() + } + ViewModel.LoginData.observe(this) { data -> + Token.accessToken = data.tokenInfo.accessToken + Token.refreshToken = data.tokenInfo.refreshToken + for (cookie in data.cookieInfo.cookies) { + when (cookie.name) { + "bili_jct" -> Token.cookieBiliJct = cookie.value + "DedeUserID" -> Token.cookieDedeUserID = cookie.value + "DedeUserID__ckMd5" -> Token.cookieDedeUserID_ckMd5 = cookie.value + "sid" -> Token.cookieSid = cookie.value + "SESSDATA" -> Token.cookieSESSDATA = cookie.value + } + } + ViewModel.getUserInfo(Token.accessToken) + } + ViewModel.UserInfo.observe(this) { data -> + ViewModel.viewModelScope.launch { + withContext(Dispatchers.IO) { + User.mid = data.mid + User.name = data.name + User.sign = data.sign + User.face = data.face + User.sex = data.sex + User.level = data.level + User.vipStatus = data.vip.status + User.vipType = data.vip.type + User.vipLabel = data.vip.label.text + + Token.isLogin = true + } + Application.onToast(this@LoginSms, R.string.text_login_success) + Application.onToast(this@LoginSms, User.name) + Home.startActivity(this@LoginSms) + finish() + } + } + } + + override fun onViewSetup() { + ViewBinding.loginAction.setOnClickListener { + val code = ViewBinding.loginCode.editText!!.text.toString() + if (code.isBlank()) { + Application.onToast(this, R.string.text_login_error_code_empty) + return@setOnClickListener + } + ViewModel.Loading.postValue(true) +// ViewModel.startAction(phone, code, ::validatePhone) + } + ViewBinding.loginCodeContent.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + ViewBinding.loginBannerLeft.setImageResource(R.drawable.pic_login_banner_left_hide) + ViewBinding.loginBannerRight.setImageResource(R.drawable.pic_login_banner_right_hide) + } else { + ViewBinding.loginBannerLeft.setImageResource(R.drawable.pic_login_banner_left_show) + ViewBinding.loginBannerRight.setImageResource(R.drawable.pic_login_banner_right_show) + } + } + ViewBinding.loginGetCode.setOnClickListener { + val phone = ViewBinding.loginPhone.editText!!.text.toString() + if (phone.isBlank()) { + Application.onToast(this, R.string.text_login_error_phone_empty) + return@setOnClickListener + } + ViewModel.getCaptcha() + } + ViewBinding.loginCountry.onItemSelectedListener = object : OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + ViewModel.CountrySelected = id.toInt() + } + override fun onNothingSelected(parent: AdapterView<*>?) { } + } + } + + override fun onDestroy() { + ViewModel.Loading.postValue(false) + geetest.destory() + super.onDestroy() + } + + override val ViewBinding: ActivityLoginSmsBinding by viewBinding() + override val ViewModel: LoginSmsModel by viewModels() + override fun isActivityAtBottom(): Boolean = true + companion object { + fun startActivity(context: Context){ + val intent = Intent().run { + setClass(context, LoginSms::class.java) + } + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginValidateAccount.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginValidateAccount.kt index ad96c59..f695f78 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginValidateAccount.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/LoginValidateAccount.kt @@ -152,6 +152,14 @@ class LoginValidateAccount: BaseActivity() { } class Injection(private val onSave: (String) -> Unit) { + @JavascriptInterface + fun logOnOpen(url: String) { + log.debug("logOnOpen: $url") + } + @JavascriptInterface + fun logOnSend(body: String) { + log.debug("logOnSend: $body") + } @JavascriptInterface fun setVerifyBody(body: String) { log.debug("save verify body: $body") @@ -171,19 +179,15 @@ class LoginValidateAccount: BaseActivity() { override fun onPageFinished(view: WebView, url: String) { view.context.resources.assets.run { -// val implCode = StringJoiner("\\n\" + \n\"") val implCode = StringJoiner("\n ", "(function() {\n ", "\n})();") open("js/ajax_interception.js").reader().readLines().forEach { - implCode.add(it.replace("\"", "\\\"")) +// implCode.add(it.replace("\"", "\\\"")) + implCode.add(it) } -// open("js/ajax_interception.js").reader().readLines().forEach { -// implCode.add(it) -// } val preCode = open("js/phone_validate.js").reader().readText() .replace("%%SRC_CODE%%", implCode.toString()) -// val preCode = implCode.toString() log.debug(preCode) - view.loadUrl("javascript:$preCode") + view.loadUrl("javascript:$implCode") } } diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/Welcome.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/Welcome.kt index 944348f..915de1a 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/Welcome.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/activity/Welcome.kt @@ -17,7 +17,7 @@ class Welcome: BaseActivity() { private fun onSetupFinish() { if (!Token.isLogin) { - LoginPwd.startActivity(this@Welcome) + LoginQrcode.startActivity(this@Welcome) return } val data = intent.data diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/ui/list/CountrySpinnerAdapter.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/ui/list/CountrySpinnerAdapter.kt new file mode 100644 index 0000000..9838d56 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/ui/list/CountrySpinnerAdapter.kt @@ -0,0 +1,44 @@ +package io.github.sgpublic.bilidownload.app.ui.list + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import io.github.sgpublic.bilidownload.R +import io.github.sgpublic.bilidownload.core.forest.data.CountryResp +import io.github.sgpublic.bilidownload.databinding.ItemCountryListBinding +import io.github.sgpublic.bilidownload.databinding.ItemCountrySelectedBinding + +class CountrySpinnerAdapter( + private val countryList: ArrayList>, +): BaseAdapter() { + override fun getCount(): Int = countryList.size + override fun getItem(pos: Int): Map.Entry = countryList[pos] + override fun getItemId(pos: Int): Long = getItem(pos).key.toLong() + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + val item = getItem(position) + val binding: ItemCountryListBinding = if (convertView != null) { + ItemCountryListBinding.bind(convertView) + } else { + ItemCountryListBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + } + binding.root.text = binding.root.context.getString(R.string.text_login_country, item.value.cname, item.value.countryId) + return binding.root + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val item = getItem(position) + val binding: ItemCountrySelectedBinding = if (convertView != null) { + ItemCountrySelectedBinding.bind(convertView) + } else { + ItemCountrySelectedBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + } + binding.root.text = binding.root.context.getString(R.string.text_login_country_selected, item.value.countryId) + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/FollowModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/FollowModel.kt index 879c479..968e504 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/FollowModel.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/FollowModel.kt @@ -32,11 +32,11 @@ class FollowModel(private val status: FollowStatus): BaseViewModel() { ForestClients.Api.follow( status.value, pageIndex, TokenPreference.accessToken ).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: FollowsResp.FollowsData) { + override suspend fun onResponse(data: FollowsResp.FollowsData) { pageIndex += 1 val liveData = Follows.value?.takeIf { !isRefresh } ?: (ArrayList() to true) diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/HomeModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/HomeModel.kt index 687ad46..31db32f 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/HomeModel.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/HomeModel.kt @@ -22,11 +22,11 @@ class HomeModel : BaseViewModel() { fun getBannerInfo() { Loading.postValue(true) ForestClients.Api.banner(TokenPreference.accessToken).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: BannerResp.BannerData) { + override suspend fun onResponse(data: BannerResp.BannerData) { val banners: List = data.find() BannerInfo.postValue(banners[0].items) } @@ -45,11 +45,11 @@ class HomeModel : BaseViewModel() { ForestClients.Api.bangumi( TokenPreference.accessToken, cursor, isRefresh.take(1, 0) ).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: BangumiPageResp.BangumiPageData) { + override suspend fun onResponse(data: BangumiPageResp.BangumiPageData) { cursor = data.nextCursor if (isRefresh) { diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginPwdModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginPwdModel.kt index 151bd08..c5bbd8e 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginPwdModel.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginPwdModel.kt @@ -19,6 +19,7 @@ import java.util.* import java.util.regex.Pattern import javax.crypto.Cipher +@Deprecated("Use sms sending instead.") class LoginPwdModel: BaseViewModel() { val CaptchaData: MutableLiveData = MutableLiveData() val LoginData: MutableLiveData = MutableLiveData() @@ -26,11 +27,11 @@ class LoginPwdModel: BaseViewModel() { fun getCaptcha() { ForestClients.Passport.captcha().biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: CaptchaResp.CaptchaData) { + override suspend fun onResponse(data: CaptchaResp.CaptchaData) { CaptchaData.postValue(data) } }, viewModelScope) @@ -38,11 +39,11 @@ class LoginPwdModel: BaseViewModel() { private fun encryptPwd(password: String, callback: (String) -> Unit) { ForestClients.Passport.pubKey().biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: GetKeyResp.GetKeyData) { + override suspend fun onResponse(data: GetKeyResp.GetKeyData) { val pwdEncrypt = try { val pubKey = data.key.replace("\n", "") .substring(26, 242) @@ -105,17 +106,15 @@ class LoginPwdModel: BaseViewModel() { it.geetest.gt = gt it.geetest.challenge = challenge it.token = token - it.url = url }) return } - ForestClients.Passport.captcha().biliapi(object : - RequestCallback() { - override fun onFailure(code: Int, message: String?) { + ForestClients.Passport.captcha().biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: CaptchaResp.CaptchaData) { + override suspend fun onResponse(data: CaptchaResp.CaptchaData) { CaptchaData.postValue(data) } }, viewModelScope) @@ -149,11 +148,11 @@ class LoginPwdModel: BaseViewModel() { fun accessToken(code: String) { ForestClients.Passport.accessToken(code).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: LoginResp.LoginData) { + override suspend fun onResponse(data: LoginResp.LoginData) { LoginData.postValue(data) } }, viewModelScope) @@ -161,11 +160,11 @@ class LoginPwdModel: BaseViewModel() { fun getUserInfo(accessToken: String) { ForestClients.App.getUserInfoRequest(accessToken).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: UserInfoResp.UserInfo) { + override suspend fun onResponse(data: UserInfoResp.UserInfo) { UserInfo.postValue(data) } }, viewModelScope) diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginQrcodeModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginQrcodeModel.kt new file mode 100644 index 0000000..f0e0091 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginQrcodeModel.kt @@ -0,0 +1,119 @@ +package io.github.sgpublic.bilidownload.app.viewmodel + +import android.graphics.Bitmap +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.github.sgpublic.bilidownload.base.app.BaseViewModel +import io.github.sgpublic.bilidownload.base.app.postValue +import io.github.sgpublic.bilidownload.core.forest.data.LoginResp +import io.github.sgpublic.bilidownload.core.forest.data.QrcodePollResp +import io.github.sgpublic.bilidownload.core.forest.data.QrcodeResp.QrcodeData +import io.github.sgpublic.bilidownload.core.forest.data.UserInfoResp +import io.github.sgpublic.bilidownload.core.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class LoginQrcodeModel: BaseViewModel() { + val LoginData: MutableLiveData = MutableLiveData() + val UserInfo: MutableLiveData = MutableLiveData() + + val Qrcode: MutableLiveData = MutableLiveData() + val QrcodeState: MutableLiveData = MutableLiveData() + private var qrcodeKey: String? = null + + fun getQrcode(width: Int, height: Int) { + ForestClients.Passport.qrcodeTv().biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: QrcodeData) { + qrcodeKey = data.authCode + Qrcode.postValue(data.url.createQRCodeBitmap(width, height)) + startQrcodeConfirm() + } + }, viewModelScope) + } + + private var QrcodeConfirmJob: Job? = null + private fun startQrcodeConfirm() { + QrcodeConfirmJob?.cancel() + QrcodeConfirmJob = viewModelScope.launch(Dispatchers.IO) { + delay(5000) + while (true) { + delay(2000) + val state = checkQrcodeState() + QrcodeState.postValue(state) + if (state == QrcodeStateEnum.Success || state == QrcodeStateEnum.Expired || state == QrcodeStateEnum.Error) { + break + } + } + } + } + + private suspend fun checkQrcodeState(): QrcodeStateEnum { + val qrcodeKey = qrcodeKey + if (qrcodeKey == null) { + return QrcodeStateEnum.Error + } + val resultChannel = Channel() + ForestClients.Passport.qrcodeTvPoll( + authCode = qrcodeKey + ).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + when (code) { + 86038 -> resultChannel.send(QrcodeStateEnum.Expired) + 86039 -> resultChannel.send(QrcodeStateEnum.Waiting) + 86090 -> resultChannel.send(QrcodeStateEnum.Scanned) + else -> resultChannel.send(QrcodeStateEnum.Error) + } + } + override suspend fun onResponse(data: LoginResp.LoginData) { + resultChannel.send(QrcodeStateEnum.Success) + LoginData.postValue(data) + } + }, viewModelScope) + return resultChannel.receive() + } + + enum class QrcodeStateEnum { + Success, + Error, + Waiting, + Scanned, + Expired, + } + + + fun accessToken(code: String) { + ForestClients.Passport.accessToken(code).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: LoginResp.LoginData) { + LoginData.postValue(data) + } + }, viewModelScope) + } + + fun getUserInfo(accessToken: String) { + ForestClients.App.getUserInfoRequest(accessToken).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: UserInfoResp.UserInfo) { + UserInfo.postValue(data) + } + }, viewModelScope) + } + + override fun onCleared() { + super.onCleared() + QrcodeConfirmJob?.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginSmsModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginSmsModel.kt new file mode 100644 index 0000000..90ee176 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/LoginSmsModel.kt @@ -0,0 +1,167 @@ +package io.github.sgpublic.bilidownload.app.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.dtflys.forest.http.ForestRequest +import com.google.gson.JsonObject +import io.github.sgpublic.bilidownload.base.app.BaseViewModel +import io.github.sgpublic.bilidownload.base.app.postValue +import io.github.sgpublic.bilidownload.core.forest.core.BiliApiException +import io.github.sgpublic.bilidownload.core.forest.data.CaptchaResp +import io.github.sgpublic.bilidownload.core.forest.data.CountryResp +import io.github.sgpublic.bilidownload.core.forest.data.GetKeyResp +import io.github.sgpublic.bilidownload.core.forest.data.LoginResp +import io.github.sgpublic.bilidownload.core.forest.data.SmsSendResp +import io.github.sgpublic.bilidownload.core.forest.data.UserInfoResp +import io.github.sgpublic.bilidownload.core.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.X509EncodedKeySpec +import java.util.* +import java.util.regex.Pattern +import javax.crypto.Cipher + +class LoginSmsModel: BaseViewModel() { + val CodeCd: MutableLiveData = MutableLiveData(-1) + val CountryData: MutableLiveData> by lazy { + getCountryCode() + MutableLiveData() + } + var CountrySelected: Int = -1 + + val CaptchaData: MutableLiveData = MutableLiveData() + val LoginData: MutableLiveData = MutableLiveData() + val UserInfo: MutableLiveData = MutableLiveData() + + private fun startCodeCd() { + viewModelScope.launch { + var time = 60 + while (time >= 0) { + CodeCd.postValue(time) + delay(1000) + time -= 1 + } + } + } + + private fun getCountryCode() { + ForestClients.Passport.countryList().biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + log.warn("country code get failed.") + } + + override suspend fun onResponse(data: CountryResp.CountryData) { + CountryData.postValue(data.common.associateBy( + keySelector = { it.id }, + )) + } + }, viewModelScope) + } + + fun sendSms( + cid: Int, tel: Long, + buvid: String, loginSessionId: String, + token: String, challenge: String, validate: String, seccode: String, + onSuccess: (String) -> Unit, + ) { + ForestClients.Passport.smsSend( + cid, tel, loginSessionId, token, challenge, validate, seccode, buvid + ).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: SmsSendResp.SmsSendData) { + startCodeCd() + onSuccess.invoke(data.captchaKey) + } + }, viewModelScope) + } + + fun getCaptcha() { + ForestClients.Passport.captcha().biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: CaptchaResp.CaptchaData) { + CaptchaData.postValue(data) + } + }, viewModelScope) + } + + + private val ct: Pattern by lazy { return@lazy Pattern.compile("ct=(.*?)&") } + private val gt: Pattern by lazy { return@lazy Pattern.compile("gt=(.*?)&") } + private val challenge: Pattern by lazy { return@lazy Pattern.compile("challenge=(.*?)&") } + private val token: Pattern by lazy { return@lazy Pattern.compile("hash=(.*?)&") } + private fun parseCaptcha(obj: JsonObject) { + val url = obj.getAsJsonObject("data").get("url").asString + log.debug("验证 URL:$url") + val ct = ct.matchString("$url&", "ct=&").let { + return@let it.substring(3, it.length - 1) + } + if (ct == "geetest" || ct == "1") { + val gt = gt.matchString("$url&", "gt=&").let { + return@let it.substring(3, it.length - 1) + } + val challenge = challenge.matchString("$url&", "challenge=&").let { + return@let it.substring(10, it.length - 1) + } + val token = token.matchString("$url&", "token=&").let { + return@let it.substring(5, it.length - 1) + } + CaptchaData.postValue(CaptchaResp.CaptchaData().also { + it.geetest.gt = gt + it.geetest.challenge = challenge + it.token = token + }) + return + } + ForestClients.Passport.captcha().biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: CaptchaResp.CaptchaData) { + CaptchaData.postValue(data) + } + }, viewModelScope) + } + + private fun loginNextAction(request: ForestRequest, onPhoneValidate: (String) -> Unit) { + val data = request.execute(LoginResp::class.java).data + if (data.status == 2) { + onPhoneValidate.invoke(data.url) + } else { + LoginData.postValue(data) + } + } + + fun accessToken(code: String) { + ForestClients.Passport.accessToken(code).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: LoginResp.LoginData) { + LoginData.postValue(data) + } + }, viewModelScope) + } + + fun getUserInfo(accessToken: String) { + ForestClients.App.getUserInfoRequest(accessToken).biliapi(object : RequestCallback() { + override suspend fun onFailure(code: Int, message: String?) { + Exception.postValue(code, message) + } + + override suspend fun onResponse(data: UserInfoResp.UserInfo) { + UserInfo.postValue(data) + } + }, viewModelScope) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/OnlinePlayerModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/OnlinePlayerModel.kt index 1c0dbf0..363347c 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/OnlinePlayerModel.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/app/viewmodel/OnlinePlayerModel.kt @@ -47,11 +47,11 @@ class OnlinePlayerModel(sid: Long, epid: Long): BasePlayerModel() { ForestClients.Api.seasonRecommend( sid, TokenPreference.accessToken ).biliapi(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { } - override fun onResponse(data: SeasonRecommendResp.SeasonRecommend) { + override suspend fun onResponse(data: SeasonRecommendResp.SeasonRecommend) { SeasonRecommend.postValue(data) } }, viewModelScope) @@ -71,11 +71,11 @@ class OnlinePlayerModel(sid: Long, epid: Long): BasePlayerModel() { log.info("getPlayUrl(epid: $epid, cid: $cid)") EpisodeId.postValue(epid to cid) AppClient.getPlayUrl(cid, epid, FittedQuality).enqueue(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: PlayViewReply) { + override suspend fun onResponse(data: PlayViewReply) { for (stream in data.videoInfo.streamListList) { QualityData[stream.info.quality] = stream.info.newDescription } diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/base/app/BaseViewModel.kt b/app/src/main/java/io/github/sgpublic/bilidownload/base/app/BaseViewModel.kt index 9fa0be0..8697883 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/base/app/BaseViewModel.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/base/app/BaseViewModel.kt @@ -22,11 +22,11 @@ abstract class BaseViewModel: ViewModel() { fun newRequestCallback(reply: (T) -> Unit): RequestCallback { return object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { Exception.postValue(code, message) } - override fun onResponse(data: T) { + override suspend fun onResponse(data: T) { reply.invoke(data) } } diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/exsp/TokenPreference.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/exsp/TokenPreference.java index f544c23..59a6aff 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/exsp/TokenPreference.java +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/exsp/TokenPreference.java @@ -10,6 +10,12 @@ public class TokenPreference { @ExValue(defVal = "false") private boolean login; + @ExValue(defVal = "") + private String buvid; + + @ExValue(defVal = "") + private String loginSessionId; + @ExValue(defVal = "") private String accessToken; diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/client/PassportClient.kt b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/client/PassportClient.kt index d8cc725..990529e 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/client/PassportClient.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/client/PassportClient.kt @@ -4,8 +4,12 @@ import com.dtflys.forest.annotation.* import com.dtflys.forest.http.ForestRequest import io.github.sgpublic.bilidownload.core.forest.annotations.BiliSign import io.github.sgpublic.bilidownload.core.forest.data.CaptchaResp +import io.github.sgpublic.bilidownload.core.forest.data.CountryResp import io.github.sgpublic.bilidownload.core.forest.data.GetKeyResp import io.github.sgpublic.bilidownload.core.forest.data.LoginResp +import io.github.sgpublic.bilidownload.core.forest.data.QrcodePollResp +import io.github.sgpublic.bilidownload.core.forest.data.QrcodeResp +import io.github.sgpublic.bilidownload.core.forest.data.SmsSendResp @Address( scheme = "https", @@ -15,17 +19,12 @@ interface PassportClient { @Get("/x/passport-login/captcha") fun captcha(): ForestRequest - @BiliSign( - appKey = "783bbb7264451d82", - appSecret = "2653583c8873dea268ab9386918b1d65", - ) + @BiliSign(appKey = appKey, appSecret = appSecret) @Get("/x/passport-login/web/key") fun pubKey(): ForestRequest - @BiliSign( - appKey = "783bbb7264451d82", - appSecret = "2653583c8873dea268ab9386918b1d65", - ) + @Deprecated("Use sms sending instead.") + @BiliSign(appKey = appKey, appSecret = appSecret) @Post("/x/passport-login/oauth2/login") fun login( @Body("username") username: String, @@ -34,10 +33,8 @@ interface PassportClient { @Header("app-key") appKey: String = "android64", ) : ForestRequest - @BiliSign( - appKey = "783bbb7264451d82", - appSecret = "2653583c8873dea268ab9386918b1d65", - ) + @Deprecated("Use sms sending instead.") + @BiliSign(appKey = appKey, appSecret = appSecret) @Post("/x/passport-login/oauth2/login") fun geetestLogin( @Body("recaptcha_token") token: String, @@ -52,23 +49,56 @@ interface PassportClient { @Header("app-key") appKey: String = "android64", ) : ForestRequest - @BiliSign( - appKey = "783bbb7264451d82", - appSecret = "2653583c8873dea268ab9386918b1d65", - ) + @Get("/web/generic/country/list") + fun countryList(): ForestRequest + + @BiliSign(appKey = appKey, appSecret = appSecret) + @Post("/x/passport-login/sms/send") + fun smsSend( + @Body("cid") cid: Int, + @Body("tel") tel: Long, + @Body("login_session_id") loginSessionId: String, + @Body("recaptcha_token") token: String, + @Body("gee_challenge") challenge: String, + @Body("gee_validate") validate: String, + @Body("gee_seccode") seccode: String, + @Body("buvid") buvid: String, + @Body("channel") channel: String = "bili", + @Body("local_id") localId: String = buvid, + @URLEncode @Body("statistics") statistics: String = PassportClient.statistics, + ): ForestRequest + + @BiliSign(appKey = appKey, appSecret = appSecret) + @Post("/x/passport-tv-login/qrcode/auth_code") + fun qrcodeTv( + @Body("local_id") localId: Int = tvLocalId, + ): ForestRequest + + @BiliSign(appKey = appKey, appSecret = appSecret) + @Post("/x/passport-tv-login/qrcode/poll") + fun qrcodeTvPoll( + @Body("auth_code") authCode: String, + @Body("local_id") localId: Int = tvLocalId, + ): ForestRequest + + @BiliSign(appKey = appKey, appSecret = appSecret) @Get("/api/v2/oauth2/access_token") fun accessToken( @Query("code") code: String, @Query("grant_type") grantType: String = "volatile_code" ): ForestRequest - @BiliSign( - appKey = "783bbb7264451d82", - appSecret = "2653583c8873dea268ab9386918b1d65", - ) + @BiliSign(appKey = appKey, appSecret = appSecret) @Post("/api/oauth2/refreshToken") fun refreshToken( @Body("access_token") accessToken: String, @Body("refresh_token") refreshToken: String, ): ForestRequest + + companion object { + const val appKey = "783bbb7264451d82" + const val appSecret = "2653583c8873dea268ab9386918b1d65" + const val statistics = "{\"appId\":1,\"platform\":3,\"version\":\"7.27.0\",\"abtest\":\"\"}" + const val tvLocalId = 0 + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CaptchaResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CaptchaResp.java index b4444c9..3294d8b 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CaptchaResp.java +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CaptchaResp.java @@ -10,8 +10,6 @@ public class CaptchaResp extends DataResp { @Data public static class CaptchaData { /** 备用方案,直接打开一个 dialog 打开阿b给的极验链接 */ - private String url; - private String token; private CaptchaDataGeetest geetest = new CaptchaDataGeetest(); diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CountryResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CountryResp.java new file mode 100644 index 0000000..da305ea --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/CountryResp.java @@ -0,0 +1,22 @@ +package io.github.sgpublic.bilidownload.core.forest.data; + +import java.util.LinkedList; + +import io.github.sgpublic.bilidownload.base.forest.DataResp; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class CountryResp extends DataResp { + @Data + public static class CountryData { + private LinkedList common; + } + @Data + public static class CountryItem { + private int id; + private String cname; + private String countryId; + } +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/LoginResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/LoginResp.java index c697fa4..353e3da 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/LoginResp.java +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/LoginResp.java @@ -1,9 +1,8 @@ package io.github.sgpublic.bilidownload.core.forest.data; -import java.util.ArrayList; -import java.util.List; - import io.github.sgpublic.bilidownload.base.forest.DataResp; +import io.github.sgpublic.bilidownload.core.forest.data.common.CookieInfo; +import io.github.sgpublic.bilidownload.core.forest.data.common.TokenInfo; import lombok.Data; import lombok.EqualsAndHashCode; @@ -17,24 +16,5 @@ public static class LoginData { private String url; private TokenInfo tokenInfo = new TokenInfo(); private CookieInfo cookieInfo = new CookieInfo(); - - @Data - public static class TokenInfo { - private String mid; - private String accessToken; - private String refreshToken; - private int expiresIn; - } - - @Data - public static class CookieInfo { - private List cookies = new ArrayList<>(); - } - - @Data - public static class Cookie { - private String name; - private String value; - } } } diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodePollResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodePollResp.java new file mode 100644 index 0000000..92a80a1 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodePollResp.java @@ -0,0 +1,15 @@ +package io.github.sgpublic.bilidownload.core.forest.data; + +import io.github.sgpublic.bilidownload.base.forest.DataResp; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class QrcodePollResp extends DataResp { + @Data + public static class QrcodePollData { + private int code; + private String refreshToken; + } +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodeResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodeResp.java new file mode 100644 index 0000000..9613910 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/QrcodeResp.java @@ -0,0 +1,15 @@ +package io.github.sgpublic.bilidownload.core.forest.data; + +import io.github.sgpublic.bilidownload.base.forest.DataResp; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class QrcodeResp extends DataResp { + @Data + public static class QrcodeData { + private String url; + private String authCode; + } +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/SmsSendResp.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/SmsSendResp.java new file mode 100644 index 0000000..46ccad3 --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/SmsSendResp.java @@ -0,0 +1,14 @@ +package io.github.sgpublic.bilidownload.core.forest.data; + +import io.github.sgpublic.bilidownload.base.forest.DataResp; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class SmsSendResp extends DataResp { + @Data + public static class SmsSendData { + private String captchaKey; + } +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/CookieInfo.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/CookieInfo.java new file mode 100644 index 0000000..105bb4d --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/CookieInfo.java @@ -0,0 +1,17 @@ +package io.github.sgpublic.bilidownload.core.forest.data.common; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +@Data +public class CookieInfo { + private List cookies = new ArrayList<>(); + + @Data + public static class Cookie { + private String name; + private String value; + } +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/TokenInfo.java b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/TokenInfo.java new file mode 100644 index 0000000..20a63cd --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/forest/data/common/TokenInfo.java @@ -0,0 +1,11 @@ +package io.github.sgpublic.bilidownload.core.forest.data.common; + +import lombok.Data; + +@Data +public class TokenInfo { + private String mid; + private String accessToken; + private String refreshToken; + private int expiresIn; +} diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Bili.kt b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Bili.kt new file mode 100644 index 0000000..ca3849d --- /dev/null +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Bili.kt @@ -0,0 +1,23 @@ +package io.github.sgpublic.bilidownload.core.util + +import java.util.StringJoiner +import kotlin.random.Random + +private fun genMacAddr(): String { + val mac = StringJoiner(":") + + val num = Random.nextInt(0, 0xff + 1) and 0b11111110 or 0b00000010 // 表示这是一个本地管理的单播地址 + mac.add(num.toString(16)) + + for (i in 0 until 5) { + val num = Random.nextInt(0, 0xff + 1) + mac.add(num.toString(16)) + } + + return mac.toString() +} + +fun genBuvid(): String { + val macMd5 = genMacAddr().MD5_FULL + return "XY${macMd5[2]}${macMd5[12]}${macMd5[22]}$macMd5" +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_File.kt b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_File.kt index b5f73d6..472a582 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_File.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_File.kt @@ -1,6 +1,8 @@ package io.github.sgpublic.bilidownload.core.util -import okhttp3.internal.closeQuietly +import android.app.Activity +import android.os.Build +import android.provider.MediaStore import java.io.File import java.nio.charset.Charset @@ -8,10 +10,16 @@ fun File.writeAndClose(content: String, charset: Charset = Charsets.UTF_8) { delete() parentFile?.mkdirs() createNewFile() - writer(charset).also { + writer(charset).use { it.write(content) - it.closeQuietly() } } -fun File.child(name: String) = File(this, name) \ No newline at end of file +fun File.writeAndClose(content: ByteArray, charset: Charset = Charsets.UTF_8) { + delete() + parentFile?.mkdirs() + createNewFile() + writeBytes(content) +} + +fun File.child(name: String) = File(this, name) diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Network.kt b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Network.kt index d4dd627..e0f953e 100644 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Network.kt +++ b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Network.kt @@ -31,11 +31,11 @@ object ForestClients { /** ForestRequest 封装异步请求 */ inline fun , Data> ForestRequest.biliapi(callback: RequestCallback, viewModelScope: CoroutineScope) { enqueue(object : RequestCallback() { - override fun onFailure(code: Int, message: String?) { + override suspend fun onFailure(code: Int, message: String?) { callback.onFailure(code, message) } - override fun onResponse(data: T) { + override suspend fun onResponse(data: T) { callback.onResponse(data.data) } }, viewModelScope) @@ -76,8 +76,8 @@ fun Exception?.requiredMessage(): String { } abstract class RequestCallback { - abstract fun onFailure(code: Int, message: String?) - abstract fun onResponse(data: Data) + abstract suspend fun onFailure(code: Int, message: String?) + abstract suspend fun onResponse(data: Data) companion object { const val CODE_NETWORK_ERROR = -1001 @@ -89,7 +89,7 @@ abstract class RequestCallback { } } -fun RequestCallback.onFailure(ex: BiliApiException) { +suspend fun RequestCallback.onFailure(ex: BiliApiException) { onFailure(ex.code, ex.requiredMessage()) } diff --git a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Url.kt b/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Url.kt deleted file mode 100644 index c49b790..0000000 --- a/app/src/main/java/io/github/sgpublic/bilidownload/core/util/_Url.kt +++ /dev/null @@ -1,2 +0,0 @@ -package io.github.sgpublic.bilidownload.core.util - diff --git a/app/src/main/res/layout/activity_login_qrcode.xml b/app/src/main/res/layout/activity_login_qrcode.xml index d0a5048..b4bc323 100644 --- a/app/src/main/res/layout/activity_login_qrcode.xml +++ b/app/src/main/res/layout/activity_login_qrcode.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorWindowBackground" - tools:context="io.github.sgpublic.bilidownload.activity.LoginQrcode"> + tools:context=".app.activity.LoginQrcode"> - + - - + + + +