| | |
| | | } |
| | | |
| | | val APPLICATION_ID = "com.xw.yunwei" |
| | | // 阿里云推送 |
| | | val PUSH_KEY = "335666352" |
| | | val PUSH_SECRET = "0252ebde2dc84f63aebd41f757f213d7" |
| | | |
| | | android { |
| | | namespace = APPLICATION_ID |
| | |
| | | // 选择要添加的对应cpu类型的.so库。此处仅为示意,推送支持所有主流类型,请根据实际需求选择 |
| | | abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))//, "x86", "x86_64" |
| | | } |
| | | |
| | | buildConfigField("String", "PUSH_KEY", "\"${PUSH_KEY}\"") |
| | | buildConfigField("String", "PUSH_SECRET", "\"${PUSH_SECRET}\"") |
| | | |
| | | } |
| | | |
| | |
| | | manifestPlaceholders["APP_NAME"] = "@string/app_name_dev" |
| | | manifestPlaceholders["icon"] = "@mipmap/ic_launcher" |
| | | |
| | | buildConfigField("String", "DEV_API_HOST", "\"http://221.182.45.100:8000/\"") |
| | | buildConfigField("String", "DEV_API_HOST", "\"\"") |
| | | buildConfigField("String", "API_HOST", "\"http://rt-admin.test.xiaopeng.local/\"") |
| | | buildConfigField("String", "WS_URL", "\"ws://221.182.45.100:8888/websocket\"") |
| | | //翼虎appId |
| | | buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-test\"") |
| | | buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin\"") |
| | | //翼虎appSecret |
| | | buildConfigField("String", "YIHU_APP_SECRET", "\"SXa7vEe7V23Xh3RN\"") |
| | | buildConfigField("String", "YIHU_APP_SECRET", "\"9UvdqMuZRRTNcpeA\"") |
| | | //翼虎公钥 |
| | | buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjT9Pl5kqJeinJyAjvW95ulxtON34ABhX pTRiPczVRJSVSTQzqLyr6UNRU6AvR2diZ4eGRHbeJ+NNtiuSverAZ+xu4B05xWhERThV0SETigkR 9IaJ9glucmAL6S6CNE5JCwWYYDfCgSIN3fzWhC48hGwEL8nauBIHrg2TRs0x4/rtmUR5oEMrm6RA eafdHDD62Dtsc7zm7uIxaOfn90cG35DKsp3iIcw7BLFT/HmBSVML2SLTruNkgDhH2qIZ1t6YQEJj C0uO+jiHXoluKRLWDMg9+SuoJHrsntCzRdwx246g6nMKfxtyC/jrPB6KUdrSR9xaxXZPcNQLb0ip y6mTkwIDAQAB\"") |
| | | buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAko2oXAtrtl2BGUZjjDZBbEtbYd8pddah hqTbQ68F59no7eZGdfE4PQyKnPZTJSR0XApwydqB6nKEID58bej3F7p/2mL7i/s9+9zdepMSpg9b 1nTi730Fs1pzEalew7V839/Wg/RPFr6xZEqU8oaKWT6fe4anRAOI7aCgA8CxEB8YwQvrMTYetFnE QRGHlziKKDHmurKXToGdu2SSXzYLy8Cl2X79z3rBUBQpMZI63g93wcRIToSP8DUkbQFY64cXa6cT ZyBrt72K2Dx4IfuLb7K1BZbZxXswN/Lx4rQ7anQ8HFtA5bE7sNznj16pgoI3aUKjVm45JWtxTAWa XrSwTQIDAQAB\"") |
| | | //翼虎网络域名 |
| | | buildConfigField("String", "YIHU_API_HOST", "\"https://sso.test.xiaopeng.com/sso/\"") |
| | | } |
| | |
| | | implementation(project(":common")) |
| | | implementation(libs.dialog.pickerview) |
| | | implementation(libs.huawei.agconnect.core) |
| | | implementation(libs.jsoup) |
| | | } |
| | | |
| | |
| | | import com.xw.common.custom.ClassicsFooter11 |
| | | import com.xw.common.custom.ClassicsHeader11 |
| | | import com.xw.common.utils.ActivityManager |
| | | import com.xw.common.utils.KeyPreferences |
| | | import com.xw.common.utils.LogUtils |
| | | import com.xw.common.utils.LogUtils.useThreeLog |
| | | import com.xw.common.utils.MMKVManager |
| | |
| | | import com.xw.yunwei.di.networkModule |
| | | import com.xw.yunwei.model.AppPermissionModel |
| | | import com.xw.yunwei.model.TaskModel |
| | | import com.xw.yunwei.model.ThirdInfo |
| | | import com.xw.yunwei.model.UserInfo |
| | | import com.xw.yunwei.model.event.TaskEvent |
| | | import com.xw.yunwei.net.TaskRepository |
| | | import com.xw.yunwei.ui.dialog.ApplyForReassignReasonDialog |
| | | import com.xw.yunwei.ui.dialog.NewTaskDialog |
| | | import com.xw.yunwei.ui.task.accident.detail.AccidentDetailActivity |
| | | import com.xw.yunwei.ui.task.normal.NormalTaskDetailActivity |
| | | import com.xw.yunwei.utils.DeviceId |
| | | import com.xw.yunwei.utils.TTSController |
| | | import com.xw.yunwei.websocket.WebSocketManager |
| | | import kotlinx.coroutines.CompletableDeferred |
| | |
| | | import org.koin.android.ext.android.inject |
| | | import org.koin.android.ext.koin.androidContext |
| | | import org.koin.core.context.GlobalContext.startKoin |
| | | import com.xw.yunwei.utils.DeviceId |
| | | |
| | | class MyApp : Application() { |
| | | |
| | |
| | | }) |
| | | } |
| | | |
| | | private fun initPush() { |
| | | fun initPush() { |
| | | val third = MMKVManager.getInstance().getModel<ThirdInfo?>(KeyPreferences.THIRD) ?: return |
| | | val pushInitConfig = PushInitConfig.Builder() |
| | | .application(this) |
| | | .appKey(BuildConfig.PUSH_KEY) |
| | | .appSecret(BuildConfig.PUSH_SECRET) |
| | | .appKey(third.pushKey) |
| | | .appSecret(third.pushSecret) |
| | | .build() |
| | | |
| | | PushServiceFactory.init(pushInitConfig) |
| | |
| | | package com.xw.yunwei.extension |
| | | |
| | | import android.util.Log |
| | | import android.view.View |
| | | import android.webkit.WebResourceRequest |
| | | import android.webkit.WebResourceResponse |
| | | import android.webkit.WebView |
| | | import android.webkit.WebViewClient |
| | | import com.xw.yunwei.utils.HtmlSanitizer |
| | | import java.net.HttpURLConnection |
| | | import java.net.URL |
| | | |
| | | //扩展函数 设置防重复点击监听器 |
| | | fun View.setOnDurationClickListener(duration: Long = 1000L, listener: View.OnClickListener) { |
| | |
| | | listener.onClick(it) |
| | | } |
| | | } |
| | | } |
| | | |
| | | //扩展函数 网页加载控件安全设置 |
| | | fun WebView.setSafeSetting() { |
| | | apply { |
| | | settings.allowFileAccess = false //设置不可以访问文件 |
| | | webViewClient = object : WebViewClient() { |
| | | override fun shouldInterceptRequest( |
| | | view: WebView, |
| | | request: WebResourceRequest |
| | | ): WebResourceResponse? { |
| | | val url = request.url.toString() |
| | | //访问文件危险操作 |
| | | val dangerousOperation = listOf("file://", "content://", "android_asset") |
| | | // 阻止文件访问 |
| | | if (dangerousOperation.any { url.contains(it) }) { |
| | | return null // 或返回空响应 |
| | | } |
| | | // 只对HTML文档添加CSP |
| | | if (isHtmlDocument(request)) { |
| | | return getResponseWithCSP(request) |
| | | } |
| | | return super.shouldInterceptRequest(view, request) |
| | | } |
| | | |
| | | private fun isHtmlDocument(request: WebResourceRequest): Boolean { |
| | | val url = request.url.toString() |
| | | val contentType = request.requestHeaders?.get("Accept") |
| | | val urlPatterns = listOf(".html", ".htm", ".php", ".jsp", ".asp") |
| | | |
| | | return urlPatterns.any { url.endsWith(it) } || |
| | | contentType?.contains("text/html") == true |
| | | } |
| | | |
| | | private fun getResponseWithCSP(request: WebResourceRequest): WebResourceResponse? { |
| | | try { |
| | | val urlConnection = |
| | | URL(request.url.toString()).openConnection() as HttpURLConnection |
| | | |
| | | // 设置请求头 |
| | | request.requestHeaders?.forEach { (key, value) -> |
| | | urlConnection.setRequestProperty(key, value) |
| | | } |
| | | |
| | | urlConnection.requestMethod = request.method |
| | | val responseCode = urlConnection.responseCode |
| | | |
| | | if (responseCode == HttpURLConnection.HTTP_OK) { |
| | | val mimeType = |
| | | urlConnection.contentType?.split(";")?.firstOrNull() ?: "text/html" |
| | | val encoding = urlConnection.contentEncoding ?: "UTF-8" |
| | | val inputStream = urlConnection.inputStream |
| | | |
| | | // 创建带有CSP头的响应 |
| | | val headers = mutableMapOf<String, String>() |
| | | |
| | | // 收集原始响应头 |
| | | urlConnection.headerFields.forEach { (key, valueList) -> |
| | | if (key != null && valueList != null && valueList.isNotEmpty()) { |
| | | headers[key] = valueList.joinToString(", ") |
| | | } |
| | | } |
| | | |
| | | // 添加或覆盖CSP头 |
| | | headers["Content-Security-Policy"] = buildCSPPolicy() |
| | | |
| | | return WebResourceResponse( |
| | | mimeType, encoding, responseCode, |
| | | urlConnection.responseMessage, headers, inputStream |
| | | ) |
| | | } |
| | | } catch (e: Exception) { |
| | | e.printStackTrace() |
| | | Log.e("CSP", "Failed to add CSP header") |
| | | } |
| | | return null |
| | | } |
| | | |
| | | private fun buildCSPPolicy(): String { |
| | | return """ |
| | | default-src 'self'; |
| | | script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted-cdn.com; |
| | | style-src 'self' 'unsafe-inline' https://trusted-cdn.com; |
| | | img-src 'self' data: https:; |
| | | font-src 'self' https://fonts.gstatic.com; |
| | | connect-src 'self' https://api.example.com; |
| | | frame-src 'self' https://trusted-iframe.com; |
| | | report-uri /csp-report-endpoint |
| | | """.trimIndent().replace("\n", " ") |
| | | } |
| | | |
| | | override fun onPageFinished(view: WebView?, url: String?) { |
| | | super.onPageFinished(view, url) |
| | | // 注入安全监控脚本 |
| | | injectSecurityMonitor(view) |
| | | } |
| | | |
| | | private fun injectSecurityMonitor(view: WebView?) { |
| | | val securityScript = """ |
| | | (function() { |
| | | 'use strict'; |
| | | |
| | | // 监控DOM变化 |
| | | const observer = new MutationObserver(function(mutations) { |
| | | mutations.forEach(function(mutation) { |
| | | mutation.addedNodes.forEach(function(node) { |
| | | if (node.nodeName === 'SCRIPT') { |
| | | console.warn('Dynamic script added:', node.src || node.textContent.substring(0, 100)); |
| | | } |
| | | if (node.nodeName === 'IFRAME') { |
| | | console.warn('Dynamic iframe added:', node.src); |
| | | } |
| | | }); |
| | | }); |
| | | }); |
| | | |
| | | observer.observe(document.documentElement, { |
| | | childList: true, |
| | | subtree: true |
| | | }); |
| | | |
| | | // 拦截危险API调用 |
| | | const originalFetch = window.fetch; |
| | | window.fetch = function() { |
| | | const url = arguments[0]; |
| | | if (typeof url === 'string') { |
| | | if (url.startsWith('file://') || url.includes('localhost')) { |
| | | console.error('Blocked fetch to local file:', url); |
| | | return Promise.reject(new Error('Access to local files is blocked')); |
| | | } |
| | | } |
| | | return originalFetch.apply(this, arguments); |
| | | }; |
| | | |
| | | // 监控表单提交 |
| | | document.addEventListener('submit', function(e) { |
| | | const form = e.target; |
| | | const action = form.getAttribute('action') || ''; |
| | | if (action.startsWith('file://')) { |
| | | e.preventDefault(); |
| | | console.error('Blocked form submission to file:', action); |
| | | } |
| | | }); |
| | | })(); |
| | | """.trimIndent() |
| | | view?.evaluateJavascript(securityScript, null) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | //扩展函数 安全加载本地数据 |
| | | fun WebView.safeLoadDataWithBaseURL( |
| | | baseUrl: String?, |
| | | data: String, |
| | | mimeType: String?, |
| | | encoding: String?, |
| | | historyUrl: String? |
| | | ) { |
| | | loadDataWithBaseURL( |
| | | baseUrl, |
| | | HtmlSanitizer().sanitizeUserContent(data), |
| | | mimeType, |
| | | encoding, |
| | | historyUrl |
| | | ) |
| | | } |
| | |
| | | |
| | | data class LoginModel( |
| | | val info: Info?, |
| | | val token: Token? |
| | | val token: Token?, |
| | | val thirdInfo: ThirdInfo? |
| | | ) |
| | | |
| | | data class Info( |
| | |
| | | val updateTime: String? |
| | | ) |
| | | |
| | | //第三方sdk信息 |
| | | data class ThirdInfo( |
| | | val pushKey: String,//阿里推送appKey |
| | | val pushSecret: String,//阿里推送appSecret |
| | | ) |
| | | |
| | | /** |
| | | * 用户信息实体类 |
| | | * 包含用户基本信息、所属车队、角色列表等完整信息 |
| | |
| | | /*个人中心-查询证件上传详情*/ |
| | | @POST("account/maintenance-app/api/v1/getCertificate") |
| | | fun getCertificate(): Call<HttpResponse<CertificateAuditInfo>> |
| | | |
| | | /*个人中心-退出登录*/ |
| | | @POST("account/maintenance-app/ssoAuthSdk/appLoginOut") |
| | | fun appLoginOut(): Call<HttpResponse<Any>> |
| | | } |
| | |
| | | |
| | | /*个人中心-查询证件上传详情*/ |
| | | suspend fun getCertificate() = api.getCertificate().interceptAwait() |
| | | |
| | | /*个人中心-退出登录*/ |
| | | suspend fun appLoginOut() = api.appLoginOut().interceptAwait() |
| | | } |
| | |
| | | import androidx.appcompat.app.AppCompatActivity |
| | | import androidx.databinding.DataBindingUtil |
| | | import androidx.lifecycle.lifecycleScope |
| | | import com.afollestad.materialdialogs.MaterialDialog |
| | | import com.afollestad.materialdialogs.customview.customView |
| | | import com.afollestad.materialdialogs.customview.getCustomView |
| | | import com.google.android.material.tabs.TabLayout |
| | |
| | | import com.xw.common.utils.ToastUtilCommon |
| | | import com.xw.yunwei.MyApp |
| | | import com.xw.yunwei.databinding.DialogProtocolBinding |
| | | import com.xw.yunwei.extension.safeLoadDataWithBaseURL |
| | | import com.xw.yunwei.extension.setSafeSetting |
| | | import com.xw.yunwei.model.SwitchTabMain |
| | | import com.xw.yunwei.model.SwitchTabTask |
| | | import com.xw.yunwei.net.UserRepository |
| | |
| | | import org.json.JSONObject |
| | | import org.koin.core.component.KoinComponent |
| | | import org.koin.core.component.inject |
| | | import kotlin.getValue |
| | | |
| | | object MessageHandler : KoinComponent { |
| | | val userRepository: UserRepository by inject() |
| | |
| | | handlePushEvent(JSONObject(content ?: ""), isPush) |
| | | } catch (e: Exception) { |
| | | ToastUtilCommon.showError("未获取到消息详情") |
| | | LogUtils.e("zmc-MessageHandler","handlePushEvent content = $content error = $e") |
| | | LogUtils.e("zmc-MessageHandler", "handlePushEvent content = $content error = $e") |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | @OptIn(DelicateCoroutinesApi::class) |
| | | fun handlePushEvent(jsob: JSONObject, isPush: Boolean) { |
| | | LogUtils.i("zmc-MessageHandler","handlePushEvent $jsob isPush = $isPush") |
| | | LogUtils.i("zmc-MessageHandler", "handlePushEvent $jsob isPush = $isPush") |
| | | TaskVM.needRefresh.value = true |
| | | /** |
| | | * content:消息内容 |
| | |
| | | } |
| | | } |
| | | |
| | | private fun handleMsg(currentShowActivity: Activity, strategy: Int, id: String, isPush: Boolean) { |
| | | private fun handleMsg( |
| | | currentShowActivity: Activity, |
| | | strategy: Int, |
| | | id: String, |
| | | isPush: Boolean |
| | | ) { |
| | | when (strategy) { |
| | | 1, 2, 3, 5 -> { // 1=新增事故,2=事故即将超时提示,3=事故超时提示,5=事故状态变更 点击都跳事故详情 |
| | | if(strategy == 5 && isPush) return |
| | | if (strategy == 5 && isPush) return |
| | | currentShowActivity.startActivity( |
| | | Intent(currentShowActivity, AccidentDetailActivity::class.java).apply { |
| | | putExtra("taskId", id.toLong()) |
| | |
| | | } |
| | | |
| | | 8, 12 -> { // 升级 |
| | | if(strategy == 8 && isPush) return |
| | | if (strategy == 8 && isPush) return |
| | | showUpdateDialog(currentShowActivity) |
| | | } |
| | | |
| | |
| | | // val tab1 = bind.protocolLayout.tabLayout.newTab().apply { text = "用户协议" } |
| | | // val tab2 = bind.protocolLayout.tabLayout.newTab().apply { text = "隐私条款" } |
| | | |
| | | val tab1 = bind.protocolLayout.tabLayout.newTab().apply { text = "小鹏Robotaxi...隐私政策" } |
| | | val tab2 = bind.protocolLayout.tabLayout.newTab().apply { text = "第三方共享信息清单" } |
| | | val tab3 = bind.protocolLayout.tabLayout.newTab().apply { text = "个人信息收集清单" } |
| | | val tab1 = bind.protocolLayout.tabLayout.newTab() |
| | | .apply { text = "小鹏Robotaxi...隐私政策" } |
| | | val tab2 = |
| | | bind.protocolLayout.tabLayout.newTab().apply { text = "第三方共享信息清单" } |
| | | val tab3 = |
| | | bind.protocolLayout.tabLayout.newTab().apply { text = "个人信息收集清单" } |
| | | val tab4 = bind.protocolLayout.tabLayout.newTab().apply { text = "权限调用" } |
| | | |
| | | bind.protocolLayout.tabLayout.addTab(tab1, true) |
| | |
| | | |
| | | //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript |
| | | webSettings.javaScriptEnabled = true //允许js调用 |
| | | webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等 |
| | | webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许 |
| | | |
| | | //缩放操作 |
| | |
| | | |
| | | //其他细节操作 |
| | | webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存 |
| | | webSettings.allowFileAccess = true //设置可以访问文件 |
| | | webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 |
| | | webSettings.loadsImagesAutomatically = true //支持自动加载图片 |
| | | webSettings.defaultTextEncodingName = "utf-8" //设置编码格式 |
| | | |
| | | webView.setSafeSetting()//安全设置 |
| | | } |
| | | |
| | | @SuppressLint("SetJavaScriptEnabled") |
| | | private fun setContent(webView: WebView, content: String?) { |
| | | content?.let { |
| | | // 加载并显示HTML代码 |
| | | webView.loadDataWithBaseURL( |
| | | webView.safeLoadDataWithBaseURL( |
| | | null, |
| | | content, |
| | | "text/html", |
| | |
| | | Constants.token = it.token?.access_token ?: "" |
| | | MMKVManager.getInstance() |
| | | .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser) |
| | | MMKVManager.getInstance() |
| | | .saveModel(KeyPreferences.THIRD, it.thirdInfo) |
| | | MyApp.myApp.initPush() |
| | | MyApp.myApp.connectWebSocket() |
| | | startActivity(MainActivity::class.java) |
| | | finish() |
| | |
| | | ToastUtilCommon.showError("登录失败次数过多,请1分钟后重试") |
| | | return |
| | | } |
| | | if (BuildConfig.DEBUG) { |
| | | launchRequestFlow( |
| | | tips = "登录中...", |
| | | flowBlock = { |
| | | val map = |
| | | mapOf("phoneNumber" to phone.value, "password" to psw, "deviceType" to 1) |
| | | userRepository.loginByPassword(map) |
| | | }, |
| | | error = { code, msg -> |
| | | ToastUtilCommon.showError(msg) |
| | | loginFailCount++ |
| | | lastFailTime = System.currentTimeMillis() |
| | | }, |
| | | onCollect = { data -> |
| | | data?.let { |
| | | Constants.token = it.token?.access_token ?: "" |
| | | MMKVManager.getInstance() |
| | | .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser) |
| | | MyApp.myApp.connectWebSocket() |
| | | startActivity(MainActivity::class.java) |
| | | finish() |
| | | } |
| | | } |
| | | ) |
| | | return |
| | | } |
| | | // if (BuildConfig.DEBUG) { |
| | | // launchRequestFlow( |
| | | // tips = "登录中...", |
| | | // flowBlock = { |
| | | // val map = |
| | | // mapOf("phoneNumber" to phone.value, "password" to psw, "deviceType" to 1) |
| | | // userRepository.loginByPassword(map) |
| | | // }, |
| | | // error = { code, msg -> |
| | | // ToastUtilCommon.showError(msg) |
| | | // loginFailCount++ |
| | | // lastFailTime = System.currentTimeMillis() |
| | | // }, |
| | | // onCollect = { data -> |
| | | // data?.let { |
| | | // Constants.token = it.token?.access_token ?: "" |
| | | // MMKVManager.getInstance() |
| | | // .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser) |
| | | // MMKVManager.getInstance() |
| | | // .saveModel(KeyPreferences.THIRD, it.thirdInfo) |
| | | // MyApp.myApp.initPush() |
| | | // MyApp.myApp.connectWebSocket() |
| | | // startActivity(MainActivity::class.java) |
| | | // finish() |
| | | // } |
| | | // } |
| | | // ) |
| | | // return |
| | | // } |
| | | launchRequestFlow( |
| | | tips = "登录中...", |
| | | flowBlock = { |
| | |
| | | Constants.token = it.token?.access_token ?: "" |
| | | MMKVManager.getInstance() |
| | | .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser) |
| | | MMKVManager.getInstance() |
| | | .saveModel(KeyPreferences.THIRD, it.thirdInfo) |
| | | MyApp.myApp.initPush() |
| | | MyApp.myApp.connectWebSocket() |
| | | startActivity(MainActivity::class.java) |
| | | finish() |
| | |
| | | ToastUtilCommon.showError("请输入密码!") |
| | | return |
| | | } |
| | | if (psw.value!!.length < 6) { |
| | | ToastUtilCommon.showError("密码长度至少6位!") |
| | | return |
| | | } |
| | | if (pswConfirm.value.isNullOrEmpty()) { |
| | | ToastUtilCommon.showError("请再次输入密码!") |
| | | return |
| | | } |
| | | if (pswConfirm.value!!.length < 6) { |
| | | ToastUtilCommon.showError("密码长度至少6位!") |
| | | return |
| | | } |
| | | if (psw.value != pswConfirm.value) { |
| | | ToastUtilCommon.showError("两次密码不一致!") |
| | | return |
| | | } |
| | | val psw = psw.value ?: "" |
| | | if (psw.length < 8) { |
| | | ToastUtilCommon.showError("密码长度至少8位!") |
| | | return |
| | | } |
| | | if (!psw.matches(Regex(".*[A-Z].*"))) { |
| | | ToastUtilCommon.showError("密码需包含大写字母!") |
| | | return |
| | | } |
| | | if (!psw.matches(Regex(".*[a-z].*"))) { |
| | | ToastUtilCommon.showError("密码需包含小写字母!") |
| | | return |
| | | } |
| | | if (!psw.matches(Regex(".*[0-9].*"))) { |
| | | ToastUtilCommon.showError("密码需包含数字!") |
| | | return |
| | | } |
| | | |
| | |
| | | val map = mapOf( |
| | | "phoneNumber" to phone.value, |
| | | "verificationCode" to code.value, |
| | | "newPassword" to psw.value, |
| | | "newPassword" to psw, |
| | | "confirmPassword" to pswConfirm.value, |
| | | ) |
| | | userRepository.modifierPsw(map) |
| | |
| | | import com.xw.yunwei.BR |
| | | import com.xw.yunwei.BaseBusinessActivity |
| | | import com.xw.yunwei.databinding.ActivityProtocolBinding |
| | | import com.xw.yunwei.extension.safeLoadDataWithBaseURL |
| | | import com.xw.yunwei.extension.setSafeSetting |
| | | |
| | | class ProtocolActivity: BaseBusinessActivity<ActivityProtocolBinding, ProtocolVM>() { |
| | | class ProtocolActivity : BaseBusinessActivity<ActivityProtocolBinding, ProtocolVM>() { |
| | | override fun getBindingVariable() = BR.viewModel |
| | | |
| | | override fun init() { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | override fun onTabUnselected(tab: TabLayout.Tab?) {} |
| | | override fun onTabReselected(tab: TabLayout.Tab?) {} |
| | | }) |
| | |
| | | |
| | | //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript |
| | | webSettings.javaScriptEnabled = true //允许js调用 |
| | | webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等 |
| | | webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许 |
| | | |
| | | //缩放操作 |
| | |
| | | |
| | | //其他细节操作 |
| | | webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存 |
| | | webSettings.allowFileAccess = true //设置可以访问文件 |
| | | webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 |
| | | webSettings.loadsImagesAutomatically = true //支持自动加载图片 |
| | | webSettings.defaultTextEncodingName = "utf-8" //设置编码格式 |
| | | |
| | | webView.setSafeSetting()//安全设置 |
| | | } |
| | | |
| | | @SuppressLint("SetJavaScriptEnabled") |
| | | private fun setContent(webView: WebView, content: String?) { |
| | | content?.let { |
| | | // 加载并显示HTML代码 |
| | | webView.loadDataWithBaseURL( |
| | | webView.safeLoadDataWithBaseURL( |
| | | null, |
| | | content, |
| | | "text/html", |
| | |
| | | import android.provider.Settings |
| | | import androidx.core.app.NotificationManagerCompat |
| | | import androidx.core.content.ContextCompat |
| | | import androidx.lifecycle.lifecycleScope |
| | | import com.blankj.utilcode.util.AppUtils |
| | | import com.hjq.permissions.OnPermissionCallback |
| | | import com.hjq.permissions.Permission |
| | | import com.hjq.permissions.XXPermissions |
| | | import com.xw.common.CommonDialog |
| | | import com.xw.common.net.UpdateManager |
| | | import com.xw.common.utils.MMKVManager |
| | | import com.xw.common.utils.ToastUtilCommon |
| | | import com.xw.yunwei.BR |
| | | import com.xw.yunwei.BaseBusinessActivity |
| | | import com.xw.yunwei.MyApp |
| | | import com.xw.yunwei.databinding.ActivitySetBinding |
| | | import kotlinx.coroutines.delay |
| | | import kotlinx.coroutines.launch |
| | | |
| | | /** |
| | | * 设置 |
| | |
| | | } |
| | | |
| | | binding.logOut.setOnClickListener { |
| | | CommonDialog.showTipsDialog(this, "确认退出当前账号登录吗?", rightBlock = { |
| | | MMKVManager.getInstance().clear() |
| | | MyApp.myApp.disconnectWebSocket() |
| | | lifecycleScope.launch { |
| | | delay(100) |
| | | AppUtils.relaunchApp(true) |
| | | } |
| | | true |
| | | CommonDialog.showCommonTipsDialog(this, "确认退出当前账号登录吗?", rightBlock = { |
| | | viewModel.loginOut(it) |
| | | }) |
| | | } |
| | | } |
| | |
| | | package com.xw.yunwei.ui.user.mine.set |
| | | |
| | | import android.app.Application |
| | | import androidx.lifecycle.viewModelScope |
| | | import com.afollestad.materialdialogs.MaterialDialog |
| | | import com.blankj.utilcode.util.AppUtils |
| | | import com.xw.common.base.BaseViewModel |
| | | import com.xw.common.utils.MMKVManager |
| | | import com.xw.yunwei.MyApp |
| | | import com.xw.yunwei.model.AppVersionModel |
| | | import com.xw.yunwei.net.UserRepository |
| | | import kotlinx.coroutines.delay |
| | | import kotlinx.coroutines.launch |
| | | import org.koin.core.component.inject |
| | | |
| | | class SetVM(application: Application) : BaseViewModel(application) { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | //退出登录 |
| | | fun loginOut(dialog: MaterialDialog) { |
| | | launchRequest { |
| | | val resultOk = userRepository.appLoginOut() |
| | | dialog.dismiss() |
| | | MMKVManager.getInstance().clear() |
| | | MyApp.myApp.disconnectWebSocket() |
| | | viewModelScope.launch { |
| | | delay(100) |
| | | AppUtils.relaunchApp(true) |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.xw.yunwei.BR |
| | | import com.xw.yunwei.BaseBusinessActivity |
| | | import com.xw.yunwei.databinding.ActivityStandardDescriptionBinding |
| | | import com.xw.yunwei.extension.safeLoadDataWithBaseURL |
| | | import com.xw.yunwei.extension.setSafeSetting |
| | | |
| | | /** |
| | | * 标准说明 |
| | |
| | | |
| | | //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript |
| | | webSettings.javaScriptEnabled = true //允许js调用 |
| | | webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等 |
| | | webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许 |
| | | |
| | | //缩放操作 |
| | |
| | | |
| | | //其他细节操作 |
| | | webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存 |
| | | webSettings.allowFileAccess = true //设置可以访问文件 |
| | | webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 |
| | | webSettings.loadsImagesAutomatically = true //支持自动加载图片 |
| | | webSettings.defaultTextEncodingName = "utf-8" //设置编码格式 |
| | | |
| | | webView.setSafeSetting()//安全设置 |
| | | } |
| | | |
| | | @SuppressLint("SetJavaScriptEnabled") |
| | | private fun setContent(webView: WebView, content: String?) { |
| | | content?.let { |
| | | // 加载并显示HTML代码 |
| | | webView.loadDataWithBaseURL( |
| | | webView.safeLoadDataWithBaseURL( |
| | | null, |
| | | content, |
| | | "text/html", |
| New file |
| | |
| | | package com.xw.yunwei.utils |
| | | |
| | | import org.jsoup.Jsoup |
| | | import org.jsoup.nodes.Document |
| | | import org.jsoup.safety.Safelist |
| | | import java.util.Locale |
| | | |
| | | class HtmlSanitizer { |
| | | |
| | | // 定义不同安全等级的白名单 |
| | | object Whitelists { |
| | | // 1. 基本文本(最严格) |
| | | val BASIC_TEXT = Safelist.none() |
| | | .addTags("p", "br", "b", "i", "strong", "em", "blockquote") |
| | | .addAttributes("p", "class") |
| | | |
| | | // 2. 简单文档 |
| | | val SIMPLE_DOCUMENT = Safelist.simpleText() |
| | | .addTags("h1", "h2", "h3", "h4", "h5", "h6", "hr", "ul", "ol", "li") |
| | | .addAttributes("a", "href", "title", "target") |
| | | .addProtocols("a", "href", "http", "https", "mailto") |
| | | |
| | | // 3. 富文本(允许格式和媒体) |
| | | val RICH_TEXT = Safelist.relaxed() |
| | | .addTags("div", "span", "table", "tr", "td", "th", "tbody", "thead") |
| | | .addAttributes("div", "class", "style", "id") |
| | | .addAttributes("span", "class", "style") |
| | | .addAttributes("table", "border", "cellpadding", "cellspacing") |
| | | .addAttributes("img", "src", "alt", "title", "width", "height") |
| | | .addProtocols("img", "src", "http", "https", "data") |
| | | .removeTags("iframe", "object", "embed", "script", "style") |
| | | |
| | | // 4. 自定义白名单(根据业务需求) |
| | | val CUSTOM = Safelist.none() |
| | | .addTags( |
| | | "p", "br", "b", "i", "u", "strong", "em", |
| | | "h1", "h2", "h3", "h4", "h5", "h6", |
| | | "ul", "ol", "li", "a", "img", "div", "span" |
| | | ) |
| | | .addAttributes("a", "href", "target", "rel") |
| | | .addAttributes("img", "src", "alt", "width", "height") |
| | | .addAttributes("div", "class", "id") |
| | | .addProtocols("a", "href", "http", "https", "mailto", "tel") |
| | | .addProtocols("img", "src", "http", "https", "data") |
| | | .addEnforcedAttribute("a", "rel", "nofollow noopener noreferrer") |
| | | } |
| | | |
| | | /** |
| | | * 基础清洗方法 |
| | | */ |
| | | fun sanitizeBasic(html: String, whitelist: Safelist = Whitelists.SIMPLE_DOCUMENT): String { |
| | | return Jsoup.clean(html, whitelist) |
| | | } |
| | | |
| | | /** |
| | | * 高级清洗:包含额外安全检查和转换 |
| | | */ |
| | | fun sanitizeAdvanced( |
| | | html: String, |
| | | baseUri: String = "" |
| | | ): String { |
| | | val document = Jsoup.parse(html, baseUri) |
| | | |
| | | // 1. 移除危险标签 |
| | | val dangerousTags = listOf("script", "iframe", "object", "embed", |
| | | "applet", "meta", "link", "base", "frame") |
| | | dangerousTags.forEach { tag -> |
| | | document.select(tag).remove() |
| | | } |
| | | |
| | | // 2. 移除危险属性 |
| | | val dangerousAttrs = listOf( |
| | | "onclick", "onload", "onerror", "onmouseover", "onmouseout", |
| | | "onkeydown", "onkeypress", "onkeyup", "onfocus", "onblur", |
| | | "onchange", "onsubmit", "href" // 特别处理href |
| | | ) |
| | | |
| | | document.allElements.forEach { element -> |
| | | val attributes = element.attributes() |
| | | attributes.forEach { attr -> |
| | | // 移除以"on"开头的事件处理器 |
| | | if (attr.key.startsWith("on")) { |
| | | attributes.remove(attr.key) |
| | | } |
| | | // 移除javascript:协议 |
| | | if (attr.key == "href" || attr.key == "src") { |
| | | val value = attr.value.lowercase(Locale.ROOT) |
| | | if (value.startsWith("javascript:") || |
| | | value.startsWith("data:text/html") || |
| | | value.startsWith("file://")) { |
| | | attributes.remove(attr.key) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 3. 净化链接(确保外部链接有安全属性) |
| | | document.select("a[href]").forEach { link -> |
| | | val href = link.attr("href") |
| | | if (href.startsWith("http://") || href.startsWith("https://")) { |
| | | link.attr("target", "_blank") |
| | | link.attr("rel", "noopener noreferrer") |
| | | } |
| | | } |
| | | |
| | | // 4. 净化图片(防止盗链和XSS) |
| | | document.select("img[src]").forEach { img -> |
| | | val src = img.attr("src") |
| | | // 检查data:协议图片 |
| | | if (src.startsWith("data:image")) { |
| | | // 可以限制data URL大小 |
| | | if (src.length > 10000) { // 限制10KB |
| | | img.remove() |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 5. 应用白名单 |
| | | val cleaned = Jsoup.clean(document.body().html(), Whitelists.RICH_TEXT) |
| | | |
| | | return cleaned |
| | | } |
| | | |
| | | /** |
| | | * 针对用户输入的富文本编辑器内容清洗 |
| | | */ |
| | | fun sanitizeUserContent(html: String): String { |
| | | // 解析文档 |
| | | val doc = Jsoup.parseBodyFragment(html) |
| | | |
| | | // 创建自定义白名单 |
| | | val whitelist = Whitelists.CUSTOM |
| | | |
| | | // 特别处理style属性 |
| | | doc.select("*[style]").forEach { element -> |
| | | val style = element.attr("style") |
| | | val sanitizedStyle = sanitizeCss(style) |
| | | if (sanitizedStyle.isNotEmpty()) { |
| | | element.attr("style", sanitizedStyle) |
| | | } else { |
| | | element.removeAttr("style") |
| | | } |
| | | } |
| | | |
| | | // 清理class属性 |
| | | doc.select("*[class]").forEach { element -> |
| | | val classes = element.attr("class").split(" ") |
| | | val safeClasses = classes.filter { isValidClassName(it) } |
| | | if (safeClasses.isNotEmpty()) { |
| | | element.attr("class", safeClasses.joinToString(" ")) |
| | | } else { |
| | | element.removeAttr("class") |
| | | } |
| | | } |
| | | |
| | | // 最终应用白名单 |
| | | return Jsoup.clean(doc.body().html(), whitelist) |
| | | } |
| | | |
| | | private fun sanitizeCss(css: String): String { |
| | | // 只允许安全的CSS属性 |
| | | val allowedProperties = listOf( |
| | | "color", "background-color", "font-size", "font-weight", |
| | | "font-family", "text-align", "margin", "padding", |
| | | "border", "width", "height", "display", "position" |
| | | ) |
| | | |
| | | val pattern = Regex("""([a-z\-]+)\s*:\s*([^;]+)""") |
| | | val sanitized = StringBuilder() |
| | | |
| | | pattern.findAll(css).forEach { match -> |
| | | val property = match.groupValues[1] |
| | | val value = match.groupValues[2] |
| | | |
| | | if (allowedProperties.contains(property) || |
| | | property.startsWith("margin-") || |
| | | property.startsWith("padding-") || |
| | | property.startsWith("border-")) { |
| | | |
| | | // 检查值是否安全 |
| | | if (isSafeCssValue(property, value)) { |
| | | if (sanitized.isNotEmpty()) sanitized.append("; ") |
| | | sanitized.append("$property: $value") |
| | | } |
| | | } |
| | | } |
| | | |
| | | return sanitized.toString() |
| | | } |
| | | |
| | | private fun isSafeCssValue(property: String, value: String): Boolean { |
| | | // 防止CSS注入攻击 |
| | | val unsafePatterns = listOf( |
| | | Regex("expression\\s*\\("), |
| | | Regex("url\\s*\\(.*javascript:"), |
| | | Regex("javascript:"), |
| | | Regex("data:text/html") |
| | | ) |
| | | |
| | | return unsafePatterns.none { pattern -> |
| | | pattern.containsMatchIn(value) |
| | | } |
| | | } |
| | | |
| | | private fun isValidClassName(className: String): Boolean { |
| | | // 只允许字母、数字、连字符、下划线 |
| | | return className.matches(Regex("^[a-zA-Z0-9_\\-]+$")) |
| | | } |
| | | } |
| | |
| | | android:layout_marginHorizontal="@dimen/dp_32" |
| | | android:background="@null" |
| | | android:gravity="center_vertical" |
| | | android:hint="请输入密码(最少6位)" |
| | | android:hint="请输入密码(最少8位)" |
| | | android:inputType="textPassword" |
| | | android:text="@={viewModel.psw}" |
| | | android:textColor="@color/gray_10" |
| | |
| | | leftBlock: () -> Boolean = { true }, |
| | | rightBlock: () -> Boolean = { true } |
| | | ) { |
| | | showCommonTipsDialog( |
| | | context = context, |
| | | content = content, |
| | | leftText = leftText, |
| | | rightText = rightText, |
| | | leftBlock = { dialog -> |
| | | if (leftBlock.invoke()) { |
| | | dialog.dismiss() |
| | | } |
| | | }, |
| | | rightBlock = { dialog -> |
| | | if (rightBlock.invoke()) { |
| | | dialog.dismiss() |
| | | } |
| | | }, |
| | | ) |
| | | } |
| | | |
| | | /** |
| | | * 通用提示弹窗 |
| | | * @param content 内容 |
| | | * @param leftBlock 回调事件 返回true dismiss |
| | | * @param rightBlock 回调事件 返回true dismiss |
| | | */ |
| | | fun showCommonTipsDialog( |
| | | context: Context, |
| | | content: String, |
| | | leftText: String? = "取消", |
| | | rightText: String? = "确定", |
| | | leftBlock: (MaterialDialog) -> Unit = { }, |
| | | rightBlock: (MaterialDialog) -> Unit = { } |
| | | ) { |
| | | Handler(Looper.getMainLooper()).post { |
| | | val dialog = MaterialDialog(context).apply { |
| | | if (!BuildConfig.DEBUG) { |
| | |
| | | btnLeft.isVisible = !leftText.isNullOrEmpty() |
| | | btnRight.isVisible = !rightText.isNullOrEmpty() |
| | | btnLeft.setOnClickListener { |
| | | if (leftBlock()) { |
| | | dismiss() |
| | | } |
| | | leftBlock(this) |
| | | } |
| | | btnRight.setOnClickListener { |
| | | if (rightBlock()) { |
| | | dismiss() |
| | | } |
| | | rightBlock(this) |
| | | } |
| | | } |
| | | dialog.show() |
| | |
| | | enum class KeyPreferences(val value: String) { |
| | | TOKEN("token"), |
| | | USER("user"), |
| | | THIRD("third"), |
| | | ACCIDENT("accident"), |
| | | NEED_READ("needRead"), // 是否需要阅读协议 |
| | | NEED_UPDATE("needUpdate") // 是否升级 |
| | |
| | | crashreport = "latest.release" |
| | | alicloud = "3.10.1" |
| | | shadowLayout = "3.4.5" |
| | | jsoup = "1.22.2" |
| | | |
| | | |
| | | [libraries] |
| | |
| | | alicloud = { module = "com.aliyun.ams:alicloud-android-push", version.ref = "alicloud" } |
| | | shadowLayout = { module = "com.github.lihangleo2:ShadowLayout", version.ref = "shadowLayout" } |
| | | huawei-agconnect-core = { module = "com.huawei.agconnect:agconnect-core", version.ref = "huawei" } |
| | | jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } |
| | | |
| | | [plugins] |
| | | android-application = { id = "com.android.application", version.ref = "agp" } |