Android 默认短信应用查询技术文档

Android 默认短信应用查询技术文档

1. 需求背景

开发一个功能,用于获取 Android 系统中当前默认短信应用的包名,并查询所有具备成为默认短信应用资格的应用列表。

2. 初始实现

2.1 基础方法

kotlin 复制代码
fun getDefaultSmsAppPackageName(context: Context): String? {
    return Telephony.Sms.getDefaultSmsPackage(context)
}

这是 Android 官方提供的 API,用于获取当前默认短信应用的包名。

3. 遇到的问题

3.1 Java 版本兼容性问题

问题:

复制代码
Android Gradle Plugin 8.5.1 requires Java 17 to run. 
You are currently using Java 11.

解决方案:

  • 降级 Android Gradle Plugin 到 7.4.2(支持 Java 11)
  • 或在 gradle.properties 中设置 org.gradle.java.home 指向 Java 17+ 的路径

3.2 PackagingOptions DSL 兼容性

问题:

复制代码
Unresolved reference: packaging

AGP 7.4.2 不支持新的 packaging {} DSL 语法。

解决方案:

kotlin 复制代码
// 旧版本语法
packagingOptions {
    resources {
        excludes += "/META-INF/{AL2.0,LGPL2.1}"
    }
}

3.3 Settings.Secure 返回 null

问题:

log 复制代码
From Settings.Secure: null
From Telephony.Sms: com.android.mms

Settings.Secure 中的 sms_default_application 键返回 null,但 Telephony.Sms API 能返回值。

原因:

  • 某些 ROM 可能没有正确保存设置到 Settings.Secure
  • 或者使用了不同的存储机制

解决方案:

同时使用两种方式,优先使用 Settings.Secure 的值:

kotlin 复制代码
fun getDefaultSmsAppPackageName(context: Context): String? {
    val fromSettings = Settings.Secure.getString(
        context.contentResolver,
        "sms_default_application"
    )
    val fromTelephony = Telephony.Sms.getDefaultSmsPackage(context)
    return fromSettings?.takeIf { it.isNotEmpty() } ?: fromTelephony
}

3.4 切换默认应用后值不更新

问题:

使用 remember { mutableStateOf(...) } 缓存了初始值,切换默认应用后显示的还是旧值。

解决方案:

使用 remember(key) 方式,当 key 改变时重新获取:

kotlin 复制代码
var refreshCounter by remember { mutableIntStateOf(0) }
val defaultSmsPackage = remember(refreshCounter) { 
    SmsUtils.getDefaultSmsAppPackageName(context) 
}

3.5 无法查询到所有短信应用 ⭐核心问题

问题:

系统设置中有 2 个短信应用(短信、测试短信),但只能查询到 1 个。

log 复制代码
具备资格的应用:
  - com.vivo.easyshare (互传)
  - com.android.mms (信息)

缺少 com.test.mms

原因分析:

通过 dumpsys package 可以看到这些应用确实注册了 sms: scheme:

复制代码
sms:
  1b41c51 com.test.mms/...ComposeSmsActivity
  1189e3d com.android.mms/.ui.ComposeMessageActivity
  8e016f1 com.vivo.easyshare/.activity.DefaultSmsActivity

queryIntentActivities 无法查询到它们。

根本原因:缺少 QUERY_ALL_PACKAGES 权限

在 Android 11(API 30)+ 中,引入了包可见性过滤。默认情况下,应用只能查询到以下内容:

  1. 自己
  2. 通过 <queries> 声明的应用
  3. 具有相同签名的应用

解决方案:

AndroidManifest.xml 中添加:

xml 复制代码
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
    tools:ignore="QueryAllPackagesPermission" />

这个权限是特殊权限,不需要用户授权,但需要在 Google Play 审核时提供正当理由。

3.6 Intent 查询方式的限制

尝试了多种 Intent 查询方式:

kotlin 复制代码
// 方式1: 使用 ACTION_VIEW + sms: scheme
Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse("sms:")
    addCategory(Intent.CATEGORY_DEFAULT)
}

// 方式2: 使用 ACTION_SENDTO + sms: scheme  
Intent(Intent.ACTION_SENDTO).apply {
    data = Uri.parse("sms:")
    addCategory(Intent.CATEGORY_DEFAULT)
}

// 方式3: 不指定 action,只用 scheme
Intent().apply {
    data = Uri.parse("sms:")
    addCategory(Intent.CATEGORY_DEFAULT)
}

即使添加了各种 category 组合,仍然无法查询到所有应用。

这说明某些应用的 intent-filter 配置可能比较特殊,或者使用了非标准的注册方式。

4. 最终实现

4.1 AndroidManifest.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- 查询所有应用的权限 (Android 11+) -->
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />

    <application
        ...>
    </application>
</manifest>

4.2 SmsUtils.kt

kotlin 复制代码
object SmsUtils {
    private const val TAG = "SmsUtils"

    /**
     * 获取当前默认短信应用的包名
     */
    fun getDefaultSmsAppPackageName(context: Context): String? {
        // 方式1: Settings.Secure
        val fromSettings = Settings.Secure.getString(
            context.contentResolver,
            "sms_default_application"
        )
        Log.d(TAG, "From Settings.Secure: $fromSettings")

        // 方式2: Telephony.Sms API
        val fromTelephony = Telephony.Sms.getDefaultSmsPackage(context)
        Log.d(TAG, "From Telephony.Sms: $fromTelephony")

        // 优先使用 Settings.Secure 的值
        val result = fromSettings?.takeIf { it.isNotEmpty() } ?: fromTelephony
        Log.d(TAG, "Final result: $result")
        return result
    }

    /**
     * 获取所有具备资格成为默认短信应用的应用列表
     */
    fun getEligibleSmsApps(context: Context): List<AppInfo> {
        val apps = mutableListOf<AppInfo>()
        val packageManager = context.packageManager
        val seenPackages = mutableSetOf<String>()

        try {
            val intents = listOf(
                Intent(Intent.ACTION_VIEW).apply {
                    data = Uri.parse("sms:")
                    addCategory(Intent.CATEGORY_DEFAULT)
                    addCategory(Intent.CATEGORY_BROWSABLE)
                },
                Intent(Intent.ACTION_SENDTO).apply {
                    data = Uri.parse("sms:")
                    addCategory(Intent.CATEGORY_DEFAULT)
                }
            )

            for (intent in intents) {
                val resolveInfos = packageManager.queryIntentActivities(
                    intent,
                    PackageManager.MATCH_DEFAULT_ONLY
                )
                for (resolveInfo in resolveInfos) {
                    val packageName = resolveInfo.activityInfo.packageName
                    if (!seenPackages.contains(packageName)) {
                        seenPackages.add(packageName)
                        val appName = resolveInfo.loadLabel(packageManager).toString()
                        apps.add(AppInfo(packageName, appName))
                    }
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error: ${e.message}")
        }

        return apps.sortedBy { it.name }
    }

    /**
     * 打开系统默认短信应用设置页面
     */
    fun openDefaultSmsAppSettings(context: Context) {
        try {
            val intent = Intent("android.settings.DEFAULT_APP_SETTINGS")
            context.startActivity(intent)
        } catch (e: Exception) {
            val intent = Intent("android.settings.SETTINGS")
            context.startActivity(intent)
        }
    }

    data class AppInfo(val packageName: String, val name: String)
}

4.3 MainActivity.kt

kotlin 复制代码
@Composable
fun SmsInfoScreen(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    var refreshCounter by remember { mutableIntStateOf(0) }
    var showDiagnosis by remember { mutableStateOf(false) }

    val defaultSmsPackage = remember(refreshCounter) { 
        SmsUtils.getDefaultSmsAppPackageName(context) 
    }
    val isDefaultApp = remember(refreshCounter) { 
        SmsUtils.isDefaultSmsApp(context) 
    }
    val diagnosis = remember(refreshCounter) { 
        SmsUtils.diagnoseDefaultSmsApp(context) 
    }

    Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
        Text("默认短信应用: ${defaultSmsPackage ?: "未设置"}")
        
        Row {
            Button(onClick = { SmsUtils.openDefaultSmsAppSettings(context) }) {
                Text("打开设置")
            }
            Button(onClick = { refreshCounter++ }) {
                Text("刷新")
            }
            Button(onClick = { showDiagnosis = !showDiagnosis }) {
                Text(if (showDiagnosis) "隐藏诊断" else "显示诊断")
            }
        }

        if (showDiagnosis) {
            Text(diagnosis, style = MaterialTheme.typography.bodySmall)
        }
    }
}

5. 关键注意事项

5.1 QUERY_ALL_PACKAGES 权限

  • 作用:允许应用查询设备上所有已安装的应用
  • 适用版本:Android 11 (API 30) 及以上
  • 授权方式:特殊权限,不需要用户运行时授权
  • Google Play 政策:使用此权限需要提供正当的使用理由

5.2 默认短信应用的条件

一个应用要成为默认短信应用,必须满足:

  1. 必需权限

    • SEND_SMS
    • RECEIVE_SMS
    • READ_SMS
    • RECEIVE_MMS
    • RECEIVE_WAP_PUSH
  2. 必需组件

    • BroadcastReceiver 处理 SMS_DELIVER action
    • BroadcastReceiver 处理 WAP_PUSH_DELIVER action
    • Service 处理 RESPOND_VIA_MESSAGE action
  3. Intent Filter

    • 处理 ACTION_SENDTOsms:smsto: scheme

5.3 调试命令

bash 复制代码
# 查看所有处理短信的应用
adb shell dumpsys package | grep -A 5 "sms:"

# 查看指定应用的权限
adb shell dumpsys package <package_name> | grep -i permission

6. 总结

  1. Android 11+ 的包可见性限制是需要特别注意的问题
  2. QUERY_ALL_PACKAGES 权限是解决查询不到应用的关键
  3. Settings.SecureTelephony.Sms 两种方式获取默认应用各有优缺点,建议结合使用
  4. remember 的使用方式会影响数据的实时更新,需要配合状态管理