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
    }
}
相关推荐
十幺卜入20 小时前
Unity3d C# 基于安卓真机调试日志抓取拓展包(Android Logcat)
android·c#·unity 安卓调试·unity 安卓模拟·unity排查问题
frontend_frank20 小时前
脱离 Electron autoUpdater:uni-app跨端更新:Windows+Android统一实现方案
android·前端·javascript·electron·uni-app
薛晓刚21 小时前
MySQL的replace使用分析
android·adb
DengDongQi21 小时前
Jetpack Compose 滚轮选择器
android
stevenzqzq21 小时前
Android Studio Logcat 基础认知
android·ide·android studio·日志
代码不停21 小时前
MySQL事务
android·数据库·mysql
朝花不迟暮21 小时前
使用Android Studio生成apk,卡在Running Gradle task ‘assembleDebug...解决方法
android·ide·android studio
yngsqq21 小时前
使用VS(.NET MAUI)开发第一个安卓APP
android·.net
Android-Flutter1 天前
android compose LazyVerticalGrid上下滚动的网格布局 使用
android·kotlin
Android-Flutter1 天前
android compose LazyHorizontalGrid水平滚动的网格布局 使用
android·kotlin