一、首先你需要先看到效果
就是将你的 activity 添加到打开方式,比如我这里有两个 activity,PdfViewerActivity 负责打开 pdf 文件,OfficeViewerActivity 负责打开 word,excel,ppt 文件
<activity
android:name=".activity.PdfViewerActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
<activity
android:name=".activity.OfficeViewerActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Word -->
<data android:mimeType="application/msword" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<!-- Excel -->
<data android:mimeType="application/vnd.ms-excel" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<!-- PowerPoint -->
<data android:mimeType="application/vnd.ms-powerpoint" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
</intent-filter>
</activity>
二、实现原理
一、发送数据
Manifest 配置完成后,如果调起了系统打开方式,系统会这样发送数据
Intent {
action = ACTION_VIEW
data = content://xxx/xxx //代表文件的 uri
type = application/pdf //代表文件类型
}
二、两种方式
自己伪装成系统系统打开方式发送数据
// 把 File 转成 content:// Uri(和系统行为一致)
val uri = FileProvider.getUriForFile(
activity,
"${activity.packageName}.fileprovider",
file
)
// 构造 ACTION_VIEW Intent(系统打开方式标准格式)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/pdf")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
// 发起跳转
activity.startActivity(intent)
有什么区别:
Intent {
action = android.intent.action.VIEW
data = content://your.package.fileprovider/...
type = application/pdf
}
Intent {
action = android.intent.action.VIEW
data = content://com.android.providers.downloads.documents/document/1234
type = application/pdf
}
可以这样判断:
uri.authority == "${context.packageName}.fileprovider"
三、接收数据
在相应的页面接收数据:
val uri: Uri = intent.data ?: return
val inputStream = contentResolver.openInputStream(uri)
如果你必须要使用文件真实路径而不用 uri,可通过 uri 复制文件到一个目录得到:
fun copyUriToCache(context: Context, uri: Uri): File {
val fileName = getFileName(context, uri) ?: "temp_file"
val destFile = File(context.cacheDir, fileName)
context.contentResolver.openInputStream(uri)?.use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
return destFile
}
获取文件名:
fun getFileName(context: Context, uri: Uri): String? {
val cursor = context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)
cursor?.use {
if (it.moveToFirst()) {
return it.getString(0)
}
}
return null
}
三、工具类
// 获取传入的文件路径和文件名
// 优先从 extra 获取(应用内调用,就是我们常用的 activity 之间跳转传参)
var filePath = intent.getStringExtra(EXTRA_PDF_FILE_PATH) ?: ""
var fileName = intent.getStringExtra(EXTRA_PDF_FILE_NAME) ?: ""
// 如果 extra 中没有文件路径,尝试从 Intent.data URI 获取(系统打开方式调用)
if (filePath.isEmpty() && intent.data != null) {
filePath = UriFileResolver.getFilePathFromUri(this, intent.data!!)
if (fileName.isEmpty()) {
// 从文件路径中提取文件名
fileName = File(filePath).name
}
}
/**
* Uri 文件路径解析工具
*
* 设计原则:
* - 不根据系统版本做假设
* - 能直接获取真实路径就直接用
* - 获取不到再复制到 App 私有缓存目录
*
* 适用于:
* - 系统“打开方式”
* - 第三方文件管理器
* - 应用内 FileProvider
*/
object UriFileResolver {
/**
* 从 Uri 获取一个可用的文件路径
*
* @return 文件路径,失败返回空字符串
*/
fun getFilePathFromUri(context: Context, uri: Uri): String {
return when (uri.scheme) {
ContentResolver.SCHEME_FILE -> {
uri.path ?: ""
}
ContentResolver.SCHEME_CONTENT -> {
try {
// 1️⃣ 自家 FileProvider,直接还原真实路径(零拷贝)
if (isOwnFileProvider(context, uri)) {
resolveFromFileProvider(context, uri)?.let {
return it
}
}
// 2️⃣ 尝试通过 MediaStore 获取真实路径(不做版本假设)
val mediaPath = getFilePathFromMediaStore(context, uri)
if (mediaPath.isNotEmpty()) {
return mediaPath
}
// 3️⃣ 拿不到路径,复制到缓存目录兜底
copyUriToTempFile(context, uri)
} catch (e: Exception) {
""
}
}
else -> ""
}
}
// ================= FileProvider =================
private fun isOwnFileProvider(context: Context, uri: Uri): Boolean {
return uri.authority == "${context.packageName}.fileprovider"
}
/**
* 解析自家 FileProvider Uri
*
* content://authority/path_name/relative_path
*/
private fun resolveFromFileProvider(context: Context, uri: Uri): String? {
val segments = uri.pathSegments
if (segments.isEmpty()) return null
val root = segments[0]
val relativePath =
if (segments.size > 1)
segments.subList(1, segments.size).joinToString(File.separator)
else ""
val baseDir = when (root) {
"files" -> context.filesDir
"cache" -> context.cacheDir
"external_files" -> context.getExternalFilesDir(null)
"external_cache" -> context.externalCacheDir
else -> null
} ?: return null
return if (relativePath.isNotEmpty()) {
File(baseDir, relativePath).absolutePath
} else {
baseDir.absolutePath
}
}
// ================= MediaStore =================
/**
* 尝试从 MediaStore 查询真实文件路径
*
* 注意:
* - 高版本系统上不保证一定成功
* - 能成功就直接用,失败交给兜底方案
*/
private fun getFilePathFromMediaStore(context: Context, uri: Uri): String {
var cursor: Cursor? = null
return try {
cursor = context.contentResolver.query(
uri,
arrayOf(MediaStore.Files.FileColumns.DATA),
null,
null,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)
if (index >= 0) cursor.getString(index) ?: "" else ""
} else {
""
}
} catch (e: Exception) {
""
} finally {
cursor?.close()
}
}
// ================= Copy =================
/**
* 将 Uri 指向的文件复制到 App 缓存目录
*/
private fun copyUriToTempFile(context: Context, uri: Uri): String {
return try {
val tempDir = File(context.cacheDir, "temp_files")
if (!tempDir.exists()) {
tempDir.mkdirs()
}
var fileName = getFileNameFromUri(context, uri)
if (fileName.isEmpty()) {
fileName = "temp_${System.currentTimeMillis()}"
}
val tempFile = File(tempDir, fileName)
if (tempFile.exists()) {
return tempFile.absolutePath
}
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
tempFile.absolutePath
} catch (e: Exception) {
""
}
}
// ================= File name =================
private fun getFileNameFromUri(context: Context, uri: Uri): String {
var fileName = ""
try {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val index =
cursor.getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME)
if (index >= 0) {
fileName = cursor.getString(index) ?: ""
}
}
}
if (fileName.isEmpty()) {
uri.path?.let {
fileName = it.substringAfterLast('/')
}
}
} catch (e: Exception) {
}
return fileName
}
}
954

被折叠的 条评论
为什么被折叠?



