第3章:AndroidManifest权限声明

3.1 权限声明原理

Android应用在访问受限系统资源或硬件设备时,必须在AndroidManifest.xml中声明所需权限。友盟SDK依赖多种权限来采集设备标识、网络状态与本地数据,以便进行用户行为统计、应用错误监控及消息推送。权限声明分为两类:正常权限(Normal Permissions) 在安装时自动授予,无需运行时请求;危险权限(Dangerous Permissions) 必须在运行时动态申请,用户可随时撤销。

权限设计原则:仅在功能确实需要时才声明权限,避免过度索取导致应用被用户拒绝或应用商店下架。友盟SDK最小权限集仅包含网络与WiFi状态,读写存储、蓝牙、OAID等为增强功能可选权限。

3.2 权限声明清单与功能对照

下表列出友盟SDK常用权限及其对应的启用场景:

权限名称声明常量所属层级友盟功能依赖是否必须
网络状态ACCESS_NETWORK_STATE正常检查网络可用性,决定是否发起埋点上报是
WiFi状态ACCESS_WIFI_STATE正常获取WiFi MAC地址(Android 10+已废弃)、SSID否
网络通信INTERNET正常允许应用联网发送统计数据与日志是
读写存储READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE危险缓存SDK本地日志,崩溃上传时读取缓存文件否(Android 11+推荐使用getExternalFilesDir替代)
蓝牙扫描BLUETOOTH / BLUETOOTH_SCAN(Android 12+)危险采集蓝牙设备信息用于设备指纹识别否
OAID 读取READ_PHONE_STATE(部分厂商)危险获取OAID替代IMEI做设备唯一标识否

3.3 AndroidManifest配置实现

在 app/src/main/AndroidManifest.xml 的 <manifest> 节点内添加以下声明。注意 Android 12(API 31)之后的权限拆分变更:

<!-- 必需权限:网络与WiFi状态 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<!-- 可选权限:读写存储 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

<!-- 可选权限:蓝牙(Android 12+ 使用细粒度权限) -->
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
<!-- Android 12+ 蓝牙扫描与连接 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- 可选权限:读取设备状态(用于OAID回退) -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"
    android:maxSdkVersion="32" />
常见错误:WRITE_EXTERNAL_STORAGE 在 Android 10+ 已废弃,若 your targetSdkVersion >= 29 却仍声明该权限,可能导致 Google Play 审核警告。建议使用 android:maxSdkVersion="29" 限定范围。Android 11+ 的存储机制已改为分区存储,SDK 日志应写入应用专属目录。

3.4 友盟特有权限说明

除通用系统权限外,友盟 SDK 依赖两个特有权限机制:

  • OAID 采集权限:友盟通过移动安全联盟(MSA)提供的 SDK 获取 OAID。无需在 AndroidManifest 额外声明,但需要在 Application 初始化时传入 OAID 提供者类名。若未配置,SDK 将自动回退到 IMEI(需 READ_PHONE_STATE 权限)或 AndroidID。
  • 本地日志读取:友盟会定期扫描 Android/data/包名/files/umeng_cache/ 目录下的日志文件。无需显式存储权限,但必须保证 WRITE_EXTERNAL_STORAGE 或分区存储适配正确,否则可能无法写入本地缓存。
// 友盟初始化时配置 OAID 提供者(示例)
UMConfigure.setOaidProvider(new OaidProvider() {
    @Override
    public void getOAid(Context context, IOAIDCallback callback) {
        // 调用 MSA SDK 获取 OAID
        MSAOaidHelper.getOAid(context, callback);
    }
});

3.5 运行时动态申请危险权限

对于 READ_EXTERNAL_STORAGE、BLUETOOTH_SCAN 或 READ_PHONE_STATE 等危险权限,在 Android 6.0+(API 23+)必须使用 ActivityCompat.requestPermissions() 在代码中触发授权弹窗。推荐在 Compose 界面中使用 rememberLauncherForActivityResult 结合 RequestPermission() API 实现:

@Composable
fun RequestStoragePermission(
    onGranted: () -> Unit,
    onDenied: () -> Unit
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (isGranted) onGranted() else onDenied()
    }

    // 在 Composition 中触发请求
    LaunchedEffect(Unit) {
        launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
    }
}

在 Activity.onCreate() 或 onResume() 中调用上述可组合项,确保用户授权后才能开启友盟的本地日志缓存功能。

3.6 权限声明流程图

下图展示了权限声明到运行时请求的完整链路:

图 3-1 权限声明与运行时请求链路 Manifest 静态声明 INTERNET / NETWORK_STATE 危险权限? 自动授权 安装时通过 运行时动态请求 requestPermissions() 授权 结果 开启友盟功能 埋点上报 / 日志写入 / 蓝牙扫描

3.7 调试与验证

完成声明后,可以通过以下方法验证权限是否生效:

  • adb 查看已安装应用权限:adb shell dumpsys package 你的包名 | grep "permission" 可列出所有声明过的权限。
  • 运行时日志:过滤 UMENG 标签,若看到 No permission to access network state 表示缺失 ACCESS_NETWORK_STATE。
  • 测试环境:在 Android 模拟器中手动撤销危险权限(设置 → 应用 → 权限管理),观察友盟功能是否退化且不崩溃。
部署建议:发布到应用市场前,使用 Android Studio > Analyze > Inspect Code 检查权限声明是否与 uses-sdk 版本兼容。针对 Android 12+ 设备务必添加 BLUETOOTH_SCAN 与 BLUETOOTH_CONNECT,否则蓝牙相关功能会在运行时静默失败。