Android 兼容(三)Android 11 浅谈

适配targetSdkVersion30

[重点] 分区存储强制执行

对外部存储目录的访问仅限于应用专属目录,以及应用已创建的特定类型的媒体。

关于分区存储,在Android10就已经推行了,简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过MediaStore进行访问。

但是在android10的时候,Google还是为开发者考虑,留了一手。在targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage="true",就可以不启动分区存储,让以前的文件读取正常使用。但是targetSdkVersion = 30中不行了,强制开启分区存储。
当然,作为人性化的android,还是为开发者留了一小手,如果是覆盖安装呢,可以增加android:preserveLegacyExternalStorage="true",暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要卸载重装,就会失效了。以下是关于分区存储会遇到的所有情况,给大家罗列出来了,先上代码:

    fun saveFile() {
        if (checkPermission()) {
            //getExternalStoragePublicDirectory被弃用,分区存储开启后就不允许访问了
            val filePath = Environment.getExternalStoragePublicDirectory("").toString() + "/test3.txt"
            val fw = FileWriter(filePath)
            fw.write("hello world")
            fw.close()
            showToast("文件写入成功")
        }
    }

分情况运行:
1)targetSdkVersion = 28,运行后正常读写。
2)targetSdkVersion = 29,不删除应用,targetSdkVersion 由28修改到29,覆盖安装,运行后正常读写。
3)targetSdkVersion = 29,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied))
4) targetSdkVersion = 29,添加android:requestLegacyExternalStorage="true"(不启用分区存储),读写正常不报错
5) targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,读写报错,程序崩溃(open failed: EACCES (Permission denied))
6)targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,增加android:preserveLegacyExternalStorage="true",读写正常不报错
7)targetSdkVersion = 30,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied))

那到底应该怎么改呢?三种方法访问文件:

1)应用专属目录

//分区存储空间
val file = File(context.filesDir, filename)

//应用专属外部存储空间
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)

2)访问公共媒体目录文件

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
        println("image uri is $uri")
    }
    cursor.close()
}

3) SAF(存储访问框架--Storage Access Framework)

    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "image/*"
    startActivityForResult(intent, 100)

    @RequiresApi(Build.VERSION_CODES.KITKAT)
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (data == null || resultCode != Activity.RESULT_OK) return
        if (requestCode == 100) {
            val uri = data.data
            println("image uri is $uri")
        }
    }


具体还有很多操作可以看看网上关于分区存储的资料,因为Android10已经出来很久了,所以资料还是很多的,这里推荐几篇
访问应用专属文件
Android 10适配要点,作用域存储
AndroidQ(10)分区存储完美适配

说到这里可能又有人问了,那我的应用就是个手机管理器,总不能不让我清其他应用的缓存了吧,有办法!Android提供了两个intent入口:

调用ACTION_MANAGE_STORAGE intent 操作检查可用空间。
调用ACTION_CLEAR_APP_CACHE intent 操作清除所有缓存。

说来说去,反正应用数据私有化是大势所趋,还是早点适配分区存储,别等以后手机只有沙盒机制的时候,就来不及了。

[重点] 媒体文件访问权限

为了在保证用户隐私的同时可以更轻松地访问媒体,Android 11 增加了以下功能。执行批量操作和使用直接文件路径和原生库访问文件。

1)执行批量操作

这里的批量操作指的是Android 11 向 MediaStore API 中添加了多种方法,用于简化特定媒体文件更改流程(例如在原位置编辑照片),分别是:

createWriteRequest() 用户向应用授予对指定媒体文件组的写入访问权限的请求。
createFavoriteRequest()用户将设备上指定的媒体文件标记为“收藏”的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为“收藏”。
createTrashRequest() 用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容会在系统定义的时间段后被永久删除。
createDeleteRequest() 用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求。

例子

val urisToModify = listOf(uri,uri,...)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)


override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}    


传入uri的集合,获取用户的同意后,就可以进行操作了。

2)直接文件路径和原生库访问文件

没错!Android11又恢复了使用直接文件路径访问访问媒体文件! 这样就方便多了。也就是除了MediaStore API之外还有两种方式可以访问媒体文件:

  • File API。
  • 原生库,例如 fopen()。

那Android10咋办呢??要不就用MediaStore,要不就直接把分区存储关了吧(requestLegacyExternalStorage=true)

[重点] 所有文件访问权限

但是还有些应用就要访问所有文件,比如杀毒软件,文件管理器。放心,有办法!MANAGE_EXTERNAL_STORAGE这不来了吗。 这个权限就是用来获取所有文件的管理权限。

    

    val intent = Intent()
    intent.action= Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
    startActivity(intent)

    //判断是否获取MANAGE_EXTERNAL_STORAGE权限:
    val isHasStoragePermission= Environment.isExternalStorageManager()

[重点] 电话号码相关权限

Android 11 更改了您的应用在读取电话号码时使用的与电话相关的权限。

具体改了什么呢?其实就是两个API:

  • TelecomManager 类中的 getLine1Number()方法
  • TelecomManager 类中的getMsisdn()方法

也就是当用到这两个API的时候,原来的 READ_PHONE_STATE权限不管用了,需要READ_PHONE_NUMBERS权限才行。

下面具体说说,targetSdkVersion修改到30,然后运行一个获取电话号码的程序:


    ActivityCompat.requestPermissions(this,
        arrayOf(Manifest.permission.READ_PHONE_STATE), 100)

        btn2.setOnClickListener {
            val tm = this.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
            val phoneNumber = tm.line1Number
            showToast(phoneNumber)
        }


会发生崩溃

java.lang.SecurityException: getLine1NumberForDisplay: Neither user 10151 nor current process has android.permission.READ_PHONE_STATE, android.permission.READ_SMS, or android.permission.READ_PHONE_NUMBERS

给权限


    
    


    ActivityCompat.requestPermissions(this,
        arrayOf(Manifest.permission.READ_PHONE_STATE,Manifest.permission.READ_PHONE_NUMBERS), 100)


搞定,如果你只需要获取手机号码这一个功能,也可以只申请READ_PHONE_NUMBERS这一个权限:

    
    

[重点] 自定义消息框视图被屏蔽 ⭐

从 Android 11 开始,已弃用自定义消息框视图。如果您的应用以 Android 11 为目标平台,包含自定义视图的消息框在从后台发布时会被屏蔽

    Toast toast = new Toast(context);
    toast.setDuration(show_length);
    toast.setView(view);
    toast.show();

自定义toast被弃用了?我们项目就是用的这个啊!不用担心,只是不允许自定义toast从后台显示了。 比如我写一个3秒后再显示toast,然后应用一打开就进入后台,看看会发生什么:

不用担心,只是不允许自定义toast从后台显示了。 比如我写一个3秒后再显示toast,然后应用一打开就进入后台,看看会发生什么:

    Handler().postDelayed({
          IToast.show("你好,我是自定义toast")
     }, 3000)


     W/NotificationService: Blocking custom toast from package com.example.studynote due to package not in the foreground

啥也没显示,只是发出来一个警告。 所以不用太过担心,如果实在需要后台显示,就用普通的toast吧!

[重点] 现在需要 APK 签名方案 v2 打包注意

对于以 Android 11(API 级别 30)为目标平台,且目前仅使用 APK 签名方案 v1 签名的应用,现在还必须使用 APK 签名方案 v2 或更高版本进行签名。用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用。

这个介绍已经很明显了吧,如果你的targetSdkVersion修改到30,那么你就必须要加上v2签名才行。否则无法安装和更新。

[重点] 媒体intent操作需要系统默认相机 ⭐

从 Android 11 开始,只有预装的系统相机应用可以响应以下 intent 操作:
android.media.action.VIDEO_CAPTURE
android.media.action.IMAGE_CAPTURE
android.media.action.IMAGE_CAPTURE_SECURE
也就是说,如果我调用intent唤起照相机,使用VIDEO_CAPTURE的action,只有系统的相机能够响应,而第三方的相机应用不会响应了。

    val intent=Intent()
    intent.action=android.provider.MediaStore.ACTION_IMAGE_CAPTURE
    startActivity(intent)

    //无法唤起第三方相机了,只能唤起系统相机

这点对普通的相机应用还是有点打击的,官方给的建议是如果要使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为intent设置软件包名称或组件来使这些intent变得明确。

[重点]5G ⭐

Android 11 添加了在您的应用中支持 5G 的功能

新的Android11也是支持了5G相关的一些功能,包括:

检测是否连接到了5G网络
检查按流量计费性
首先是检测5G网络,通过 TelephonyManager的监听方法:

    private fun getNetworkType(){
        val tManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
        tManager.listen(object : PhoneStateListener() {

            @RequiresApi(Build.VERSION_CODES.R)
            override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {
                if (ActivityCompat.checkSelfPermission(this@Android11Test2Activity, android.Manifest.permission.READ_PHONE_STATE) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
                    return
                }
                super.onDisplayInfoChanged(telephonyDisplayInfo)

                when(telephonyDisplayInfo.networkType) {
                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO -> showToast("高级专业版 LTE (5Ge)")
                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> showToast("NR (5G) - 5G Sub-6 网络")
                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> showToast("5G+/5G UW - 5G mmWave 网络")
                    else -> showToast("other")
                }
            }

        }, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)
    }

如果是5g网络,就免不了要去判断是不是按流量计费的,否则5G的流量可不是开玩笑的。

检测流量计费方法也很简单,监听网络,在回调中判断:

    val manager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
     manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
        override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
          super.onCapabilitiesChanged(network, networkCapabilities)

            //true 代表连接不按流量计费
            val isNotFlowPay=networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ||
                            networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
          }
    })

判断该值,如果为 true,则将连接视为不按流量计费。

[重点] 后台位置信息访问权限 ⭐

在搭载 Android 11 的设备上,当应用中的某项功能请求在后台访问位置信息时,用户看到的系统对话框不再包含用于启用后台位置信息访问权限的按钮。如需启用后台位置信息访问权限,用户必须在设置页面上针对应用的位置权限设置一律允许选项。

什么意思呢?主要涉及到两点:

  • 从Android10系统的设备开始,就需要请求后台位置权限( ACCESS_BACKGROUND_LOCATION),并选择Allow all the time(始终允许)才能获得后台位置权限。Android11设备上再次加强对后台权限的管理,主要表现在系统对话框上,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要在设置页面选择始终允许才能获得后台位置权限。
  • 在搭载Android11系统的设备上,targetVersion小于30的时候,可以前台后台位置权限一起申请,并且对话框提供了文字说明,表示需要随时获取用户位置信息,进入设置选择始终允许即可。但是targetVersion为30的时候,你必须单独申请后台位置权限,而且要在获取前台权限之后,顺序不能乱。并且无任何提示,需要开发者自己设计提示样式。

可能有点绕,操作几个例子说明:
Android10设备,申请前台和后台位置权限(任意targetSdkVersion):

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

Android11设备,targetSdkVersion=30(Android 11),申请前台和后台位置权限:

执行无反应

targetSdkVersion<30情况下,如果你之前就有判断过前台和后台位置权限,那就无需担心,没有什么需要适配。
targetSdkVersion>30情况下,需要分开申请前后台位置权限,并且对后台位置权限申请做好说明和引导,当然也是为了更好的服务用户

所以,该怎么适配呢?

权限申请的demo代码:

    val permissionAccessCoarseLocationApproved = ActivityCompat
        .checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION) ==
        PackageManager.PERMISSION_GRANTED

    if (permissionAccessCoarseLocationApproved) {
       val backgroundLocationPermissionApproved = ActivityCompat
           .checkSelfPermission(this, permission.ACCESS_BACKGROUND_LOCATION) ==
           PackageManager.PERMISSION_GRANTED

       if (backgroundLocationPermissionApproved) {
            //前后台位置权限都有
       } else {
            //申请后台权限
            if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
                ActivityCompat.requestPermissions(this,
                        arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                        200)
            }else{
                AlertDialog.Builder(this).setMessage("需要提供后台位置权限,请在设置页面选择始终允许")
                        .setPositiveButton("确定", DialogInterface.OnClickListener { dialog, which ->
                            ActivityCompat.requestPermissions(this,
                                    arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                                    200)
                        }).create().show()
            }

       }
    } else {
        if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
            //申请前台和后台位置权限
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    100)
        }else{
            //申请前台位置权限
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                    100)
        }
    }
    

[重点] 软件包可见性⭐⭐⭐

Android 11 更改了应用查询用户已在设备上安装的其他应用以及与之交互的方式。使用新的 元素,应用可以定义一组自身可访问的其他应用。通过告知系统应向您的应用显示哪些其他应用,此元素有助于鼓励最小权限原则。此外,此元素还可帮助 Google Play 等应用商店评估应用为用户提供的隐私权和安全性。

也就是说,Android11中,如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须在清单文件中添加元素,告知系统你要获取哪些应用信息或者哪一类应用。

比如我这段查询应用信息的代码:

    val pm = this.packageManager
    val listAppcations: List = pm
            .getInstalledApplications(PackageManager.GET_META_DATA)
    for (app in listAppcations) {
        Log.e("lz",app.packageName)
    }

在Android11版本,只能查询到自己应用和系统应用的信息,查不到其他应用的信息了。怎么呢?添加元素,两种方式:

1)元素中加入具体包名


    
        
        
    
    ...


2)元素中加入固定过滤的intent


    
        
            
            
        
    


可能还是有人会疑惑,那我的应用是浏览器或者设备管理器咋办呢?我就要获取所有包名啊?
放心,Android11还引入了 QUERY_ALL_PACKAGES 权限,清单文件中加入即可。但是Google Play可不一定能滥用哦,它为需要QUERY_ALL_PACKAGES 权限的应用会提供相关指南,但是还没出来,具体要看后面的消息了。

其他的去官网发掘吧
官网


转载感谢:积木zz,参考链接

你可能感兴趣的:(Android 兼容(三)Android 11 浅谈)