Android将应用添加到默认打开方式

文章目录

一、首先你需要先看到效果

就是将你的 activity 添加到打开方式,比如我这里有两个 activity,PdfViewerActivity 负责打开 pdf 文件,OfficeViewerActivity 负责打开 word,excel,ppt 文件

xml 复制代码
<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 配置完成后,如果调起了系统打开方式,系统会这样发送数据

kotlin 复制代码
Intent {
  action = ACTION_VIEW
  data   = content://xxx/xxx //代表文件的 uri
  type   = application/pdf //代表文件类型
}

二、两种方式

自己伪装成系统系统打开方式发送数据

kotlin 复制代码
// 把 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)

有什么区别:

kotlin 复制代码
Intent {
  action = android.intent.action.VIEW
  data   = content://your.package.fileprovider/...
  type   = application/pdf
}
kotlin 复制代码
Intent {
  action = android.intent.action.VIEW
  data   = content://com.android.providers.downloads.documents/document/1234
  type   = application/pdf
}

可以这样判断:

kotlin 复制代码
uri.authority == "${context.packageName}.fileprovider"

三、接收数据

在相应的页面接收数据:

kotlin 复制代码
val uri: Uri = intent.data ?: return
val inputStream = contentResolver.openInputStream(uri)

如果你必须要使用文件真实路径而不用 uri,可通过 uri 复制文件到一个目录得到:

kotlin 复制代码
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
}

获取文件名:

kotlin 复制代码
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
}

三、工具类

kotlin 复制代码
// 获取传入的文件路径和文件名
// 优先从 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
    }
}
kotlin 复制代码
/**
 * 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
    }
}
相关推荐
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android