Browse Source

feat(分享): Android侧添加音频文件分享和打开使用Voitrans/meeting导入

实现通过系统分享或文件打开方式将音频文件传递到应用的功能
在Android端添加相关intent-filter和处理逻辑
在Flutter端添加文件接收和处理逻辑
添加文件缓存和重试机制确保处理可靠性
newdev_shunjiawei
lxm 9 months ago
parent
commit
572d0ec0ee
  1. 14
      android/app/src/main/AndroidManifest.xml
  2. 103
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  3. 10
      ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements
  4. 43
      lib/main.dart

14
android/app/src/main/AndroidManifest.xml

@ -111,6 +111,20 @@
<data android:scheme="voitrans"/> <data android:scheme="voitrans"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" android:mimeType="audio/*" />
<data android:scheme="file" android:mimeType="audio/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
</intent-filter>
<!-- PayPal 网页回跳的深链:deepvoiceclient://paypal/return 与 /cancel --> <!-- PayPal 网页回跳的深链:deepvoiceclient://paypal/return 与 /cancel -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>

103
android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt

@ -15,11 +15,16 @@ import android.os.Looper
import android.util.Log import android.util.Log
import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel // 添加这行导入 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() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"
private var pendingDeepLink: Intent? = null private var pendingDeepLink: Intent? = null
private var pendingNavigationIntent: 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 EXTRA_NAVIGATE_TO = "com.saitong.voitrans.extra.NAVIGATE_TO"
private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord" private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord"
@ -36,10 +41,20 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "Intent extras: ${it.extras}") Log.d(TAG, "Intent extras: ${it.extras}")
} }
// 暂存深链接,等Flutter引擎完全准备好后处理 intent?.let {
if (intent?.action == Intent.ACTION_VIEW && intent?.data != null) { if (it.action == Intent.ACTION_SEND) {
pendingDeepLink = intent pendingShareIntent = it
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") 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) { if (intent?.hasExtra(EXTRA_NAVIGATE_TO) == true) {
@ -110,6 +125,12 @@ class MainActivity: FlutterActivity() {
// Flutter引擎配置完成后,处理待处理的深链接 // Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
pendingShareIntent?.let { intent ->
Log.d(TAG, "📎 处理待处理的分享/打开文件意图")
handleNavigationIntent(intent)
pendingShareIntent = null
}
pendingDeepLink?.let { intent -> pendingDeepLink?.let { intent ->
Log.d(TAG, "🔗 处理待处理的深链接") Log.d(TAG, "🔗 处理待处理的深链接")
handleNavigationIntent(intent) handleNavigationIntent(intent)
@ -135,6 +156,21 @@ class MainActivity: FlutterActivity() {
return 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) { if (it.action == Intent.ACTION_VIEW && it.data != null) {
val uri = it.data val uri = it.data
Log.d(TAG, "🔗 处理深链接: $uri") 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) { private fun pushRouteToFlutter(route: String) {
try { try {
val engine = flutterEngine val engine = flutterEngine

10
ios/VoitransShareExtension/VoitransShareExtensionProfile.entitlements

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.saitong.voitrans</string>
</array>
</dict>
</plist>

43
lib/main.dart

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -21,6 +22,8 @@ import 'data/services/login_service.dart';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'modules/splash/controllers/splash_controller.dart'; import 'modules/splash/controllers/splash_controller.dart';
bool _deepLinkHandlerInitialized = false;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -46,6 +49,9 @@ void main() async {
// 初始化深链接处理器 // 初始化深链接处理器
void _initDeepLinkHandler() { void _initDeepLinkHandler() {
if (_deepLinkHandlerInitialized) return;
_deepLinkHandlerInitialized = true;
print("🔗 初始化深链接处理器"); print("🔗 初始化深链接处理器");
const platform = MethodChannel('com.saitong.voitrans/deeplink'); const platform = MethodChannel('com.saitong.voitrans/deeplink');
@ -62,6 +68,15 @@ void _initDeepLinkHandler() {
print("🔗 处理深链接 URL: $url"); print("🔗 处理深链接 URL: $url");
await _handleDeepLinkNavigation(url); await _handleDeepLinkNavigation(url);
} }
return;
}
if (call.method == 'handleSharedFile') {
final arguments = call.arguments as Map<dynamic, dynamic>;
final filePath = arguments['filePath'] as String?;
if (filePath == null || filePath.isEmpty) return;
await _handleSharedAudioFile(filePath);
return;
} }
}); });
@ -107,6 +122,34 @@ Future<void> _handleDeepLinkNavigation(String url) async {
} }
} }
Future<void> _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 { class MainApp extends StatelessWidget {
const MainApp({super.key}); const MainApp({super.key});

Loading…
Cancel
Save