各个版本Android上的Location与Notification权限的问题

一. 前言

最近在搞这一块的东西, 要求用户给我们app以位置与通知的权限. 尤其是位置, 这样我们可以根据用户位置, 推荐用户旁边的店铺之类的. 既然要推荐, 那自然用户不可能一直开着我们app, 所以我们请求用户给予我们app在后台也能请求location的权限, 以及得到了位置后, 我们要能发通知给用户, 告诉用户旁边有好店好物. 总之, 我们就是要请求用户给我们:

  • precise (也叫fine)的location权限
  • background的location权限
  • notification权限

当我开始动手做时, 我就发现了麻烦的地方了, 也就是每个Android版本的权限不一样, 这给我的代码编写带来了不少麻烦. 我一一克服后, 也就在此分享下, 希望能帮到有需要的同学.

二. 各个Android版本的Loation权限的变化

《android定位权限适配看这篇就够了》其实讲得不错, 讲了各个版本的情况, 但这文章目前看来有几个缺陷:

  • 成文较早, 只覆盖到Android 11. 而Android 12上location权限有又变化, 此文因此未能覆盖到Android 12+的情况
  • 代码较少. 当涉及到如何读取是否有权限 , 以及如何申请权限, 都没有涉及到.

所以我要针对这些问题加以一定的修正, 并做一些补强, 也就是:

  • 对于已经有部分权限, 再申请全权限时, 会发生什么? 我也想讲清楚. 这样就能覆盖更多的测试用例/使用场景.
  • 使用更多对比型的图片, 这样让大家更容易理解各个版本间的区别.

2.1 各个版本的变化

备注: 下方出现的launcher, 是指ActivityResultLauncher, 即这样的:

kotlin 复制代码
    private lateinit var launcher : ActivityResultLauncher<Array<String>>

    launcher = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
        // .....
    }
  • Android 9: 位置相关的权限只有一个就是location权限

  • Android 10: 新加了一个background_location权限. 分别表示app在前台与后台时能允许定位的权限

    • Android 10还有一个特点, 就是它能允许app请求background_location权限, 即launcher.launch(fine_location, bg_location)
  • Android 11: app不能同时请求location与bg_location权限了. 这样的请求会啥都不出现, 自然launcher也不会有任何结果

    • Android 11只能在请求Location成功后, 再请求bg_location权限. 而同时声明location + bg_location, 不会有任何反应的.
  • Android 12 : 普通的location权限再次被细分成了precise(也叫fine)与approximate(也叫coarse)权限. 这样一来, 我们的location相关的权限就有三种了: precise, coarse, bg.

2.2 图示

其实打开任一个app的app info, 即它们的setting页, 然后再找到location相关权限的那一页, 就能看出各个版本的区别:

说明

Android10+上的:

  • "Allow all the time": 其实就是说有了precise/coarse location权限, 并再给予bg_location权限:
  • "Allow while using": 其实就是说 只有precise/coarse location权限, 并没有bg_location权限

三. 如何查询Location权限?

我们查询一个权限, 可以用下面的ext方法:

kotlin 复制代码
fun Context.isPermissionGranted(permission: String): Boolean =
    ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED

但每个Android版本都不一样啊, 所以我就有了两个大疑问:

1). 既然fine_location在Android 12+才出现; 那我们在Android 11-的设置上运行 ctx.isPermissionGranted(fine_loc)能拿到正确的值吗?
2). 既然bg_location是Android10之后才有的权限; 那在Android 9-的设备上运行 ctx.isPermissionGranted(bg_loc)是否会有结果呢? 会有什么样的结果 呢?

多说无益, 在各个版本上运行下代码来看下结果:

kotlin 复制代码
    val isFineLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
    val isCoarseLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
    val isBackgroundLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
    println("fine = $isFineLocationGranted, coarse = $isCoarseLocationGranted, bg = $isBackgroundLocationGranted")    

图示

总结成一个图那就是:

这里要注意几点哦:

  • 因为Android 9-并没有限制后台请求定位的权限. 所以其实只要打开了location权限, 就相当于后台也能请求定位了. 这时我们预期的结果应该是 fine == coarse == bg.
    • 但结果不是这样的. 结果是fine == corase == true, bg = false, 或是fine == coarse == false, bg = false.

总之就是, 在Android 9-上, 虽然可能已经有了bg定位的能力了, 但去请求BACKGROUND_LOCATION权限给予了否, 结果总是false. 这是个坑, 请注意哦!

  • 另外就是在Android 11-的设备上, fine权限始终和coarse权限是一样的. 都能真实反应用户是否给予了普通定位权限, 可以放心用.

代码实践

kotlin 复制代码
fun Context.isPreciseLocationGranted() =
    this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)

// Android 12-的普通定位权限, 并没有fine与coarse之分的; coarse等同于fine权限 
fun Context.isCoarseLocationGranted() : Boolean {    
    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        return this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
    } else {
        return this.isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
    }
}

fun Context.isBackgroundLocationGranted() : Boolean{
    // Android 9-的bg_location权限, 在请求时总是返回false; 所以这里要用普通定位来得到其真实的值
    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
        return this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
    } else {
        return this.isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
    }
}

四, 如何申请location权限

申请权限并不难, 我们可以用Androidx的ActivityResult的新API来申请权限

kotlin 复制代码
// 声明与注册callback
    private lateinit var launcher : ActivityResultLauncher<Array<String>>

    launcher = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
        // .....
    }
 
 // 申请权限
    launcher.launch(arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
    ))
    
    //或是申请单一权限:
    launcher.launch(arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
    ))

但麻烦其实是隐藏在不同版本的不同行为中的. 而且没有权限时去申请, 以及有了部分权限再去申请也不一样.

4.1 无任何权限时去请求

这个用文字说不太明显了, 来看下图吧, 对比性一眼就看出来了.

(图太大了, 只好分成几个图, sorry)

说明下:

  • 图中的"三权限"就是指全权限, 也就是: ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION + ACCESS_BACKGROUND_LOCATION

  • Android 11+就不允许用户同时请求普通定位与bg定位权限了. 所以下面的代码, 在Android11+上不会出现权限申请dialog, 是啥都不会发生.

kotlin 复制代码
launcher.launch(arrayOf(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.ACCESS_BACKGROUND_LOCATION
))
  • 但是在Android 11+的机器上, 若已经有了普通定位权限, 再申请bg是可以的. 这个具体的结果请见下面.

4.2 有部分权限时再去请求

仍是用图说话

这里的区别其实在上面讲过, 就已经很容易理解了.

多说一句那就是在Android 11+中, 若真的想一个按钮点击就得到所有location权限, 可以考虑这样:

kotlin 复制代码
private lateinit var launcher2 : ActivityResultLauncher<Array<String>>

// callback中判断是否是是普通权限已经到手了. 
launcher2 = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
    val isForegroundLocations = map.keys == setOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,)
    val isForegroundLocationsGranted = map.values.contains(true) //任一权限有了, 就可以去请求bg权限了
    if(isForegroundLocations && isForegroundLocationsGranted) {
        launcher2.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION))
    }
}

// 点击就申请权限
vb.btn.text = "And11+上想一个按钮就请求全权限?"
vb.btn.setOnClickListener {
    launcher2.launch(arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
    ))
}

4.3 做下封装

我们app的需求是:

  • 没有precise权限就申请precise的location权限
  • 若有precise权限那就是打开OS Setting页, 让用户来选择"Allow all the time"

其实实现这需求有点小麻烦, 主要是各个版本的Android上行为不一样.

特别是Android10可以允许请求后台定位权限, 但Android11+又不让.

于是我们小小封装一下:

kotlin 复制代码
fun computeNecessaryLocationPermissions(): Array<String> {
    // 1). Android 11+ (R+), background_location 权限不能被直接请求
    // 2). Android 12+, 最好是连着Fine & Coarse一起请求. (这一点也不影响Android 11系统)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        return arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
    }
    // Android 10(Q), background_location permissio是可以被app请求的
    else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
        return arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION)
    }
    // Android 9-, 没有background_location权限, 直接申请 Fine_location即可
    else {
        return arrayOf(ACCESS_FINE_LOCATION)
    }
}

然后我们的请求申请权限的按钮点击后是这样的:

kotlin 复制代码
    val isBackgroundLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
    val isCoarseLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
    val isFineLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)


    if(isFineLocationGranted && !isBackgroundLocationGranted) {
        context.openOsSetting()
    } else {
        launcher.launch(computeNecessaryLocationPermissions())
    }


fun Context.openOsSetting() {
    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    intent.data = Uri.fromParts("package", packageName, null)
    startActivity(intent)
}

fun Context.isPermissionGranted(permission: String): Boolean =
    ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
    

五. 各个Android版本的Notification权限的变化

上面讲完了Location权限. 现在轮到了Notification权限. 其实通知权限也有类似问题, 只不过没这么复杂.

  • Android 12-是没有POST_NOTIFICATION权限的.
    • 新安装的app, 其通知权限默认是打开的
  • Android 13+才新加了POST_NOTIFICATION权限的
    • 新安装的app, 其通知权限默认是关闭的

既然Manifest.permission.POST_NOTIFICATIONS 是 Android 13 (api level 33, TIRAMISU)里才有的新权限, 那在旧版本上查询与申请这个POST_NOTIFICATION权限, 会能用吗?

5.1 查询

经过我的实践, 在任意平台, 都可以查询这个结果, 而且结果都是正确的.

  • 即你开了通知权限, 查询到的结果就是true (给了权限) ;
  • 若你去设置页面关了通知权限, 那查询到的结果就是false
kotlin 复制代码
    private lateinit var singlePermissionLauncher : ActivityResultLauncher<String>

    singlePermissionLauncher = registerForActivityResult(RequestPermission()) {
        // ....
    }
   
   //申请权限
   singlePermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)

5.2 申请权限

这里才是个坑. 尽管如上所说, 你在全平台, 查询POST_NOTIFICATIONS权限都没问题, 而且结果都是正确的.

但你想在Android 12-上申请POST_NOTIFICATIONS权限, 那就啥反应都没有. 而且你的launcher的callback也不会被调用!!

在Android 12-要想打开通知权限, 就只能去OS Setting页了.

总结一下, 申请通知权限的代码就是:

kotlin 复制代码
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        singlePermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
    } else {
        this.openNotificationOsSetting()
    }

其中的openNotificationOsSetting函数是个扩展函数, 内容是:

kotlin 复制代码
fun Activity.openNotificationOsSetting() {
    try {
        val aIntent = Intent()
        aIntent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

        //8.0及以后版本使用这两个extra.  >=API 26
        aIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
        aIntent.putExtra(Settings.EXTRA_CHANNEL_ID, applicationInfo.uid)

        //5.0-7.1 使用这两个extra.  <= API 25, >=API 21
        aIntent.putExtra("app_package", packageName)
        aIntent.putExtra("app_uid", applicationInfo.uid)

        startActivity(aIntent)
    } catch (ex: Exception) {
        //有些系统会crash说找不到activity来接收这个intent, 所以要try-catch一下
    }
}

六. 总结

其实经过上面的描述与各种图示, 我们已经知道location与notification权限在各个平台的不一致性给我们带来了不少麻烦. 我在实践过程中一一做了封装, 方便了查询权限与申请权限. 希望本文也能帮到需要请求权限的你.

相关推荐
Yawesh_best1 小时前
MySQL(5)【数据类型 —— 字符串类型】
android·mysql·adb
曾经的三心草4 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束
guoruijun_2012_48 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood8 小时前
一文了解Android中的AudioFlinger
android·音频
一头小火烧9 小时前
flutter打包签名问题
flutter
sunly_9 小时前
Flutter:异步多线程结合
flutter
AiFlutter9 小时前
Flutter网络通信-封装Dio
flutter
B.-9 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克9 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart
大耳猫15 小时前
主动测量View的宽高
android·ui