diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..3d207df 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 946741c..07a0971 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { manifestPlaceholders.putAll(mapOf("UMENG_CHANNEL" to name)) } manifestPlaceholders.putAll(mapOf( - "GETUI_APPID" to "40qbPjPkYs7TnVAYCX0Ig6", + "GETUI_APPID" to (project.findProperty("GETUI_APPID") as? String ?: ""), "GT_INSTALL_CHANNEL" to "general", )) } @@ -145,11 +145,33 @@ dependencies { implementation(libs.face.detection) implementation(libs.android.gif.drawable) implementation(libs.gif.encoder) - implementation("com.caverock:androidsvg:1.4") - implementation("io.github.lucksiege:pictureselector:v3.11.2") - // 压缩库 (可选,建议长图拼接前先压缩防止OOM) - implementation("io.github.lucksiege:compress:v3.11.2") - implementation("io.github.leavesczy:matisse:2.3.0") - implementation("com.github.moyuruaizawa:cropify:0.5.2") + //implementation("com.caverock:androidsvg:1.4") + implementation(libs.pictureselector) + implementation(libs.compress) + implementation(libs.matisse) + implementation(libs.cropify) + //noinspection GradleDynamicVersion + api("com.alipay.sdk:alipaysdk-android:+@aar") + implementation(libs.wechat.sdk) //微信 + //Retrofit 依赖 + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization) + implementation (libs.retrofit.converter.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + + //友盟 + implementation (libs.umeng.umsdk.common)// 必选 + implementation (libs.umeng.umsdk.asms)// 必选 + implementation (libs.umeng.umsdk.apm) // U-APM包依赖(必选) + implementation (libs.umeng.umsdk.share.core)//分享核心库,必选 + implementation (libs.umeng.umsdk.share.wx) //微信完整版 + //分包 + implementation (libs.tencent.helper) //腾讯分包 + implementation (files("libs/channelsdk-0.2.2.aar")) //快手分包 + implementation (files("libs/humesdk-1.0.0.aar")) //巨量分包 + + implementation(libs.android.cn.oaid) //获取手机设备id + implementation(libs.fastaes) //解密 } \ No newline at end of file diff --git a/app/libs/channelsdk-0.2.2.aar b/app/libs/channelsdk-0.2.2.aar new file mode 100644 index 0000000..93ce247 Binary files /dev/null and b/app/libs/channelsdk-0.2.2.aar differ diff --git a/app/libs/humesdk-1.0.0.aar b/app/libs/humesdk-1.0.0.aar new file mode 100644 index 0000000..2db4091 Binary files /dev/null and b/app/libs/humesdk-1.0.0.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1611682..3dd512f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,22 @@ + + + + + + + + + + + + + + + + + android:exported="true" + android:launchMode="singleTask"> @@ -56,6 +73,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/BaseApplication.kt b/app/src/main/java/com/img/rabbit/BaseApplication.kt index af420a6..4b03f44 100644 --- a/app/src/main/java/com/img/rabbit/BaseApplication.kt +++ b/app/src/main/java/com/img/rabbit/BaseApplication.kt @@ -4,7 +4,11 @@ import android.app.Application import android.util.Log import com.img.rabbit.utils.NetworkMonitor import com.g.gysdk.GYManager +import com.img.rabbit.config.Constants import com.tencent.mmkv.MMKV +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure +import com.umeng.socialize.PlatformConfig class BaseApplication : Application() { @@ -18,6 +22,21 @@ class BaseApplication : Application() { initMMKV() // 初始化个推SDK initGeTuiOneKeyLogin() + // 初始化友盟 + initUM() + } + + /** + * 初始化友盟 + */ + private fun initUM() { + UMConfigure.setLogEnabled(true) + + PlatformConfig.setFileProvider("${BuildConfig.APPLICATION_ID}.fileprovider") + PlatformConfig.setWeixin(Constants.WechatAppId, Constants.WechatAppSecret) + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + + UMConfigure.setProcessEvent(true) } private fun initGeTuiOneKeyLogin() { diff --git a/app/src/main/java/com/img/rabbit/MainActivity.kt b/app/src/main/java/com/img/rabbit/MainActivity.kt index 7d3086e..1423157 100644 --- a/app/src/main/java/com/img/rabbit/MainActivity.kt +++ b/app/src/main/java/com/img/rabbit/MainActivity.kt @@ -1,5 +1,6 @@ package com.img.rabbit +import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -7,11 +8,21 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -21,23 +32,43 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.viewmodel.compose.viewModel +import com.img.rabbit.config.Constants +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.LoginScreen +import com.img.rabbit.pages.LoginScreenType import com.img.rabbit.pages.MainScreen +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.ChannelUtils +import com.img.rabbit.utils.UrlLinkUtils.openAgreement import com.img.rabbit.viewmodel.GeneralViewModel import com.img.rabbit.viewmodel.LoginViewModel import com.img.rabbit.viewmodel.SplashViewModel +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure import kotlinx.coroutines.delay +import kotlin.system.exitProcess class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 必须在 super.onCreate 之前调用 val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + initUM() // 启用Edge-to-Edge模式(沉浸模式) enableEdgeToEdge() @@ -46,6 +77,15 @@ class MainActivity : ComponentActivity() { val splashViewModel: SplashViewModel = viewModel() val generalViewModel: GeneralViewModel = viewModel() val loginViewModel: LoginViewModel = viewModel() + val context = LocalContext.current + var showSplash by remember { mutableStateOf(false) } + + //获取服务器时间 + generalViewModel.getServerTime() + // 获取用户配置 + loginViewModel.requestUserConfig() + //初始化微信登录 + loginViewModel.initWXApi(this) // 设置启动页显示条件 splashScreen.setKeepOnScreenCondition { @@ -53,26 +93,69 @@ class MainActivity : ComponentActivity() { } AppTheme { - SplashScreenContent { - val token = generalViewModel.kv.decodeString("token") - // 未登录,显示登录页 - if (token?.isNotEmpty() == false && !loginViewModel.isLogin.value) { - // 显示登录页 - LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) - } else { - //已登录,显示主页面 - MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + SplashScreenContent{ + //未同意提示政策弹窗 + if (generalViewModel.agreementStatus.value == false){ + //同意继续 + PrivacyPolicyScreen( + viewModel = loginViewModel, + ) { isAllowPrivacyPolicy -> + if (isAllowPrivacyPolicy) { + generalViewModel.setIsAgreement(true) + showSplash = true + } else { + // 不同意隐私协议政策,直接退出应用 + (context as MainActivity).finish() + // 强制退出应用进程 + exitProcess(0) + } + } + }else{ + showSplash = true + } + + if(showSplash){ + val token = PreferenceUtil.getAccessToken() + // 未登录,显示登录页 + if (token.isNullOrEmpty() && !loginViewModel.isLogin.value) { + // 同意隐私协议政策,检验是否有一键登录权限 + loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> + if (isAllowShowOneKeyScreen) { + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY + } else { + // 检验是否有一键登录权限失败,显示验证码登录 + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA + } + } + + // 显示登录页 + LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel, isVisibilityBreak = false) + } else { + //已登录,显示主页面 + MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + } } } } // 模拟加载过程,2秒后关闭启动页 LaunchedEffect(Unit) { - delay(100L) + delay(500L) splashViewModel.setLoading(false) } } } + + + + /** + * 初始化友盟 + */ + private fun initUM() { + UMConfigure.preInit(applicationContext, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext)) + UMConfigure.init(this, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext), UMConfigure.DEVICE_TYPE_PHONE, "") + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + } } @@ -128,6 +211,212 @@ fun SplashScreenContent( } +@Composable +private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) { + val context = LocalContext.current + Box( + modifier = Modifier.fillMaxSize() + ){ + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xCC000000)) + ){ + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 38.dp, vertical = 213.dp) + .align(Alignment.Center) + .background(Color.White, shape = RoundedCornerShape(26.dp)) + ){ + Image( + painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + alignment = Alignment.TopCenter + ) + + Column( + modifier = Modifier + .padding(top = 36.dp) + .align(Alignment.TopCenter) + ) { + Text( + text = "用户协议与隐私政策", + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally), + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = Color(0xFF1A1A1A) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + ) + + val agreement = "请您务必审慎阅读、充分理解《服务协议》与《隐私政策》各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读《隐私政策》了解详细信息。如果您同意,请点击下面同意按钮开始接受我们的服务。" + val annotatedText = buildAnnotatedString { + append(agreement) + val startIndexForService = agreement.indexOf("《服务协议》") + val startIndexPrivacy1 = agreement.indexOf("《隐私政策》") + val startIndexPrivacy2 = agreement.lastIndexOf("《隐私政策》") + + val serviceLength = "《服务协议》".length + val privacyLength = "《服务协议》".length + + // 高亮显示 "《服务协议》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexForService, // "《服务协议》" 开始下标13 + end = startIndexForService + serviceLength // "《服务协议》" 结束下标19 + ) + addStringAnnotation( + tag = "AGREEMENT", + annotation = "service_agreement", + start = startIndexForService, + end = startIndexForService + serviceLength + ) + + // 高亮显示 "《隐私政策》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexPrivacy1, // "《隐私政策》" 开始下标20 + end = startIndexPrivacy1 + privacyLength // "《隐私政策》" 结束下标26 + ) + addStringAnnotation( + tag = "PRIVACY", + annotation = "privacy_policy", + start = startIndexPrivacy1, + end = startIndexPrivacy1 + privacyLength + ) + + // 高亮显示 "《隐私政策》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexPrivacy2, // "《隐私政策》" 开始下标 + end = startIndexPrivacy2 + privacyLength // "《隐私政策》" 结束下标 + ) + addStringAnnotation( + tag = "PRIVACY", + annotation = "privacy_policy", + start = startIndexPrivacy2, + end = startIndexPrivacy2 + privacyLength + ) + } + + ClickableText( + text = annotatedText, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + .padding(horizontal = 12.dp), + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Color(0xFF1A1A1A) + ), + onClick = { offset -> + val agreementAnnotation = annotatedText.getStringAnnotations("AGREEMENT", offset, offset).firstOrNull() + val privacyAnnotation = annotatedText.getStringAnnotations("PRIVACY", offset, offset).firstOrNull() + + when { + agreementAnnotation != null -> { + openAgreement(context, "服务协议", agreementUrl, false) + } + privacyAnnotation != null -> { + openAgreement(context, "隐私政策", privacyUrl, false) + } + } + } + ) + + } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.BottomCenter) + ) { + //同意按钮,用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(true) + viewModel.setIsPolicyAgreement(true) + } + ) { + Text( + "同意", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + //不同按钮,意用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(false) + viewModel.setIsPolicyAgreement(false) + } + ) { + Text( + "不同意", + color = Color(0xFFAAAAAA), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + } + } +} + @Preview(showBackground = true) @Composable diff --git a/app/src/main/java/com/img/rabbit/WebViewActivity.kt b/app/src/main/java/com/img/rabbit/WebViewActivity.kt new file mode 100644 index 0000000..c1c0a00 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/WebViewActivity.kt @@ -0,0 +1,38 @@ +package com.img.rabbit + +import android.annotation.SuppressLint +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity + +class WebViewActivity : AppCompatActivity() { + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + + // 启用Edge-to-Edge模式(沉浸模式) + enableEdgeToEdge() + + setContentView(R.layout.layout_web) + + val webView: WebView = findViewById(R.id.webView) + val ivWebBreak: ImageView = findViewById(R.id.iv_web_break) + val ivWebTitle: TextView = findViewById(R.id.iv_web_title) + + val title = intent.getStringExtra("title") ?: "" + val url = intent.getStringExtra("url") ?: "" + + ivWebBreak.setOnClickListener { finish() } + ivWebTitle.text = title + + webView.settings.javaScriptEnabled = true + webView.webViewClient = WebViewClient() + webView.loadUrl(url) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/UserInfo.kt deleted file mode 100644 index fe4922f..0000000 --- a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.img.rabbit.bean - -data class UserInfo( - val id: Int, - val name: String, - val login: Boolean, -) diff --git a/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt new file mode 100644 index 0000000..28a12f7 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.local + +data class AlipayBean( + val resultStatus: String?, // 状态码:9000-成功,6001-取消,4000-失败 + val result: String?, // 包含 auth_code 等关键信息 + val memo: String? // 提示语 +) +fun Map.toAlipayResult(): AlipayBean { + return AlipayBean( + resultStatus = this["resultStatus"], + result = this["result"], + memo = this["memo"] + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt similarity index 86% rename from app/src/main/java/com/img/rabbit/bean/ClothingBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt index bd26ebd..aec2e3d 100644 --- a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class ClothingBean( //衣服索引(区分男女) diff --git a/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt new file mode 100644 index 0000000..d935ab3 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt @@ -0,0 +1,3 @@ +package com.img.rabbit.bean.local + +data class ErrorBean(var code: String,var message: String) diff --git a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt similarity index 70% rename from app/src/main/java/com/img/rabbit/bean/FormatBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt index 8d5c80c..35a3281 100644 --- a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class FormatBean( //格式id diff --git a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt similarity index 86% rename from app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt index 3079eeb..b74a162 100644 --- a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class HairstyleBean( //发型索引(区分男女) diff --git a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt similarity index 89% rename from app/src/main/java/com/img/rabbit/bean/LongImageBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt index d83ab6a..5e6ae9d 100644 --- a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local import android.graphics.Bitmap import android.net.Uri diff --git a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt similarity index 94% rename from app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt rename to app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt index 4708b1a..fcf1323 100644 --- a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt similarity index 81% rename from app/src/main/java/com/img/rabbit/bean/ResizeBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt index e8259ce..e0d8a5b 100644 --- a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class ResizeBean( //尺寸id diff --git a/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt new file mode 100644 index 0000000..b74e058 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt @@ -0,0 +1,9 @@ +package com.img.rabbit.bean.local + +data class UserInfo( + val user_id: Int, + val name: String, + val avater: String, + val token: String, + val login: Boolean, +) diff --git a/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt new file mode 100644 index 0000000..efad729 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt @@ -0,0 +1,6 @@ +package com.img.rabbit.bean.local + +data class WxBean( + val code: String, + val state: String +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt new file mode 100644 index 0000000..57b712d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt @@ -0,0 +1,8 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class AlipayParamEntity( + val param: String = "" +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt new file mode 100644 index 0000000..d832972 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt @@ -0,0 +1,8 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CaptchaCodeEntity( + val timestamp: String = "" +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt new file mode 100644 index 0000000..2a0587a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt @@ -0,0 +1,47 @@ +package com.img.rabbit.bean.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +class ConfigEntity { + @SerializedName("client.popup.display") //显示开关控制 + var popupConfig: PopupConfigEntity? = null + + @SerializedName("client.time.setting") //弹窗时间控制 + var popupTimeConfig: PopupTimeConfigEntity? = null + + @SerializedName("client.weixin.open.appid") //微信appid + var wxAppId: String = "" + + @SerializedName("client.version.upgrade") //版本更新 + var versionEntity: VersionEntity? = null + + @SerializedName("client.weixin.share") //微信分享 + var wxShareEntity: WxShareEntity? = null + + @SerializedName("client.guide.enable") + var guideEnable: Boolean? = true //是否开启引导页 + + @SerializedName("client.pay.agreement") //是否显示支付协议 + var payAgreementEnable: Boolean? = true + + @SerializedName("client.login.type") //登录方式 + var loginType: List? = emptyList() + + @SerializedName("client.ad.switch") //广告总开关 + var adSwitch: Boolean = false + + @SerializedName("client.service.phone") //客服电话 + var servicePhoneList: List = emptyList() + + @SerializedName("client.chatwarning") //聊天安全提示 + var chatWarning: String? = null + + @SerializedName("client.travel.ad") //聊天安全提示 + val travelAd: List = emptyList() + + // 圈子-顶部banner占位图配置 + @SerializedName("client.team.ad") //聊天安全提示 + val teamAd: List = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt new file mode 100644 index 0000000..ac66fdc --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt @@ -0,0 +1,28 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +data class PopupConfigEntity( + val android_alipay: Boolean = true, + val android_wxpay: Boolean = true, + val guest_payment: Boolean = false, + val honor_tip_double_install: Boolean = false, + val huawei_map: Boolean = false, + val nonvip_search_results: Boolean = true, + val search_phone: Boolean = true, + val sos_entry: Boolean = true, + val timelimit_discount: Boolean = true, + val vip_back_popup: Boolean = true, + val vivo_login_before_payment: Boolean = false, + val chat_non_vip_Banned: Boolean = false, + val hot_group: Boolean = false, + val group_list: Boolean = false, + val search_location: Boolean = true, + val service_display: Boolean = true, + val home_tip_double_install: Boolean = false, + // Vip优惠全屏页面,默认不显示:从进入程序开始计算到弹出框的时间(当pay_pop=true时生效,需要PopupTimeConfigEntity下的pay_pop_time进行倒计时) + val pay_pop: Boolean = false, + +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt new file mode 100644 index 0000000..512386d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt @@ -0,0 +1,17 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class PopupTimeConfigEntity( + val buy_vip_tip: Long = 45L, + val search_phone: Long = 7L, + val signup_tip: Long = 45L, + val min_discount: Long = 28L, + val min_member_tip: Long = 10000L, + val order_item_top: Int = 3, + /// 进入APP后是否弹出开通会员界面弹出,默认20s后显示 + val pay_pop_time: Long = 20L, + //// 默认支付类型,1支付宝,2微信 + val pay_pop_type: Int = 1, +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt new file mode 100644 index 0000000..c6a0017 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +data class UserConfigEntity( + var token: String = "", + var temp: Boolean = false, + var name: String = "", + var user_id: String = "", + var nowtime: String = "", + var config: ConfigEntity? = null +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt new file mode 100644 index 0000000..db9d590 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +class UserEntity { + val user_id: String = "" + val name: String = "" + val avater: String = "" + val token: String = "" + + //是否登录,有本地登录后写入(非接口字段) + var isLogin: Boolean = false +} diff --git a/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt new file mode 100644 index 0000000..6f73feb --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt @@ -0,0 +1,15 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +class VersionEntity( + var description: String = "", + var force: Boolean = false, + var last_version_force: String = "", + var title: String = "", + var url: String = "", + var app_size: String = "", + var version: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt new file mode 100644 index 0000000..5c3db64 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt @@ -0,0 +1,11 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class WxShareEntity( + val content: String = "", + val image: String = "", + val link: String = "", + val title: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt index 015c715..040430b 100644 --- a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt +++ b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt @@ -43,10 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.FormatBean -import com.img.rabbit.bean.HairstyleBean -import com.img.rabbit.bean.ResizeBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.FormatBean +import com.img.rabbit.bean.local.HairstyleBean +import com.img.rabbit.bean.local.ResizeBean /** * 底部画板(证件)选择器 diff --git a/app/src/main/java/com/img/rabbit/config/Common.kt b/app/src/main/java/com/img/rabbit/config/Common.kt deleted file mode 100644 index 3dc9b14..0000000 --- a/app/src/main/java/com/img/rabbit/config/Common.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.img.rabbit.config - -object Common { - const val privacyUrl = "https://www.baidu.com" - const val agreementUrl = "https://www.baidu.com" -} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/config/CommonData.kt b/app/src/main/java/com/img/rabbit/config/CommonData.kt index 98a974c..4ff9b79 100644 --- a/app/src/main/java/com/img/rabbit/config/CommonData.kt +++ b/app/src/main/java/com/img/rabbit/config/CommonData.kt @@ -2,10 +2,10 @@ package com.img.rabbit.config import androidx.compose.ui.graphics.Color import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.FormatBean -import com.img.rabbit.bean.HairstyleBean -import com.img.rabbit.bean.ResizeBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.FormatBean +import com.img.rabbit.bean.local.HairstyleBean +import com.img.rabbit.bean.local.ResizeBean object CommonData { //背景颜色 diff --git a/app/src/main/java/com/img/rabbit/config/Constants.kt b/app/src/main/java/com/img/rabbit/config/Constants.kt new file mode 100644 index 0000000..cb33f07 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/config/Constants.kt @@ -0,0 +1,19 @@ +package com.img.rabbit.config + +object Constants { + const val RELEASE_BASE_URL = "https://jitutu.batiao8.com" //release + const val DEBUG_BASE_URL = "https://jitutu.batiao8.com" + const val LOG_REQUEST = "RabbitRequest" + const val agreementUrl = "https://jitutu.batiao8.com/static/policy-jietutu/user.html"//用户协议 + const val privacyUrl = "https://jitutu.batiao8.com/static/policy-jietutu/privacy-ios.html"//隐私政策 + + //const val getuiAppId = "40qbPjPkYs7TnVAYCX0Ig6"//个推appid (gradle.properties) + const val WechatAppId = "wx7d1a7d1507482cef"// 微信APPID + const val WechatAppSecret = ""//微信secret + const val UmengAppkey = ""//TODO 友盟appKey + + //解密 + const val AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB" + //加密字符 + const val Signature = "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil" +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt index 877d825..c111bb2 100644 --- a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt +++ b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt @@ -10,7 +10,6 @@ import android.view.LayoutInflater import android.widget.CheckBox import android.widget.TextView import android.widget.Toast -import androidx.compose.animation.core.Animatable import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -38,11 +36,13 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -64,6 +64,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import com.img.rabbit.R import com.img.rabbit.utils.AgreementTextHelper import com.img.rabbit.viewmodel.GeneralViewModel @@ -72,19 +73,25 @@ import com.g.gysdk.EloginActivityParam import com.g.gysdk.GYManager import com.g.gysdk.GYResponse import com.g.gysdk.GyCallBack -import com.img.rabbit.config.Common.agreementUrl -import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.bean.local.toAlipayResult +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.provider.storage.GlobalStateManager +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.StringUtils import com.img.rabbit.utils.UrlLinkUtils.openAgreement import kotlinx.coroutines.delay +import org.json.JSONObject +@SuppressLint("UnrememberedMutableState") @Composable -fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) { +fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel, isVisibilityBreak: Boolean) { val context = LocalContext.current - val scale = remember { Animatable(0f) } val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true) var showNetworkDisconnected by remember { mutableStateOf(false) } + // 网络状态监听 LaunchedEffect(networkStatus) { if (!networkStatus) { @@ -94,8 +101,53 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene Log.w("NetworkStatus","网络已连接") } } - Scaffold{ + // 登录成功后,保存 token + LaunchedEffect(loginViewModel.loginState.value) { + if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) { + //登录成功 + PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token) + loginViewModel.setLogin(true) + + Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show() + }else if(loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token == null){ + //登录失败 + loginViewModel.setLogin(false) + Log.w("LoginScreen","登录失败,无有效的Token") + Toast.makeText(context, "登录失败,请重新登录", Toast.LENGTH_SHORT).show() + } + } + + var globalWxAuthorization by mutableStateOf(GlobalStateManager(context).globalWxAuthorizationFlow().collectAsState(initial = "")) + LaunchedEffect(globalWxAuthorization.value) { + globalWxAuthorization.value?.let { loginViewModel.requestWxLogin(it) } + } + + LaunchedEffect(loginViewModel.authInfoForAlipay.value) { + if(loginViewModel.authInfoForAlipay.value.isEmpty()) return@LaunchedEffect + loginViewModel.loginWithAliPay(context){rawResult -> + Log.i("loginWithAliPay", "支付宝登录结果:$rawResult") + val alipayResult = rawResult.toAlipayResult() + // 处理支付宝登录结果 + when (alipayResult.resultStatus) { + "9000" -> { + // 登录成功,result 字段中包含 auth_code + val authCode = StringUtils.parseAlipayResult(alipayResult.result ?: "")["auth_code"] ?: "" + loginViewModel.requestAlipayLogin(authCode) + } + + "6001" -> { + "用户取消登录" + } + + else -> { + alipayResult.memo ?: "登录失败" + } + } + } + } + + Scaffold{ Box( modifier = Modifier.fillMaxSize() ) { @@ -109,78 +161,46 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene ) // 顶部栏 - TitleBar(navController = navController, paddingValues = it, title = "", showSave = false) + TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak) Column( modifier = Modifier .fillMaxSize() ) { - when (loginViewModel.loginScreenType.value) { - LoginScreenType.LOGIN_ONE_KEY -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - // 检验是否有一键登录权限成功,显示一键登录页 + Box(modifier = Modifier.fillMaxSize()){ + when(loginViewModel.loginScreenType.value){ + LoginScreenType.LOGIN_ONE_KEY -> { + //一键登录 OneKeyLoginScreen(context, loginViewModel, generalViewModel) - - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = loginViewModel) - } } - } - LoginScreenType.LOGIN_CAPTCHA -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - // 显示验证码登录页 + LoginScreenType.LOGIN_WX -> { + //微信登录 + Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ + WxLoginScreen(context, loginViewModel) + } + + } + LoginScreenType.LOGIN_ALIPAY -> { + //支付宝登录 + Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ + AliPayLoginScreen(context, loginViewModel) + } + + } + else -> { + //默认验证码登录 CaptchaLoginScreen(context, loginViewModel, generalViewModel) - - - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = loginViewModel) - } } } - else -> { - // 显示隐私协议政策(同意后才能继续登录) - PrivacyPolicyScreen(viewModel = loginViewModel) { //isAllowPrivacyPolicy -> - loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> - if (isAllowShowOneKeyScreen) { - loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY - } else { - // 检验是否有一键登录权限失败,显示验证码登录 - loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA - } - } - /* - if (isAllowPrivacyPolicy) { - // 同意隐私协议政策,检验是否有一键登录权限 - viewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> - if (isAllowShowOneKeyScreen) { - viewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY - } else { - // 检验是否有一键登录权限失败,显示验证码登录 - viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA - } - } - } else { - // 不同意隐私协议政策,直接退出应用 - (context as MainActivity).onBackPressed() - } - */ - } + + // 其他登录方式Bar + Column ( + modifier = Modifier + .fillMaxSize() + .padding(top = 27.dp), + verticalArrangement = Arrangement.Bottom + ){ + OtherLoginBar(context = context, viewModel = loginViewModel) } } } @@ -191,139 +211,21 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene } if(showNetworkDisconnected){ if(!networkStatus){ - NetworkDisconnectedPage(onNetworkStatus = { - if(it){ + NetworkDisconnectedPage(onNetworkStatus = {isNetworkAvailable-> + if(isNetworkAvailable){ Toast.makeText(context, "网络已连接", Toast.LENGTH_SHORT).show() }else{ Toast.makeText(context, "网络已断开", Toast.LENGTH_SHORT).show() } - generalViewModel.setNetworkStatus(it) + generalViewModel.setNetworkStatus(isNetworkAvailable) }) } } - } } } - -@Composable -private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) { - val context = LocalContext.current - Box( - modifier = Modifier.fillMaxSize() - ){ - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = viewModel) - } - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0xCC000000)) - ){ - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 38.dp, vertical = 213.dp) - .align(Alignment.Center) - .background(Color.White, shape = RoundedCornerShape(26.dp)) - ){ - Image( - painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask), - contentDescription = null, - modifier = Modifier - .fillMaxSize(), - alignment = Alignment.TopCenter - ) - Text( - text = "用户协议与隐私政策", - modifier = Modifier - .padding(top = 54.dp) - .align(Alignment.TopCenter), - fontWeight = FontWeight.Normal, - fontSize = 18.sp, - color = Color(0xFF1A1A1A) - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .align(Alignment.BottomCenter) - ) { - //同意按钮,用户协议与隐私政策 - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(start = 33.dp, end = 33.dp) - .background( - Color(0xFF252525), - shape = RoundedCornerShape(359.dp), - ) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onAgreementChange(true) - viewModel.setIsPolicyAgreement(true) - } - ) { - Text( - "同意", - color = Color(0xFFC2FF43), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(vertical = 12.dp) - .align(Alignment.Center) - ) - } - //不同按钮,意用户协议与隐私政策 - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(start = 33.dp, end = 33.dp) - .background( - Color(0x00000000), - shape = RoundedCornerShape(359.dp), - ) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onAgreementChange(false) - viewModel.setIsPolicyAgreement(false) - } - ) { - Text( - "不同意", - color = Color(0xFFAAAAAA), - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(vertical = 12.dp) - .align(Alignment.Center) - ) - } - } - } - } - } -} - /** * 验证码登录 */ @@ -542,8 +444,8 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene showToast = true ) ) { - //TODO 请求验证码(请完善requestCaptcha函数) - viewModel.requestCaptcha() + // 请求验证码(请完善requestCaptcha函数) + viewModel.requestCaptcha(viewModel.userName.value) // 开始倒计时(倒计时应该在requestCaptcha完成后开始) isCaptchaCountdown = true @@ -597,12 +499,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene showToast = true ) ) { - //TODO 验证码登录请求 - Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show() - //TODO 登录成功后,保存 token - generalViewModel.kv.encode("token", "123232123231231") - // 登录成功后,设置登录状态为 true - viewModel.setLogin(true) + // 验证通过(通过验证码验证),请求登录 + viewModel.requestLoginForCaptcha( + viewModel.userName.value, + viewModel.captcha.value + ) } } ) { @@ -685,11 +586,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene when (annotation.tag) { "USER_AGREEMENT" -> { // 打开用户协议 - openAgreement(context, agreementUrl) + openAgreement(context = context, title = "用户协议", url = agreementUrl) } "PRIVACY_POLICY" -> { // 打开隐私政策 - openAgreement(context, privacyUrl) + openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } } @@ -783,9 +684,9 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener // 设置可点击的协议文本 AgreementTextHelper.setupAgreementTextView(agreementText, targets, agreementTextView, isUnderlineText = false) { agreementType -> when (agreementType) { - "serviceAgreement" -> openAgreement(context, privacyUrl) - "userAgreement" -> openAgreement(context, agreementUrl) - "privacyAgreement" -> openAgreement(context, privacyUrl) + "serviceAgreement" -> openAgreement(context = context, title = privacyName, url = privacyUrl) + "userAgreement" -> openAgreement(context = context, title = "用户协议", url = agreementUrl) + "privacyAgreement" -> openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } @@ -808,6 +709,357 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener } } +@Composable +private fun WxLoginScreen( + context: Context, + viewModel: LoginViewModel, +) { + Column { + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = null, + modifier = Modifier + .size(86.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "截图兔", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + ) + + Text( + text = "为了更好地为您提供服务,请先完成微信授权", + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color(0xFFAAAAAA), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .align(Alignment.CenterHorizontally) + ) + + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 8.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 启动微信验证,请求登录 + if (viewModel.isPolicyAgreement.value) { + //TODO 打开微信登录 + viewModel.loginWithWechat(context) + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.drawable.ic_wx_icon), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .padding(start = 12.dp) + .align(Alignment.CenterVertically) + ) + + Text( + "微信授权登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "USER_AGREEMENT" -> { + // 打开用户协议 + openAgreement(context = context, title = "用户协议", url = agreementUrl) + } + "PRIVACY_POLICY" -> { + // 打开隐私政策 + openAgreement(context = context, title = "隐私政策", url = privacyUrl) + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} + +@Composable +private fun AliPayLoginScreen( + context: Context, + viewModel: LoginViewModel, +) { + Column{ + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = null, + modifier = Modifier + .size(86.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "截图兔", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + ) + + Text( + text = "为了更好地为您提供服务,请先完成支付宝授权", + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color(0xFFAAAAAA), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .align(Alignment.CenterHorizontally) + ) + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 8.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 启动支付宝验证,请求登录 + if (viewModel.isPolicyAgreement.value) { + // 打开支付宝登录 + viewModel.requestAliPayAuthParam() + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.drawable.ic_alipay_icon), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .padding(start = 12.dp) + .align(Alignment.CenterVertically) + ) + + Text( + "支付宝授权登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "USER_AGREEMENT" -> { + // 打开用户协议 + openAgreement(context = context, title = "用户协议", url = agreementUrl) + } + "PRIVACY_POLICY" -> { + // 打开隐私政策 + openAgreement(context = context, title = "隐私政策", url = privacyUrl) + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} + /** * 一键登录页 * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) @@ -993,7 +1245,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) { @Composable private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { - + val scope = rememberCoroutineScope() Column( modifier = Modifier .fillMaxWidth() @@ -1057,8 +1309,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { indication = null, interactionSource = remember { MutableInteractionSource() } ) { - //TODO 打开微信登录 - Toast.makeText(context, "打开微信登录", Toast.LENGTH_SHORT).show() + /* + if (viewModel.isPolicyAgreement.value) { + //TODO 打开微信登录 + viewModel.loginWithWechat(context) + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + */ + // 微信登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_WX } ) Image( @@ -1072,8 +1336,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { indication = null, interactionSource = remember { MutableInteractionSource() } ) { - //TODO 打开支付宝登录 - Toast.makeText(context, "打开支付宝登录", Toast.LENGTH_SHORT).show() + /* + if (viewModel.isPolicyAgreement.value) { + // 打开支付宝登录 + viewModel.requestAliPayAuthParam() + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + */ + // 支付宝登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_ALIPAY } ) Image( @@ -1152,9 +1428,14 @@ private fun oneKeyLogin( override fun onSuccess(response: GYResponse?) { //TODO 登录成功,需要与后端交互 Log.i("OneKeyLogin", "onSuccess:$response") - //TODO 登录成功后,保存 token - generalViewModel.kv.encode("token", "123232123231231") - viewModel.setLogin(true) + try { + val jsonObject = JSONObject(response?.msg?:"{}") + val data = jsonObject.getJSONObject("data") + val token = data.getString("token") + viewModel.requestOneKeyLogin(response?.gyuid?:"", token) + } catch (e: Exception) { + e.printStackTrace() + } } override fun onFailed(p0: GYResponse?) { @@ -1169,10 +1450,32 @@ enum class LoginScreenType { LOGIN_NORMAL, LOGIN_ONE_KEY, LOGIN_CAPTCHA, + LOGIN_WX, + LOGIN_ALIPAY, } -@Preview +@Preview(showBackground = true) @Composable private fun PreviewOneKeyLoginScreen() { OneKeyLoginScreen(LocalContext.current, viewModel(), viewModel()) -} \ No newline at end of file +} + + +@Preview(showBackground = true) +@Composable +private fun PreviewWxLoginScreen() { + WxLoginScreen(LocalContext.current, viewModel()) +} + + +@Preview(showBackground = true) +@Composable +private fun PreviewAliPayLoginScreen() { + AliPayLoginScreen(LocalContext.current, viewModel()) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewLoginScreen() { + LoginScreen(navController = rememberNavController(), generalViewModel = viewModel(), loginViewModel = viewModel(), isVisibilityBreak = false) +} diff --git a/app/src/main/java/com/img/rabbit/pages/MainPage.kt b/app/src/main/java/com/img/rabbit/pages/MainPage.kt index f8dca7d..37ed9d3 100644 --- a/app/src/main/java/com/img/rabbit/pages/MainPage.kt +++ b/app/src/main/java/com/img/rabbit/pages/MainPage.kt @@ -196,7 +196,8 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode LoginScreen( navController = navController, generalViewModel = generalViewModel, - loginViewModel = loginViewModel + loginViewModel = loginViewModel, + isVisibilityBreak = true ) } } diff --git a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt index 51c4310..70efe43 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -37,10 +38,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage import com.img.rabbit.R +import com.img.rabbit.provider.storage.PreferenceUtil import com.img.rabbit.viewmodel.GeneralViewModel @Composable @@ -50,6 +54,7 @@ fun MineScreen( ) { val context = LocalContext.current val vipMember by remember { mutableStateOf(false) } + val userInfo by remember { mutableStateOf(PreferenceUtil.loginUserInfo()) } // 监听返回事件 val currentBackStackEntry = navController.currentBackStackEntry @@ -62,6 +67,7 @@ fun MineScreen( } } + Box( modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9)) ){ @@ -80,21 +86,41 @@ fun MineScreen( Row( modifier = Modifier.fillMaxWidth() ) { - Image( - painter = painterResource(id = R.mipmap.ic_user_avatar_default), - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .size(64.dp) - .clip(RoundedCornerShape(90.dp)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - // 处理点击事件 - Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() - } - ) + if(userInfo == null){ + Image( + painter = painterResource(id = R.mipmap.ic_user_avatar_default), + contentDescription = "用户头像", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(90.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() + } + ) + }else{ + AsyncImage( + model = userInfo?.avater, + contentDescription = "用户头像", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(90.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() + }, + fallback = painterResource(id = R.mipmap.ic_user_avatar_default), + error = painterResource(id = R.mipmap.ic_user_avatar_default) + ) + } Column( modifier = Modifier .padding(start = 16.dp) @@ -103,28 +129,62 @@ fun MineScreen( indication = null, interactionSource = remember { MutableInteractionSource() } ) { - // 隐藏TabBar - generalViewModel.setNavigationBarVisible(false) - // 跳转登录页面 - navController.navigate("login") + if(userInfo == null){ + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转登录页面 + navController.navigate("login") + } else { + //TODO 已登录,跳转个人信息页面 + //navController.navigate("userInfo") + } } ) { Text( - text = "登录/注册", + text = if(userInfo == null){ "登录/注册" }else{ userInfo?.name?:"" }, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1A1A1A), modifier = Modifier.wrapContentSize() ) - Text( - text = "登录体验更多功能哦~", - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = Color(0xFF767676), + Row( modifier = Modifier - .wrapContentSize() .padding(top = 10.dp) - ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + // 点击复制ID + if (userInfo != null) { + val clipboardManager = android.content.Context.CLIPBOARD_SERVICE + val clipboard = context.getSystemService(clipboardManager) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("User ID", userInfo?.user_id) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show() + } + } + ){ + Text( + text = if(userInfo == null){ "登录体验更多功能哦~" }else{ "ID:${userInfo?.user_id?:""}" }, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentSize() + ) + Box( + modifier = Modifier + .size(4.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_copy), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(12.dp) + .align(Alignment.CenterVertically) + ) + } } } diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt index d2564f0..b26d1ae 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt @@ -64,8 +64,8 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import coil3.compose.AsyncImage import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.HairstyleBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.HairstyleBean import com.img.rabbit.components.AppearanceType import com.img.rabbit.components.DrawingBoardPicker import com.img.rabbit.config.CommonData.clothingForFemales diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt index 4f3ca39..03e517b 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt @@ -1,75 +1,46 @@ package com.img.rabbit.pages.screen.make -import android.annotation.SuppressLint import android.graphics.Bitmap import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController -import com.img.rabbit.bean.LongImageBean +import com.img.rabbit.bean.local.LongImageBean import com.img.rabbit.pages.toolbar.TitleBar import com.img.rabbit.utils.ExportFormat import com.img.rabbit.utils.ImageUtils.getBitmapFromUri diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt index 5546738..fa11b97 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,8 +33,8 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.img.rabbit.R -import com.img.rabbit.config.Common.agreementUrl -import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.toolbar.TitleBar import com.img.rabbit.utils.UrlLinkUtils.openAgreement @@ -101,7 +100,7 @@ fun AboutScreen(navController: NavHostController) { interactionSource = remember { MutableInteractionSource() } ) { // 跳转用户协议页面 - openAgreement(context, agreementUrl) + openAgreement(context = context, title = "用户协议", url = agreementUrl) }, verticalAlignment = Alignment.CenterVertically ) { @@ -144,7 +143,7 @@ fun AboutScreen(navController: NavHostController) { interactionSource = remember { MutableInteractionSource() } ) { // 跳转隐私政策页面 - openAgreement(context, privacyUrl) + openAgreement(context = context, title = "隐私政策", url = privacyUrl) }, verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt index 63db93b..03fab5c 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,7 +39,7 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.img.rabbit.R -import com.img.rabbit.bean.UserInfo +import com.img.rabbit.bean.local.UserInfo import com.img.rabbit.pages.toolbar.TitleBar @Composable @@ -48,9 +47,9 @@ fun AccountManagerScreen(navController: NavHostController) { val userList by remember { mutableStateOf( listOf( - UserInfo(1, "张三", true), - UserInfo(2, "李四", false), - UserInfo(3, "王五", false), + UserInfo(1, "张三", "https://cdn.batiao8.com/jietutu/logo.png","",true), + UserInfo(2, "李四", "https://cdn.batiao8.com/jietutu/logo.png","",false), + UserInfo(3, "王五", "https://cdn.batiao8.com/jietutu/logo.png","",false), ) ) } diff --git a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt index d3ff909..bdcbc18 100644 --- a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt +++ b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt @@ -31,7 +31,7 @@ import androidx.navigation.compose.rememberNavController import com.img.rabbit.R @Composable -fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, onSubmit: (() -> Unit)? = null) { +fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, showBreak: Boolean = true, onSubmit: (() -> Unit)? = null) { Box( modifier = Modifier.fillMaxWidth().padding(paddingValues) ){ @@ -40,17 +40,19 @@ fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: .fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - // 返回按钮 - Icon( - painter = painterResource(id = R.mipmap.ic_back), - contentDescription = "返回", - modifier = Modifier - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { navController?.popBackStack() } - .padding(end = 26.dp) - ) + if(showBreak){ + // 返回按钮 + Icon( + painter = painterResource(id = R.mipmap.ic_back), + contentDescription = "返回", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { navController?.popBackStack() } + .padding(end = 26.dp) + ) + } Column( modifier = Modifier.fillMaxWidth().weight(1f) ) { diff --git a/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt new file mode 100644 index 0000000..186c392 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt @@ -0,0 +1,110 @@ +@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + +package com.img.rabbit.provider.api + +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.provider.utils.HeaderInterceptor +import com.img.rabbit.provider.utils.RequestInterceptor +import com.img.rabbit.provider.utils.ResponseInterceptor +import com.img.rabbit.provider.utils.getUnsafeOkHttpClient +import com.img.rabbit.viewmodel.`interface`.ServiceVo +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object ApiManager { + private lateinit var retrofit: Retrofit + private lateinit var unsafeRetrofit: Retrofit + lateinit var serviceVo: ServiceVo + + init { + initialize() + } + + // 初始化方法,抽离出来以便重新初始化 + private fun initialize() { + // 获取基础URL并确保其包含http/https协议 + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + + if (!::retrofit.isInitialized) { + val client = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .addInterceptor(RequestInterceptor()) + .addInterceptor(HeaderInterceptor()) + .build() + + retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + if (!::unsafeRetrofit.isInitialized) { + val client = getUnsafeOkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .addInterceptor(HeaderInterceptor()) + .addInterceptor(RequestInterceptor()) + .addInterceptor(ResponseInterceptor()) + .build() + + unsafeRetrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + if (!::serviceVo.isInitialized) { + serviceVo = unsafeRetrofit.create(ServiceVo::class.java) + } + } + + // 添加重新初始化方法,用于在baseUrl改变时调用 + fun reinitialize() { + // 重置所有实例,使其在下一次访问时重新初始化 + if (::retrofit.isInitialized) { + synchronized(this) { + // 标记为未初始化 + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("retrofit").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + if (::unsafeRetrofit.isInitialized) { + synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("unsafeRetrofit").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + if (::serviceVo.isInitialized) { + synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("serviceVo").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + // 重新初始化 + initialize() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt new file mode 100644 index 0000000..8cf0621 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt @@ -0,0 +1,9 @@ +package com.img.rabbit.provider.api + +class ResultVo(val code: Int, val data: T) { + val status: Boolean + get() { + return code == 0 + } + val message: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt new file mode 100644 index 0000000..31550eb --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt @@ -0,0 +1,49 @@ +package com.img.rabbit.provider.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.storeData: DataStore by preferencesDataStore(name = "global_state") + +class GlobalStateManager( + private val context: Context +) { + companion object { + private val GLOBAL_LOADING = booleanPreferencesKey("global_loading") + private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization") + } + + suspend fun storeGlobalLoading(value: Boolean) { + context.storeData.edit { preferences -> + preferences[GLOBAL_LOADING] = value + } + } + fun isGlobalLoadingFlow(): Flow { + return context.storeData.data.map { + preferences -> + preferences[GLOBAL_LOADING] + } + } + + + + suspend fun storeGlobalWxAuthorization(value: String) { + context.storeData.edit { preferences -> + preferences[GLOBAL_WX_AUTHORIZATION] = value + } + } + fun globalWxAuthorizationFlow(): Flow { + return context.storeData.data.map { + preferences -> + preferences[GLOBAL_WX_AUTHORIZATION] + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt new file mode 100644 index 0000000..6defd3f --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt @@ -0,0 +1,93 @@ +package com.img.rabbit.provider.storage + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.img.rabbit.bean.response.UserEntity +import com.tencent.mmkv.MMKV + +/** + * SharedPreferences工具类,用于简化数据持久化操作 + */ +object PreferenceUtil { + private const val KEY_ACCESS_TOKEN = "access_token" + private const val KEY_X_TOKEN = "x_token" + + private const val KEY_USER_INFO = "user_info" + + + + // Gson实例 + private val gson: Gson = GsonBuilder().create() + + val kv by lazy { MMKV.defaultMMKV() } + + + //服务器时间和本地时间的偏移量 + private var timeDiff = 0L + + + /** + * 保存AccessToken + */ + fun saveAccessToken(token: String?) { + kv.encode(KEY_ACCESS_TOKEN, token) + } + + /** + * 获取保存的AccessToken + */ + fun getAccessToken(): String? { + return kv.decodeString(KEY_ACCESS_TOKEN, null) + } + + fun saveXToken(token: String?) { + kv.encode(KEY_X_TOKEN, token) + } + + fun getXToken(): String? { + return kv.decodeString(KEY_X_TOKEN, null) + } + + + fun getUserInfos(): MutableList? { + /** + *[{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"},{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"}] + */ + return gson.fromJson(kv.decodeString(KEY_USER_INFO, "[]"), Array::class.java)?.toMutableList() + } + + fun loginUserInfo(): UserEntity?{ + return getUserInfos()?.find { it.isLogin } + } + + fun saveUserInfo(userEntity: UserEntity) { + val userInfos = getUserInfos()?: mutableListOf() + val isContain = userInfos.find { it.user_id == userEntity.user_id } != null + if(!isContain){ + userInfos.add(userEntity) + } + + kv.encode(KEY_USER_INFO, gson.toJson(userInfos)) + } + + fun removeUserInfo(userEntity: UserEntity) { + val userInfos = getUserInfos()?: mutableListOf() + userInfos.removeIf { it.user_id == userEntity.user_id } + kv.encode(KEY_USER_INFO, gson.toJson(userInfos)) + } + + /** + * 清除所有数据 + */ + fun clearAll() { + kv.clearAll() + } + + //真实的服务器时间 + fun serverTimeMillis(): Long { + return System.currentTimeMillis() + timeDiff * 1000 + } + fun setTimeDiff(timeDiff: Long) { + this.timeDiff = timeDiff + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt new file mode 100644 index 0000000..e9b6496 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt @@ -0,0 +1,53 @@ +package com.img.rabbit.provider.utils + +import android.annotation.SuppressLint +import okhttp3.OkHttpClient +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +@SuppressLint("CustomX509TrustManager") +fun getUnsafeOkHttpClient(): OkHttpClient { + try { + // 创建一个信任所有证书的 TrustManager + val trustAllCerts = arrayOf( + + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } + + override fun getAcceptedIssuers(): Array = + arrayOf() + } + ) + + // 初始化 SSLContext + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, SecureRandom()) + + // 创建一个允许所有主机名验证的 HostnameVerifier + val allHostsValid = HostnameVerifier { _, _ -> true } + + // 创建 OkHttpClient 并配置 SSL 和主机名验证 + return OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier(allHostsValid) + .build() + } catch (e: Exception) { + throw RuntimeException(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt new file mode 100644 index 0000000..953283e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt @@ -0,0 +1,46 @@ +package com.img.rabbit.provider.utils + +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.BuildConfig +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class HeaderInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + try { + val mBuilder = original.newBuilder() + val accessToken = PreferenceUtil.getXToken()?:"" + mBuilder.header("x-token", accessToken) + mBuilder.header("x-platform", "android") + mBuilder.header("x-mobile-brand", android.os.Build.BRAND) + mBuilder.header("x-mobile-model", android.os.Build.MODEL) + mBuilder.header("x-package", BuildConfig.APPLICATION_ID) + + val request = mBuilder + .method(original.method, original.body) + .build() + + val response = chain.proceed(request) + /* + // 获取响应头 + val headers = response.headers + // 处理响应头 + headers.forEach { header -> + //Log.d(LOG_REQUEST, "ResponseHeader:${header.first} ${header.second}") + if (header.first == "Access-Token") { + authorization = header.second + } + } + */ + + return response + } catch (e: Exception) { + e.printStackTrace() + return chain.proceed(original) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt new file mode 100644 index 0000000..980a03e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt @@ -0,0 +1,177 @@ +package com.img.rabbit.provider.utils + +import android.text.TextUtils +import android.util.Log +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.config.Constants.LOG_REQUEST +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.StringUtils +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Arrays +import java.util.Date +import java.util.Locale + +/** + * 加密数据 + */ +class RequestInterceptor : Interceptor { + private val TAG = "Encryption" + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) + + override fun intercept(chain: Interceptor.Chain): Response { + val startTime = System.currentTimeMillis() + val request = chain.request() + + // 记录请求信息 + logRequest(request) + + var modifiedRequest = request + val method = request.method.lowercase(Locale.getDefault()).trim { it <= ' ' } + val url = request.url + val apiPath = String.format("%s", url) + + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + + // 如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(baseUrl)) { + val response = chain.proceed(modifiedRequest) + return response + } + + /* + L.d("TAG-->>url=$apiPath") + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(Constants.BaseUrl)) { + L.d("TAG-->>content-type=${request.headers["Content-Type"]}") + val requestBody = request.body + L.d("TAG-->>${requestBody.toString()}") + return chain.proceed(request) + } + */ + + var queryString = url.encodedQuery + queryString = if (!TextUtils.isEmpty(queryString)) { + (queryString + "&nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000 + } else { + ("nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000 + } + val sortQueryString = Arrays.stream(queryString.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .sorted { obj: String, anotherString: String? -> obj.compareTo(anotherString!!) } + .reduce { x: String, y: String -> "$x&$y" } + .get() + //如果请求方式是get或者delete 则没有body + var paramsStr = "" + var bytes = ByteArray(0) + var signature: String? = "" + if (method == "put" || method == "post") { + val requestBody = request.body + Log.w(TAG, "TAG-->>${requestBody.toString()}") + val buffer = Buffer() + requestBody!!.writeTo(buffer) + bytes = StringUtils.addByte(bytes, buffer.readByteArray()) + Log.w(TAG, "签名后bodyByte的长度为" + bytes.size) + paramsStr = String(bytes, StandardCharsets.UTF_8) + // paramsStr = buffer.readUtf8(); + Log.w(TAG, "签名后body的长度为" + paramsStr.length) + } + signature = if (bytes.isNotEmpty()) { + Log.w(TAG, "当前的数组长度为----" + bytes.size) + StringUtils.getMD5Byte( + StringUtils.addByte( + StringUtils.addByte("$sortQueryString&".toByteArray(), bytes), + ("&" + StringUtils.getMD5String(Constants.Signature)).toByteArray() + ) + ) + } else { + StringUtils.getMD5String(sortQueryString + "&" + StringUtils.getMD5String(Constants.Signature)) + } + val newUrl = apiPath.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + "?" + queryString + "&signature=" + signature + Log.w(TAG, "签名后的路径--->$newUrl") + /* + request = request.newBuilder().url(newUrl).build() + return chain.proceed(request) + */ + + modifiedRequest = request.newBuilder().url(newUrl).build() + val response = chain.proceed(modifiedRequest) + logResponse(response, startTime) + return response + } + + //---------------------------------> 以下为格式化请求日志打印 <--------------------------------- + private fun logRequest(request: okhttp3.Request) { + val timestamp = dateFormat.format(Date()) + val method = request.method + val url = request.url.toString() + + Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ 📤 请求部分 [$timestamp]") + Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ -->方法: $method") + Log.i(LOG_REQUEST,"│ -->URL: $url") + + // 记录请求体(如果有) + val requestBody = request.body + if (requestBody != null) { + try { + val buffer = Buffer() + requestBody.writeTo(buffer) + val bodyString = buffer.readUtf8() + if (bodyString.isNotEmpty()) { + Log.i(LOG_REQUEST,"│ -->请求参数:") + Log.i(LOG_REQUEST,"│ --> $bodyString") + } + } catch (e: IOException) { + Log.e(LOG_REQUEST,"│ -->读取请求体失败: ${e.message}") + } + }else{ + Log.i(LOG_REQUEST,"│ -->无请求参数") + } + Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────") + } + + private fun logResponse(response: Response, startTime: Long) { + val timestamp = dateFormat.format(Date()) + val duration = System.currentTimeMillis() - startTime + val code = response.code + val message = response.message + val url = response.request.url.toString() + val apiPath = url.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + + Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ 📥 响应部分 [$timestamp]") + Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ URL: $url") + Log.i(LOG_REQUEST,"│ API: $apiPath") + Log.i(LOG_REQUEST,"│ 状态码: $code $message") + Log.i(LOG_REQUEST,"│ 耗时: ${duration}ms") + + // 记录响应体 + try { + val responseBody = response.peekBody(1024 * 1024L) // 最多读取1MB + val bodyString = responseBody.string() + if (bodyString.isNotEmpty()) { + Log.i(LOG_REQUEST,"│ 响应内容: (前${minOf(bodyString.length, 2000)}字符):") + val preview = if (bodyString.length > 2000) { + bodyString.take(2000) + "..." + } else { + bodyString + } + Log.i(LOG_REQUEST,"│ $preview") + } + } catch (e: IOException) { + Log.e(LOG_REQUEST,"│ 读取响应体失败: ${e.message}") + } + Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt new file mode 100644 index 0000000..5f656c5 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt @@ -0,0 +1,86 @@ +package com.img.rabbit.provider.utils + +import android.content.Intent +import android.text.TextUtils +import android.util.Log +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.utils.AESpkcs7paddingUtil +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONObject +import java.nio.charset.Charset +import java.nio.charset.UnsupportedCharsetException + + +/** + * 解密数据 + */ +class ResponseInterceptor : Interceptor { + private val TAG = "Decode" + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url + val apiPath = String.format("%s", url) + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(baseUrl)) { + return chain.proceed(request) + } + val response = chain.proceed(request) + var charset = Charset.forName("UTF-8") + val contentType = response.header("Content-Type") + if (contentType != null && contentType.contains("application/json")) { + val responseBody = response.body + val source = responseBody.source() + source.request(Long.MAX_VALUE) + val buffer = source.buffer + val mediaType = responseBody.contentType() + if (mediaType != null) { + try { + charset = mediaType.charset(Charset.forName("UTF-8")) + } catch (e: UnsupportedCharsetException) { + e.printStackTrace() + } + } + val respBody = buffer.clone().readString(charset!!) + Log.w(TAG, "response=${respBody}") + if (TextUtils.isEmpty(respBody)) { + val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(responseBody).build() + } + + val isEncrypt = JSONObject(respBody).optBoolean("encrypt") + Log.w(TAG, "是否需要解密:$isEncrypt") + if (!isEncrypt) { //如果不需要加密 直接返回 + Log.w(TAG, "response=${response.body}") + val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(responseBody).build() + } + val decrybody = JSONObject(respBody).optString("data") + Log.w(TAG, "Json解析后的字符串为---->$decrybody") + var decryString: String? + try { + decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt) + Log.e("ResponseInterceptor", "解密后返回的字符串为---->$decryString") + + //这里可以通过code处理token过去过期的情况 + //val decCode = JSONObject(decryString).optInt("code") + + //返回新创建的response + val responseCode = if (response.code == 400 && decrybody != null) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(decryString.toResponseBody("text/plain".toMediaType())).build() + } catch (e: Exception) { + e.printStackTrace() + } + } + return response + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt new file mode 100644 index 0000000..e4578fe --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt @@ -0,0 +1,30 @@ +package com.img.rabbit.utils + + +import android.util.Base64 +import io.github.fastaes.FastAES + +object AESpkcs7paddingUtil { + + /** + * 编码格式 + */ + const val ENCODING = "utf-8" + + /** + * AES解密 + * + * @param encryptStr 加密后的密文 + * @param key 密钥 + * @return 源字符串 + * @throws Exception + */ + @Throws(Exception::class) + fun decryptNormal(encryptStr: String?, key: String): String { + val sourceBytes = Base64.decode(encryptStr, Base64.NO_WRAP) + val keyBytes = key.toByteArray(charset(ENCODING)) + val plain: ByteArray = FastAES.decrypt(sourceBytes, keyBytes, key.substring(0, 16).toByteArray(charset(ENCODING))) + return String(plain) + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java index c339e10..08c19cd 100644 --- a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java +++ b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java @@ -39,8 +39,8 @@ public class Bitmap2SVG return true; } - private boolean trc_del; - private PrintWriter pw; + private final boolean trc_del; + private final PrintWriter pw; private int w, h; private Bitmap2SVG( PrintWriter pw, boolean trc_delete ) @@ -116,11 +116,13 @@ public class Bitmap2SVG byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { + assert os != null; os.write(buffer, 0, len); } // 此时文件已带上正确的 MIME 类型存入相册 Toast.makeText(context, "SVG已保存", Toast.LENGTH_SHORT).show(); } catch (IOException e) { + //noinspection CallToPrintStackTrace e.printStackTrace(); } } diff --git a/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt new file mode 100644 index 0000000..547fd5c --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt @@ -0,0 +1,56 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.text.TextUtils +import com.bytedance.hume.readapk.HumeSDK +import com.img.rabbit.BuildConfig +import com.kwai.monitor.payload.TurboHelper +import com.tencent.vasdolly.helper.ChannelReaderUtil + + +object ChannelUtils { + + fun getChannel(context: Context): String { + if (BuildConfig.DEBUG) { + MMKVUtils.put("app_channel", "test") + } else { + if (TextUtils.isEmpty(MMKVUtils.getString("app_channel"))) { + MMKVUtils.put("app_channel", getChannelBy(context)) + } + } + return MMKVUtils.getString("app_channel") ?: "" + } + + private fun getChannelBy(context: Context): String? { + val kuaishou = TurboHelper.getChannel(context) + if (!TextUtils.isEmpty(kuaishou)) { + return kuaishou + } + val tengxun = ChannelReaderUtil.getChannel(context) + if (!TextUtils.isEmpty(tengxun)) { + return tengxun + } + val juliang = HumeSDK.getChannel(context) + if (!TextUtils.isEmpty(juliang)) { + return juliang + } + return channel(context) + } + + private fun channel(context: Context): String { + try { + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + val channel = appInfo.metaData.getString("UMENG_CHANNEL") + if (!TextUtils.isEmpty(channel)) { + return channel!! + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + +} + diff --git a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt index 6b9c403..c9dfb6c 100644 --- a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt @@ -20,10 +20,9 @@ import kotlin.apply import android.graphics.* import androidx.core.graphics.createBitmap -import com.img.rabbit.bean.LongImageBean +import com.img.rabbit.bean.local.LongImageBean import java.io.ByteArrayOutputStream import java.io.OutputStream -import androidx.core.graphics.withClip object ImageUtils { fun decodeSampledBitmapFromResource( diff --git a/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt new file mode 100644 index 0000000..29214b1 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt @@ -0,0 +1,99 @@ +package com.img.rabbit.utils + +import android.os.Parcelable +import com.tencent.mmkv.MMKV +import java.util.Collections + +object MMKVUtils { + + var mmkv: MMKV? = null + + init { + mmkv = MMKV.defaultMMKV() + } + + fun put(key: String, value: Any?): Boolean { + return when (value) { + is String -> mmkv?.encode(key, value)!! + is Float -> mmkv?.encode(key, value)!! + is Boolean -> mmkv?.encode(key, value)!! + is Int -> mmkv?.encode(key, value)!! + is Long -> mmkv?.encode(key, value)!! + is Double -> mmkv?.encode(key, value)!! + is ByteArray -> mmkv?.encode(key, value)!! + else -> false + } + } + + /** + * 这里使用安卓自带的Parcelable序列化,它比java支持的Serializer序列化性能好些 + */ + fun put(key: String, t: T?): Boolean { + if (t == null) { + return false + } + return mmkv?.encode(key, t)!! + } + + fun put(key: String, sets: Set?): Boolean { + if (sets == null) { + return false + } + return mmkv?.encode(key, sets)!! + } + + fun getInt(key: String): Int? { + return mmkv?.decodeInt(key, 0) + } + + fun getDouble(key: String): Double? { + return mmkv?.decodeDouble(key, 0.00) + } + + fun getLong(key: String): Long? { + return mmkv?.decodeLong(key, 0L) + } + + fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { + return mmkv?.decodeBool(key, defaultValue) ?: defaultValue + } + + fun getFloat(key: String): Float? { + return mmkv?.decodeFloat(key, 0F) + } + + fun getByteArray(key: String): ByteArray? { + return mmkv?.decodeBytes(key) + } + + fun getString(key: String): String? { + return mmkv?.decodeString(key, "") + } + + /** + * SpUtils.getParcelable("") + */ + inline fun getParcelable(key: String): T? { + return mmkv?.decodeParcelable(key, T::class.java) + } + + fun getStringSet(key: String): Set? { + return mmkv?.decodeStringSet(key, Collections.emptySet()) + } + + /** + * 是否已经存在 + */ + fun contains(key: String): Boolean? { + return mmkv?.containsKey(key) + } + + fun removeKey(key: String) { + mmkv?.removeValueForKey(key) + } + + fun clearAll() { + mmkv?.clearAll() + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/StringUtils.kt b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt new file mode 100644 index 0000000..f70907a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt @@ -0,0 +1,494 @@ +package com.img.rabbit.utils + +import android.text.TextUtils +import java.net.URLDecoder +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Hashtable +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ThreadLocalRandom +import java.util.regex.Matcher +import java.util.regex.Pattern + + +object StringUtils { + /** + * 功能:身份证的有效验证 + * + * @param IDStr 身份证号 + * @return 有效:返回"" 无效:返回String信息 + * @throws ParseException + */ + @Throws(ParseException::class) + fun IDCardValidate(IDStr: String): Boolean { + var errorInfo = "" // 记录错误信息 + val ValCodeArr = arrayOf( + "1", "0", "x", "9", "8", "7", "6", "5", "4", + "3", "2" + ) + val Wi = arrayOf( + "7", "9", "10", "5", "8", "4", "2", "1", "6", "3", "7", + "9", "10", "5", "8", "4", "2" + ) + var Ai = "" + // ================号码的长度 15位或18位 ================ + if (IDStr.length != 15 && IDStr.length != 18) { + errorInfo = "身份证号码长度应该为15位或18位。" + return false + } + // =======================(end)======================== + + // ================ 数字 除最后以为都为数字================ + if (IDStr.length == 18) { + Ai = IDStr.substring(0, 17) + } else if (IDStr.length == 15) { + Ai = IDStr.substring(0, 6) + "19" + IDStr.substring(6, 15) + } + if (isNumeric(Ai) == false) { + errorInfo = "身份证15位号码都应为数字 ; 18位号码除最后一位外,都应为数字。" + return false + } + // =======================(end)======================== + + // ================ 出生年月是否有效 ================ + val strYear = Ai.substring(6, 10) // 年份 + val strMonth = Ai.substring(10, 12) // 月份 + val strDay = Ai.substring(12, 14) // 月份 + if (isDataFormat("$strYear-$strMonth-$strDay") == false) { + errorInfo = "身份证生日无效。" + return false + } + val gc = GregorianCalendar() + val s = SimpleDateFormat("yyyy-MM-dd") + try { + if (gc[Calendar.YEAR] - strYear.toInt() > 150 + || gc.time.time - s.parse( + "$strYear-$strMonth-$strDay" + ).time < 0 + ) { + errorInfo = "身份证生日不在有效范围。" + return false + } + } catch (e: NumberFormatException) { + // TODO Auto-generated catch block + e.printStackTrace() + } catch (e: ParseException) { + // TODO Auto-generated catch block + e.printStackTrace() + } + if (strMonth.toInt() > 12 || strMonth.toInt() == 0) { + errorInfo = "身份证月份无效" + return false + } + if (strDay.toInt() > 31 || strDay.toInt() == 0) { + errorInfo = "身份证日期无效" + return false + } + // =====================(end)===================== + + // ================ 地区码时候有效================ + val h = GetAreaCode() + if (h[Ai.substring(0, 2)] == null) { + errorInfo = "身份证地区编码错误。" + return false + } + // ============================================== + + // ================ 判断最后一位的值================ + var TotalmulAiWi = 0 + for (i in 0..16) { + TotalmulAiWi = (TotalmulAiWi + + Ai[i].toString().toInt() * Wi[i].toInt()) + } + val modValue = TotalmulAiWi % 11 + val strVerifyCode = ValCodeArr[modValue] + Ai = Ai + strVerifyCode + if (IDStr.length == 18) { + if (Ai == IDStr == false) { + errorInfo = "身份证无效,不是合法的身份证号码" + return false + } + } else { + return true + } + // =====================(end)===================== + return true + } + + /** + * 功能:判断字符串是否为数字 + * + * @param str + * @return + */ + private fun isNumeric(str: String): Boolean { + val pattern = Pattern.compile("[0-9]*") + val isNum = pattern.matcher(str) + return if (isNum.matches()) { + true + } else { + false + } + } + + /** + * 功能:设置地区编码 + * + * @return Hashtable 对象 + */ + private fun GetAreaCode(): Hashtable { + val hashtable = Hashtable() + hashtable["11"] = "北京" + hashtable["12"] = "天津" + hashtable["13"] = "河北" + hashtable["14"] = "山西" + hashtable["15"] = "内蒙古" + hashtable["21"] = "辽宁" + hashtable["22"] = "吉林" + hashtable["23"] = "黑龙江" + hashtable["31"] = "上海" + hashtable["32"] = "江苏" + hashtable["33"] = "浙江" + hashtable["34"] = "安徽" + hashtable["35"] = "福建" + hashtable["36"] = "江西" + hashtable["37"] = "山东" + hashtable["41"] = "河南" + hashtable["42"] = "湖北" + hashtable["43"] = "湖南" + hashtable["44"] = "广东" + hashtable["45"] = "广西" + hashtable["46"] = "海南" + hashtable["50"] = "重庆" + hashtable["51"] = "四川" + hashtable["52"] = "贵州" + hashtable["53"] = "云南" + hashtable["54"] = "西藏" + hashtable["61"] = "陕西" + hashtable["62"] = "甘肃" + hashtable["63"] = "青海" + hashtable["64"] = "宁夏" + hashtable["65"] = "新疆" + hashtable["71"] = "台湾" + hashtable["81"] = "香港" + hashtable["82"] = "澳门" + hashtable["91"] = "国外" + return hashtable + } + + /** + * 验证日期字符串是否是YYYY-MM-DD格式 + * + * @param str + * @return + */ + private fun isDataFormat(str: String): Boolean { + var flag = false + // String + // regxStr="[1-9][0-9]{3}-[0-1][0-2]-((0[1-9])|([12][0-9])|(3[01]))"; + val regxStr = + "^((\\d{2}(([02468][048])|([13579][26]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))(\\s(((0?[0-9])|([1-2][0-3]))\\:([0-5]?[0-9])((\\s)|(\\:([0-5]?[0-9])))))?$" + val pattern1 = Pattern.compile(regxStr) + val isNo = pattern1.matcher(str) + if (isNo.matches()) { + flag = true + } + return flag + } + //2.判断字符串是否是邮箱: + /** + * 描述:是否是邮箱. + * + * @param str 指定的字符串 + * @return 是否是邮箱:是为true,否则false + */ + fun isEmail(str: String): Boolean { + var isEmail = false + val expr = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$" + if (str.matches(expr.toRegex())) { + isEmail = true + } + return isEmail + } + //3.判断字符串是否是银行卡 + /** + * 判断是否是银行卡号 + * + * @param cardId + * @return + */ + fun checkBankCard(cardId: String): Boolean { + val bit = getBankCardCheckCode( + cardId + .substring(0, cardId.length - 1) + ) + return if (bit == 'N') { + false + } else cardId[cardId.length - 1] == bit + } + + private fun getBankCardCheckCode(nonCheckCodeCardId: String?): Char { + if (nonCheckCodeCardId == null || nonCheckCodeCardId.trim { it <= ' ' }.length == 0 || !nonCheckCodeCardId.matches("\\d+".toRegex())) { + // 如果传的不是数据返回N + return 'N' + } + val chs = nonCheckCodeCardId.trim { it <= ' ' }.toCharArray() + var luhmSum = 0 + var i = chs.size - 1 + var j = 0 + while (i >= 0) { + var k = chs[i].code - '0'.code + if (j % 2 == 0) { + k *= 2 + k = k / 10 + k % 10 + } + luhmSum += k + i-- + j++ + } + return if (luhmSum % 10 == 0) '0' else (10 - luhmSum % 10 + '0'.code).toChar() + } + //4、判断字符串是否是手机号 + /** + * 判断是否是手机号 + * + * @param phone + * @return + */ + fun checkPhone(phone: String?): Boolean { + val pattern = Pattern + .compile("^(13[0-9]|15[0-3]|15[5-9]|18[0-9]|14[57]|17[0678])\\d{8}$") + val matcher = pattern.matcher(phone) + return if (matcher.matches()) { + true + } else false + } + //5.判断字符串是否是中文或者包含中文 + /** + * 描述:判断一个字符串是否为null或空值. + * + * @param str 指定的字符串 + * @return true or false + */ + fun isEmpty(str: String?): Boolean { + return str == null || str.trim { it <= ' ' }.length == 0 + } + + /** + * 描述:是否是中文. + * + * @param str 指定的字符串 + * @return 是否是中文:是为true,否则false + */ + fun isChinese(str: String): Boolean { + var isChinese = true + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + } else { + isChinese = false + } + } + } + return isChinese + } + + /** + * 描述:是否包含中文. + * + * @param str 指定的字符串 + * @return 是否包含中文:是为true,否则false + */ + fun isContainChinese(str: String): Boolean { + var isChinese = false + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + isChinese = true + } else { + } + } + } + return isChinese + } + + /** + * 比较两个String的list 是否改变过 + * + * @param listNew + * @param listOld + * @return + */ + fun compareList(listNew: List?, listOld: List?): Boolean { + return if (listOld == null && listNew == null) { + false + } else if (listOld != null && listNew != null) { + if (listNew.size != listOld.size) { + true + } else !(listOld.containsAll(listNew) && listNew.containsAll(listOld)) + } else { + true + } + } + + /** + * 比较两个String 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareString(newStr: String?, oldStr: String?): Boolean { + return if (newStr == null && oldStr == null) { + false + } else if (newStr != null && oldStr != null) { + newStr != oldStr + } else { + true + } + } + + /** + * 比较签名 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareSign(newStr: String, oldStr: String): Boolean { + return if (TextUtils.isEmpty(oldStr)) { //当原始值没有的时候 无需验证 因为上报时需验证是否已签过字 而且当原始值有的时候 依据现有业务新值不可能清空 故无需再判断其他情况 + true + } else newStr != oldStr + } + + /** + * 随机生成一个UUID + */ + fun createUUID(): String { + return UUID.randomUUID().toString() + } + + fun createUUIDFromLong(): String { + return UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()).toString() + } + + /** + * MD5加密 + */ + fun getMD5String(data: String): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(data.toByteArray()) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + /** + * 对数组进行MD5加密 + * + * @param bytes + * @return + */ + fun getMD5Byte(bytes: ByteArray?): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(bytes) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + //两个数组进行相加 + fun addByte(array1: ByteArray, array2: ByteArray): ByteArray { + val combined = ByteArray(array1.size + array2.size) + System.arraycopy(array1, 0, combined, 0, array1.size) + System.arraycopy(array2, 0, combined, array1.size, array2.size) + return combined + } + + //格式化时间yyyy-mm-dd + fun getSimpleYYYYMMDD(str: String): String { + if (TextUtils.isEmpty(str)) return "" + + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) + val date = simpleDateFormat.parse(str) + return simpleDateFormat.format(date!!) + + } + + + /** + * 将字符串中的unicode字符转换为中文字符 + */ + fun convertUnicodeToCh(str: String): String { + var newStr = str + val pattern: Pattern = Pattern.compile("(\\\\u(\\w{4}))") + val matcher: Matcher = pattern.matcher(newStr) + + // 迭代,将str中的所有unicode转换为正常字符 + while (matcher.find()) { + val unicodeFull = matcher.group(1) // 匹配出的每个字的unicode,比如\u83b7 + val unicodeNum = matcher.group(2) // 匹配出每个字的数字,比如\u83b7,会匹配出u83b7 + + // 将匹配出的数字按照16进制转换为10进制,转换为char类型,就是对应的正常字符了 + @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + val singleChar = unicodeNum.toInt(16).toChar() + + // 替换原始字符串中的unicode码 + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + newStr = newStr.replace(unicodeFull, singleChar.toString() + "") + } + return newStr + } + + // 解析函数 + fun parseAlipayResult(rawResult: String): Map { + val resultMap = mutableMapOf() + val pairs = rawResult.split("&") + + for (pair in pairs) { + val keyValue = pair.split("=") + if (keyValue.size == 2) { + val key = URLDecoder.decode(keyValue[0], "UTF-8") + val value = URLDecoder.decode(keyValue[1], "UTF-8") + resultMap[key] = value + } + } + return resultMap + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt index ce376fa..ed09ad8 100644 --- a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt @@ -3,13 +3,22 @@ package com.img.rabbit.utils import android.content.Context import android.content.Intent import androidx.core.net.toUri +import com.img.rabbit.WebViewActivity object UrlLinkUtils { - fun openAgreement(context: Context, url: String) { - // 打开服务协议 - Intent(Intent.ACTION_VIEW, url.toUri()).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }.let { intent -> + fun openAgreement(context: Context, title: String, url: String, isExternal:Boolean = false) { + if(isExternal){ + // 打开服务协议 + Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.let { intent -> + context.startActivity(intent) + } + }else{ + val intent = Intent(context, WebViewActivity::class.java).apply { + putExtra("url", url) + putExtra("title", title) + } context.startActivity(intent) } } diff --git a/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..10c68e5 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt @@ -0,0 +1,26 @@ +package com.img.rabbit.viewmodel +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +open class BaseViewModel : ViewModel() { + var isShowMsg = mutableStateOf(false) + var msgContent = mutableStateOf("") + val isLoading = mutableStateOf(false) + + fun mLaunch(block: suspend () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + block() + } catch (e: Exception) { + withContext(Dispatchers.Main){ + isShowMsg.value = true + msgContent.value = "接口请求失败" + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt index 2bd5445..f462ce6 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt @@ -11,7 +11,12 @@ import android.os.Build import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.img.rabbit.provider.api.ApiManager +import com.img.rabbit.provider.storage.PreferenceUtil import com.tencent.mmkv.MMKV +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch @SuppressLint("ObsoleteSdkInt") class GeneralViewModel(application: Application) : AndroidViewModel(application) { @@ -38,6 +43,12 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) } } + private val _agreementStatus = MutableLiveData() + val agreementStatus: LiveData = _agreementStatus + private fun getIsAgreement(): Boolean{ + return kv.getBoolean("isAgreement", false) + } + init { // 注册网络监听 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -51,6 +62,8 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) // 初始化状态 _networkStatus.value = isNetworkAvailable() + // 初始化隐私政策状态 + _agreementStatus.value = getIsAgreement() } private fun isNetworkAvailable(): Boolean { @@ -69,8 +82,23 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) _isNavigationBarVisible.value = visible } + fun setIsAgreement(agreement: Boolean){ + kv.putBoolean("isAgreement", agreement) + _agreementStatus.value = agreement + } + override fun onCleared() { super.onCleared() connectivityManager.unregisterNetworkCallback(networkCallback) } + + @OptIn(DelicateCoroutinesApi::class) + fun getServerTime() { + GlobalScope.launch { + val response = ApiManager.serviceVo.getServerTime() + if (response.status) { + PreferenceUtil.setTimeDiff(response.data - System.currentTimeMillis() / 1000) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt index c42d8e9..2c3f7bf 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt @@ -1,29 +1,69 @@ package com.img.rabbit.viewmodel import android.app.Activity +import android.content.Context +import android.os.Build import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel import androidx.compose.runtime.State +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.alipay.sdk.app.AuthTask import com.img.rabbit.pages.LoginScreenType import com.g.gysdk.GYManager import com.g.gysdk.GYResponse import com.g.gysdk.GyCallBack import com.g.gysdk.GyConfig -import com.img.rabbit.bean.OnekeyPreLogin +import com.github.gzuliyujiang.oaid.DeviceIdentifier +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.img.rabbit.bean.local.ErrorBean +import com.img.rabbit.bean.local.OnekeyPreLogin +import com.img.rabbit.bean.local.WxBean +import com.img.rabbit.bean.response.UserEntity +import com.img.rabbit.bean.response.UserConfigEntity +import com.img.rabbit.config.Constants +import com.img.rabbit.provider.api.ApiManager +import com.img.rabbit.provider.api.ResultVo +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.MMKVUtils +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.internal.platform.PlatformRegistry.applicationContext -class LoginViewModel : ViewModel() { +class LoginViewModel : BaseViewModel() { private val TAG = "LoginViewModel" private val ONEKEY_TAG = "OneKeyLoginViewModel" + + private lateinit var api: IWXAPI + + private val _wxState = MutableLiveData() + val wxState: LiveData = _wxState + + fun updateWxState(newState: WxBean) { + _wxState.value = newState + } + +// private val _authInfo = MutableLiveData() + val authInfoForAlipay: MutableState = mutableStateOf("") + val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL) // 登录用户名 val userName = mutableStateOf("") // 登录验证码 val captcha = mutableStateOf("") + // 登录验证码发送时间戳 + val captchaTimestamp = mutableStateOf("") // 是否同意政策协议 - private val _policyAgreement = mutableStateOf(false) + private val _policyAgreement = mutableStateOf(true) val isPolicyAgreement: State = _policyAgreement @@ -44,7 +84,10 @@ class LoginViewModel : ViewModel() { _policyAgreement.value = isAgreement } + //用户配置 + val _userConfig = MutableLiveData() + val userConfig: UserConfigEntity? get() = _userConfig.value private val _isLogin = mutableStateOf(false) val isLogin: State = _isLogin @@ -53,6 +96,31 @@ class LoginViewModel : ViewModel() { _isLogin.value = isLogin } + + + val loginState = mutableStateOf?>(null) + val errorState = mutableStateOf(null) + + fun requestUserConfig(){ + mLaunch { + val oaid = MMKVUtils.getString("oaid") ?: "" + val response = ApiManager.serviceVo.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(applicationContext), MMKVUtils.getString("gt_cid") ?: "") + if (response.status) { + PreferenceUtil.saveXToken(response.data.token) + PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000) + _userConfig.postValue(response.data) + + val resultJson = Gson().toJson(response.data) + MMKVUtils.put("userConfig", resultJson) + + Log.w("LoginViewModel", "获取配置成功: $resultJson") + }else{ + Log.w("LoginViewModel", "获取配置失败: code=${response.code}, message=${response.message}") + } + isLoading.value = false // 加载完成 + } + } + fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) { // 初始化 SDK GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack { @@ -141,11 +209,203 @@ class LoginViewModel : ViewModel() { } } + fun requestOneKeyLogin(gyuid: String, token: String) { + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonOneKey = JsonObject() +// jsonOneKey.addProperty("gyuid", gyuid) +// jsonOneKey.addProperty("token", token) +// val jsonObject = JsonObject() +// jsonObject.addProperty("login_type", "onekey") +// jsonObject.add("onekey", jsonOneKey) +// jsonObject.addProperty("bind", false) + + jsonOneKey.addProperty("gyuid", gyuid) + jsonOneKey.addProperty("token", token) + val jsonObject = JsonObject() + jsonObject.addProperty("type", "onekey") + jsonObject.addProperty("bind", "") + jsonObject.add("data", jsonOneKey) + + requestLogin(jsonObject) + } + /** * 请求验证码 */ - fun requestCaptcha() { - // TODO: 发送请求获取验证码 + fun requestCaptcha(phone: String) { + // 发送请求获取验证码 + isLoading.value = true // 开始加载 + + mLaunch { + // 调用 API 获取数据 + val jsonObject = JsonObject() + jsonObject.addProperty("phone", phone) + val response = ApiManager.serviceVo.sendCode(jsonObject.toString().toRequestBody()) + if (response.status) { + Log.w("LoginViewModel", "请求验证码成功: ${response.data.timestamp}") + captchaTimestamp.value = response.data.timestamp + }else{ + errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "获取验证码失败" }) + } + isLoading.value = false // 加载完成 + } + } + + /** + * 请求登录(验证码) + */ + fun requestLoginForCaptcha(phone: String, code: String) { + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonPhone = JsonObject() + jsonPhone.addProperty("timestamp", captchaTimestamp.value) + jsonPhone.addProperty("phone", phone) + jsonPhone.addProperty("code", code) + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "phone") + jsonObject.add("phone", jsonPhone) + + requestLogin(jsonObject) + } + + + fun initWXApi(context: Context) { + api = WXAPIFactory.createWXAPI(context, Constants.WechatAppId) + } + + fun loginWithWechat(context: Context) { + if (isPolicyAgreement.value) { + doWxAuth(context) + }else{ + Toast.makeText(context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show() + } + } + + /** + * 拿着微信授权码完成登录(在WXEntryActivity中调用) + * @param wechatCode 微信授权码 + */ + fun requestWxLogin(wechatCode: String) { + if(wechatCode.isEmpty()){ + return + } + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonWx = JsonObject() + jsonWx.addProperty("code", wechatCode) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.add("weixin", jsonWx) + + requestLogin(jsonObject) + } + + //获取微信授权 + private fun doWxAuth(context: Context) { + if (!api.isWXAppInstalled) { + Toast.makeText(context, "您没有安装微信客户端,请先下载安装", Toast.LENGTH_SHORT).show() + return + } + val req = SendAuth.Req() + req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo + req.state = context.packageName + Math.random() * 1000 + "_phone" + api.sendReq(req) + } + + + /** + * 支付宝登录 + * authInfo: 该参数需由后端生成并加签,包含 app_id、pid、target_id 等信息 + */ + fun loginWithAliPay(context: Context,onAuthResult: (Map) -> Unit) { + if(authInfoForAlipay.value.isEmpty()){ + Toast.makeText(context, "请先获取支付宝登录授权码", Toast.LENGTH_SHORT).show() + return + } + // 发送请求获取支付宝登录授权码 + mLaunch { + onAuthResult(doAlipayLogin(context as Activity, authInfoForAlipay.value)) + } + } + + /** + * 请求支付宝登录参数 + */ + fun requestAliPayAuthParam() { + isLoading.value = true // 开始加载 + + mLaunch { + val response = ApiManager.serviceVo.getAlipayAuthParam() + val data = response.data + val param = data.param + + authInfoForAlipay.value = param + isLoading.value = false // 加载完成 + } + } + + /** + * 封装支付宝登录逻辑 + * @param activity 当前 Activity 上下文 + * @param authInfo 后端生成的授权字符串 + * @return 支付宝返回的原始 Map 结果 + */ + private suspend fun doAlipayLogin(activity: Activity, authInfo: String): Map { + return withContext(Dispatchers.IO) { + // 初始化 AuthTask + val authTask = AuthTask(activity) + + // 调用 authV2。第二个参数为 true 表示如果未安装支付宝则展示 Loading 界面, 该方法会阻塞当前线程直到用户操作结束 + val result = authTask.authV2(authInfo, true) + Log.i(TAG, "doAlipayLogin: $result") + + result ?: emptyMap() + } + } + + /** + * 拿着微信授权码完成登录(在WXEntryActivity中调用) + * @param authCode 微信授权码 + */ + fun requestAlipayLogin(authCode: String) { + if(authCode.isEmpty()){ + return + } + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonWx = JsonObject() + jsonWx.addProperty("auth_code", authCode)//code + + val jsonObject = JsonObject() + jsonObject.addProperty("type", "alipay") + jsonObject.addProperty("bind", "") + jsonObject.add("data", jsonWx) + + requestLogin(jsonObject) + } + + private fun requestLogin(jsonObject: JsonObject){ + mLaunch { + val response = ApiManager.serviceVo.login(jsonObject.toString().toRequestBody()) + if (response.status) { + loginState.value = response + val userEntity = response.data + userEntity.isLogin = true + //记录登录数据 + PreferenceUtil.saveUserInfo(userEntity) + }else{ + errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "登录失败" }) + } + isLoading.value = false // 加载完成 + } } } \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt new file mode 100644 index 0000000..62341b6 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt @@ -0,0 +1,63 @@ +package com.img.rabbit.viewmodel.`interface` + +import com.img.rabbit.bean.response.AlipayParamEntity +import com.img.rabbit.bean.response.CaptchaCodeEntity +import com.img.rabbit.bean.response.UserEntity +import com.img.rabbit.bean.response.UserConfigEntity +import com.img.rabbit.provider.api.ResultVo +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface ServiceVo { + /* + @POST("/dictionary/getTsSyspara") + suspend fun requestHospitalName(): Response>> + @POST("/dictionary/getLogo") + suspend fun requestHospitalLogo(): Response> + @POST("/nurse/login") + suspend fun requestLogin(@Body request: LoginRequest): Response> + */ + + /** + * 获取服务器时间 + */ + @GET("/api/time") + suspend fun getServerTime(): ResultVo + + /** + * 获取客户端配置 + */ + @GET("/api/user/config") + suspend fun getUserConfig( + @Query("oaid") oaid: String, + @Query("os_version") osVersion: Int, + @Query("ua") ua: String, + @Query("imei") imei: String, + @Query("cid") cid: String, + ): ResultVo + + @GET("api/alipay/app_param") + suspend fun getAlipayAuthParam(): ResultVo + + /** + * 发送验证码 + */ + @POST("api/user/code") + suspend fun sendCode(@Body requestBody: RequestBody): ResultVo + + /** + * 登录 + */ + @POST("/api/user/login") + suspend fun login(@Body requestBody: RequestBody): ResultVo + + + /** + * 退出登录 + */ + @POST("/mapi/user/logout") + suspend fun logout(): ResultVo +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt new file mode 100644 index 0000000..2e19ac4 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt @@ -0,0 +1,34 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package com.img.rabbit.wxapi + +import com.img.rabbit.provider.storage.GlobalStateManager +import com.tencent.mm.opensdk.constants.ConstantsAPI +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.umeng.socialize.weixin.view.WXCallbackActivity +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class WXEntryActivity : WXCallbackActivity() { + override fun onResp(resp: BaseResp?) { + if (resp?.type == ConstantsAPI.COMMAND_SENDAUTH) { + when (resp.errCode) { + BaseResp.ErrCode.ERR_OK -> { + val authResp = resp as SendAuth.Resp + //val wxState = WxBean(code = authResp.code,state = authResp.state) + + GlobalScope.launch { + GlobalStateManager(this@WXEntryActivity).storeGlobalWxAuthorization(authResp.code) + + finish() + } + } + + else -> {} + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_alipay_icon.xml b/app/src/main/res/drawable/ic_alipay_icon.xml new file mode 100644 index 0000000..5e96051 --- /dev/null +++ b/app/src/main/res/drawable/ic_alipay_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wx_icon.xml b/app/src/main/res/drawable/ic_wx_icon.xml new file mode 100644 index 0000000..3c32362 --- /dev/null +++ b/app/src/main/res/drawable/ic_wx_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/layout_web.xml b/app/src/main/res/layout/layout_web.xml new file mode 100644 index 0000000..82b3f17 --- /dev/null +++ b/app/src/main/res/layout/layout_web.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxhdpi/ic_copy.webp b/app/src/main/res/mipmap-xxhdpi/ic_copy.webp new file mode 100644 index 0000000..6b804b1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_copy.webp differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 445f5ec..5a14f64 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,10 @@ androidx-test-ext-junit = "1.1.5" androidx-test-espresso-core = "3.5.1" navigationRuntimeKtx = "2.9.7" +# Network Version +retrofit = "3.0.0" +okhttp = "5.1.0" + # Third-party version gson = "2.13.2" gysdk = "3.2.3.0" @@ -37,6 +41,20 @@ faceDetection = "16.1.5" foundation = "1.10.2" androidGifDrawableEncoder = "1.2.30" gifeEncoder = "0.10.1" +cropify = "0.5.2" +matisse = "2.3.0" +pictureselector = "v3.11.2" +compress = "v3.11.2" +wechatSdkAndroidWithoutMta = "6.8.0" +# Umeng version +umengUmsdkCommon = "9.8.9" +umengUmsdkAsms = "1.8.7.2" +umengUmsdkApm = "2.0.6" +umengUmsdkShareCore = "7.3.7" +tencentHelper = "3.0.6" +#oaid +android_cn_oaid = "4.2.12" +fastaes = "1.1.5" [libraries] @@ -70,17 +88,41 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" } androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } +# Network dependencies +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } +retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "1.0.0" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + + # Third-party dependencies gson = { module = "com.google.code.gson:gson", version.ref = "gson" } gysdk = { module = "com.getui:gysdk", version.ref = "gysdk" } +matisse = { module = "io.github.leavesczy:matisse", version.ref = "matisse" } mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } +pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" } +compress = { module = "io.github.lucksiege:compress", version.ref = "compress" } segmentation-selfie = { module = "com.google.mlkit:segmentation-selfie", version.ref = "segmentationSelfie" } face-detection = { module = "com.google.mlkit:face-detection", version.ref = "faceDetection" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableEncoder" } gif-encoder = { module = "com.squareup:gifencoder", version.ref = "gifeEncoder" } +cropify = { module = "com.github.moyuruaizawa:cropify", version.ref = "cropify" } +wechat-sdk = { module = "com.tencent.mm.opensdk:wechat-sdk-android-without-mta", version.ref = "wechatSdkAndroidWithoutMta" } +#Umeng Sdk +umeng-umsdk-common = { module = "com.umeng.umsdk:common", version.ref = "umengUmsdkCommon" } +umeng-umsdk-asms = { module = "com.umeng.umsdk:asms", version.ref = "umengUmsdkAsms" } +#noinspection Aligned16KB +umeng-umsdk-apm = { module = "com.umeng.umsdk:apm", version.ref = "umengUmsdkApm" } +umeng-umsdk-share-core = { module = "com.umeng.umsdk:share-core", version.ref = "umengUmsdkShareCore" } +umeng-umsdk-share-wx = { module = "com.umeng.umsdk:share-wx", version.ref = "umengUmsdkShareCore" } +tencent-helper = { module = "com.tencent.vasdolly:helper", version.ref = "tencentHelper" } +android_cn_oaid = { module = "com.github.gzu-liyujiang:Android_CN_OAID", version.ref = "android_cn_oaid" } +#Decrypt +fastaes = { module = "io.github.billywei01:fastaes", version.ref = "fastaes" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7da8671..f2788e5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,10 @@ pluginManagement { includeGroupByRegex("androidx.*") } } + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { url = uri("https://developer.huawei.com/repo/") } mavenCentral() gradlePluginPortal() } @@ -17,9 +21,19 @@ dependencyResolutionManagement { google() mavenCentral() maven { url = uri("https://jitpack.io") } + maven { url = uri("https://company/com/maven2") } maven { url = uri("https://repo1.maven.org/maven2/") } maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } - maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") } + maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") } //个推 + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { url = uri("https://developer.huawei.com/repo/") }//添加华为仓库 获取oaid + maven { url = uri("https://developer.hihonor.com/repo/") } + maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } //巨量融合 + maven { url = uri("https://repo.eclipse.org/content/repositories/paho-snapshots/") } //mqtt + mavenCentral() + google() } }