You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 16:22:54