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});