feat: 添加 Android 文件选择器支持,更新 H5ShellPage 以集成文件选择功能
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user