expo-gaode-map-navigation 2.0.12-next.0 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +296 -7
- package/android/build.gradle +12 -4
- package/android/src/main/AndroidManifest.xml +10 -1
- package/android/src/main/cpp/cluster_jni.cpp +56 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +49 -8
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
- package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
- package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +23 -17
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +37 -25
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +6 -6
- package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +103 -0
- package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +1408 -394
- package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +121 -1
- package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +14 -28
- package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/IndependentRouteListener.kt +28 -3
- package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/RouteCalculateListener.kt +2 -2
- package/android/src/main/java/expo/modules/gaodemap/navigation/managers/IndependentRouteManager.kt +114 -15
- package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +24 -35
- package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +50 -36
- package/android/src/main/java/expo/modules/gaodemap/navigation/services/NavigationForegroundService.kt +661 -0
- package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +21 -12
- package/android/src/main/res/drawable/ic_nav_notification_small.xml +10 -0
- package/android/src/main/res/drawable/landback_0.png +0 -0
- package/android/src/main/res/drawable/landback_1.png +0 -0
- package/android/src/main/res/drawable/landback_2.png +0 -0
- package/android/src/main/res/drawable/landback_3.png +0 -0
- package/android/src/main/res/drawable/landback_4.png +0 -0
- package/android/src/main/res/drawable/landback_5.png +0 -0
- package/android/src/main/res/drawable/landback_6.png +0 -0
- package/android/src/main/res/drawable/landback_7.png +0 -0
- package/android/src/main/res/drawable/landback_8.png +0 -0
- package/android/src/main/res/drawable/landback_9.png +0 -0
- package/android/src/main/res/drawable/landback_a.png +0 -0
- package/android/src/main/res/drawable/landback_b.png +0 -0
- package/android/src/main/res/drawable/landback_c.png +0 -0
- package/android/src/main/res/drawable/landback_d.png +0 -0
- package/android/src/main/res/drawable/landback_e.png +0 -0
- package/android/src/main/res/drawable/landback_f.png +0 -0
- package/android/src/main/res/drawable/landback_g.png +0 -0
- package/android/src/main/res/drawable/landback_h.png +0 -0
- package/android/src/main/res/drawable/landback_i.png +0 -0
- package/android/src/main/res/drawable/landback_j.png +0 -0
- package/android/src/main/res/drawable/landback_k.png +0 -0
- package/android/src/main/res/drawable/landback_l.png +0 -0
- package/android/src/main/res/drawable/landfront_0.png +0 -0
- package/android/src/main/res/drawable/landfront_00.png +0 -0
- package/android/src/main/res/drawable/landfront_1.png +0 -0
- package/android/src/main/res/drawable/landfront_11.png +0 -0
- package/android/src/main/res/drawable/landfront_20.png +0 -0
- package/android/src/main/res/drawable/landfront_21.png +0 -0
- package/android/src/main/res/drawable/landfront_22.png +0 -0
- package/android/src/main/res/drawable/landfront_3.png +0 -0
- package/android/src/main/res/drawable/landfront_33.png +0 -0
- package/android/src/main/res/drawable/landfront_40.png +0 -0
- package/android/src/main/res/drawable/landfront_43.png +0 -0
- package/android/src/main/res/drawable/landfront_44.png +0 -0
- package/android/src/main/res/drawable/landfront_5.png +0 -0
- package/android/src/main/res/drawable/landfront_55.png +0 -0
- package/android/src/main/res/drawable/landfront_61.png +0 -0
- package/android/src/main/res/drawable/landfront_63.png +0 -0
- package/android/src/main/res/drawable/landfront_66.png +0 -0
- package/android/src/main/res/drawable/landfront_70.png +0 -0
- package/android/src/main/res/drawable/landfront_71.png +0 -0
- package/android/src/main/res/drawable/landfront_73.png +0 -0
- package/android/src/main/res/drawable/landfront_77.png +0 -0
- package/android/src/main/res/drawable/landfront_8.png +0 -0
- package/android/src/main/res/drawable/landfront_88.png +0 -0
- package/android/src/main/res/drawable/landfront_90.png +0 -0
- package/android/src/main/res/drawable/landfront_95.png +0 -0
- package/android/src/main/res/drawable/landfront_99.png +0 -0
- package/android/src/main/res/drawable/landfront_a0.png +0 -0
- package/android/src/main/res/drawable/landfront_a8.png +0 -0
- package/android/src/main/res/drawable/landfront_aa.png +0 -0
- package/android/src/main/res/drawable/landfront_b1.png +0 -0
- package/android/src/main/res/drawable/landfront_b5.png +0 -0
- package/android/src/main/res/drawable/landfront_bb.png +0 -0
- package/android/src/main/res/drawable/landfront_c3.png +0 -0
- package/android/src/main/res/drawable/landfront_c8.png +0 -0
- package/android/src/main/res/drawable/landfront_cc.png +0 -0
- package/android/src/main/res/drawable/landfront_d.png +0 -0
- package/android/src/main/res/drawable/landfront_dd.png +0 -0
- package/android/src/main/res/drawable/landfront_e1.png +0 -0
- package/android/src/main/res/drawable/landfront_e5.png +0 -0
- package/android/src/main/res/drawable/landfront_ee.png +0 -0
- package/android/src/main/res/drawable/landfront_f0.png +0 -0
- package/android/src/main/res/drawable/landfront_f1.png +0 -0
- package/android/src/main/res/drawable/landfront_f5.png +0 -0
- package/android/src/main/res/drawable/landfront_ff.png +0 -0
- package/android/src/main/res/drawable/landfront_g3.png +0 -0
- package/android/src/main/res/drawable/landfront_g5.png +0 -0
- package/android/src/main/res/drawable/landfront_gg.png +0 -0
- package/android/src/main/res/drawable/landfront_h1.png +0 -0
- package/android/src/main/res/drawable/landfront_h3.png +0 -0
- package/android/src/main/res/drawable/landfront_h5.png +0 -0
- package/android/src/main/res/drawable/landfront_hh.png +0 -0
- package/android/src/main/res/drawable/landfront_i0.png +0 -0
- package/android/src/main/res/drawable/landfront_i3.png +0 -0
- package/android/src/main/res/drawable/landfront_i5.png +0 -0
- package/android/src/main/res/drawable/landfront_ii.png +0 -0
- package/android/src/main/res/drawable/landfront_j1.png +0 -0
- package/android/src/main/res/drawable/landfront_j8.png +0 -0
- package/android/src/main/res/drawable/landfront_jj.png +0 -0
- package/android/src/main/res/drawable/landfront_kk.png +0 -0
- package/android/src/main/res/drawable/landfront_ll.png +0 -0
- package/android/src/main/res/drawable/nav_notification_brand_icon.xml +16 -0
- package/android/src/main/res/drawable/navi_arrow_leftline.png +0 -0
- package/android/src/main/res/drawable/navi_lane_shape_bg_center.xml +5 -0
- package/android/src/main/res/drawable/navi_lane_shape_bg_left.xml +8 -0
- package/android/src/main/res/drawable/navi_lane_shape_bg_over.xml +6 -0
- package/android/src/main/res/drawable/navi_lane_shape_bg_right.xml +8 -0
- package/android/src/main/res/drawable-nodpi/nav_tracker_car.png +0 -0
- package/build/ExpoGaodeMapNaviView.d.ts +16 -0
- package/build/ExpoGaodeMapNaviView.d.ts.map +1 -1
- package/build/ExpoGaodeMapNaviView.js +74 -1
- package/build/ExpoGaodeMapNaviView.js.map +1 -1
- package/build/index.d.ts +56 -8
- package/build/index.d.ts.map +1 -1
- package/build/index.js +452 -10
- package/build/index.js.map +1 -1
- package/build/map/ExpoGaodeMapModule.d.ts +15 -13
- package/build/map/ExpoGaodeMapModule.d.ts.map +1 -1
- package/build/map/ExpoGaodeMapModule.js +31 -39
- package/build/map/ExpoGaodeMapModule.js.map +1 -1
- package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
- package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
- package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
- package/build/map/ExpoGaodeMapView.d.ts +3 -4
- package/build/map/ExpoGaodeMapView.d.ts.map +1 -1
- package/build/map/ExpoGaodeMapView.js +28 -25
- package/build/map/ExpoGaodeMapView.js.map +1 -1
- package/build/map/components/overlays/Circle.d.ts.map +1 -1
- package/build/map/components/overlays/Circle.js +1 -30
- package/build/map/components/overlays/Circle.js.map +1 -1
- package/build/map/components/overlays/Cluster.d.ts.map +1 -1
- package/build/map/components/overlays/Cluster.js +1 -42
- package/build/map/components/overlays/Cluster.js.map +1 -1
- package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/map/components/overlays/HeatMap.js +21 -21
- package/build/map/components/overlays/HeatMap.js.map +1 -1
- package/build/map/components/overlays/Marker.d.ts.map +1 -1
- package/build/map/components/overlays/Marker.js +76 -80
- package/build/map/components/overlays/Marker.js.map +1 -1
- package/build/map/components/overlays/Polygon.d.ts.map +1 -1
- package/build/map/components/overlays/Polygon.js +1 -25
- package/build/map/components/overlays/Polygon.js.map +1 -1
- package/build/map/components/overlays/Polyline.d.ts.map +1 -1
- package/build/map/components/overlays/Polyline.js +1 -31
- package/build/map/components/overlays/Polyline.js.map +1 -1
- package/build/map/index.d.ts +9 -2
- package/build/map/index.d.ts.map +1 -1
- package/build/map/index.js +9 -2
- package/build/map/index.js.map +1 -1
- package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
- package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
- package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
- package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
- package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
- package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
- package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
- package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
- package/build/map/search/index.d.ts +156 -0
- package/build/map/search/index.d.ts.map +1 -0
- package/build/map/search/index.js +171 -0
- package/build/map/search/index.js.map +1 -0
- package/build/map/types/index.d.ts +2 -2
- package/build/map/types/index.d.ts.map +1 -1
- package/build/map/types/index.js.map +1 -1
- package/build/map/types/map-view.types.d.ts +4 -2
- package/build/map/types/map-view.types.d.ts.map +1 -1
- package/build/map/types/map-view.types.js.map +1 -1
- package/build/map/types/native-module.types.d.ts +11 -12
- package/build/map/types/native-module.types.d.ts.map +1 -1
- package/build/map/types/native-module.types.js.map +1 -1
- package/build/map/types/overlays.types.d.ts +9 -14
- package/build/map/types/overlays.types.d.ts.map +1 -1
- package/build/map/types/overlays.types.js.map +1 -1
- package/build/map/types/route-playback.types.d.ts +16 -0
- package/build/map/types/route-playback.types.d.ts.map +1 -1
- package/build/map/types/route-playback.types.js.map +1 -1
- package/build/map/utils/ErrorHandler.js +11 -11
- package/build/map/utils/ErrorHandler.js.map +1 -1
- package/build/map/utils/OfflineMapManager.d.ts +4 -0
- package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
- package/build/map/utils/OfflineMapManager.js +6 -0
- package/build/map/utils/OfflineMapManager.js.map +1 -1
- package/build/types/coordinates.types.d.ts +3 -0
- package/build/types/coordinates.types.d.ts.map +1 -1
- package/build/types/coordinates.types.js.map +1 -1
- package/build/types/independent.types.d.ts +111 -12
- package/build/types/independent.types.d.ts.map +1 -1
- package/build/types/independent.types.js.map +1 -1
- package/build/types/native-module.types.d.ts +1 -1
- package/build/types/native-module.types.js.map +1 -1
- package/build/types/naviview.types.d.ts +304 -14
- package/build/types/naviview.types.d.ts.map +1 -1
- package/build/types/naviview.types.js.map +1 -1
- package/build/types/route.types.d.ts +12 -2
- package/build/types/route.types.d.ts.map +1 -1
- package/build/types/route.types.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/ExpoGaodeMapNaviView.swift +2331 -201
- package/ios/ExpoGaodeMapNaviViewModule.swift +109 -1
- package/ios/ExpoGaodeMapNavigation.podspec +2 -1
- package/ios/ExpoGaodeMapNavigationModule.swift +7 -5
- package/ios/managers/IndependentRouteManager.swift +90 -26
- package/ios/map/ExpoGaodeMapModule.swift +72 -21
- package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
- package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
- package/ios/map/ExpoGaodeMapView.swift +23 -5
- package/ios/map/GaodeMapPrivacyManager.swift +26 -18
- package/ios/map/cpp/GeometryEngine.cpp +112 -0
- package/ios/map/cpp/GeometryEngine.hpp +21 -0
- package/ios/map/modules/LocationManager.swift +37 -5
- package/ios/map/overlays/MarkerView.swift +11 -11
- package/ios/map/overlays/MarkerViewModule.swift +4 -4
- package/ios/map/overlays/PolylineView.swift +6 -12
- package/ios/map/utils/ClusterNative.h +8 -0
- package/ios/map/utils/ClusterNative.mm +27 -0
- package/ios/map/utils/PermissionManager.swift +115 -6
- package/ios/routes/drive/DriveTruckRouteCalculator.swift +165 -77
- package/ios/routes/walkride/WalkRideRouteCalculator.swift +127 -1
- package/ios/services/IndependentRouteService.swift +198 -39
- package/ios/services/NavigationLiveActivityAttributes.swift +48 -0
- package/ios/services/NavigationLiveActivityManager.swift +359 -0
- package/package.json +22 -7
- package/plugin/build/withGaodeMap.d.ts +8 -0
- package/plugin/build/withGaodeMap.js +60 -4
- package/scripts/check-expo-modules.js +68 -0
- package/shared/cpp/GeometryEngine.cpp +112 -0
- package/shared/cpp/GeometryEngine.hpp +21 -0
- package/widget-template/README.md +46 -0
- package/widget-template/ios/NavigationLiveActivityWidget.swift +367 -0
- package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
package expo.modules.gaodemap.navigation.services
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.app.Notification
|
|
6
|
+
import android.app.NotificationChannel
|
|
7
|
+
import android.app.NotificationManager
|
|
8
|
+
import android.app.PendingIntent
|
|
9
|
+
import android.app.Service
|
|
10
|
+
import android.content.Context
|
|
11
|
+
import android.content.Intent
|
|
12
|
+
import android.content.pm.PackageManager
|
|
13
|
+
import android.graphics.Bitmap
|
|
14
|
+
import android.graphics.BitmapFactory
|
|
15
|
+
import android.graphics.Canvas
|
|
16
|
+
import android.graphics.Color
|
|
17
|
+
import android.graphics.Matrix
|
|
18
|
+
import android.graphics.Paint
|
|
19
|
+
import android.graphics.Typeface
|
|
20
|
+
import android.os.Build
|
|
21
|
+
import android.os.Bundle
|
|
22
|
+
import android.os.IBinder
|
|
23
|
+
import android.net.Uri
|
|
24
|
+
import android.util.Log
|
|
25
|
+
import androidx.annotation.RequiresApi
|
|
26
|
+
import androidx.core.app.NotificationCompat
|
|
27
|
+
import androidx.core.content.ContextCompat
|
|
28
|
+
import androidx.core.graphics.drawable.IconCompat
|
|
29
|
+
import expo.modules.gaodemap.navigation.R
|
|
30
|
+
import kotlin.math.ceil
|
|
31
|
+
import kotlin.math.roundToInt
|
|
32
|
+
import androidx.core.graphics.createBitmap
|
|
33
|
+
import androidx.core.graphics.toColorInt
|
|
34
|
+
|
|
35
|
+
internal data class NavigationNotificationSnapshot(
|
|
36
|
+
val currentRoadName: String? = null,
|
|
37
|
+
val nextRoadName: String? = null,
|
|
38
|
+
val pathRetainDistance: Int? = null,
|
|
39
|
+
val routeTotalDistance: Int? = null,
|
|
40
|
+
val pathRetainTime: Int? = null,
|
|
41
|
+
val curStepRetainDistance: Int? = null,
|
|
42
|
+
val iconType: Int? = null,
|
|
43
|
+
val turnIconImageUri: String? = null
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
class NavigationForegroundService : Service() {
|
|
47
|
+
companion object {
|
|
48
|
+
private const val TAG = "NavigationForegroundService"
|
|
49
|
+
private const val NOTIFICATION_ID = 1002
|
|
50
|
+
private const val CHANNEL_ID = "navigation_service_channel"
|
|
51
|
+
private const val CHANNEL_NAME = "导航进行中"
|
|
52
|
+
private const val PERMISSION_POST_PROMOTED_NOTIFICATIONS = "android.permission.POST_PROMOTED_NOTIFICATIONS"
|
|
53
|
+
|
|
54
|
+
private const val ACTION_START_OR_UPDATE = "expo.modules.gaodemap.navigation.action.START_OR_UPDATE"
|
|
55
|
+
|
|
56
|
+
private const val EXTRA_CURRENT_ROAD = "extra_current_road"
|
|
57
|
+
private const val EXTRA_NEXT_ROAD = "extra_next_road"
|
|
58
|
+
private const val EXTRA_PATH_RETAIN_DISTANCE = "extra_path_retain_distance"
|
|
59
|
+
private const val EXTRA_ROUTE_TOTAL_DISTANCE = "extra_route_total_distance"
|
|
60
|
+
private const val EXTRA_PATH_RETAIN_TIME = "extra_path_retain_time"
|
|
61
|
+
private const val EXTRA_STEP_RETAIN_DISTANCE = "extra_step_retain_distance"
|
|
62
|
+
private const val EXTRA_ICON_TYPE = "extra_icon_type"
|
|
63
|
+
private const val EXTRA_TURN_ICON_IMAGE_URI = "extra_turn_icon_image_uri"
|
|
64
|
+
@Volatile
|
|
65
|
+
private var cachedRouteTotalDistanceMeters: Int? = null
|
|
66
|
+
|
|
67
|
+
internal fun startOrUpdate(context: Context, snapshot: NavigationNotificationSnapshot?) {
|
|
68
|
+
val appContext = context.applicationContext
|
|
69
|
+
snapshot?.routeTotalDistance
|
|
70
|
+
?.takeIf { it > 0 }
|
|
71
|
+
?.let { cachedRouteTotalDistanceMeters = it }
|
|
72
|
+
Log.d(
|
|
73
|
+
TAG,
|
|
74
|
+
"startOrUpdate called: stepDistance=${snapshot?.curStepRetainDistance}, " +
|
|
75
|
+
"remainDistance=${snapshot?.pathRetainDistance}, routeTotal=${snapshot?.routeTotalDistance}, " +
|
|
76
|
+
"remainTime=${snapshot?.pathRetainTime}, " +
|
|
77
|
+
"nextRoad=${snapshot?.nextRoadName}, turnIconUri=${snapshot?.turnIconImageUri}"
|
|
78
|
+
)
|
|
79
|
+
val intent = Intent(appContext, NavigationForegroundService::class.java).apply {
|
|
80
|
+
action = ACTION_START_OR_UPDATE
|
|
81
|
+
snapshot?.let { payload ->
|
|
82
|
+
putExtra(EXTRA_CURRENT_ROAD, payload.currentRoadName)
|
|
83
|
+
putExtra(EXTRA_NEXT_ROAD, payload.nextRoadName)
|
|
84
|
+
payload.pathRetainDistance?.let { putExtra(EXTRA_PATH_RETAIN_DISTANCE, it) }
|
|
85
|
+
payload.routeTotalDistance?.let { putExtra(EXTRA_ROUTE_TOTAL_DISTANCE, it) }
|
|
86
|
+
payload.pathRetainTime?.let { putExtra(EXTRA_PATH_RETAIN_TIME, it) }
|
|
87
|
+
payload.curStepRetainDistance?.let { putExtra(EXTRA_STEP_RETAIN_DISTANCE, it) }
|
|
88
|
+
payload.iconType?.let { putExtra(EXTRA_ICON_TYPE, it) }
|
|
89
|
+
payload.turnIconImageUri?.let { putExtra(EXTRA_TURN_ICON_IMAGE_URI, it) }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
94
|
+
appContext.startForegroundService(intent)
|
|
95
|
+
} else {
|
|
96
|
+
appContext.startService(intent)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
internal fun stop(context: Context) {
|
|
101
|
+
val appContext = context.applicationContext
|
|
102
|
+
Log.d(TAG, "stop called")
|
|
103
|
+
appContext.stopService(Intent(appContext, NavigationForegroundService::class.java))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
107
|
+
private fun hasNotificationPermission(context: Context): Boolean {
|
|
108
|
+
return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)== PackageManager.PERMISSION_GRANTED
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private var latestSnapshot = NavigationNotificationSnapshot()
|
|
113
|
+
private var isForeground = false
|
|
114
|
+
private var routeTotalDistanceMeters: Int? = null
|
|
115
|
+
private val trackerIconCache = mutableMapOf<Int, IconCompat>()
|
|
116
|
+
private var customCarTrackerIcon: IconCompat? = null
|
|
117
|
+
|
|
118
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
119
|
+
|
|
120
|
+
override fun onCreate() {
|
|
121
|
+
super.onCreate()
|
|
122
|
+
routeTotalDistanceMeters = cachedRouteTotalDistanceMeters
|
|
123
|
+
Log.d(TAG, "onCreate")
|
|
124
|
+
createNotificationChannel()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
128
|
+
Log.d(TAG, "onStartCommand action=${intent?.action}, startId=$startId, flags=$flags")
|
|
129
|
+
if (intent?.action != ACTION_START_OR_UPDATE) {
|
|
130
|
+
Log.d(TAG, "Ignoring action=${intent?.action}")
|
|
131
|
+
return START_NOT_STICKY
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
latestSnapshot = latestSnapshot.mergeFrom(intent)
|
|
135
|
+
Log.d(
|
|
136
|
+
TAG,
|
|
137
|
+
"merged snapshot: stepDistance=${latestSnapshot.curStepRetainDistance}, " +
|
|
138
|
+
"remainDistance=${latestSnapshot.pathRetainDistance}, routeTotal=${latestSnapshot.routeTotalDistance}, " +
|
|
139
|
+
"remainTime=${latestSnapshot.pathRetainTime}, " +
|
|
140
|
+
"currentRoad=${latestSnapshot.currentRoadName}, nextRoad=${latestSnapshot.nextRoadName}, " +
|
|
141
|
+
"turnIconUri=${latestSnapshot.turnIconImageUri}"
|
|
142
|
+
)
|
|
143
|
+
// Android 13+ forbids posting most notifications without POST_NOTIFICATIONS,
|
|
144
|
+
// but foreground services still must call startForeground() in time.
|
|
145
|
+
// If the permission is denied, the ongoing notice is still surfaced in Task Manager
|
|
146
|
+
// even though it may be hidden from the notification drawer.
|
|
147
|
+
val hasPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
|
148
|
+
hasNotificationPermission(this)
|
|
149
|
+
if (!hasPermission) {
|
|
150
|
+
Log.w(
|
|
151
|
+
TAG,
|
|
152
|
+
"POST_NOTIFICATIONS not granted; foreground service notification may be hidden from the drawer."
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
val notification = buildNotification(latestSnapshot)
|
|
157
|
+
|
|
158
|
+
if (!isForeground) {
|
|
159
|
+
try {
|
|
160
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
161
|
+
isForeground = true
|
|
162
|
+
Log.d(TAG, "startForeground success, notificationId=$NOTIFICATION_ID")
|
|
163
|
+
} catch (e: Exception) {
|
|
164
|
+
Log.e(TAG, "Failed to start foreground service: ${e.message}", e)
|
|
165
|
+
stopSelf()
|
|
166
|
+
return START_NOT_STICKY
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
getNotificationManager().notify(NOTIFICATION_ID, notification)
|
|
170
|
+
Log.d(TAG, "notify update pushed, notificationId=$NOTIFICATION_ID")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return START_STICKY
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override fun onDestroy() {
|
|
177
|
+
Log.d(TAG, "onDestroy isForeground=$isForeground")
|
|
178
|
+
if (isForeground) {
|
|
179
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
180
|
+
isForeground = false
|
|
181
|
+
}
|
|
182
|
+
super.onDestroy()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun buildNotification(snapshot: NavigationNotificationSnapshot): Notification {
|
|
186
|
+
val appName = resolveAppName()
|
|
187
|
+
val maneuverText = resolveManeuverText(snapshot.iconType)
|
|
188
|
+
val instructionText = buildInstructionText(snapshot, maneuverText)
|
|
189
|
+
val roadText = buildRoadText(snapshot, fallback = "导航进行中")
|
|
190
|
+
val routeSummary = buildRouteSummaryText(snapshot)
|
|
191
|
+
val shortCriticalText = buildShortCriticalText(snapshot, maneuverText)
|
|
192
|
+
val pendingIntent = createLaunchPendingIntent()
|
|
193
|
+
val extras = Bundle().apply {
|
|
194
|
+
// Keep detailed navigation payload in extras for OEM/system smart-surface readers.
|
|
195
|
+
putString("expo_gaode_nav_instruction", instructionText)
|
|
196
|
+
putString("expo_gaode_nav_summary", routeSummary)
|
|
197
|
+
putInt("expo_gaode_nav_remain_distance", snapshot.pathRetainDistance ?: -1)
|
|
198
|
+
putInt("expo_gaode_nav_remain_time", snapshot.pathRetainTime ?: -1)
|
|
199
|
+
putInt("expo_gaode_nav_step_distance", snapshot.curStepRetainDistance ?: -1)
|
|
200
|
+
putInt("expo_gaode_nav_icon_type", snapshot.iconType ?: -1)
|
|
201
|
+
putString("expo_gaode_nav_short_critical_text", shortCriticalText)
|
|
202
|
+
}
|
|
203
|
+
val requestPromotedOngoing = shouldRequestPromotedOngoing()
|
|
204
|
+
val canPostPromoted = canPostPromotedNotificationsCompat()
|
|
205
|
+
val style = buildProgressStyle(snapshot)
|
|
206
|
+
Log.d(
|
|
207
|
+
TAG,
|
|
208
|
+
"buildNotification promotedRequested=$requestPromotedOngoing, canPostPromoted=$canPostPromoted, sdkInt=${Build.VERSION.SDK_INT}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
212
|
+
.setContentTitle(instructionText)
|
|
213
|
+
.setContentText(roadText)
|
|
214
|
+
.setSubText(appName)
|
|
215
|
+
.setStyle(style)
|
|
216
|
+
.setSmallIcon(resolveNotificationSmallIconResId())
|
|
217
|
+
.setLargeIcon(resolveManeuverLargeIconBitmap(snapshot))
|
|
218
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
219
|
+
.setCategory(NotificationCompat.CATEGORY_NAVIGATION)
|
|
220
|
+
.setOnlyAlertOnce(true)
|
|
221
|
+
.setOngoing(true)
|
|
222
|
+
.setSilent(true)
|
|
223
|
+
.addExtras(extras)
|
|
224
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
225
|
+
.setRequestPromotedOngoing(requestPromotedOngoing)
|
|
226
|
+
.setShortCriticalText(shortCriticalText)
|
|
227
|
+
|
|
228
|
+
if (pendingIntent != null) {
|
|
229
|
+
builder.setContentIntent(pendingIntent)
|
|
230
|
+
}
|
|
231
|
+
val notification = builder.build()
|
|
232
|
+
logPromotionDiagnostics(
|
|
233
|
+
notification = notification,
|
|
234
|
+
requested = requestPromotedOngoing,
|
|
235
|
+
canPostPromoted = canPostPromoted
|
|
236
|
+
)
|
|
237
|
+
return notification
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun buildProgressStyle(snapshot: NavigationNotificationSnapshot): NotificationCompat.Style {
|
|
241
|
+
val style = NotificationCompat.ProgressStyle()
|
|
242
|
+
.setStyledByProgress(true)
|
|
243
|
+
.setProgressSegments(
|
|
244
|
+
listOf(
|
|
245
|
+
NotificationCompat.ProgressStyle.Segment(33).setColor("#4B63FF".toColorInt()),
|
|
246
|
+
NotificationCompat.ProgressStyle.Segment(33).setColor("#4B63FF".toColorInt()),
|
|
247
|
+
NotificationCompat.ProgressStyle.Segment(34).setColor("#4B63FF".toColorInt())
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
.setProgressPoints(
|
|
251
|
+
listOf(
|
|
252
|
+
NotificationCompat.ProgressStyle.Point(33).setColor("#BAC7FF".toColorInt()),
|
|
253
|
+
NotificationCompat.ProgressStyle.Point(66).setColor("#BAC7FF".toColorInt()),
|
|
254
|
+
NotificationCompat.ProgressStyle.Point(100).setColor("#BAC7FF".toColorInt())
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
(resolveTurnTrackerIcon(snapshot) ?: resolveDefaultTrackerIcon())
|
|
259
|
+
?.let { style.setProgressTrackerIcon(it) }
|
|
260
|
+
|
|
261
|
+
val remainDistance = snapshot.pathRetainDistance
|
|
262
|
+
if (remainDistance == null || remainDistance <= 0) {
|
|
263
|
+
style.setProgressIndeterminate(true)
|
|
264
|
+
Log.d(TAG, "progress style indeterminate: remainDistance=$remainDistance")
|
|
265
|
+
return style
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
val previousTotal = routeTotalDistanceMeters ?: 0
|
|
269
|
+
val cachedTotal = cachedRouteTotalDistanceMeters ?: 0
|
|
270
|
+
val snapshotTotal = snapshot.routeTotalDistance?.coerceAtLeast(0) ?: 0
|
|
271
|
+
val updatedTotal = maxOf(previousTotal, cachedTotal, snapshotTotal, remainDistance)
|
|
272
|
+
routeTotalDistanceMeters = updatedTotal
|
|
273
|
+
if (updatedTotal > 0) {
|
|
274
|
+
cachedRouteTotalDistanceMeters = updatedTotal
|
|
275
|
+
}
|
|
276
|
+
val progress = (updatedTotal - remainDistance).coerceAtLeast(0)
|
|
277
|
+
val progressPercent = ((progress.toFloat() / updatedTotal.toFloat()) * 100f)
|
|
278
|
+
.coerceIn(0f, 100f)
|
|
279
|
+
.roundToInt()
|
|
280
|
+
style
|
|
281
|
+
.setProgressIndeterminate(false)
|
|
282
|
+
.setProgress(progressPercent)
|
|
283
|
+
|
|
284
|
+
Log.d(
|
|
285
|
+
TAG,
|
|
286
|
+
"progress style determinate: progress=$progress, max=$updatedTotal, remainDistance=$remainDistance, " +
|
|
287
|
+
"progressPercent=$progressPercent"
|
|
288
|
+
)
|
|
289
|
+
return style
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun resolveAppName(): String {
|
|
293
|
+
return try {
|
|
294
|
+
packageManager.getApplicationLabel(applicationInfo).toString()
|
|
295
|
+
.takeIf { it.isNotBlank() }
|
|
296
|
+
?: "导航"
|
|
297
|
+
} catch (_: Throwable) {
|
|
298
|
+
"导航"
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private fun resolveNotificationSmallIconResId(): Int {
|
|
303
|
+
return R.drawable.ic_nav_notification_small
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private fun resolveManeuverLargeIconBitmap(snapshot: NavigationNotificationSnapshot): Bitmap? {
|
|
307
|
+
val fromUri = resolveTurnIconBitmapFromUri(snapshot.turnIconImageUri)
|
|
308
|
+
if (fromUri != null) {
|
|
309
|
+
return fromUri
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
val iconType = snapshot.iconType ?: return null
|
|
313
|
+
return runCatching { createTurnTrackerBitmap(iconType) }
|
|
314
|
+
.onFailure { Log.w(TAG, "Failed to build maneuver large icon bitmap", it) }
|
|
315
|
+
.getOrNull()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun resolveTurnIconBitmapFromUri(uriString: String?): Bitmap? {
|
|
319
|
+
val uri = uriString?.takeIf { it.isNotBlank() } ?: return null
|
|
320
|
+
return try {
|
|
321
|
+
contentResolver.openInputStream(Uri.parse(uri))?.use { input ->
|
|
322
|
+
BitmapFactory.decodeStream(input)
|
|
323
|
+
}
|
|
324
|
+
} catch (error: Throwable) {
|
|
325
|
+
Log.w(TAG, "Failed to decode turn icon bitmap from uri=$uri", error)
|
|
326
|
+
null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@SuppressLint("UseKtx")
|
|
331
|
+
private fun resolveTurnTrackerIcon(snapshot: NavigationNotificationSnapshot): IconCompat? {
|
|
332
|
+
resolveCustomCarTrackerIcon()?.let { return it }
|
|
333
|
+
|
|
334
|
+
resolveTurnTrackerIconFromIconType(snapshot.iconType)?.let {
|
|
335
|
+
return it
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
val uri = snapshot.turnIconImageUri?.takeIf { it.isNotBlank() } ?: return null
|
|
339
|
+
return try {
|
|
340
|
+
Log.d(TAG, "Using turn tracker icon from uri=$uri")
|
|
341
|
+
IconCompat.createWithContentUri(uri)
|
|
342
|
+
} catch (error: Throwable) {
|
|
343
|
+
Log.w(TAG, "Failed to resolve turn tracker icon from uri=$uri", error)
|
|
344
|
+
null
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun resolveCustomCarTrackerIcon(): IconCompat? {
|
|
349
|
+
customCarTrackerIcon?.let { return it }
|
|
350
|
+
return try {
|
|
351
|
+
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.nav_tracker_car)
|
|
352
|
+
if (bitmap == null) {
|
|
353
|
+
Log.w(TAG, "Failed to decode nav_tracker_car bitmap")
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
val rotated = rotateBitmap(bitmap, 90f)
|
|
357
|
+
IconCompat.createWithBitmap(rotated).also {
|
|
358
|
+
customCarTrackerIcon = it
|
|
359
|
+
Log.d(TAG, "Using custom car tracker icon from drawable nav_tracker_car (rotated 90deg)")
|
|
360
|
+
}
|
|
361
|
+
} catch (error: Throwable) {
|
|
362
|
+
Log.w(TAG, "Failed to load custom car tracker icon, fallback to default logic", error)
|
|
363
|
+
null
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private fun rotateBitmap(source: Bitmap, angleDegrees: Float): Bitmap {
|
|
368
|
+
val matrix = Matrix().apply { postRotate(angleDegrees) }
|
|
369
|
+
return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private fun resolveTurnTrackerIconFromIconType(iconType: Int?): IconCompat? {
|
|
373
|
+
val safeIconType = iconType ?: return null
|
|
374
|
+
trackerIconCache[safeIconType]?.let { return it }
|
|
375
|
+
|
|
376
|
+
return try {
|
|
377
|
+
val icon = IconCompat.createWithBitmap(createTurnTrackerBitmap(safeIconType))
|
|
378
|
+
trackerIconCache[safeIconType] = icon
|
|
379
|
+
Log.d(TAG, "Using generated turn tracker icon from iconType=$safeIconType")
|
|
380
|
+
icon
|
|
381
|
+
} catch (error: Throwable) {
|
|
382
|
+
Log.w(TAG, "Failed to generate tracker icon for iconType=$safeIconType", error)
|
|
383
|
+
null
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private fun createTurnTrackerBitmap(iconType: Int): Bitmap {
|
|
388
|
+
val sizePx = 72
|
|
389
|
+
val bitmap = createBitmap(sizePx, sizePx)
|
|
390
|
+
val canvas = Canvas(bitmap)
|
|
391
|
+
|
|
392
|
+
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
393
|
+
color = "#4556A8".toColorInt()
|
|
394
|
+
style = Paint.Style.FILL
|
|
395
|
+
}
|
|
396
|
+
val radius = sizePx * 0.24f
|
|
397
|
+
canvas.drawRoundRect(0f, 0f, sizePx.toFloat(), sizePx.toFloat(), radius, radius, bgPaint)
|
|
398
|
+
|
|
399
|
+
val arrow = resolveArrowGlyph(iconType)
|
|
400
|
+
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
401
|
+
color = Color.WHITE
|
|
402
|
+
textAlign = Paint.Align.CENTER
|
|
403
|
+
textSize = sizePx * 0.52f
|
|
404
|
+
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
405
|
+
}
|
|
406
|
+
val y = sizePx / 2f - (textPaint.descent() + textPaint.ascent()) / 2f
|
|
407
|
+
canvas.drawText(arrow, sizePx / 2f, y, textPaint)
|
|
408
|
+
return bitmap
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private fun resolveArrowGlyph(iconType: Int): String {
|
|
412
|
+
return when (iconType) {
|
|
413
|
+
2 -> "←"
|
|
414
|
+
3 -> "→"
|
|
415
|
+
4 -> "↖"
|
|
416
|
+
5 -> "↗"
|
|
417
|
+
6 -> "↙"
|
|
418
|
+
7 -> "↘"
|
|
419
|
+
8 -> "↶"
|
|
420
|
+
9 -> "↑"
|
|
421
|
+
15, 31 -> "⇦"
|
|
422
|
+
16, 32 -> "⇨"
|
|
423
|
+
else -> "↑"
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private fun resolveDefaultTrackerIcon(): IconCompat? {
|
|
428
|
+
return try {
|
|
429
|
+
IconCompat.createWithResource(this, android.R.drawable.ic_menu_directions)
|
|
430
|
+
} catch (error: Throwable) {
|
|
431
|
+
Log.w(TAG, "Failed to resolve default tracker icon", error)
|
|
432
|
+
null
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun resolveManeuverText(iconType: Int?): String {
|
|
437
|
+
return when (iconType ?: -1) {
|
|
438
|
+
2 -> "左转"
|
|
439
|
+
3 -> "右转"
|
|
440
|
+
4 -> "向左前方行驶"
|
|
441
|
+
5 -> "向右前方行驶"
|
|
442
|
+
6 -> "向左后方行驶"
|
|
443
|
+
7 -> "向右后方行驶"
|
|
444
|
+
8 -> "调头"
|
|
445
|
+
9 -> "直行"
|
|
446
|
+
11 -> "进入环岛"
|
|
447
|
+
12 -> "驶出环岛"
|
|
448
|
+
14 -> "左转弯待转"
|
|
449
|
+
15, 31 -> "靠左行驶"
|
|
450
|
+
16, 32 -> "靠右行驶"
|
|
451
|
+
17 -> "进入隧道"
|
|
452
|
+
29 -> "进入匝道"
|
|
453
|
+
30 -> "驶出匝道"
|
|
454
|
+
else -> ""
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun buildShortCriticalText(snapshot: NavigationNotificationSnapshot, maneuverText: String): String {
|
|
459
|
+
val stepDistance = snapshot.curStepRetainDistance?.let { formatDistance(it) }
|
|
460
|
+
if (!stepDistance.isNullOrBlank() && maneuverText.isNotBlank()) {
|
|
461
|
+
return "$stepDistance $maneuverText"
|
|
462
|
+
}
|
|
463
|
+
if (!stepDistance.isNullOrBlank()) {
|
|
464
|
+
return "${stepDistance}后继续"
|
|
465
|
+
}
|
|
466
|
+
if (maneuverText.isNotBlank()) {
|
|
467
|
+
return maneuverText
|
|
468
|
+
}
|
|
469
|
+
val remainDistance = snapshot.pathRetainDistance
|
|
470
|
+
return if (remainDistance != null) {
|
|
471
|
+
formatDistance(remainDistance)
|
|
472
|
+
} else {
|
|
473
|
+
"导航中"
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private fun shouldRequestPromotedOngoing(): Boolean {
|
|
478
|
+
// 1. 必须是 Android 13+
|
|
479
|
+
if (Build.VERSION.SDK_INT < 33) {
|
|
480
|
+
Log.d(TAG, "promoted disabled: sdkInt=${Build.VERSION.SDK_INT} < 33")
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
// 2. 检查运行时权限(关键修改)
|
|
484
|
+
if (!hasNotificationPermission(this)) {
|
|
485
|
+
Log.w(TAG, "Missing POST_NOTIFICATIONS runtime permission. Falling back to normal notification.")
|
|
486
|
+
return false
|
|
487
|
+
}
|
|
488
|
+
if (Build.VERSION.SDK_INT >= 36 && !hasManifestPermission(PERMISSION_POST_PROMOTED_NOTIFICATIONS)) {
|
|
489
|
+
Log.w(TAG, "Missing POST_PROMOTED_NOTIFICATIONS in manifest. Cannot request promoted ongoing.")
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
return true
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private fun hasManifestPermission(permission: String): Boolean {
|
|
496
|
+
return try {
|
|
497
|
+
packageManager.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED
|
|
498
|
+
} catch (_: Throwable) {
|
|
499
|
+
false
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private fun canPostPromotedNotificationsCompat(): Boolean? {
|
|
504
|
+
if (Build.VERSION.SDK_INT < 36) {
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
return try {
|
|
508
|
+
canPostPromotedNotificationsApi36(getNotificationManager())
|
|
509
|
+
} catch (error: Throwable) {
|
|
510
|
+
Log.w(TAG, "canPostPromotedNotifications check failed", error)
|
|
511
|
+
null
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@RequiresApi(36)
|
|
516
|
+
private fun canPostPromotedNotificationsApi36(manager: NotificationManager): Boolean {
|
|
517
|
+
return manager.canPostPromotedNotifications()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private fun logPromotionDiagnostics(
|
|
521
|
+
notification: Notification,
|
|
522
|
+
requested: Boolean,
|
|
523
|
+
canPostPromoted: Boolean?
|
|
524
|
+
) {
|
|
525
|
+
val hasPromotableCharacteristics = if (Build.VERSION.SDK_INT >= 36) {
|
|
526
|
+
hasPromotableCharacteristicsApi36(notification)
|
|
527
|
+
} else {
|
|
528
|
+
null
|
|
529
|
+
}
|
|
530
|
+
val promotedFlagEnabled = if (Build.VERSION.SDK_INT >= 36) {
|
|
531
|
+
isPromotedFlagEnabledApi36(notification)
|
|
532
|
+
} else {
|
|
533
|
+
null
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
Log.d(
|
|
537
|
+
TAG,
|
|
538
|
+
"promotion diagnostics: requested=$requested, canPostPromoted=$canPostPromoted, promotable=$hasPromotableCharacteristics, promotedFlag=$promotedFlagEnabled"
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@RequiresApi(36)
|
|
543
|
+
private fun hasPromotableCharacteristicsApi36(notification: Notification): Boolean {
|
|
544
|
+
return notification.hasPromotableCharacteristics()
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@RequiresApi(36)
|
|
548
|
+
private fun isPromotedFlagEnabledApi36(notification: Notification): Boolean {
|
|
549
|
+
return (notification.flags and Notification.FLAG_PROMOTED_ONGOING) != 0
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private fun createLaunchPendingIntent(): PendingIntent? {
|
|
553
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return null
|
|
554
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
555
|
+
val flags =
|
|
556
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
557
|
+
return PendingIntent.getActivity(this, 0, launchIntent, flags)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private fun buildInstructionText(snapshot: NavigationNotificationSnapshot, maneuverText: String): String {
|
|
561
|
+
val stepDistanceText = snapshot.curStepRetainDistance?.let { formatDistance(it) }
|
|
562
|
+
if (!stepDistanceText.isNullOrBlank() && maneuverText.isNotBlank()) {
|
|
563
|
+
return "${stepDistanceText}后$maneuverText"
|
|
564
|
+
}
|
|
565
|
+
if (!stepDistanceText.isNullOrBlank()) {
|
|
566
|
+
return "${stepDistanceText}后执行下一步"
|
|
567
|
+
}
|
|
568
|
+
if (maneuverText.isNotBlank()) {
|
|
569
|
+
return maneuverText
|
|
570
|
+
}
|
|
571
|
+
return "导航进行中"
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private fun buildRoadText(snapshot: NavigationNotificationSnapshot, fallback: String): String {
|
|
575
|
+
val nextRoad = snapshot.nextRoadName?.trim().orEmpty()
|
|
576
|
+
if (nextRoad.isNotBlank()) {
|
|
577
|
+
return nextRoad
|
|
578
|
+
}
|
|
579
|
+
val currentRoad = snapshot.currentRoadName?.trim().orEmpty()
|
|
580
|
+
if (currentRoad.isNotBlank()) {
|
|
581
|
+
return currentRoad
|
|
582
|
+
}
|
|
583
|
+
return fallback
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private fun buildRouteSummaryText(snapshot: NavigationNotificationSnapshot): String {
|
|
587
|
+
val retainDistanceText = snapshot.pathRetainDistance?.let { formatDistance(it) } ?: "--"
|
|
588
|
+
val retainTimeText = snapshot.pathRetainTime?.let { formatDuration(it) } ?: "--"
|
|
589
|
+
val road = snapshot.currentRoadName?.trim().orEmpty().ifBlank { "前方道路" }
|
|
590
|
+
return "剩余${retainDistanceText} · 约${retainTimeText} · $road"
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
@SuppressLint("DefaultLocale")
|
|
594
|
+
private fun formatDistance(distanceMeters: Int): String {
|
|
595
|
+
return if (distanceMeters < 1000) {
|
|
596
|
+
"${distanceMeters.coerceAtLeast(0)}米"
|
|
597
|
+
} else {
|
|
598
|
+
String.format("%.1f公里", distanceMeters / 1000.0)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private fun formatDuration(durationSeconds: Int): String {
|
|
603
|
+
val safeSeconds = durationSeconds.coerceAtLeast(0)
|
|
604
|
+
if (safeSeconds < 3600) {
|
|
605
|
+
return "${ceil(safeSeconds / 60.0).toInt()}分钟"
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
val hours = safeSeconds / 3600
|
|
609
|
+
val minutes = ceil((safeSeconds % 3600) / 60.0).toInt().coerceAtMost(59)
|
|
610
|
+
return "${hours}小时${minutes}分钟"
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private fun createNotificationChannel() {
|
|
614
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
val channel = NotificationChannel(
|
|
619
|
+
CHANNEL_ID,
|
|
620
|
+
CHANNEL_NAME,
|
|
621
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
622
|
+
).apply {
|
|
623
|
+
description = "导航进行中通知"
|
|
624
|
+
setShowBadge(false)
|
|
625
|
+
}
|
|
626
|
+
getNotificationManager().createNotificationChannel(channel)
|
|
627
|
+
Log.d(TAG, "notification channel ensured: id=$CHANNEL_ID, importance=${channel.importance}")
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private fun getNotificationManager(): NotificationManager {
|
|
631
|
+
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private fun NavigationNotificationSnapshot.mergeFrom(intent: Intent): NavigationNotificationSnapshot {
|
|
635
|
+
return copy(
|
|
636
|
+
currentRoadName = intent.getStringExtra(EXTRA_CURRENT_ROAD) ?: currentRoadName,
|
|
637
|
+
nextRoadName = intent.getStringExtra(EXTRA_NEXT_ROAD) ?: nextRoadName,
|
|
638
|
+
pathRetainDistance = if (intent.hasExtra(EXTRA_PATH_RETAIN_DISTANCE)) {
|
|
639
|
+
intent.getIntExtra(EXTRA_PATH_RETAIN_DISTANCE, pathRetainDistance ?: 0)
|
|
640
|
+
} else {
|
|
641
|
+
pathRetainDistance
|
|
642
|
+
},
|
|
643
|
+
pathRetainTime = if (intent.hasExtra(EXTRA_PATH_RETAIN_TIME)) {
|
|
644
|
+
intent.getIntExtra(EXTRA_PATH_RETAIN_TIME, pathRetainTime ?: 0)
|
|
645
|
+
} else {
|
|
646
|
+
pathRetainTime
|
|
647
|
+
},
|
|
648
|
+
curStepRetainDistance = if (intent.hasExtra(EXTRA_STEP_RETAIN_DISTANCE)) {
|
|
649
|
+
intent.getIntExtra(EXTRA_STEP_RETAIN_DISTANCE, curStepRetainDistance ?: 0)
|
|
650
|
+
} else {
|
|
651
|
+
curStepRetainDistance
|
|
652
|
+
},
|
|
653
|
+
iconType = if (intent.hasExtra(EXTRA_ICON_TYPE)) {
|
|
654
|
+
intent.getIntExtra(EXTRA_ICON_TYPE, iconType ?: 0)
|
|
655
|
+
} else {
|
|
656
|
+
iconType
|
|
657
|
+
},
|
|
658
|
+
turnIconImageUri = intent.getStringExtra(EXTRA_TURN_ICON_IMAGE_URI) ?: turnIconImageUri
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
}
|