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

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

就是将你的 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
    }
}
Android应用中,如果你希望你的应用能够自动成为某个数据类型的默认打开应用,你可以通过以下步骤来实现: 1. **注册意图过滤器** (Intent Filter):在你的应用主Activity的`<activity>`标签中添加`<intent-filter>`元素,声明你想要处理的数据类型。例如,如果你想让应用成为文本文件的默认打开应用,可以指定`action="android.intent.action.VIEW"`,并加上`dataScheme`属性指定你的app能解析的URL模式。 ```xml <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:scheme="your-scheme" /> </intent-filter> ``` 2. **在AndroidManifest.xml中设置**:确保在`<application>`标签下添加`android:allowBackup="true"`,这是为了让系统允许其他应用将你的应用设为默认。 3. **提供Content Provider** (如果适用):如果你的应用处理的是文件或者其他内容资源,可能需要创建Content Provider以便其他应用可以访问。 4. **获取系统的请求权限**:调用`startActivities()`方法启动一个活动,并传递包含意图的信息,当用户选择你的应用作为默认时,系统会询问用户授予这个权限。 5. **处理ACTION_DEFAULT_CHANGED intent**:监听`ACTION_DEFAULT_CHANGED`广播接收器,当用户更改默认应用时,你的应用可以接收到通知并做相应的处理。 记住,不是所有的数据类型都能直接设置默认应用,一些系统级的行为比如联系人管理、短信等通常是由系统内置的应用负责的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值