From 529c8f1197518a6cff5e1165d88b2cb238a87312 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 16 Jun 2025 11:38:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=B8=8A=E4=BC=A0iOS=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 11 ++ .../agent_service/AgentServiceImpl.swift | 37 ++++- .../chat_api/SystemFunctionHandler.swift | 2 +- .../ios/Classes/LocationService.swift | 131 ++++++++++++++++++ .../ios/Classes/LocationServicePlugin.swift | 44 ++++++ .../ios/location_service.podspec | 18 ++- .../ios/location_service/Package.swift | 32 ----- .../LocationServicePlugin.swift | 19 --- .../location_service/PrivacyInfo.xcprivacy | 14 -- .../lib/location_service.dart | 27 +--- local_plugins/location_service/pubspec.yaml | 43 ------ 11 files changed, 230 insertions(+), 148 deletions(-) create mode 100644 local_plugins/location_service/ios/Classes/LocationService.swift create mode 100644 local_plugins/location_service/ios/Classes/LocationServicePlugin.swift delete mode 100644 local_plugins/location_service/ios/location_service/Package.swift delete mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift delete mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy diff --git a/ios/Podfile.lock b/ios/Podfile.lock index dda0379b0..4ef33cfa7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,8 @@ PODS: - AMapNavi (~> 10.0.900) - Flutter - AMapFoundation (1.8.2) + - AMapLocation (2.10.0): + - AMapFoundation (>= 1.8.0) - AMapNavi (10.0.900): - AMapFoundation (>= 1.8.2) - audio_waveforms (0.0.1): @@ -54,6 +56,9 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv + - location_service (0.0.1): + - AMapLocation (~> 2.10.0) + - Flutter - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) @@ -98,6 +103,7 @@ DEPENDENCIES: - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) - fluwx (from `.symlinks/plugins/fluwx/ios`) - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) + - location_service (from `.symlinks/plugins/location_service/ios`) - ota (from `.symlinks/plugins/ota/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`) @@ -107,6 +113,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - AMapFoundation + - AMapLocation - AMapNavi - FBAEMKit - FBSDKCoreKit @@ -146,6 +153,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluwx/ios" haptic_feedback: :path: ".symlinks/plugins/haptic_feedback/ios" + location_service: + :path: ".symlinks/plugins/location_service/ios" ota: :path: ".symlinks/plugins/ota/ios" permission_handler_apple: @@ -160,6 +169,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec + AMapLocation: 5248aec2455ebb5d104b367813c946430a2ee033 AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d @@ -177,6 +187,7 @@ SPEC CHECKSUMS: fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + location_service: fab4ef23d473296a215c49e12c129e466574d94e Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d ota: 20c3e522c57e7ba70ce118109e73650acc9fec76 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 9f4ac434a..e5035bf17 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -12,7 +12,11 @@ import music_service protocol AgentServiceListener: AnyObject { func onEvent(eventName: String, data: [String: Any]) } - +extension Notification.Name { + static let navigationdEvent = Notification.Name("navigationdEvent") //导航事件 + static let locationstartEvent = Notification.Name("locationstartEvent") //启动定位 + static let updatelocationEvent = Notification.Name("updatelocationEvent") //更新位置事件 +} class AgentServiceImpl: NSObject { private let TAG = "AgentServiceImpl" @@ -55,6 +59,7 @@ class AgentServiceImpl: NSObject { private let maxIdleSeconds: TimeInterval = 10 internal var audioPlayer: AudioPlayer? + private var locationInfo:[String: String]? internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl") @@ -161,7 +166,9 @@ class AgentServiceImpl: NSObject { sendError("Azure语音区域缺失", code: "CONFIG_ERROR") return false } - + //启动定位服务 + NotificationCenter.default.post(name: .locationstartEvent, object: nil) + addlocationmonitor() if let mcpServer = config["mcpServer"] as? String { self.mcpServer = mcpServer } @@ -441,7 +448,10 @@ class AgentServiceImpl: NSObject { var messages: [[String: Any]] = [] if !systemPrompt.isEmpty { - messages.append(chatApiService.createSystemMessage(content: systemPrompt)) + + let info: [String: String] = self.locationInfo ?? ["user_location": "还未获取到位置信息", "user_lat": "未开启定位", "user_lng": "未开启定位"] + let _systemPrompt = fillTemplate(systemPrompt, with: info) + messages.append(chatApiService.createSystemMessage(content: _systemPrompt)) os_log("添加系统提示消息", log: logger, type: .info) } @@ -690,6 +700,27 @@ class AgentServiceImpl: NSObject { MusicService.shared.playPlaylist(songs: songs) } + // 添加定位监听 + private func addlocationmonitor(){ + // 通常放在启动后或模块初始化时 + NotificationCenter.default.addObserver( + forName: .updatelocationEvent, + object: nil, + queue: .main + ) { notification in + if let info = notification.userInfo as? [String: String] { + print("📤 地址信息:\(info)") + self.locationInfo = info + } + } + } + func fillTemplate(_ template: String, with values: [String: String]) -> String { + var result = template + for (key, value) in values { + result = result.replacingOccurrences(of: "{\(key)}", with: value) + } + return result + } func clearChatHistory() -> Bool { chatHistory.removeAll() diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index 610ccdc00..1d7b7746d 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -67,7 +67,7 @@ public class SystemFunctionHandler: NSObject { registerGetCurrentTimeFunction(client: client) // 注册获取当前位置函数 - registerGetCurrentLocationFunction(client: client) + // registerGetCurrentLocationFunction(client: client) // 注册媒体控制函数 registerMediaPlayFunction(client: client) diff --git a/local_plugins/location_service/ios/Classes/LocationService.swift b/local_plugins/location_service/ios/Classes/LocationService.swift new file mode 100644 index 000000000..d71179ee3 --- /dev/null +++ b/local_plugins/location_service/ios/Classes/LocationService.swift @@ -0,0 +1,131 @@ +import Foundation +import AMapLocationKit + +extension Notification.Name { + static let locationstartEvent = Notification.Name("locationstartEvent") //启动定位 + static let updatelocationEvent = Notification.Name("updatelocationEvent") //更新位置事件 +} +class LocationService: NSObject { + static let shared = LocationService() + private let TAG = "LocationService" + + private var locationManager: AMapLocationManager! + private var currentLocation: CLLocation? + private var currentReGeocode: AMapLocationReGeocode? + private var initialized = false + + private override init() { + super.init() + } + + // 初始化定位服务 + func initialize() { + if initialized { return } + print("\(TAG)--------- 初始化高德定位服务") + + locationManager = AMapLocationManager() + locationManager.delegate = self + + locationManager.desiredAccuracy = kCLLocationAccuracyBest // 定位精度 + locationManager.distanceFilter = 10.0 // 移动距离阈值(单位:米) + locationManager.locatingWithReGeocode = true // 是否返回逆地理编码信息 + locationManager.allowsBackgroundLocationUpdates = true // 是否允许后台定位 + locationManager.locationTimeout = 10 // 定位超时时间(秒) + locationManager.reGeocodeTimeout = 5 // 逆地理编码超时时间(秒) + + locationManager.startUpdatingLocation() + + initialized = true + } + + // 获取缓存的位置信息 + func getCachedLocation() -> [String: String]? { + guard let location = currentLocation else { return nil } + + var locationInfo: [String: String] = [ + "user_lat": "\(location.coordinate.latitude)", + "user_lng": "\(location.coordinate.longitude)" + ] + + if let regeocode = currentReGeocode { + let address = [ + regeocode.province, // 省 + regeocode.city, // 市 + regeocode.district, // 区 + regeocode.street, // 街道 + regeocode.number // 门牌号 + ].compactMap { $0 }.joined(separator: "") + + locationInfo["user_location"] = address + } + + return locationInfo + } + + // 获取当前位置(带回调) + func getCurrentLocation(completion: @escaping ([String: String]?, Error?) -> Void) { + if let cached = getCachedLocation() { + completion(cached, nil) + } else { + let error = NSError( + domain: "LocationService", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "当前位置还未获取"] + ) + completion(nil, error) + } + } + + // 停止定位服务 + func stop() { + locationManager.stopUpdatingLocation() + locationManager = nil + currentLocation = nil + currentReGeocode = nil + initialized = false + } +} + +// MARK: - AMapLocationManagerDelegate +extension LocationService: AMapLocationManagerDelegate { + // 定位更新回调 + func amapLocationManager(_ manager: AMapLocationManager!, didUpdate location: CLLocation!, reGeocode: AMapLocationReGeocode!) { + currentLocation = location + currentReGeocode = reGeocode + + print("\(TAG)--------- 定位更新: \(location.coordinate.latitude), \(location.coordinate.longitude)") + + var locationInfo: [String: String] = [ + "user_lat": "\(location.coordinate.latitude)", + "user_lng": "\(location.coordinate.longitude)" + ] + if let regeocode = currentReGeocode { + let address = [ + reGeocode.province, // 省 + reGeocode.city, // 市 + reGeocode.district, // 区 + reGeocode.street, // 街道 + reGeocode.number // 门牌号 + ].compactMap { $0 }.joined(separator: "") + print("\(TAG)--------- 定位地址: \(address)") + locationInfo["user_location"] = address + } + + + NotificationCenter.default.post( + name: .updatelocationEvent, + object: nil, + userInfo: locationInfo + ) + } + + // 定位失败回调 + func amapLocationManager(_ manager: AMapLocationManager!, didFailWithError error: Error!) { + print("\(TAG)--------- 定位失败: \(error.localizedDescription)") + } + + // 权限变更回调 + func amapLocationManager(_ manager: AMapLocationManager!, doRequireLocationAuth locationManager: CLLocationManager!) { + locationManager.requestWhenInUseAuthorization() + } +} diff --git a/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift b/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift new file mode 100644 index 000000000..ad30a36f7 --- /dev/null +++ b/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift @@ -0,0 +1,44 @@ +import Flutter +import UIKit +import AMapLocationKit + +public class LocationServicePlugin: NSObject, FlutterPlugin { + private static var channel: FlutterMethodChannel? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.location_service", binaryMessenger: registrar.messenger()) + let instance = LocationServicePlugin() + // 必须先设置隐私合规 + DispatchQueue.main.async { + // 2. API Key + AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03" + AMapServices.shared().enableHTTPS = true + // 3. 打印验证(调试用) + print("高德SDK初始化完成") + } + registrar.addMethodCallDelegate(instance, channel: channel) + self.channel = channel + instance.addstartlocationmonitor() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "location": + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + + private func addstartlocationmonitor(){ + // 通常放在启动后或模块初始化时 + NotificationCenter.default.addObserver( + forName: .locationstartEvent, + object: nil, + queue: .main + ) { notification in + print("📥 收到 启动定位服务事件") + LocationService.shared.initialize() + } + } +} diff --git a/local_plugins/location_service/ios/location_service.podspec b/local_plugins/location_service/ios/location_service.podspec index b5409ce22..549e6c473 100644 --- a/local_plugins/location_service/ios/location_service.podspec +++ b/local_plugins/location_service/ios/location_service.podspec @@ -13,17 +13,15 @@ A new Flutter project. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'location_service/Sources/location_service/**/*' + s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.dependency 'AMapLocation', '~> 2.10.0' - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.static_framework = true + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' + } + s.platform = :ios, '12.0' s.swift_version = '5.0' - - # If your plugin requires a privacy manifest, for example if it uses any - # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your - # plugin's privacy impact, and then uncomment this line. For more information, - # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'location_service_privacy' => ['location_service/Sources/location_service/PrivacyInfo.xcprivacy']} end diff --git a/local_plugins/location_service/ios/location_service/Package.swift b/local_plugins/location_service/ios/location_service/Package.swift deleted file mode 100644 index 0b0dd0fbd..000000000 --- a/local_plugins/location_service/ios/location_service/Package.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "location_service", - platforms: [ - .iOS("12.0") - ], - products: [ - .library(name: "location-service", targets: ["location_service"]) - ], - dependencies: [], - targets: [ - .target( - name: "location_service", - dependencies: [], - resources: [ - // If your plugin requires a privacy manifest, for example if it uses any required - // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's - // privacy impact, and then uncomment these lines. For more information, see - // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - // .process("PrivacyInfo.xcprivacy"), - - // If you have other resources that need to be bundled with your plugin, refer to - // the following instructions to add them: - // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package - ] - ) - ] -) diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift deleted file mode 100644 index 1b6f10f28..000000000 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Flutter -import UIKit - -public class LocationServicePlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "location_service", binaryMessenger: registrar.messenger()) - let instance = LocationServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("iOS " + UIDevice.current.systemVersion) - default: - result(FlutterMethodNotImplemented) - } - } -} diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy b/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy deleted file mode 100644 index a34b7e2e6..000000000 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - diff --git a/local_plugins/location_service/lib/location_service.dart b/local_plugins/location_service/lib/location_service.dart index c662819c6..a760675ce 100644 --- a/local_plugins/location_service/lib/location_service.dart +++ b/local_plugins/location_service/lib/location_service.dart @@ -11,33 +11,8 @@ class LocationService { 'com.yunqiinnovation.location_service', ); - // 事件通道 - 简化为只使用数据和状态通道 - late final EventChannel _dataEventChannel; - late final EventChannel _statusEventChannel; - - // 事件流 - late final Stream _dataStream; - late final Stream _statusStream; - - // 获取事件流 - Stream get onDataReceived => _dataStream; - - // 状态流直接传递原始数据 - Stream get onStatusUpdated => _statusStream; - // 内部构造函数 - LocationService._internal() { - _dataEventChannel = const EventChannel( - 'com.yunqiinnovation.music_service/data_events', - ); - _statusEventChannel = const EventChannel( - 'com.yunqiinnovation.music_service/status_events', - ); - - // 初始化事件流 - _dataStream = _dataEventChannel.receiveBroadcastStream(); - _statusStream = _statusEventChannel.receiveBroadcastStream(); - } + LocationService._internal() {} Future> location() async { final result = await _methodChannel.invokeMethod('location'); diff --git a/local_plugins/location_service/pubspec.yaml b/local_plugins/location_service/pubspec.yaml index b575ce3b7..a7809ba9f 100644 --- a/local_plugins/location_service/pubspec.yaml +++ b/local_plugins/location_service/pubspec.yaml @@ -17,21 +17,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter packages. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -40,33 +27,3 @@ flutter: ios: pluginClass: LocationServicePlugin - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package From 91b2e732d9021d82754b99c78c7da34209fdb3b6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 16 Jun 2025 15:56:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E6=8E=A5=E5=8F=A3=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_api/SystemFunctionHandler.kt | 24 +++++- .../music_service/MusicService.kt | 74 +++++++++++++++++-- .../music_service/MusicServicePlugin.swift | 32 ++++++++ 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 058cb0876..655408e71 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -8,7 +8,9 @@ import android.net.Uri import android.os.Build import android.provider.CalendarContract import android.telephony.SmsManager +import android.util.Log import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import java.text.SimpleDateFormat import java.util.* @@ -393,7 +395,10 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio private class MediaPlayHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { // 发送媒体播放广播 - context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PLAY")) + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } + context?.sendBroadcast(intent) return "{\"result\": \"已开始播放媒体\"}" } } @@ -403,8 +408,12 @@ private class MediaPlayHandler(private val context: Context?) : FunctionHandler */ private class MediaPauseHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { + Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") // 发送媒体暂停广播 - context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PAUSE")) + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } + context?.sendBroadcast(intent) return "{\"result\": \"已暂停媒体播放\"}" } } @@ -415,7 +424,11 @@ private class MediaPauseHandler(private val context: Context?) : FunctionHandler private class MediaPreviousHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { // 发送切换上一首广播 - context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PREVIOUS")) +// context?.applicationContext?.sendBroadcast(Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS")) + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } + context?.sendBroadcast(intent) return "{\"result\": \"已切换到上一首\"}" } } @@ -426,7 +439,10 @@ private class MediaPreviousHandler(private val context: Context?) : FunctionHand private class MediaNextHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { // 发送切换下一首广播 - context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_NEXT")) +// context?.applicationContext?.sendBroadcast(Intent("com.yunqiinnovation.music_service.MEDIA_NEXT")) + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } return "{\"result\": \"已切换到下一首\"}" } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index cd9f61791..16104dacb 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -4,8 +4,10 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.graphics.BitmapFactory import android.media.AudioManager import android.media.MediaPlayer @@ -41,7 +43,7 @@ class MusicService : MediaBrowserServiceCompat() { fun isPlaying() = currentStatus fun getCurrentUrl(): String? = currentUrlInternal } - + private var isReceiverRegistered = false private lateinit var mediaSession: MediaSessionCompat private lateinit var audioManager: AudioManager private var mediaPlayer: MediaPlayer? = null @@ -52,6 +54,22 @@ class MusicService : MediaBrowserServiceCompat() { setupMediaSession() startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + + // 注册广播监听器 + val filter = IntentFilter().apply { + addAction("com.yunqiinnovation.music_service.MEDIA_PLAY") + addAction("com.yunqiinnovation.music_service.MEDIA_PAUSE") + addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS") + addAction("com.yunqiinnovation.music_service.MEDIA_NEXT") + } + + // Android 14+ 需要指定 flag + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(controlReceiver, filter) + } + isReceiverRegistered = true Log.i(TAG, "MediaService 启动完成") } @@ -100,6 +118,32 @@ class MusicService : MediaBrowserServiceCompat() { return START_STICKY } + + private val controlReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.i(TAG, "接受到通知事件 ${intent?.action}") + when (intent?.action) { + "com.yunqiinnovation.music_service.MEDIA_PAUSE" -> { + Log.i(TAG, "收到 MEDIA_PAUSE 广播") + pause() + } + "com.yunqiinnovation.music_service.MEDIA_PLAY" -> { + Log.i(TAG, "收到 MEDIA_RESUME 广播") + resume() + } + "com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> { + Log.i(TAG, "收到 MEDIA_RESUME 广播") + playPrevious() + } + "com.yunqiinnovation.music_service.MEDIA_NEXT" -> { + Log.i(TAG, "收到 MEDIA_RESUME 广播") + playNext() + } + // 你也可以扩展支持更多指令 + } + } + } + private fun setupMediaSession() { mediaSession = MediaSessionCompat(this, "MusicMediaSession").apply { setFlags( @@ -190,19 +234,33 @@ class MusicService : MediaBrowserServiceCompat() { return builder.build() } - fun mediaButtonPendingIntent(action: Long): PendingIntent { - val intent = Intent(this, MusicService::class.java).apply { - this.action = Intent.ACTION_MEDIA_BUTTON - putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode())) + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode()) + val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { + setPackage(packageName) + putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) } + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT } - return PendingIntent.getService(this, action.toInt(), intent, flags) + + return PendingIntent.getBroadcast(this, action.toInt(), intent, flags) } +// fun mediaButtonPendingIntent(action: Long): PendingIntent { +// val intent = Intent(this, MusicService::class.java).apply { +// this.action = Intent.ACTION_MEDIA_BUTTON +// putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode())) +// } +// val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { +// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE +// } else { +// PendingIntent.FLAG_UPDATE_CURRENT +// } +// return PendingIntent.getService(this, action.toInt(), intent, flags) +// } // 转换 PlaybackStateCompat.ACTION 到 KeyEvent fun Long.toKeyCode(): Int = when (this) { PlaybackStateCompat.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY @@ -339,6 +397,10 @@ class MusicService : MediaBrowserServiceCompat() { ) = result.sendResult(mutableListOf()) override fun onDestroy() { + if (isReceiverRegistered) { + unregisterReceiver(controlReceiver) + isReceiverRegistered = false + } mediaPlayer?.release() stopForeground(true) isRunning.set(false) diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift index a81039fa6..5a9a1180e 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift @@ -9,6 +9,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { let instance = MusicServicePlugin() registrar.addMethodCallDelegate(instance, channel: channel) self.channel = channel + instance.addNSNotification() } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -68,4 +69,35 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { result(FlutterMethodNotImplemented) } } + + private func addNSNotification(){ + NotificationCenter.default.addObserver( + forName: NSNotification.Name("MediaPause"), + object: nil, + queue: .main + ) { _ in + MusicService.shared.pause() + } + NotificationCenter.default.addObserver( + forName: NSNotification.Name("MediaPlay"), + object: nil, + queue: .main + ) { _ in + MusicService.shared.resume() + } + NotificationCenter.default.addObserver( + forName: NSNotification.Name("MediaPrevious"), + object: nil, + queue: .main + ) { _ in + MusicService.shared.previous() + } + NotificationCenter.default.addObserver( + forName: NSNotification.Name("MediaNext"), + object: nil, + queue: .main + ) { _ in + MusicService.shared.next() + } + } } From 1ec4ef8f032752ef973bf25c05c32253f3b2bdc6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 16 Jun 2025 16:25:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E4=B8=8B=E4=B8=80=E6=9B=B2=20=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yunqiinnovation/chat_api/SystemFunctionHandler.kt | 1 + .../com/yunqiinnovation/music_service/MusicService.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 655408e71..794b66547 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -443,6 +443,7 @@ private class MediaNextHandler(private val context: Context?) : FunctionHandler val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply { setPackage(context?.packageName) // 设置包名使其成为显式 Intent } + context?.sendBroadcast(intent) return "{\"result\": \"已切换到下一首\"}" } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 3fa1e2132..d346baed0 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -129,15 +129,15 @@ class MusicService : MediaBrowserServiceCompat() { pause() } "com.yunqiinnovation.music_service.MEDIA_PLAY" -> { - Log.i(TAG, "收到 MEDIA_RESUME 广播") + Log.i(TAG, "收到 MEDIA_PLAY 广播") resume() } "com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> { - Log.i(TAG, "收到 MEDIA_RESUME 广播") + Log.i(TAG, "收到 MEDIA_PREVIOUS 广播") playPrevious() } "com.yunqiinnovation.music_service.MEDIA_NEXT" -> { - Log.i(TAG, "收到 MEDIA_RESUME 广播") + Log.i(TAG, "收到 MEDIA_NEXT 广播") playNext() } // 你也可以扩展支持更多指令