18034727885
2026-06-20 3ae8b314e3d4a87e60894df1ce63f2ce35cd4743
同步代码
20个文件已修改
442 ■■■■■ 已修改文件
app/src/main/java/com/xw/yunwei/BaseBusinessActivity.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/task/TaskFragment.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/task/accident/detail/AccidentDetailActivity.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/task/create/CreateTaskActivity.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/checkin/CheckInActivity.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/car/manage/CarManageActivity.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/RoutePlanningActivity.kt 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/util/AMapNaviListenerWrap.kt 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/viewmodel/RoutePlanningViewModel.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/site/SiteNavActivity.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/utils/DialogFactory.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_site_nav.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_accident_filter.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_record_list.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/item_car_repair.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/xml/network_security_config.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/CommonDialog.kt 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/utils/GpsHelper.kt 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/BaseBusinessActivity.kt
@@ -7,9 +7,9 @@
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.databinding.ViewDataBinding
import com.xw.common.SecureScreenShot
import com.xw.common.base.BaseActivity
import com.xw.common.base.BaseViewModel
import com.xw.common.utils.BindUtils
@@ -22,9 +22,7 @@
    BaseActivity<V, VM>() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (!BuildConfig.DEBUG) {
            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)//禁止截屏
        }
        SecureScreenShot.apply(window)
    }
    override fun setContentView(view: View?) {
app/src/main/java/com/xw/yunwei/ui/task/TaskFragment.kt
@@ -448,10 +448,7 @@
     */
    override fun location(pLocation: AMapLocation?) {
        if (pLocation == null) {
            CommonDialog.showTipsDialog(
                requireActivity(),
                "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                null)
            CommonDialog.showLocationNotGrantedTipsDialog(requireActivity())
        } else {
            viewModel.location = pLocation
            EventBus.getDefault().post(locationData)
app/src/main/java/com/xw/yunwei/ui/task/accident/detail/AccidentDetailActivity.kt
@@ -221,10 +221,7 @@
     */
    override fun location(pLocation: AMapLocation?) {
        if (pLocation == null) {
            CommonDialog.showTipsDialog(
                this@AccidentDetailActivity,
                "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                null)
            CommonDialog.showLocationNotGrantedTipsDialog(this@AccidentDetailActivity)
        } else {
            viewModel.location = pLocation
            viewModel.locationCheck()
app/src/main/java/com/xw/yunwei/ui/task/create/CreateTaskActivity.kt
@@ -386,10 +386,7 @@
     */
    override fun location(pLocation: AMapLocation?) {
        if (pLocation == null) {
            CommonDialog.showTipsDialog(
                this@CreateTaskActivity,
                "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                null)
            CommonDialog.showLocationNotGrantedTipsDialog(this@CreateTaskActivity)
        } else {
            //viewModel.location.value = pLocation
        }
app/src/main/java/com/xw/yunwei/ui/user/mine/checkin/CheckInActivity.kt
@@ -60,11 +60,7 @@
        mapFrag.locationChangeCallback = {
            dismissDialog()
            if (it == null) {
                CommonDialog.showTipsDialog(
                    this@CheckInActivity,
                    "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                    null
                )
                CommonDialog.showLocationNotGrantedTipsDialog(this@CheckInActivity)
            } else {
                if (viewModel.pageType.value == 2) {
                    mapMarkRefresh()
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt
@@ -12,6 +12,7 @@
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.xw.common.CommonDialog
import com.xw.common.SecureScreenShot
import com.xw.common.net.UpdateManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.BR
@@ -83,6 +84,7 @@
        }
        binding.about.setOnClickListener {
            handleAboutClickForScreenShot()
            val versionCode = AppUtils.getAppVersionCode()
            viewModel.getVersion {
                if (versionCode < (it.forceUpdateFlag ?: 0)) {
@@ -109,6 +111,23 @@
        }
    }
    /** 连续点击「关于」的次数,达到阈值后开启截屏权限 */
    private var aboutClickCount = 0
    /**
     * 连续点击「关于」5 次后,允许 APP 被截屏(清除 FLAG_SECURE)。
     */
    private fun handleAboutClickForScreenShot() {
        if (SecureScreenShot.allowScreenShot) return
        aboutClickCount++
        if (aboutClickCount >= 5) {
            // 赋值变化会自动刷新所有已登记 window 的截屏标记
            SecureScreenShot.allowScreenShot = true
            aboutClickCount = 0
            ToastUtilCommon.show("已开启截屏权限")
        }
    }
    private fun checkNotice() {
        // 使用 NotificationManagerCompat 检查通知权限状态
        val isEnabled = NotificationManagerCompat.from(this).areNotificationsEnabled()
app/src/main/java/com/xw/yunwei/ui/work/car/manage/CarManageActivity.kt
@@ -90,10 +90,7 @@
        binding.arrow.setOnClickListener { toggleBottomSheetState() }
        mapFrag.locationChangeCallback = { location ->
            if (location == null) {
                CommonDialog.showTipsDialog(
                    this@CarManageActivity,
                    "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                    null)
                CommonDialog.showLocationNotGrantedTipsDialog(this@CarManageActivity)
            } else {
                if (viewModel.fences.value.isNullOrEmpty()) {
                    val cityCode = location.adCode.substring(0, 4) + "00"
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/RoutePlanningActivity.kt
@@ -26,6 +26,7 @@
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.BR
import com.xw.yunwei.BaseBusinessActivity
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.R
import com.xw.yunwei.constants.Key
import com.xw.yunwei.databinding.ActivityRoutePlanningBinding
@@ -190,13 +191,15 @@
    override fun setListener() {
        super.setListener()
        binding.llWalk.setOnClickListener {
            viewModel.naviPaths.value?.getValue(0)?.also {
            // 用 get 而非 getValue:步行路线未算出时 key 0 不存在,getValue 会抛 NoSuchElementException 导致闪退
            viewModel.naviPaths.value?.get(0)?.also {
                viewModel.checkWalk.postValue(true)
                drawRoutes(it)
            }
        }
        binding.llBicycle.setOnClickListener {
            viewModel.naviPaths.value?.getValue(1)?.also {
            // 用 get 而非 getValue:骑行路线未算出时 key 1 不存在,getValue 会抛 NoSuchElementException 导致闪退
            viewModel.naviPaths.value?.get(1)?.also {
                viewModel.checkWalk.postValue(false)
                drawRoutes(it)
            }
@@ -218,17 +221,29 @@
    override fun location(pLocation: AMapLocation?) {
        if (pLocation == null) {
            CommonDialog.showTipsDialog(
                this,
                "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                null
            )
            CommonDialog.showLocationNotGrantedTipsDialog(this)
        } else {
            val hadNotInitLocation = aMapLocation == null
            aMapLocation = pLocation
            // 拿到定位后立即把地图相机移到当前位置,避免默认停留在北京(高德地图默认中心点)
            moveCameraToCurrentLocation(pLocation)
            if (hadNotInitLocation) {
                calculateRoute()
            }
        }
    }
    //移动地图相机到当前定位位置
    private fun moveCameraToCurrentLocation(location: AMapLocation) {
        if (location.latitude == 0.0 && location.longitude == 0.0) return
        // 路线尚未绘制时才移动相机,避免覆盖 drawRoutes 中 zoomToSpan 的路线视角
        if (routeOverLay == null) {
            binding.mapView.map?.moveCamera(
                CameraUpdateFactory.newLatLngZoom(
                    LatLng(location.latitude, location.longitude),
                    16f
                )
            )
        }
    }
@@ -260,6 +275,14 @@
        val passingPoints = viewModel.getRoutePlanningPassingPoints()
        aMapNavi?.also { it ->
            walkListener = object : AMapNaviListenerWrap() {
                override fun onCalculateRouteFailure(p0: AMapCalcRouteResult?) {
                    super.onCalculateRouteFailure(p0)
                    if (BuildConfig.DEBUG) {
                        p0?.errorDescription?.let {
                            ToastUtilCommon.showError(it)
                        }
                    }
                }
                override fun onCalculateRouteSuccess(p0: AMapCalcRouteResult?) {
                    it.removeAMapNaviListener(this)
                    super.onCalculateRouteSuccess(p0)
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/util/AMapNaviListenerWrap.kt
@@ -1,5 +1,6 @@
package com.xw.yunwei.ui.work.checkInMission.util
import android.util.Log
import com.amap.api.navi.AMapNaviListener
import com.amap.api.navi.model.AMapCalcRouteResult
import com.amap.api.navi.model.AMapLaneInfo
@@ -16,68 +17,73 @@
//高德地图导航监听包裹类
open class AMapNaviListenerWrap : AMapNaviListener {
    override fun onInitNaviFailure() {
    companion object {
        private const val TAG = "AMapNaviListenerWrap"
    }
    override fun onInitNaviFailure() {
        Log.d(TAG, "onInitNaviFailure")
    }
    override fun onInitNaviSuccess() {
        Log.d(TAG, "onInitNaviSuccess")
    }
    override fun onStartNavi(p0: Int) {
        Log.d(TAG, "onStartNavi: type=$p0")
    }
    override fun onTrafficStatusUpdate() {
        Log.d(TAG, "onTrafficStatusUpdate")
    }
    override fun onLocationChange(p0: AMapNaviLocation?) {
        Log.d(TAG, "onLocationChange: location=$p0")
    }
    override fun onGetNavigationText(p0: Int, p1: String?) {
        Log.d(TAG, "onGetNavigationText: type=$p0, text=$p1")
    }
    override fun onGetNavigationText(p0: String?) {
        Log.d(TAG, "onGetNavigationText: text=$p0")
    }
    override fun onEndEmulatorNavi() {
        Log.d(TAG, "onEndEmulatorNavi")
    }
    override fun onArriveDestination() {
        Log.d(TAG, "onArriveDestination")
    }
    override fun onCalculateRouteFailure(p0: Int) {
        Log.d(TAG, "onCalculateRouteFailure: errorCode=$p0")
    }
    override fun onReCalculateRouteForYaw() {
        Log.d(TAG, "onReCalculateRouteForYaw")
    }
    override fun onReCalculateRouteForTrafficJam() {
        Log.d(TAG, "onReCalculateRouteForTrafficJam")
    }
    override fun onArrivedWayPoint(p0: Int) {
        Log.d(TAG, "onArrivedWayPoint: wayPointIndex=$p0")
    }
    override fun onGpsOpenStatus(p0: Boolean) {
        Log.d(TAG, "onGpsOpenStatus: enabled=$p0")
    }
    override fun onNaviInfoUpdate(p0: NaviInfo?) {
        Log.d(TAG, "onNaviInfoUpdate: naviInfo=$p0")
    }
    override fun updateCameraInfo(p0: Array<out AMapNaviCameraInfo?>?) {
        Log.d(TAG, "updateCameraInfo: cameraInfoCount=${p0?.size}")
    }
    override fun updateIntervalCameraInfo(
@@ -85,27 +91,27 @@
        p1: AMapNaviCameraInfo?,
        p2: Int
    ) {
        Log.d(TAG, "updateIntervalCameraInfo: start=$p0, end=$p1, type=$p2")
    }
    override fun onServiceAreaUpdate(p0: Array<out AMapServiceAreaInfo?>?) {
        Log.d(TAG, "onServiceAreaUpdate: serviceAreaCount=${p0?.size}")
    }
    override fun showCross(p0: AMapNaviCross?) {
        Log.d(TAG, "showCross: cross=$p0")
    }
    override fun hideCross() {
        Log.d(TAG, "hideCross")
    }
    override fun showModeCross(p0: AMapModelCross?) {
        Log.d(TAG, "showModeCross: modelCross=$p0")
    }
    override fun hideModeCross() {
        Log.d(TAG, "hideModeCross")
    }
    override fun showLaneInfo(
@@ -113,58 +119,62 @@
        p1: ByteArray?,
        p2: ByteArray?
    ) {
        Log.d(TAG, "showLaneInfo: laneInfoCount=${p0?.size}")
    }
    override fun showLaneInfo(p0: AMapLaneInfo?) {
        Log.d(TAG, "showLaneInfo: laneInfo=$p0")
    }
    override fun hideLaneInfo() {
        Log.d(TAG, "hideLaneInfo")
    }
    override fun onCalculateRouteSuccess(p0: IntArray?) {
        Log.d(TAG, "onCalculateRouteSuccess: routeIds=${p0?.contentToString()}")
    }
    override fun notifyParallelRoad(p0: Int) {
        Log.d(TAG, "notifyParallelRoad: roadType=$p0")
    }
    override fun OnUpdateTrafficFacility(p0: Array<out AMapNaviTrafficFacilityInfo?>?) {
        Log.d(TAG, "OnUpdateTrafficFacility: facilityCount=${p0?.size}")
    }
    override fun OnUpdateTrafficFacility(p0: AMapNaviTrafficFacilityInfo?) {
        Log.d(TAG, "OnUpdateTrafficFacility: facility=$p0")
    }
    override fun updateAimlessModeStatistics(p0: AimLessModeStat?) {
        Log.d(TAG, "updateAimlessModeStatistics: stat=$p0")
    }
    override fun updateAimlessModeCongestionInfo(p0: AimLessModeCongestionInfo?) {
        Log.d(TAG, "updateAimlessModeCongestionInfo: congestionInfo=$p0")
    }
    override fun onPlayRing(p0: Int) {
        Log.d(TAG, "onPlayRing: ringType=$p0")
    }
    override fun onCalculateRouteSuccess(p0: AMapCalcRouteResult?) {
        Log.d(TAG, "onCalculateRouteSuccess: result=$p0。errorCode: ${p0?.errorCode}.")
    }
    override fun onCalculateRouteFailure(p0: AMapCalcRouteResult?) {
        Log.d(TAG, "onCalculateRouteFailure: result=$p0。" +
                "errorCode: ${p0?.errorCode}, " +
                "errorDetail: ${p0?.errorDetail}, " +
                "errorDescription: ${p0?.errorDescription}"
        )
    }
    override fun onNaviRouteNotify(p0: AMapNaviRouteNotifyData?) {
        Log.d(TAG, "onNaviRouteNotify: notifyData=$p0")
    }
    override fun onGpsSignalWeak(p0: Boolean) {
        Log.d(TAG, "onGpsSignalWeak: weak=$p0")
    }
}
}
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/viewmodel/RoutePlanningViewModel.kt
@@ -57,7 +57,8 @@
    //导航
    fun navigate(startPoint: NaviPoi) {
        naviPaths.value?.getValue(if (checkWalk.value ?: false) 0 else 1)?.let {
        // 用 get 而非 getValue:对应出行方式路线未算出时 key 不存在,getValue 会抛 NoSuchElementException
        naviPaths.value?.get(if (checkWalk.value ?: false) 0 else 1)?.let {
            startActivity(
                CruiseCheckInActivity::class.java,
                CruiseCheckInActivity.getParams(
app/src/main/java/com/xw/yunwei/ui/work/site/SiteNavActivity.kt
@@ -131,11 +131,7 @@
        mapFrag.locationChangeCallback = { location ->
            dismissDialog()
            if (location == null) {
                CommonDialog.showTipsDialog(
                    this@SiteNavActivity,
                    "您未开启gps权限,无法获取当前定位,请打开后重新操作",
                    null
                )
                CommonDialog.showLocationNotGrantedTipsDialog(this@SiteNavActivity)
            } else {
                if (viewModel.fences.value.isNullOrEmpty()) {
                    viewModel.cityCode = location.adCode.substring(0, 4) + "00"
app/src/main/java/com/xw/yunwei/utils/DialogFactory.kt
@@ -1,11 +1,10 @@
package com.xw.yunwei.utils
import android.content.Context
import android.view.WindowManager
import android.widget.FrameLayout
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.getCustomView
import com.xw.common.BuildConfig
import com.xw.common.SecureScreenShot
import com.xw.common.utils.BindUtils
import com.xw.yunwei.Constants
import com.xw.yunwei.R
@@ -18,9 +17,7 @@
    //生产
    fun produce(context: Context): MaterialDialog {
        return MaterialDialog(context).apply {
            if (!BuildConfig.DEBUG) {
                window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)//禁止截屏
            }
            SecureScreenShot.apply(window)
            this.view.post {
                getCustomView().post {
                    val root = window!!.decorView as FrameLayout
app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt
@@ -171,47 +171,54 @@
    }
    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*([^;]+)""")
        // 采用“危险值黑名单”策略:保留所有正常 CSS 属性,仅剔除真正的 XSS 向量,
        // 避免误删 line-height、text-decoration、list-style、background 等常用属性导致样式异常。
        val pattern = Regex("""([a-zA-Z\-]+)\s*:\s*([^;]+)""")
        val sanitized = StringBuilder()
        pattern.findAll(css).forEach { match ->
            val property = match.groupValues[1]
            val value = match.groupValues[2]
            val property = match.groupValues[1].trim().lowercase(Locale.ROOT)
            val value = match.groupValues[2].trim()
            if (allowedProperties.contains(property) ||
                property.startsWith("margin-") ||
                property.startsWith("padding-") ||
                property.startsWith("border-")) {
            // 过滤危险属性(老式浏览器脚本注入向量)
            if (isDangerousCssProperty(property)) {
                return@forEach
            }
                // 检查值是否安全
                if (isSafeCssValue(property, value)) {
                    if (sanitized.isNotEmpty()) sanitized.append("; ")
                    sanitized.append("$property: $value")
                }
            // 过滤危险值
            if (isSafeCssValue(property, value)) {
                if (sanitized.isNotEmpty()) sanitized.append("; ")
                sanitized.append("$property: $value")
            }
        }
        return sanitized.toString()
    }
    /**
     * 危险 CSS 属性(可触发脚本执行的历史浏览器特性)
     */
    private fun isDangerousCssProperty(property: String): Boolean {
        val dangerous = listOf("behavior", "-moz-binding", "-o-link", "-o-link-source")
        return dangerous.contains(property) || property.endsWith("-binding")
    }
    private fun isSafeCssValue(property: String, value: String): Boolean {
        // 防止CSS注入攻击
        // 防止 CSS 注入攻击:拦截脚本协议、表达式、外部绑定与可执行 data URL
        val v = value.lowercase(Locale.ROOT)
        val unsafePatterns = listOf(
            Regex("expression\\s*\\("),
            Regex("url\\s*\\(.*javascript:"),
            Regex("javascript:"),
            Regex("data:text/html")
            Regex("vbscript:"),
            Regex("-moz-binding"),
            Regex("@import"),
            Regex("""url\s*\(\s*['"]?\s*javascript:"""),
            Regex("""url\s*\(\s*['"]?\s*vbscript:"""),
            Regex("""url\s*\(\s*['"]?\s*data:text/html""")
        )
        return unsafePatterns.none { pattern ->
            pattern.containsMatchIn(value)
            pattern.containsMatchIn(v)
        }
    }
app/src/main/res/layout/activity_site_nav.xml
@@ -196,6 +196,18 @@
        </androidx.coordinatorlayout.widget.CoordinatorLayout>
        <View
            android:id="@+id/filterOverlay"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#80000000"
            android:elevation="@dimen/dp_4"
            android:visibility="gone"
            app:layout_constraintTop_toBottomOf="@+id/topLayout"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
        <LinearLayout
            showAnim="@{viewModel.siteItem!=null}"
            android:layout_width="match_parent"
app/src/main/res/layout/dialog_accident_filter.xml
@@ -40,7 +40,6 @@
            app:layout_constraintBottom_toBottomOf="@+id/title"
            app:layout_constraintTop_toTopOf="@+id/title" />
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
@@ -55,7 +54,6 @@
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        <!-- 左边列表 -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/leftRecyclerView"
@@ -65,7 +63,6 @@
            android:background="#ffF4F5F7"
            app:layout_constraintLeft_toLeftOf="parent"
            android:scrollbars="vertical"
            app:layout_constraintBottom_toTopOf="@+id/view"
            app:layout_constraintTop_toBottomOf="@+id/title" />
        <!-- 右边列表 -->
@@ -74,23 +71,21 @@
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginHorizontal="@dimen/dp_20"
            android:layout_marginTop="@dimen/dp_40"
            android:layout_weight="1"
            android:background="@android:color/white"
            android:scrollbars="vertical"
            app:layout_constraintBottom_toTopOf="@+id/view"
            app:layout_constraintLeft_toRightOf="@+id/leftRecyclerView"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/title" />
            app:layout_constraintBottom_toBottomOf="@id/leftRecyclerView"
            app:layout_constraintTop_toTopOf="@id/leftRecyclerView" />
        <View
            android:id="@+id/view"
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:layout_marginTop="@dimen/dp_386"
            android:background="@color/gray_4"
            app:layout_constraintTop_toBottomOf="@+id/title" />
            app:layout_constraintTop_toBottomOf="@+id/leftRecyclerView" />
        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnLeft"
app/src/main/res/layout/fragment_record_list.xml
@@ -30,7 +30,8 @@
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:gravity="center_vertical|start"
                    android:paddingStart="@dimen/dp_16"
                    android:text="操作人"
                    android:textColor="@color/gray_9"
                    android:textSize="@dimen/sp_12" />
app/src/main/res/layout/item_car_repair.xml
@@ -18,7 +18,8 @@
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:gravity="center_vertical|start"
            android:paddingStart="@dimen/dp_16"
            android:text="李中冰"
            android:textColor="@color/gray_9"
            android:textSize="@dimen/sp_12"
app/src/main/res/xml/network_security_config.xml
@@ -1,4 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
    <!-- 仅 debug 构建生效:信任用户安装的 CA(Proxyman/Charles 抓包用),release 不受影响 -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>
common/src/main/java/com/xw/common/CommonDialog.kt
@@ -7,7 +7,6 @@
import android.text.method.LinkMovementMethod
import android.util.TypedValue
import android.view.Gravity
import android.view.WindowManager
import android.widget.Button
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
@@ -16,11 +15,97 @@
import com.afollestad.materialdialogs.LayoutMode
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.xw.common.databinding.DialogShowSelectBinding
import java.util.WeakHashMap
object CommonDialog {
    fun showLocationNotGrantedTipsDialog(context: Context) {
        /*
        CommonDialog.showSingleTipsDialog(
            context,
            "您未开启gps权限,无法获取当前定位,请打开后重新操作",
            null
        )
        */
        // CommonDialog.showCommonTipsDialog(
        CommonDialog.showSingleTipsDialog(
            context = context,
            content = "您未开启gps权限,无法获取当前定位,请打开后重新操作", // "定位权限已被拒绝,将无法获取当前位置,请前往系统设置开启定位权限。",
            leftText = "取消",
            rightText = "去设置",
            // leftBlock = { dialog -> dialog.dismiss() },
            rightBlock = { dialog ->
                // 跳转到当前应用的权限设置页
                XXPermissions.startPermissionActivity(
                    context,
                    arrayOf(Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION)
                )
                true
            }
        )
    }
    // 记录每个页面(Context)当前正在展示的"单例"提示弹窗,避免同一界面重复弹出相同弹窗。
    // 用 WeakHashMap 以 Context 为 key,Activity 销毁后可被回收,不会内存泄漏。
    private val singleTipsDialogMap = WeakHashMap<Context, MaterialDialog>()
    /**
     * 单例提示弹窗:同一个 Activity(Context) 内若已弹出且尚未取消,则不再重复弹出。
     *
     * 适用于定位/GPS 等可能被多处回调(多次 onLocationChanged 等)同时触发、
     * 导致同一界面叠加出多个相同提示弹窗的场景。
     *
     * @param content 内容
     * @param leftBlock 回调事件  返回true dismiss
     * @param rightBlock 回调事件  返回true dismiss
     */
    private fun showSingleTipsDialog(
        context: Context,
        content: String,
        leftText: String? = null,
        rightText: String? = "确定",
        leftBlock: (MaterialDialog) -> Boolean = { true },
        rightBlock: (MaterialDialog) -> Boolean = { true }
    ) {
        Handler(Looper.getMainLooper()).post {
            // 该界面已弹出同类弹窗且仍在显示,直接跳过,避免重复弹出
            singleTipsDialogMap[context]?.let { if (it.isShowing) return@post }
            val dialog = MaterialDialog(context).apply {
                SecureScreenShot.apply(window)
                customView(
                    R.layout.dialog_confirm, noVerticalPadding = true, dialogWrapContent = false
                )
                cancelable(true)
                cornerRadius(15f)
                val tvContent = findViewById<AppCompatTextView>(R.id.content)
                val btnLeft = findViewById<Button>(R.id.btnLeft)
                val btnRight = findViewById<Button>(R.id.btnRight)
                tvContent.text = content
                btnLeft.text = leftText ?: ""
                btnRight.text = rightText ?: ""
                btnLeft.isVisible = !leftText.isNullOrEmpty()
                btnRight.isVisible = !rightText.isNullOrEmpty()
                btnLeft.setOnClickListener { if (leftBlock(this)) dismiss() }
                btnRight.setOnClickListener { if (rightBlock(this)) dismiss() }
                // 弹窗被关闭后移除记录,下次可正常再次弹出
                onDismiss { singleTipsDialogMap.remove(context) }
            }
            singleTipsDialogMap[context] = dialog
            dialog.show()
            dialog.window?.setGravity(Gravity.CENTER)
            dialog.window?.setWindowAnimations(R.style.AnimLeft)
        }
    }
    /**
     * 通用提示弹窗
@@ -70,9 +155,7 @@
    ) {
        Handler(Looper.getMainLooper()).post {
            val dialog = MaterialDialog(context).apply {
                if (!BuildConfig.DEBUG) {
                    window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)//禁止截屏
                }
                SecureScreenShot.apply(window)
                customView(
                    R.layout.dialog_confirm, noVerticalPadding = true, dialogWrapContent = false
                )
@@ -106,9 +189,7 @@
            .cornerRadius(15f)
            .cancelable(true)
            .show {
                if (!BuildConfig.DEBUG) {
                    window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)//禁止截屏
                }
                SecureScreenShot.apply(window)
                customView(
                    R.layout.dialog_show_select,
                    noVerticalPadding = true,
@@ -169,9 +250,7 @@
    ) {
        Handler(Looper.getMainLooper()).post {
            val dialog = MaterialDialog(context).apply {
                if (!BuildConfig.DEBUG) {
                    window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)//禁止截屏
                }
                SecureScreenShot.apply(window)
                customView(
                    R.layout.dialog_notice, noVerticalPadding = true, dialogWrapContent = false
                )
common/src/main/java/com/xw/common/utils/GpsHelper.kt
@@ -21,6 +21,7 @@
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.xw.common.CommonDialog
import com.xw.common.Constants
/**
@@ -49,6 +50,16 @@
    // 标记是否正在监听 GPS 状态(用于避免重复注册)
    private var isWatchingGpsStatus = false
    // 是否已经发起过位置权限申请(避免每次 onResume 都重复 request 导致权限 Activity 反复拉起、onResume 死循环)
    private var hasRequestedPermission = false
    // 是否已经弹过"权限被拒绝"提示框(避免多次弹出)
    private var hasShownDeniedDialog = false
    // 定位权限组
    private val locationPermissions =
        arrayOf(Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION)
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun resume() {
        LogUtils.i(TAG, "onResume isAutomatic = $isAutomatic")
@@ -73,18 +84,48 @@
    }
    fun startLocation() {
        val pl = arrayOf(Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION)
        XXPermissions.with(context).permission(pl).request(object : OnPermissionCallback {
            override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) { // 权限同意
                initLocation()
            }
        // 已授权:直接定位,绝不再调 request()。
        // request() 在 XXPermissions 18+ 会拉起一个透明的权限 Activity,
        // 权限被永久拒绝时该 Activity 会立即返回并 finish,从而再次触发宿主 onResume,
        // 若每次 onResume 都无条件 request 就会形成 onResume <-> 权限 Activity 的死循环。
        if (XXPermissions.isGranted(context, locationPermissions)) {
            hasShownDeniedDialog = false // 已授权,重置提示状态,便于下次被撤销时能再次提醒
            initLocation()
            return
        }
            override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                super.onDenied(permissions, doNotAskAgain)
                //权限禁止
                listener.location(null)
            }
        })
        // 之前已经申请过且现在仍未授权 => 已被拒绝:不再 request,避免死循环,只提示一次
        if (hasRequestedPermission) {
            listener.location(null)
            showDeniedDialogOnce()
            return
        }
        // 首次申请,最多触发一次
        hasRequestedPermission = true
        XXPermissions.with(context).permission(locationPermissions)
            .request(object : OnPermissionCallback {
                override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) { // 权限同意
                    hasShownDeniedDialog = false
                    initLocation()
                }
                override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                    super.onDenied(permissions, doNotAskAgain)
                    //权限禁止
                    listener.location(null)
                    showDeniedDialogOnce()
                }
            })
    }
    /**
     * 位置权限被拒绝后,弹框告知用户(同一生命周期内只弹一次,避免重复打扰)
     */
    private fun showDeniedDialogOnce() {
        if (hasShownDeniedDialog) return
        hasShownDeniedDialog = true
        CommonDialog.showLocationNotGrantedTipsDialog(context)
    }
    private fun initLocation() {