feat: 添加 Android 文件选择器支持,更新 H5ShellPage 以集成文件选择功能
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package io.openim.flutter.openim
|
package io.openim.flutter.openim
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.app.Activity
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -23,8 +24,11 @@ import java.io.File
|
|||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
private val CHANNEL = "io.openim.flutter.openim/apk_info"
|
private val CHANNEL = "io.openim.flutter.openim/apk_info"
|
||||||
private val SHELL_BRANDING_CHANNEL = "io.openim.flutter.im_webview_app/shell_branding"
|
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 TAG = "MainActivity"
|
||||||
private val MAX_BRANDING_ICON_SIZE = 192
|
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?) {
|
override fun onCreate(savedInstanceState: android.os.Bundle?) {
|
||||||
// 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt,修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整)
|
// 华为/荣耀/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 {
|
private fun getCurrentAppName(): String {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||||
|
|
||||||
import 'config/app_config.dart';
|
import 'config/app_config.dart';
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ const _shellAccent = Color(0xFF0089FF);
|
|||||||
const _shellSubText = Color(0xFF8E9AB0);
|
const _shellSubText = Color(0xFF8E9AB0);
|
||||||
const _shellBrandingChannel =
|
const _shellBrandingChannel =
|
||||||
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
||||||
|
const _androidFilePickerChannel =
|
||||||
|
MethodChannel('io.openim.flutter.openim/file_picker');
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -120,7 +123,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebViewController _buildController(int lineIndex) {
|
WebViewController _buildController(int lineIndex) {
|
||||||
return WebViewController(
|
final controller = WebViewController(
|
||||||
onPermissionRequest: (request) {
|
onPermissionRequest: (request) {
|
||||||
unawaited(_handleWebViewPermissionRequest(request));
|
unawaited(_handleWebViewPermissionRequest(request));
|
||||||
},
|
},
|
||||||
@@ -176,6 +179,39 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
|||||||
onNavigationRequest: _handleNavigationRequest,
|
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
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user