【AndroidAPP】权限被拒绝:[android.permission.READ_EXTERNAL_STORAGE],USB设备访问权限系统报错
一、问题原因
1.安卓安全性变更
Android 12+ 的安全性变更,Google 引入了更严格的 PendingIntent 安全管理,强制要求开发者明确指定 PendingIntent 的可变性(Mutable)或不可变性(Immutable)。
但是,在从 Android 14+ (API 34) 开始,FLAG_MUTABLE 和隐式 Intent 的组合会被禁止。因此,在使用静态的广播请求的时候,FLAG_MUTABLE多余,且违反安全规则。
2.关键点
关键在于安卓 14+ 版本的安全策略变化,导致无法再继续使用 FLAG_MUTABLE。
二、解决办法
1.代码示例
先给出代码示例,供大家参考,然后解释关键点在哪。
MainActivity.kt(Kotlin class)代码示例:
package com.example.serialportdebugapp
import android.app.PendingIntent
import android.content.Intent
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var statusTextView: TextView
private lateinit var logTextView: TextView
private lateinit var checkPortButton: Button
private lateinit var usbManager: UsbManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化视图
statusTextView = findViewById(R.id.statusTextView)
logTextView = findViewById(R.id.logTextView)
checkPortButton = findViewById(R.id.checkPortButton)
// 获取 USB 管理器
usbManager = getSystemService(USB_SERVICE) as UsbManager
// 按钮点击事件
checkPortButton.setOnClickListener {
detectUsbDevices()
}
}
/**
* 检测 USB 设备
*/
private fun detectUsbDevices() {
val deviceList = usbManager.deviceList
if (deviceList.isEmpty()) {
logTextView.append("No USB devices found\n")
return
}
for ((_, device) in deviceList) {
logTextView.append("Detected device: ${device.deviceName}\n")
requestPermission(device)
}
}
/**
* 请求 USB 权限
*/
private fun requestPermission(device: UsbDevice) {
val intent = PendingIntent.getBroadcast(
this, 0, Intent("com.android.example.USB_PERMISSION"),
PendingIntent.FLAG_IMMUTABLE
)
usbManager.requestPermission(device, intent)
}
}
UsbBroadcastReceiver.kt(Kotlin class)代码示例:
package com.example.serialportdebugapp
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.util.Log
class UsbBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == "com.android.example.USB_PERMISSION") {
synchronized(this) {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
device?.let {
Log.d("UsbBroadcastReceiver", "Permission granted for device: ${device.deviceName}")
}
} else {
Log.d("UsbBroadcastReceiver", "Permission denied for device: ${device?.deviceName}")
}
}
}
}
}
AndroidManifest.xml 代码示例(总配置文件)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.usb.host" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SerialPortDebugApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 广播接收器,处理 USB 权限 -->
<receiver android:name=".UsbBroadcastReceiver" android:exported="false">
<intent-filter>
<action android:name="com.android.example.USB_PERMISSION" />
</intent-filter>
</receiver>
</application>
</manifest>
build.gradle.kts(:app) 相关依赖代码示例
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.serialportdebugapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.serialportdebugapp"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.purejavacomm)
}
libs.versions.toml 代码示例:
[versions]
agp = "8.7.2"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
appcompat = "1.7.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
purejavacomm = { group = "com.github.purejavacomm", name = "purejavacomm", version = "1.0.2.RELEASE" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
activity_main.xml 代码示例 (APP界面,UI,用于测试USB串口调试APP的界面UI设计)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/statusTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="USB Status: Not connected"
android:textSize="18sp" />
<TextView
android:id="@+id/logTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log output:\n"
android:padding="8dp"
android:scrollbars="vertical" />
<Button
android:id="@+id/checkPortButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Check USB Devices" />
</LinearLayout>
2.关键点
关键点在于,MainActivity.kt 中的 这段代码:
/**
* 请求 USB 权限
*/
private fun requestPermission(device: UsbDevice) {
val intent = PendingIntent.getBroadcast(
this, 0, Intent("com.android.example.USB_PERMISSION"),
PendingIntent.FLAG_IMMUTABLE
)
usbManager.requestPermission(device, intent)
}
能够看到,在使用 PendingIntent 的时候,我们告知了“FLAG_IMMUTABLE”,这是关键,回到文章最开始的时候我们说到过的:
这是 Android 14+ 的特性,我们只能使用 FLAG_IMMUTABLE,也就是不可变的 PendingIntent。
只要这个写对了,权限被拒绝:[android.permission.READ_EXTERNAL_STORAGE] 的 BUG基本就会被解决。
3.关于 Intent 和 PendingIntent:
Intent 是 Android 中的一种消息对象,用于描述应用程序要执行的操作。
作用是用来启动 活动(Activity)、服务(Service) 或 广播(Broadcast)。
可以携带数据,以便被启动的组件可以接收到并使用这些数据。
分为显示和隐式:
常见的 Intent 的用途:
PendingIntent 是一种特殊类型的 Intent,可以在 未来 的某个时间由系统或其他应用触发。
它充当一个 “授权”,允许其他应用或系统在您的应用上下文中执行操作。
通常用于将操作 “延迟执行”,而不是立即执行。
适用于一些 异步场景,例如:通知(Notification)的点击事件。定时任务。广播接收器。
说白了,它就好像嵌入式和VUE中的 “监听”,是用来等待消息的,而不是主动出击。
而且,重要的是:Intent 是瞬发的,使用后就销毁。
但是 PendingIntent 是持续的,会一直存在到 被触发、被取消 为止。
Intent 和 PendingIntent 的对比
PendingIntent 的 3 种类型