Android:能否在ACTION_SEND选择器中添加文件管理器选项?
如何实现包含云存储应用和文件管理器的统一保存选择器?
可以实现这种统一的选择器,但不能直接依赖系统默认的ACTION_SEND选择器——它不会包含支持目录选择的文件管理器。你需要通过自定义选择器结合两类Intent的方式整合云应用和文件管理器选项,具体步骤如下:
1. 收集两类目标应用的组件信息
分别获取支持文件分享的云应用(如Google Drive、OneDrive)和支持目录选择的文件管理器的组件列表:
获取支持ACTION_SEND的云应用组件
val internalFileUri = FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", internalStorageFile ) val sendIntent = Intent(Intent.ACTION_SEND).apply { type = "application/octet-stream" // 根据你的文件类型调整,比如图片用image/* putExtra(Intent.EXTRA_STREAM, internalFileUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } val cloudAppResolveInfos = packageManager.queryIntentActivities(sendIntent, 0)
获取支持ACTION_OPEN_DOCUMENT_TREE的文件管理器组件
val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val fileManagerResolveInfos = packageManager.queryIntentActivities(treeIntent, 0)
2. 合并并去重组件列表
将两类组件合并,避免重复(少数应用可能同时支持两种Intent,但概率极低):
val allResolveInfos = mutableListOf<ResolveInfo>() allResolveInfos.addAll(cloudAppResolveInfos) allResolveInfos.addAll(fileManagerResolveInfos) // 去重:根据组件包名+类名判断唯一性 val uniqueResolveInfos = allResolveInfos.distinctBy { "${it.activityInfo.packageName}.${it.activityInfo.name}" }
3. 创建自定义选择器对话框
用系统对话框(如AlertDialog)展示所有选项,每个选项显示应用名称和图标:
val options = uniqueResolveInfos.map { it.loadLabel(packageManager).toString() } val icons = uniqueResolveInfos.map { it.loadIcon(packageManager) } AlertDialog.Builder(context) .setTitle("选择保存位置") .setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, options) { view, position, _ -> val textView = view as TextView textView.setCompoundDrawablesWithIntrinsicBounds(icons[position], null, null, null) textView.compoundDrawablePadding = 16 }) { _, position -> val selectedInfo = uniqueResolveInfos[position] handleSelectedComponent(selectedInfo, internalFileUri) } .show()
4. 处理用户选择的组件
根据选中组件支持的Intent类型,分别处理逻辑:
private fun handleSelectedComponent(info: ResolveInfo, fileUri: Uri) { val componentName = ComponentName( info.activityInfo.packageName, info.activityInfo.name ) // 优先判断是否支持ACTION_SEND(云应用逻辑) val sendIntent = Intent(Intent.ACTION_SEND).apply { component = componentName type = "application/octet-stream" putExtra(Intent.EXTRA_STREAM, fileUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } if (packageManager.resolveActivity(sendIntent, 0) != null) { startActivity(sendIntent) return } // 否则启动目录选择Intent(文件管理器逻辑) val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { component = componentName } treeLauncher.launch(treeIntent) } // 用ActivityResultContracts处理目录选择结果 private val treeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { treeUri -> // 保存目录Uri的持久权限,避免后续操作失效 contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) // 执行内部文件到目标目录的复制操作 copyFileToTree(internalStorageFile, treeUri) } } } // 复制文件到指定目录的核心实现 private fun copyFileToTree(sourceFile: File, treeUri: Uri) { val destFileName = sourceFile.name val destUri = DocumentsContract.buildDocumentUriUsingTree( treeUri, DocumentsContract.getTreeDocumentId(treeUri) ).buildUpon().appendPath(destFileName).build() // 执行文件流复制,处理IO异常 runCatching { contentResolver.openOutputStream(destUri)?.use { outputStream -> sourceFile.inputStream().use { inputStream -> inputStream.copyTo(outputStream) } } }.onFailure { // 处理复制失败的情况,比如弹出提示 } }
关键注意事项
- 内部文件必须通过
FileProvider共享,否则会触发FileUriExposedException - 不同厂商的文件管理器对
ACTION_OPEN_DOCUMENT_TREE的实现可能有差异,需在主流设备上测试 - 云应用的
type参数要匹配文件类型,否则部分应用不会出现在选择列表中 - 复制文件时要处理IO异常,保证操作的健壮性
内容的提问来源于stack exchange,提问作者user13756297
相关产品推荐
相关产品推荐

