各个版本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权限在各个平台的不一致性给我们带来了不少麻烦. 我在实践过程中一一做了封装, 方便了查询权限与申请权限. 希望本文也能帮到需要请求权限的你.

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui