feat: 添加 Android 文件选择器支持,更新 H5ShellPage 以集成文件选择功能

This commit is contained in:
Booker
2026-05-27 15:34:16 +07:00
parent 3f71ba0e23
commit 76897c6e0f
2 changed files with 172 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
package io.openim.flutter.openim
import android.content.Intent
import android.app.Activity
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
@@ -23,8 +24,11 @@ import java.io.File
class MainActivity : FlutterActivity() {
private val CHANNEL = "io.openim.flutter.openim/apk_info"
private val SHELL_BRANDING_CHANNEL = "io.openim.flutter.im_webview_app/shell_branding"
private val FILE_PICKER_CHANNEL = "io.openim.flutter.openim/file_picker"
private val TAG = "MainActivity"
private val MAX_BRANDING_ICON_SIZE = 192
private val FILE_PICKER_REQUEST_CODE = 4201
private var pendingFilePickerResult: MethodChannel.Result? = null
override fun onCreate(savedInstanceState: android.os.Bundle?) {
// 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整)
@@ -107,6 +111,137 @@ class MainActivity : FlutterActivity() {
}
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, FILE_PICKER_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"pickFiles" -> {
val acceptTypes = call.argument<List<String>>("acceptTypes") ?: emptyList()
val allowMultiple = call.argument<Boolean>("allowMultiple") ?: false
openNativeFilePicker(acceptTypes, allowMultiple, result)
}
else -> {
result.notImplemented()
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_PICKER_REQUEST_CODE) {
val result = pendingFilePickerResult
pendingFilePickerResult = null
if (result == null) {
super.onActivityResult(requestCode, resultCode, data)
return
}
if (resultCode != Activity.RESULT_OK || data == null) {
result.success(emptyList<String>())
return
}
val uris = mutableListOf<String>()
val clipData = data.clipData
if (clipData != null) {
for (index in 0 until clipData.itemCount) {
clipData.getItemAt(index).uri?.let { uri ->
grantPickedFileReadPermission(uri)
uris.add(uri.toString())
}
}
} else {
data.data?.let { uri ->
grantPickedFileReadPermission(uri)
uris.add(uri.toString())
}
}
result.success(uris)
return
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun openNativeFilePicker(
acceptTypes: List<String>,
allowMultiple: Boolean,
result: MethodChannel.Result
) {
if (pendingFilePickerResult != null) {
result.error("FILE_PICKER_BUSY", "A file picker request is already active", null)
return
}
val mimeTypes = normalizeMimeTypes(acceptTypes)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = when (mimeTypes.size) {
0 -> "*/*"
1 -> mimeTypes.first()
else -> "*/*"
}
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
if (mimeTypes.size > 1) {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
pendingFilePickerResult = result
try {
startActivityForResult(
Intent.createChooser(intent, "选择文件"),
FILE_PICKER_REQUEST_CODE
)
} catch (e: Exception) {
pendingFilePickerResult = null
result.error("FILE_PICKER_ERROR", e.message ?: "Unable to open file picker", null)
}
}
private fun normalizeMimeTypes(acceptTypes: List<String>): List<String> {
val mimeTypes = linkedSetOf<String>()
acceptTypes
.map { it.trim().lowercase() }
.filter { it.isNotEmpty() && it != "*/*" }
.forEach { type ->
when {
type.startsWith(".") -> mimeTypes.addAll(mimeTypesForExtension(type))
type.contains("/") -> mimeTypes.add(type)
}
}
return mimeTypes.toList()
}
private fun mimeTypesForExtension(extension: String): List<String> {
return when (extension) {
".jpg", ".jpeg" -> listOf("image/jpeg")
".png" -> listOf("image/png")
".gif" -> listOf("image/gif")
".webp" -> listOf("image/webp")
".heic" -> listOf("image/heic")
".heif" -> listOf("image/heif")
".mp4", ".m4v" -> listOf("video/mp4")
".mov" -> listOf("video/quicktime")
".webm" -> listOf("video/webm")
".mp3" -> listOf("audio/mpeg")
".m4a" -> listOf("audio/mp4")
".aac" -> listOf("audio/aac")
".wav" -> listOf("audio/wav")
else -> emptyList()
}
}
private fun grantPickedFileReadPermission(uri: Uri) {
try {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (_: Exception) {
// Not all providers grant persistable permissions. The transient
// read grant is enough for WebView's file upload handoff.
}
}
private fun getCurrentAppName(): String {

View File

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'config/app_config.dart';
@@ -14,6 +15,8 @@ const _shellAccent = Color(0xFF0089FF);
const _shellSubText = Color(0xFF8E9AB0);
const _shellBrandingChannel =
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
const _androidFilePickerChannel =
MethodChannel('io.openim.flutter.openim/file_picker');
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -120,7 +123,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
}
WebViewController _buildController(int lineIndex) {
return WebViewController(
final controller = WebViewController(
onPermissionRequest: (request) {
unawaited(_handleWebViewPermissionRequest(request));
},
@@ -176,6 +179,39 @@ class _H5ShellPageState extends State<H5ShellPage> {
onNavigationRequest: _handleNavigationRequest,
),
);
_configurePlatformController(controller);
return controller;
}
void _configurePlatformController(WebViewController controller) {
final platformController = controller.platform;
if (platformController is AndroidWebViewController) {
unawaited(
platformController.setOnShowFileSelector(_handleAndroidFileSelection),
);
}
}
Future<List<String>> _handleAndroidFileSelection(
FileSelectorParams params,
) async {
if (params.mode == FileSelectorMode.save) {
return <String>[];
}
try {
final result = await _androidFilePickerChannel.invokeListMethod<String>(
'pickFiles',
{
'acceptTypes': params.acceptTypes,
'allowMultiple': params.mode == FileSelectorMode.openMultiple,
},
);
return result ?? <String>[];
} catch (_) {
return <String>[];
}
}
@override