From 572d0ec0eee36a35934e6b3b9a4fe55cc361e730 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 14 Jan 2026 16:36:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=88=86=E4=BA=AB):=20Android=E4=BE=A7?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9F=B3=E9=A2=91=E6=96=87=E4=BB=B6=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E5=92=8C=E6=89=93=E5=BC=80=E4=BD=BF=E7=94=A8Voitrans/?= =?UTF-8?q?meeting=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现通过系统分享或文件打开方式将音频文件传递到应用的功能 在Android端添加相关intent-filter和处理逻辑 在Flutter端添加文件接收和处理逻辑 添加文件缓存和重试机制确保处理可靠性 --- android/app/src/main/AndroidManifest.xml | 14 +++ .../com/saitong/voitrans/MainActivity.kt | 103 +++++++++++++++++- ...VoitransShareExtensionProfile.entitlements | 10 ++ lib/main.dart | 43 ++++++++ 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4e3d6a8c6..e82e00f29 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -111,6 +111,20 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt index 2a992b381..e11ffb7eb 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt @@ -15,11 +15,16 @@ import android.os.Looper import android.util.Log import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.plugin.common.MethodChannel // 添加这行导入 +import android.net.Uri +import android.provider.OpenableColumns +import java.io.File +import java.io.FileOutputStream class MainActivity: FlutterActivity() { private val TAG = "MainActivity" private var pendingDeepLink: Intent? = null private var pendingNavigationIntent: Intent? = null + private var pendingShareIntent: Intent? = null private val EXTRA_NAVIGATE_TO = "com.saitong.voitrans.extra.NAVIGATE_TO" private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord" @@ -36,10 +41,20 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "Intent extras: ${it.extras}") } - // 暂存深链接,等Flutter引擎完全准备好后处理 - if (intent?.action == Intent.ACTION_VIEW && intent?.data != null) { - pendingDeepLink = intent - Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") + intent?.let { + if (it.action == Intent.ACTION_SEND) { + pendingShareIntent = it + Log.d(TAG, "📎 暂存分享文件意图,等待Flutter引擎准备完成") + } else if (it.action == Intent.ACTION_VIEW && it.data != null) { + val uri = it.data + if (uri != null && isLikelyAudio(it, uri)) { + pendingShareIntent = it + Log.d(TAG, "📎 暂存打开文件意图,等待Flutter引擎准备完成") + } else { + pendingDeepLink = it + Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") + } + } } if (intent?.hasExtra(EXTRA_NAVIGATE_TO) == true) { @@ -110,6 +125,12 @@ class MainActivity: FlutterActivity() { // Flutter引擎配置完成后,处理待处理的深链接 Handler(Looper.getMainLooper()).postDelayed({ + pendingShareIntent?.let { intent -> + Log.d(TAG, "📎 处理待处理的分享/打开文件意图") + handleNavigationIntent(intent) + pendingShareIntent = null + } + pendingDeepLink?.let { intent -> Log.d(TAG, "🔗 处理待处理的深链接") handleNavigationIntent(intent) @@ -135,6 +156,21 @@ class MainActivity: FlutterActivity() { return } + if (it.action == Intent.ACTION_SEND || it.action == Intent.ACTION_VIEW) { + try { + val path = extractAudioPathFromIntent(it) + if (path != null) { + flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger -> + val channel = MethodChannel(messenger, "com.saitong.voitrans/deeplink") + channel.invokeMethod("handleSharedFile", mapOf("filePath" to path)) + return + } + } + } catch (e: Exception) { + Log.e(TAG, "❌ 处理分享/打开文件失败", e) + } + } + if (it.action == Intent.ACTION_VIEW && it.data != null) { val uri = it.data Log.d(TAG, "🔗 处理深链接: $uri") @@ -169,6 +205,65 @@ class MainActivity: FlutterActivity() { } } + private fun extractAudioPathFromIntent(intent: Intent): String? { + val uri: Uri? = when (intent.action) { + Intent.ACTION_SEND -> intent.getParcelableExtra(Intent.EXTRA_STREAM) + Intent.ACTION_VIEW -> intent.data + else -> null + } + if (uri == null) return null + if (!isLikelyAudio(intent, uri)) return null + return copyUriToCache(uri) + } + + private fun isLikelyAudio(intent: Intent, uri: Uri): Boolean { + val type = intent.type ?: "" + if (type.startsWith("audio/")) return true + val scheme = uri.scheme ?: "" + if (scheme == "content" || scheme == "file") return true + val path = uri.toString().lowercase() + return path.endsWith(".wav") || path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".aac") + } + + private fun copyUriToCache(uri: Uri): String? { + return try { + val resolver = applicationContext.contentResolver + val cursor = resolver.query(uri, null, null, null, null) + var displayName = "shared_audio" + cursor?.use { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex >= 0 && it.moveToFirst()) { + displayName = it.getString(nameIndex) ?: displayName + } + } + val ext = when { + displayName.endsWith(".wav", true) -> "wav" + displayName.endsWith(".mp3", true) -> "mp3" + displayName.endsWith(".m4a", true) -> "m4a" + displayName.endsWith(".aac", true) -> "aac" + else -> displayName.substringAfterLast('.', "audio") + } + val sharedDir = File(applicationContext.filesDir, "shared_audio") + if (!sharedDir.exists()) sharedDir.mkdirs() + val outFile = File(sharedDir, "audio_${System.currentTimeMillis()}.$ext") + resolver.openInputStream(uri)?.use { inStream -> + FileOutputStream(outFile).use { out -> + val buffer = ByteArray(8192) + var len: Int + while (true) { + len = inStream.read(buffer) + if (len <= 0) break + out.write(buffer, 0, len) + } + } + } + outFile.absolutePath + } catch (e: Exception) { + Log.e(TAG, "复制分享文件到缓存失败: ${e.message}", e) + null + } + } + private fun pushRouteToFlutter(route: String) { try { val engine = flutterEngine diff --git a/ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements b/ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements new file mode 100644 index 000000000..4f50aba57 --- /dev/null +++ b/ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.saitong.voitrans + + + diff --git a/lib/main.dart b/lib/main.dart index 001c35645..ac646c1fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,6 +22,8 @@ import 'data/services/login_service.dart'; import 'dart:ui' as ui; import 'modules/splash/controllers/splash_controller.dart'; +bool _deepLinkHandlerInitialized = false; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -46,6 +49,9 @@ void main() async { // 初始化深链接处理器 void _initDeepLinkHandler() { + if (_deepLinkHandlerInitialized) return; + _deepLinkHandlerInitialized = true; + print("🔗 初始化深链接处理器"); const platform = MethodChannel('com.saitong.voitrans/deeplink'); @@ -62,6 +68,15 @@ void _initDeepLinkHandler() { print("🔗 处理深链接 URL: $url"); await _handleDeepLinkNavigation(url); } + return; + } + + if (call.method == 'handleSharedFile') { + final arguments = call.arguments as Map; + final filePath = arguments['filePath'] as String?; + if (filePath == null || filePath.isEmpty) return; + await _handleSharedAudioFile(filePath); + return; } }); @@ -107,6 +122,34 @@ Future _handleDeepLinkNavigation(String url) async { } } +Future _handleSharedAudioFile(String filePath) async { + try { + if (!File(filePath).existsSync()) { + print("❌ 分享文件不存在: $filePath"); + return; + } + + int retryCount = 0; + const maxRetries = 10; + while (retryCount < maxRetries) { + try { + Get.toNamed( + Routes.meeting, + arguments: { + 'filePath': filePath, + }, + ); + return; + } catch (e) { + retryCount++; + await Future.delayed(const Duration(milliseconds: 300)); + } + } + } catch (e) { + print("❌ 处理分享文件失败: $e"); + } +} + class MainApp extends StatelessWidget { const MainApp({super.key});