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
    }
}
相关推荐
百锦再2 小时前
京东云鼎入驻方案解读——通往协同的“高架桥”与“快速路”
android·java·python·rust·django·restful·京东云
成都大菠萝2 小时前
1-2-3 Kotlin与C++基础-JNI原理与使用
android
TimeFine3 小时前
Android AI解放生产力(六)实战:解放页面开发前的繁琐工作
android·架构
心静财富之门3 小时前
a.py打包加密
android
成都大菠萝3 小时前
1-2-2 Kotlin与C++基础-C++基础语法与内存管理
android
陈希瑞3 小时前
【保姆级教程】安卓手机免Root一键部署AutoGLM:支持语音控制与自动化操作
android·智能手机·自动化
TheNextByte13 小时前
如何将联系人从Android传输到计算机的 6 种方法
android
喂_balabala3 小时前
excludeFromRecents
android
TimeFine4 小时前
Android AI解放生产力(五)实战:解放写API接口的繁琐工作
android