2. 创建 Android 项目并配置蓝牙权限
本章将从零搭建 BluetoothStorageDemo 项目骨架,完成 AndroidManifest 蓝牙权限声明和运行时权限请求代码的编写。蓝牙权限是后续所有扫描、连接、数据传输操作的前提,错误的权限配置将直接导致功能不可用或应用崩溃。本章内容严格区分 Android 12(API 31)前后的权限模型差异,确保项目适配主流机型。
2.1 项目结构概览
项目采用单 Activity 架构,核心文件及职责如下表所示:
| 路径 / 文件 | 类型 | 职责 |
|---|---|---|
app/src/main/java/com/example/bluetoothstorage/ |
源码根目录 | 存放所有 Kotlin 源文件 |
MainActivity.kt |
Activity | 主入口,处理权限请求与蓝牙初始化 |
BluetoothPermissionHelper.kt |
工具类 | 封装权限检查、请求及回调逻辑 |
app/src/main/AndroidManifest.xml |
清单文件 | 声明蓝牙相关权限与组件 |
app/build.gradle.kts |
构建配置 | 指定 compileSdk、minSdk、targetSdk |
app/src/main/res/values/strings.xml |
资源文件 | 权限弹窗提示文案 |
项目基调: compileSdk = 34,minSdk = 24,targetSdk = 34。minSdk 设为 24 可覆盖 Android 7.0 以上设备,同时确保运行时权限 API 完整可用。
2.2 AndroidManifest 蓝牙权限声明
蓝牙权限声明需要根据目标设备的 API 级别分别配置。以下代码同时兼容 Android 12(API 31)以上和以下设备:
<!-- AndroidManifest.xml 中声明蓝牙相关权限 -->
<!-- ========== 通用权限(所有版本) ========== -->
<!-- 访问粗略位置(蓝牙扫描需要,Android 10+ 可用 coarse 替代 fine) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />
<!-- ========== Android 12+ 细粒度蓝牙权限 ========== -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- ========== Android 12 以下 传统蓝牙权限 ========== -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<!-- 声明硬件特性(非必需,但有助于过滤不支持的设备) -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
权限版本差异对照表:
| 权限名称 | 适用 API 范围 | 用途 | 是否运行时请求 |
|---|---|---|---|
BLUETOOTH |
≤ 30 | 基础蓝牙通信(连接、数据传输) | 否 |
BLUETOOTH_ADMIN |
≤ 30 | 蓝牙管理(扫描、开关) | 否 |
BLUETOOTH_SCAN |
≥ 31 | 扫描蓝牙设备 | 是 |
BLUETOOTH_CONNECT |
≥ 31 | 连接已配对的蓝牙设备 | 是 |
BLUETOOTH_ADVERTISE |
≥ 31 | 蓝牙广播(外设模式) | 是 |
ACCESS_FINE_LOCATION |
≤ 30 | 蓝牙扫描需位置权限(Android 10-12 强制) | 是 |
ACCESS_COARSE_LOCATION |
≤ 30 | 替代 FINE_LOCATION(Android 10+ 可用) | 是 |
关键提醒: Android 12 以上BLUETOOTH_SCAN权限已不再隐含位置权限,但部分厂商定制系统仍可能要求位置权限。建议在运行时同时检查BLUETOOTH_CONNECT和BLUETOOTH_SCAN,并在 API 30 以下请求ACCESS_FINE_LOCATION。
2.3 运行时权限请求代码
使用 registerForActivityResult + RequestMultiplePermissions 实现权限请求,避免已废弃的 onRequestPermissionsResult 回调分散逻辑。
2.3.1 权限检查与请求封装
// BluetoothPermissionHelper.kt
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
object BluetoothPermissionHelper {
/**
* 获取当前设备需要的蓝牙权限列表
*/
fun getRequiredPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
// Android 12 以下
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
}
/**
* 检查所有蓝牙权限是否已授予
*/
fun hasBluetoothPermissions(context: Context): Boolean {
return getRequiredPermissions().all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 获取未授予的权限列表
*/
fun getDeniedPermissions(context: Context): List<String> {
return getRequiredPermissions().filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
}
}
2.3.2 在 Activity 中发起请求
// MainActivity.kt 关键片段
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// 权限请求 launcher
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { grantResults ->
val allGranted = grantResults.values.all { it }
if (allGranted) {
onPermissionGranted()
} else {
val deniedList = grantResults.filter { !it.value }.keys.joinToString(", ")
Toast.makeText(this, "权限被拒绝: $deniedList", Toast.LENGTH_LONG).show()
onPermissionDenied()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 启动时检查权限
checkAndRequestPermissions()
}
private fun checkAndRequestPermissions() {
if (BluetoothPermissionHelper.hasBluetoothPermissions(this)) {
onPermissionGranted()
} else {
val denied = BluetoothPermissionHelper.getDeniedPermissions(this)
// 可在此处添加 shouldShowRequestPermissionRationale 判断
permissionLauncher.launch(denied.toTypedArray())
}
}
private fun onPermissionGranted() {
// 权限已授予 → 初始化蓝牙、加载已保存设备
Toast.makeText(this, "蓝牙权限已就绪", Toast.LENGTH_SHORT).show()
// 后续章节将在此处接入蓝牙扫描与 SharedPreferences 存储
}
private fun onPermissionDenied() {
// 权限被拒绝 → 提示用户手动开启
Toast.makeText(this, "蓝牙功能将不可用,请在设置中授予权限", Toast.LENGTH_LONG).show()
}
}
2.3.3 可选的 Rationale 提示
当用户之前拒绝过权限时,应显示解释说明:
// 在 checkAndRequestPermissions 中增加 Rationale 判断
private fun checkAndRequestPermissions() {
if (BluetoothPermissionHelper.hasBluetoothPermissions(this)) {
onPermissionGranted()
} else {
val denied = BluetoothPermissionHelper.getDeniedPermissions(this)
val shouldShowRationale = denied.any {
shouldShowRequestPermissionRationale(it)
}
if (shouldShowRationale) {
// 显示弹窗解释为什么需要权限
showRationaleDialog(denied)
} else {
permissionLauncher.launch(denied.toTypedArray())
}
}
}
private fun showRationaleDialog(permissions: Array<String>) {
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("需要蓝牙权限")
.setMessage("本应用需要通过蓝牙扫描和连接设备,以存储和同步数据。")
.setPositiveButton("授予权限") { _, _ ->
permissionLauncher.launch(permissions)
}
.setNegativeButton("拒绝") { _, _ ->
onPermissionDenied()
}
.show()
}
2.4 权限请求流程总览
下方 SVG 流程图展示了从权限检查到最终决策的完整路径:
2.5 常见错误与最佳实践
- 权限声明遗漏:未在 AndroidManifest 中声明
BLUETOOTH_CONNECT会导致请求时系统直接返回拒绝,且不弹对话框。 - SDK 版本判断错误:在 API 31 设备上仍请求
ACCESS_FINE_LOCATION会导致权限冗余弹窗,建议按Build.VERSION.SDK_INT >= Build.VERSION_CODES.S分支处理。 - 权限被永久拒绝:用户连续两次拒绝后,系统不再弹窗。此时应引导用户进入「设置 → 应用 → 权限」手动开启。
- shouldShowRequestPermissionRationale 误判:首次请求前调用会返回 false,第一次拒绝后才变为 true,不要将 false 等同于「不需要解释」。
最佳实践: 权限请求结果通过registerForActivityResult回调统一处理,避免在onResume中反复检查。权限就绪后使用SharedPreferences缓存一次授权状态,减少重复检查开销(后续章节将实现)。
2.6 调试与验证
完成上述代码后,按以下步骤验证权限配置是否正确:
- 首次安装启动:应弹出系统权限对话框,选择「允许」后看到 Toast "蓝牙权限已就绪"。
- 拒绝权限后重试:清除应用数据,再次启动并拒绝,应显示 "权限被拒绝" 提示,且后续不再次弹窗。
- adb 命令快速测试:
# 授予所有蓝牙权限 adb shell pm grant com.example.bluetoothstorage android.permission.BLUETOOTH_SCAN adb shell pm grant com.example.bluetoothstorage android.permission.BLUETOOTH_CONNECT # 撤销权限模拟拒绝场景 adb shell pm revoke com.example.bluetoothstorage android.permission.BLUETOOTH_SCAN - 日志过滤:在 Logcat 中搜索
Permission或BluetoothPermission标签,确认权限请求生命周期完整。
注意: 使用模拟器测试蓝牙权限时,部分 API 31+ 模拟器镜像可能不包含蓝牙硬件,导致 BLUETOOTH_SCAN 请求无响应。建议在真机或带有蓝牙支持的模拟器镜像上进行验证。
至此,项目已具备完整的蓝牙权限基础设施。从第 3 章开始,将基于已获得的权限实现蓝牙设备扫描并利用 SharedPreferences 持久化设备信息。