diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0b53e4a8f..6f29c8c95 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -61,7 +61,8 @@ android { manifestPlaceholders.putAll( mapOf( "redirectSchemeName" to "spotify-sdk", - "redirectHostName" to "auth" + "redirectHostName" to "auth", + "applicationName" to "com.saitong.voitrans.MyApp" // 新增:声明自定义 Application ) ) // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e1ca0f551..24d99722b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ xmlns:tools="http://schemas.android.com/tools"> + @@ -84,8 +85,7 @@ + Log.d(TAG, "🔗 处理待处理的深链接") + handleDeepLink(intent) + pendingDeepLink = null + } + }, 1000) // 延迟1秒确保Flutter完全准备好 + + Log.d(TAG, "✅ configureFlutterEngine 完成") + } + + // 处理深链接的统一方法 + private fun handleDeepLink(intent: Intent?) { + intent?.let { + if (it.action == Intent.ACTION_VIEW && it.data != null) { + val uri = it.data + Log.d(TAG, "🔗 处理深链接: $uri") + + try { + Log.d(TAG, "🔗 开始发送深链接到 Flutter") + + // 通过 MethodChannel 将深链接信息传递给 Flutter + flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger -> + Log.d(TAG, "🔗 获取到 BinaryMessenger") + + val channel = MethodChannel( + messenger, + "com.saitong.voitrans/deeplink" + ) + + val arguments = mapOf( + "url" to uri.toString(), + "scheme" to uri?.scheme, + "host" to uri?.host, + "path" to uri?.path, + "query" to uri?.query, + "queryParameters" to uri?.queryParameterNames?.associateWith { + uri.getQueryParameter(it) + } + ) + + Log.d(TAG, "🔗 准备发送参数: $arguments") + + channel.invokeMethod("handleDeepLink", arguments) + Log.d(TAG, "✅ 深链接信息已发送到 Flutter") + } ?: run { + Log.w(TAG, "❌ BinaryMessenger 为空,无法发送深链接") + } + } catch (e: Exception) { + Log.e(TAG, "❌ 处理深链接时出错", e) + } + } else { + Log.d(TAG, "🔗 Intent 不是深链接或数据为空") + } + } } override fun onDestroy() { @@ -112,5 +237,5 @@ class MainActivity: FlutterActivity() { // 反射方法可能不存在,忽略 } } -} +} diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt new file mode 100644 index 000000000..17e5ee3b0 --- /dev/null +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt @@ -0,0 +1,144 @@ +package com.saitong.voitrans + +import android.app.ActivityManager +import android.content.Context +import android.util.Log +import io.flutter.app.FlutterApplication +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.plugins.GeneratedPluginRegistrant + +// Application.kt +class MyApp : FlutterApplication() { + + companion object { + private const val TAG = "MyApp" + private const val ENGINE_ID = "main_engine" + + // 标记 Engine 是否已经执行过 Dart 入口点 + @Volatile + private var isDartEntrypointExecuted = false + + // 记录应用启动时间,用于判断是否为新启动 + @Volatile + private var appStartTime = 0L + } + + override fun onCreate() { + super.onCreate() + appStartTime = System.currentTimeMillis() + Log.d(TAG, "MyApp onCreate - 进程启动") + + val cache = FlutterEngineCache.getInstance() + val existingEngine = cache.get(ENGINE_ID) + + if (existingEngine == null) { + Log.d(TAG, "创建新的 FlutterEngine") + createAndCacheEngine(cache) + } else { + Log.d(TAG, "FlutterEngine 已存在,检查状态") + // 检查现有 Engine 的状态 + try { + val isExecuting = existingEngine.dartExecutor.isExecutingDart + Log.d(TAG, "Engine Dart执行状态: $isExecuting") + + if (!isExecuting && isMainProcess()) { + Log.d(TAG, "Engine 存在但Dart未执行,重新执行") + existingEngine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + isDartEntrypointExecuted = true + } + } catch (e: Exception) { + Log.e(TAG, "检查或重新执行 Dart 入口点失败", e) + // 如果失败,重新创建 Engine + cache.remove(ENGINE_ID) + createAndCacheEngine(cache) + } + } + } + + private fun createAndCacheEngine(cache: FlutterEngineCache) { + // 在主进程中总是创建并执行Dart入口点 + if (isMainProcess()) { + Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart") + val flutterEngine = FlutterEngine(this) + + // 注册插件(只在这里注册一次) + Log.d(TAG, "📦 注册 Flutter 插件") + GeneratedPluginRegistrant.registerWith(flutterEngine) + + try { + Log.d(TAG, "🚀 开始执行 Dart 入口点") + flutterEngine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + isDartEntrypointExecuted = true + cache.put(ENGINE_ID, flutterEngine) + Log.d(TAG, "✅ FlutterEngine 创建并缓存成功") + } catch (e: Exception) { + Log.e(TAG, "❌ 执行 Dart 入口点失败", e) + // 如果执行失败,仍然缓存 Engine,但标记为未执行 + isDartEntrypointExecuted = false + cache.put(ENGINE_ID, flutterEngine) + } + } else { + Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine") + // 非主进程只创建 Engine,不执行 Dart 入口点 + val flutterEngine = FlutterEngine(this) + cache.put(ENGINE_ID, flutterEngine) + } + } + + private fun isMainProcess(): Boolean { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processes = am.runningAppProcesses ?: return true + val mainProcessName = packageName + val myPid = android.os.Process.myPid() + + for (process in processes) { + if (process.pid == myPid) { + return process.processName == mainProcessName + } + } + return true + } + + private fun isAppInForeground(): Boolean { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processes = am.runningAppProcesses ?: return false + val pkg = packageName + for (p in processes) { + if (p.processName == pkg && + p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return true + } + } + return false + } + + private fun isAppBeingLaunched(): Boolean { + // 改进的启动检测逻辑 + val currentTime = System.currentTimeMillis() + val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动 + + if (isRecentStart) { + Log.d(TAG, "检测到应用新启动") + return true + } + + // 备用检测方法(兼容旧版本) + return try { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val tasks = am.getRunningTasks(1) + val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName + Log.d(TAG, "通过 RunningTasks 检测启动状态: $result") + result + } catch (e: Exception) { + Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e) + // 如果无法获取任务信息,默认认为是启动状态 + true + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 6cfa3d668..5a0941bdf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,37 +1,104 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_intro/flutter_intro.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'core/bindings/initial_binding.dart'; + import 'routes/app_pages.dart'; import 'routes/app_routes.dart'; import 'core/translations/app_translations.dart'; import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; -import '../../data/services/login_service.dart'; +import 'core/bindings/initial_binding.dart'; +import 'data/services/login_service.dart'; +import 'dart:ui' as ui; +import 'modules/splash/controllers/splash_controller.dart'; void main() async { - // 确保Flutter绑定初始化 WidgetsFlutterBinding.ensureInitialized(); - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - - // 初始化存储 - 在创建应用之前初始化 + // 初始化存储 await GetStorage.init(); - // 初始化环境变量 - await dotenv.load(); + // 加载环境变量 + await dotenv.load(fileName: ".env"); + print("环境变量加载成功"); + + // 初始化登录服务 + Get.put(LoginService()); + + // 延迟初始化深链接处理器,确保Flutter完全准备好 + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 1500), () { + _initDeepLinkHandler(); + }); + }); + + runApp(MainApp()); +} + +// 初始化深链接处理器 +void _initDeepLinkHandler() { + print("🔗 初始化深链接处理器"); + + const platform = MethodChannel('com.saitong.voitrans/deeplink'); + + platform.setMethodCallHandler((call) async { + print("🔗 Flutter 收到深链接调用: ${call.method}"); + + if (call.method == 'handleDeepLink') { + final arguments = call.arguments as Map; + print("🔗 深链接参数: $arguments"); - // 初始化登录服务(包含Firebase初始化) - LoginService(); + final url = arguments['url'] as String?; + if (url != null) { + print("🔗 处理深链接 URL: $url"); + await _handleDeepLinkNavigation(url); + } + } + }); - // 运行应用 - runApp(const MainApp()); + print("✅ 深链接处理器初始化完成"); +} + +// 处理深链接导航 +Future _handleDeepLinkNavigation(String url) async { + print("🔗 开始处理深链接导航: $url"); + + try { + final uri = Uri.parse(url); + print("🔗 解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}"); + + if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') { + print("🔗 检测到QQ音乐登录回调"); + + // 等待SplashController注册 + int retryCount = 0; + const maxRetries = 10; + + while (retryCount < maxRetries) { + try { + if (Get.isRegistered()) { + print("🔗 找到SplashController,处理QQ音乐登录"); + final controller = Get.find(); + controller.handleQQMusicLogin(); + return; + } + } catch (e) { + print("🔗 获取SplashController失败: $e"); + } + + retryCount++; + print("🔗 等待SplashController注册 ($retryCount/$maxRetries)"); + await Future.delayed(const Duration(milliseconds: 500)); + } + + print("❌ 超时等待SplashController注册"); + } + } catch (e) { + print("❌ 处理深链接时出错: $e"); + } } class MainApp extends StatelessWidget { @@ -39,46 +106,27 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - // 获取保存的主题设置 - final themeMode = ThemeService().getThemeMode(); + // 在应用构建时立即初始化深链接处理 + _initDeepLinkHandler(); + // 使用 ScreenUtilInit 包装整个应用 return ScreenUtilInit( - designSize: const Size(375, 812), + designSize: const Size(375, 812), // 设计稿尺寸,根据你的设计稿调整 minTextAdapt: true, splitScreenMode: true, - builder: (_, child) { + builder: (context, child) { return GetMaterialApp( - title: 'Voitrans', + title: 'VoiTrans', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, - themeMode: themeMode, // 使用提前获取的主题模式 - initialBinding: InitialBinding(), + themeMode: ThemeService().getThemeMode(), initialRoute: Routes.splash, getPages: AppPages.pages, - debugShowCheckedModeBanner: false, + initialBinding: InitialBinding(), translations: AppTranslations(), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - locale: AppTranslations.deviceLocale(), - fallbackLocale: AppTranslations.getSmartFallbackLocale(), - supportedLocales: AppTranslations.supportedLocales, - builder: EasyLoading.init( - builder: (BuildContext context, Widget? widget) { - EasyLoading.instance.userInteractions = false; - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.noScaling, - ), - child: Intro( - padding: EdgeInsets.zero, - child: widget!, - ), - ); - }, - ), + locale: Get.deviceLocale, + fallbackLocale: const Locale('en', 'US'), + debugShowCheckedModeBanner: false, ); }, ); diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 4da069bc9..1ac931498 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -289,6 +289,21 @@ class SplashController extends GetxController { return false; } } + + /// 处理QQ音乐登录回调 + void handleQQMusicLogin() { + try { + Logger.i(_tag, '处理QQ音乐登录回调'); + + // 这里可以添加具体的QQ音乐登录处理逻辑 + // 例如:验证登录状态、更新用户信息、导航到相应页面等 + + // 如果需要导航到特定页面,可以使用: + // Get.toNamed(Routes.qqmusic); + } catch (e) { + Logger.e(_tag, '处理QQ音乐登录回调失败: $e'); + } + } } /// 显示无网络连接对话框并等待用户操作(阻塞版本) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 99e0ed8e5..c4cab4819 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -1956,7 +1956,7 @@ object AgentService : CoroutineScope { ) } if (playlist.isNotEmpty()) { - processQQMusicPlayList(playlist) +// processQQMusicPlayList(playlist) } else { Log.w(TAG, "qqmusic_searchandplay_songs 中没有有效的音乐条目") } @@ -1969,6 +1969,10 @@ object AgentService : CoroutineScope { val start = cardNavigation.optString("start", "") val end = cardNavigation.optString("end", "") processNavigation(start, end); + }else if (toolname == "map_weather") { //导航服务 + iscard = true + }else if (toolname == "maps_weather") { //导航服务 + iscard = true } return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply,"iscard" to iscard); } 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 b294b6a05..b13d17233 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 @@ -834,6 +834,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -> [String: Any] { var broadcast = true var notreply = false + var iscard = false var metadata: [String: Any] = [:] metadata[toolName] = jsonDict @@ -843,7 +844,11 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - if let _notreply = jsonDict["notreply"] as? Bool { notreply = _notreply } + if let _iscard = jsonDict["iscard"] as? Bool { + iscard = _iscard + } metadata["notreply"] = notreply + metadata["iscard"] = iscard // 处理卡片数据 // if let cardMusic = jsonDict["card_music"] as? [String: Any] { // processMusicCard(cardMusic) @@ -854,8 +859,12 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - processNavigationCard(cardNavigation) } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { processQQMusicPlayList(cardQQMusiclist) + } else if toolName == "maps_weather" { + iscard = true + }else if toolName == "map_weather" { + iscard = true } - + metadata["iscard"] = iscard let metaStr: String if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: []), let jsonString = String(data: jsonData, encoding: .utf8) { diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt index 396c6cf76..077fdf10c 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt @@ -108,7 +108,7 @@ class QQMusicLoginManager(private val context: Context) { // 已授权,直接请求登录信息 Log.d(TAG, "已授权,直接获取登录信息") // isBindQQMusicService = true - startAuthProcess2(callback) + startAuthProcess(callback) } ErrorCodes.ERROR_API_NO_PERMISSION -> { // 需要授权 @@ -156,11 +156,11 @@ class QQMusicLoginManager(private val context: Context) { ) // Log.d(TAG, "当前是前台服务") } else { - // CommonCmd.loginQQMusic( - // context, - // context.packageName, - // BASE_SCHEME, - // ) + CommonCmd.loginQQMusic( + context, + context.packageName, + BASE_SCHEME, + ) Log.d(TAG, "当前是后台服务") } // 启动QQ音乐登录