应谷歌应用商店要求,自11月1日起,所有上传到谷歌应用商店的应用将被强制要求升级目标 API 版本到 30, upgrade_to_30_policy.png
这里记录我升级目标版本到 30 的过程中遇到的问题。
一般来说,这种 API 级别的变更不会被记录到官方的文档中,但是,遇到了就是坑,
1sToast = Toast.makeText(UtilsApp.getApp(), text, duration);
2final TextView tvMessage = sToast.getView().findViewById(android.R.id.message);
3if (sMsgColor != COLOR_DEFAULT) {
4 tvMessage.setTextColor(sMsgColor);
5}
6if (sMsgTextSize != -1) {
7 tvMessage.setTextSize(sMsgTextSize);
8}
9if (sGravity != -1 || sXOffset != -1 || sYOffset != -1) {
10 sToast.setGravity(sGravity, sXOffset, sYOffset);
11}
12// View from getter is prior then global toastViewCallback.
13if (getter != null) {
14 sToast.setView(getter.getView(text));
15} else {
16 View view;
17 if (toastViewCallback != null && (view = toastViewCallback.getView(text, style)) != null) {
18 sToast.setView(view);
19 }
20}
21showToast();
如果你像上面这样 Toast.makeText 之后使用 getView()
方法获取 android.R.id.message
对应的控件,那么将会抛出空指针异常。
根据这个 API 的注释,
1Return the view.
2Toasts constructed with Toast(Context) that haven't called setView(View) with a non-null view will return null here.
3Starting from Android Build.VERSION_CODES.R, in apps targeting API level Build.VERSION_CODES.R or higher, toasts constructed with makeText(Context, CharSequence, int) or its variants will also return null here unless they had called setView(View) with a non-null view. If you want to be notified when the toast is shown or hidden, use addCallback(Toast.Callback).
4Deprecated
5Custom toast views are deprecated. Apps can create a standard text toast with the makeText(Context, CharSequence, int) method, or use a Snackbar when in the foreground. Starting from Android Build.VERSION_CODES.R, apps targeting API level Build.VERSION_CODES.R or higher that are in the background will not have custom toast views displayed.
6See Also:
7setView
8
显然是从 target API 30开始这个方法只返回 null. 不过,如果我们使用自定义的 View 调用 setView
方法还是可以继续使用的。只是 Toast 的 ui 要自己定义。
方法一:如果不需要自定义 Toast 展示的文本的样式,直接使用原生的书写方式即可,即 Toast.makeText(...)
方法二:调用 Toast 的 setView
方法自己传入用来自定义的 View 来进行 UI 样式自定义。
当 Target API 提升到了 30 之后,许多获取设备信息的方法将无法使用,这包括(目前遇到的 API 如下所示)
1TelephonyManager#getImei
2TelephonyManager#getMeid
3TelephonyManager#getSubscriberId
4TelephonyManager#getDeviceId
5TelephonyManager#getSimSerialNumber
6Build#getSerial
读取这些信息的时候将会抛出如下异常,
12021-11-04 22:54:30.340 15085-15085/me.shouheng.samples E/AndroidRuntime: FATAL EXCEPTION: main
2 Process: me.shouheng.samples, PID: 15085
3 java.lang.RuntimeException: Unable to start activity ComponentInfo{me.shouheng.samples/me.shouheng.samples.device.TestDeviceUtilsActivity}: java.lang.SecurityException: getImeiForSlot: The user 10165 does not meet the requirements to access device identifiers.
4 at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
5 at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
6 at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
7 at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
8 at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
9 at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
10 at android.os.Handler.dispatchMessage(Handler.java:106)
11 at android.os.Looper.loop(Looper.java:223)
12 at android.app.ActivityThread.main(ActivityThread.java:7656)
13 at java.lang.reflect.Method.invoke(Native Method)
14 at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
15 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
16 Caused by: java.lang.SecurityException: getImeiForSlot: The user 10165 does not meet the requirements to access device identifiers.
17 at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
18 at android.os.Parcel.createException(Parcel.java:2357)
19 at android.os.Parcel.readException(Parcel.java:2340)
20 at android.os.Parcel.readException(Parcel.java:2282)
21 at com.android.internal.telephony.ITelephony$Stub$Proxy.getImeiForSlot(ITelephony.java:11511)
22 at android.telephony.TelephonyManager.getImei(TelephonyManager.java:2049)
23 at android.telephony.TelephonyManager.getImei(TelephonyManager.java:2004)
24 at me.shouheng.utils.device.DeviceUtils.getDeviceId(DeviceUtils.java:232)
25 at me.shouheng.samples.device.TestDeviceUtilsActivity$1.onGetPermission(TestDeviceUtilsActivity.java:30)
26 at me.shouheng.utils.permission.PermissionUtils.checkPermissions(PermissionUtils.java:227)
27 at me.shouheng.utils.permission.PermissionUtils.checkPhonePermission(PermissionUtils.java:109)
28 at me.shouheng.samples.device.TestDeviceUtilsActivity.onCreate(TestDeviceUtilsActivity.java:24)
29 at android.app.Activity.performCreate(Activity.java:8000)
30 at android.app.Activity.performCreate(Activity.java:7984)
31 at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
32 at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
33 at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
34 at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
35 at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
36 at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
37 at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
38 at android.os.Handler.dispatchMessage(Handler.java:106)
39 at android.os.Looper.loop(Looper.java:223)
40 at android.app.ActivityThread.main(ActivityThread.java:7656)
41 at java.lang.reflect.Method.invoke(Native Method)
42 at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
43 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
在 Target API 提升到 30 之后,需要增加如下权限才可以使用上述方法,
1<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
不过这个权限只有系统应用才可以获取,我们的应用即便在 manifest 中注册了这个权限也一样会在获取上述信息的时候发生崩溃。
关于 requestLegacyExternalStorage 属性的问题:虽然 Android10 上面提出了外部存储分区的概念,不过之前的版本中,我们只要为应用添加了 android:requestLegacyExternalStorage="true"
就可以像之前的方式一样访问手机的外部存储空间。但是当升级 Target API 到 30 之后将强制要求使用分区存储。但是如果覆盖安装的话, android:requestLegacyExternalStorage="true"
还是继续生效的。不过,既然我们要适配 Android11,就应该卸载重装,然后重构读写外部存储的逻辑。
下面是升级目标版本到 30 之后关于读取手机内文件的一些问题或者现象,
Android/data/package_name
下面,跟之前一致,不需要申请任何权限。MANAGE_EXTERNAL_STORAGE
来获取外部存储空间的管理权限。但是,不建议使用这种方式进行适配,因为请求的权限过多。这里,我使用 Androidx 提供的 documentfile 进行适配,大致的逻辑是,
首先,为应用添加依赖,
implementation 'androidx.documentfile:documentfile:1.0.1'
1. 请求权限外部存储权限
下面是兼容的请求方案,对于 Android 11 及以上的版本使用 Intent+startActivityForResult 打开应用选择外部存储目录;对于 Android11 以下的版本,走请求外部存储权限的逻辑,
1override fun <T> checkExternalPermission(
2 activity: T,
3 onGetPermission: () -> Unit
4) where T : PermissionResultResolver, T : AppCompatActivity {
5 if (AppManager.isAboveAndroidR()) {
6 // 适用于 Android11
7 val uriString = SPUtils.get().getString("__external_storage_path")
8 if (TextUtils.isEmpty(uriString)) {
9 requestExternalPermission(activity)
10 return
11 }
12 val uri = Uri.parse(uriString)
13 val file = DocumentFile.fromTreeUri(UtilsApp.getApp(), uri)
14 if (file == null || !file.canWrite() || !file.canRead()) {
15 requestExternalPermission(activity)
16 } else {
17 root = file
18 onGetPermission.invoke()
19 }
20 } else {
21 // 适用于 Android11 以下,通过之前的方式获读写权限
22 PermissionUtils.checkStoragePermission(activity) {
23 onGetPermission.invoke()
24 }
25 }
26}
27
28@RequiresApi(Build.VERSION_CODES.O)
29private fun requestExternalPermission(activity: AppCompatActivity) {
30 var uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary")
31 uri = DocumentFile.fromTreeUri(activity, uri)?.uri
32 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
33 intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
34 or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
35 or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
36 or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
37 intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
38 activity.startActivityForResult(intent, 0x01111111)
39}
之前我对请求外部存储权限的逻辑做了封装,这里其实可以考虑通过封装,内部隐藏实现细节,然后根据 API 版本,统一处理请求和请求到结果的逻辑。
2. 保存请求的外部存储路径的逻辑
这里获取到用户选择的外部存储路径之后使用 SharedPreferences 保存起来,并调用 ContentResolver 的 takePersistableUriPermission
方法存储请求结果。
1override fun savePermissionState(
2 activity: AppCompatActivity,
3 requestCode: Int,
4 resultCode: Int,
5 data: Intent?
6) {
7 if (resultCode != Activity.RESULT_OK || requestCode != 0x01111111) return
8 try {
9 val uri: Uri = data?.data ?: return
10 SPUtils.get().put("__external_storage_path", uri.toString())
11 activity.contentResolver.takePersistableUriPermission(uri,
12 Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
13 )
14 root = DocumentFile.fromTreeUri(UtilsApp.getApp(), uri)
15 } catch (e: Exception) {
16 e.printStackTrace()
17 }
18}
那么,下次我们可以使用 SharedPreferences 中是否有 __external_storage_path
的信息来判断当前应用是否已经选择了外部存储目录,并决定是否需要再次请求。
3. 写入文件到外部存储空间
这里仅以写文件作为示例。首先,让我们把脑洞打开,尝试使用之前的对 File 操作的方式来访问磁盘文件。
下面是使用存储分区之后的按照老的方式读写文件方式示例,
1val uriString = SPUtils.get().getString("__external_storage_path")
2val left = uriString.removePrefix("content://com.android.externalstorage.documents/tree/primary%3A")
3val path = EncodeUtils.urlDecode(left)
4val root = PathUtils.getExternalStoragePath()
5val file = File("$root${File.separator}$path", "write_old.text")
6IOUtils.writeFileFromString(file, "test test")
即,因为上面请求权限的时候,我们保存了外部存储的目录,所以,可以根据保存的 uri,移除前缀之后获取用户选择的相对目录,然后使用相对路径,按照之前的方式读写。因为 uri 是编码之后的,所以这里需要先做解码操作。
这里是我的一种写法,亲测写入时有效。但是按照这种方式读取文件的时候,当我们调用 File.listFiles() 方法时只会返回目录和按照这种方式写入的文件,不会返回通过 Documentfile 写入的文件,所以这个方法是行不通的。
如果使用 Documentfile 进行读写,该逻辑如下,
1val uriString = SPUtils.get().getString("__external_storage_path")
2try {
3 val uri = Uri.parse(uriString)
4 val root = DocumentFile.fromTreeUri(this, uri)
5 var doc = createOrExistsFile(root, "test_a", "application/txt", "${System.currentTimeMillis()}.txt")
6 var ous = this.contentResolver.openOutputStream(doc!!.uri)
7 var ret = writeToOutputStream(ous, "sample a")
8} catch (e: Exception) {
9 e.printStackTrace()
10 toast("failed!")
11}
12
13private fun createOrExistsFile(
14 root: DocumentFile?,
15 directoryPath: String,
16 mimeType: String,
17 fileName: String
18): DocumentFile? {
19 if (root == null) return null
20 val dir = createOrExistsDirectory(root, directoryPath)
21 val file = dir?.findFile(fileName)
22 return if (file != null && file.isFile) file else dir?.createFile(mimeType, fileName)
23}
24
25private fun createOrExistsDirectory(root: DocumentFile?, directoryPath: String): DocumentFile? {
26 if (root == null) return null
27 val parts = directoryPath.split(File.separator).toTypedArray()
28 var dir = root
29 parts.filter { it.isNotEmpty() }.forEach { part ->
30 dir = dir?.listFiles()?.find {
31 part == it.name && it.isDirectory
32 } ?: dir?.createDirectory(part)
33 }
34 return dir
35}
36
37private fun writeToOutputStream(ous: OutputStream?, text: String): Boolean {
38 return try {
39 ous?.write(text.toByteArray())
40 true
41 } catch (e: IOException) {
42 e.printStackTrace()
43 false
44 } finally {
45 IOUtils.safeCloseAll(ous)
46 }
47}
这里的逻辑稍微复杂点,主要是处理了可能写入到子目录中的情况。从上面的代码也可以看出,这种读写方式是需要通过 listFiles()
获取所有文件并遍历,通过匹配文件名的方式来判断指定的文件是否存在的。而写入操作这是通过打开 OutputStream,然后使用 OutputStream 写入到流来实现的。
综合对比:显然使用 documentfile 进行读写逻辑更加复杂,而且可能需要在代码中同时存在 File 和 documentfile 两套逻辑,而使用老的方式进行读写的话,我们可以复用之前的读写逻辑。不过,按照上面对字符串处理获取相对路径的方式在生产的实际表现如何,仍然有待验证。
小结:通常,我们在开发应用的时候会在外部存储空间创建一个专属的目录并进行读写,但是之前的外部存储管理方式过于宽泛,特别是相册和外部存储混合的情况,导致用户不得不给予外部存储权限,而这很可能把用户暴露在危险中。按照新的分区规范,我们一样可以请求用户给予一个专门的文件夹供我们读写,不过用户拥有了更多的自主权,可以指定我们使用的目录。这对 Android 的安全和发展当然是一件好事,不过对开发而言就比较头疼了。
这里记录的是升级目标版本到 30 遇到的一些问题以及实际解决办法,当然 AndroidR 上所做的变更比这更多,只是这里没有遇到。后续遇到升级问题会继续更新~
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/caFvpNla3NdIPhe3ifCc2Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。