| | |
| | | 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 |
| | |
| | | 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?) { |
| | |
| | | */ |
| | | override fun location(pLocation: AMapLocation?) { |
| | | if (pLocation == null) { |
| | | CommonDialog.showTipsDialog( |
| | | requireActivity(), |
| | | "您未开启gps权限,无法获取当前定位,请打开后重新操作", |
| | | null) |
| | | CommonDialog.showLocationNotGrantedTipsDialog(requireActivity()) |
| | | } else { |
| | | viewModel.location = pLocation |
| | | EventBus.getDefault().post(locationData) |
| | |
| | | */ |
| | | override fun location(pLocation: AMapLocation?) { |
| | | if (pLocation == null) { |
| | | CommonDialog.showTipsDialog( |
| | | this@AccidentDetailActivity, |
| | | "您未开启gps权限,无法获取当前定位,请打开后重新操作", |
| | | null) |
| | | CommonDialog.showLocationNotGrantedTipsDialog(this@AccidentDetailActivity) |
| | | } else { |
| | | viewModel.location = pLocation |
| | | viewModel.locationCheck() |
| | |
| | | */ |
| | | override fun location(pLocation: AMapLocation?) { |
| | | if (pLocation == null) { |
| | | CommonDialog.showTipsDialog( |
| | | this@CreateTaskActivity, |
| | | "您未开启gps权限,无法获取当前定位,请打开后重新操作", |
| | | null) |
| | | CommonDialog.showLocationNotGrantedTipsDialog(this@CreateTaskActivity) |
| | | } else { |
| | | //viewModel.location.value = pLocation |
| | | } |
| | |
| | | mapFrag.locationChangeCallback = { |
| | | dismissDialog() |
| | | if (it == null) { |
| | | CommonDialog.showTipsDialog( |
| | | this@CheckInActivity, |
| | | "您未开启gps权限,无法获取当前定位,请打开后重新操作", |
| | | null |
| | | ) |
| | | CommonDialog.showLocationNotGrantedTipsDialog(this@CheckInActivity) |
| | | } else { |
| | | if (viewModel.pageType.value == 2) { |
| | | mapMarkRefresh() |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | binding.about.setOnClickListener { |
| | | handleAboutClickForScreenShot() |
| | | val versionCode = AppUtils.getAppVersionCode() |
| | | viewModel.getVersion { |
| | | if (versionCode < (it.forceUpdateFlag ?: 0)) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** 连续点击「关于」的次数,达到阈值后开启截屏权限 */ |
| | | 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() |
| | |
| | | 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" |
| | |
| | | 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 |
| | |
| | | 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) |
| | | } |
| | |
| | | |
| | | 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 |
| | | ) |
| | | ) |
| | | } |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | |
| | | //高德地图导航监听包裹类 |
| | | 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( |
| | |
| | | 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( |
| | |
| | | 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") |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | //导航 |
| | | 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( |
| | |
| | | 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" |
| | |
| | | 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 |
| | |
| | | //生产 |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | </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:layout_constraintBottom_toBottomOf="@+id/title" |
| | | app:layout_constraintTop_toTopOf="@+id/title" /> |
| | | |
| | | |
| | | <androidx.appcompat.widget.AppCompatTextView |
| | | android:id="@+id/title" |
| | | android:layout_width="wrap_content" |
| | |
| | | app:layout_constraintRight_toRightOf="parent" |
| | | app:layout_constraintTop_toTopOf="parent" /> |
| | | |
| | | |
| | | <!-- 左边列表 --> |
| | | <androidx.recyclerview.widget.RecyclerView |
| | | android:id="@+id/leftRecyclerView" |
| | |
| | | android:background="#ffF4F5F7" |
| | | app:layout_constraintLeft_toLeftOf="parent" |
| | | android:scrollbars="vertical" |
| | | app:layout_constraintBottom_toTopOf="@+id/view" |
| | | app:layout_constraintTop_toBottomOf="@+id/title" /> |
| | | |
| | | <!-- 右边列表 --> |
| | |
| | | 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" |
| | |
| | | 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" /> |
| | |
| | | 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" |
| | |
| | | <?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> |
| | |
| | | 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 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 通用提示弹窗 |
| | |
| | | ) { |
| | | 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 |
| | | ) |
| | |
| | | .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, |
| | |
| | | ) { |
| | | 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 |
| | | ) |
| | |
| | | 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 |
| | | |
| | | /** |
| | |
| | | // 标记是否正在监听 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") |
| | |
| | | } |
| | | |
| | | 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() { |