首页 > 其他分享 >Toast自定义颜色抛出空指针异常

Toast自定义颜色抛出空指针异常

时间:2023-10-27 15:58:20浏览次数:49  
标签:Toast java 自定义 app uri ActivityThread android Android 指针

   

实战 Android 升级目标版本到 30 过程中遇到的问题及解决办法

开发者如是说 2021-11-162,006阅读5分钟   专栏:  Android 开发文章合集  

应谷歌应用商店要求,自11月1日起,所有上传到谷歌应用商店的应用将被强制要求升级目标 API 版本到 30,

upgrade_to_30_policy.png

这里记录我升级目标版本到 30 的过程中遇到的问题。

1、Toast API 内部变更

1.1 问题详情

一般来说,这种 API 级别的变更不会被记录到官方的文档中,但是,遇到了就是坑,

  java 复制代码
sToast = Toast.makeText(UtilsApp.getApp(), text, duration);
final TextView tvMessage = sToast.getView().findViewById(android.R.id.message);
if (sMsgColor != COLOR_DEFAULT) {
    tvMessage.setTextColor(sMsgColor);
}
if (sMsgTextSize != -1) {
    tvMessage.setTextSize(sMsgTextSize);
}
if (sGravity != -1 || sXOffset != -1 || sYOffset != -1) {
    sToast.setGravity(sGravity, sXOffset, sYOffset);
}
// View from getter is prior then global toastViewCallback.
if (getter != null) {
    sToast.setView(getter.getView(text));
} else {
    View view;
    if (toastViewCallback != null && (view = toastViewCallback.getView(text, style)) != null) {
        sToast.setView(view);
    }
}
showToast();

如果你像上面这样 Toast.makeText 之后使用 getView() 方法获取 android.R.id.message 对应的控件,那么将会抛出空指针异常。

根据这个 API 的注释,

  java 复制代码
Return the view.
Toasts constructed with Toast(Context) that haven't called setView(View) with a non-null view will return null here.
Starting 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).
Deprecated
Custom 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.
See Also:
setView

显然是从 target API 30开始这个方法只返回 null. 不过,如果我们使用自定义的 View 调用 setView 方法还是可以继续使用的。只是 Toast 的 ui 要自己定义。

1.2 适配方案

方法一:如果不需要自定义 Toast 展示的文本的样式,直接使用原生的书写方式即可,即 Toast.makeText(...)

方法二:调用 Toast 的 setView 方法自己传入用来自定义的 View 来进行 UI 样式自定义。

2、获取设备信息方法变更

2.1 问题详情

当 Target API 提升到了 30 之后,许多获取设备信息的方法将无法使用,这包括(目前遇到的 API 如下所示)

  java 复制代码
TelephonyManager#getImei
TelephonyManager#getMeid
TelephonyManager#getSubscriberId
TelephonyManager#getDeviceId
TelephonyManager#getSimSerialNumber
Build#getSerial

读取这些信息的时候将会抛出如下异常,

  java 复制代码
2021-11-04 22:54:30.340 15085-15085/me.shouheng.samples E/AndroidRuntime: FATAL EXCEPTION: main
    Process: me.shouheng.samples, PID: 15085
    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.
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.SecurityException: getImeiForSlot: The user 10165 does not meet the requirements to access device identifiers.
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
        at android.os.Parcel.createException(Parcel.java:2357)
        at android.os.Parcel.readException(Parcel.java:2340)
        at android.os.Parcel.readException(Parcel.java:2282)
        at com.android.internal.telephony.ITelephony$Stub$Proxy.getImeiForSlot(ITelephony.java:11511)
        at android.telephony.TelephonyManager.getImei(TelephonyManager.java:2049)
        at android.telephony.TelephonyManager.getImei(TelephonyManager.java:2004)
        at me.shouheng.utils.device.DeviceUtils.getDeviceId(DeviceUtils.java:232)
        at me.shouheng.samples.device.TestDeviceUtilsActivity$1.onGetPermission(TestDeviceUtilsActivity.java:30)
        at me.shouheng.utils.permission.PermissionUtils.checkPermissions(PermissionUtils.java:227)
        at me.shouheng.utils.permission.PermissionUtils.checkPhonePermission(PermissionUtils.java:109)
        at me.shouheng.samples.device.TestDeviceUtilsActivity.onCreate(TestDeviceUtilsActivity.java:24)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 

在 Target API 提升到 30 之后,需要增加如下权限才可以使用上述方法,

  xml 复制代码
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />

不过这个权限只有系统应用才可以获取,我们的应用即便在 manifest 中注册了这个权限也一样会在获取上述信息的时候发生崩溃。

2.2 适配方案

不要使用上述信息作为用户标识。

3、存储权限变更

3.1 问题详情

关于 requestLegacyExternalStorage 属性的问题:虽然 Android10 上面提出了外部存储分区的概念,不过之前的版本中,我们只要为应用添加了 android:requestLegacyExternalStorage="true" 就可以像之前的方式一样访问手机的外部存储空间。但是当升级 Target API 到 30 之后将强制要求使用分区存储。但是如果覆盖安装的话, android:requestLegacyExternalStorage="true" 还是继续生效的。不过,既然我们要适配 Android11,就应该卸载重装,然后重构读写外部存储的逻辑。

下面是升级目标版本到 30 之后关于读取手机内文件的一些问题或者现象,

  • 写入到应用专属外部存储权限规则不变:应用专属外部权限 Android/data/package_name 下面,跟之前一致,不需要申请任何权限。

  • 可以通过请求 MANAGE_EXTERNAL_STORAGE 来获取外部存储空间的管理权限。但是,不建议使用这种方式进行适配,因为请求的权限过多。

  • 写入到外部存储更加复杂,下面是适配的方案。

3.2 适配方案

这里,我使用 Androidx 提供的 documentfile 进行适配,大致的逻辑是,

  1. 写入外部存储之前先请求用户获取专属存储路径;
  2. 获取到之后保存到 SP(SharedPreference) 中,下次使用的时候从 SP 读取,通过 SP 中是否存在这个值来判断是否需要重新获取外部存储空间;
  3. 校验读写权限,然后通过 DocumentFile/或者 File 读写文件。步骤如下,

首先,为应用添加依赖,

  groovy 复制代码
implementation 'androidx.documentfile:documentfile:1.0.1'

1. 请求权限外部存储权限

下面是兼容的请求方案,对于 Android 11 及以上的版本使用 Intent+startActivityForResult 打开应用选择外部存储目录;对于 Android11 以下的版本,走请求外部存储权限的逻辑,

  kotlin 复制代码
override fun <T> checkExternalPermission(
    activity: T,
    onGetPermission: () -> Unit
) where T : PermissionResultResolver, T : AppCompatActivity {
    if (AppManager.isAboveAndroidR()) {
        // 适用于 Android11
        val uriString = SPUtils.get().getString("__external_storage_path")
        if (TextUtils.isEmpty(uriString)) {
            requestExternalPermission(activity)
            return
        }
        val uri = Uri.parse(uriString)
        val file = DocumentFile.fromTreeUri(UtilsApp.getApp(), uri)
        if (file == null || !file.canWrite() || !file.canRead()) {
            requestExternalPermission(activity)
        } else {
            root = file
            onGetPermission.invoke()
        }
    } else {
        // 适用于 Android11 以下,通过之前的方式获读写权限
        PermissionUtils.checkStoragePermission(activity) {
            onGetPermission.invoke()
        }
    }
}

@RequiresApi(Build.VERSION_CODES.O)
private fun requestExternalPermission(activity: AppCompatActivity) {
    var uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary")
    uri = DocumentFile.fromTreeUri(activity, uri)?.uri
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
            or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
            or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
    activity.startActivityForResult(intent, 0x01111111)
}

之前我对请求外部存储权限的逻辑做了封装,这里其实可以考虑通过封装,内部隐藏实现细节,然后根据 API 版本,统一处理请求和请求到结果的逻辑。

2. 保存请求的外部存储路径的逻辑

这里获取到用户选择的外部存储路径之后使用 SharedPreferences 保存起来,并调用 ContentResolver 的 takePersistableUriPermission 方法存储请求结果。

  kotlin 复制代码
override fun savePermissionState(
    activity: AppCompatActivity,
    requestCode: Int,
    resultCode: Int,
    data: Intent?
) {
    if (resultCode != Activity.RESULT_OK || requestCode != 0x01111111) return
    try {
        val uri: Uri = data?.data ?: return
        SPUtils.get().put("__external_storage_path", uri.toString())
        activity.contentResolver.takePersistableUriPermission(uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        root = DocumentFile.fromTreeUri(UtilsApp.getApp(), uri)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

那么,下次我们可以使用 SharedPreferences 中是否有 __external_storage_path 的信息来判断当前应用是否已经选择了外部存储目录,并决定是否需要再次请求。

3. 写入文件到外部存储空间

这里仅以写文件作为示例。首先,让我们把脑洞打开,尝试使用之前的对 File 操作的方式来访问磁盘文件。

下面是使用存储分区之后的按照老的方式读写文件方式示例,

  kotlin 复制代码
val uriString = SPUtils.get().getString("__external_storage_path")
val left = uriString.removePrefix("content://com.android.externalstorage.documents/tree/primary%3A")
val path = EncodeUtils.urlDecode(left)
val root = PathUtils.getExternalStoragePath()
val file = File("$root${File.separator}$path", "write_old.text")
IOUtils.writeFileFromString(file, "test test")

即,因为上面请求权限的时候,我们保存了外部存储的目录,所以,可以根据保存的 uri,移除前缀之后获取用户选择的相对目录,然后使用相对路径,按照之前的方式读写。因为 uri 是编码之后的,所以这里需要先做解码操作。

这里是我的一种写法,亲测写入时有效。但是按照这种方式读取文件的时候,当我们调用 File.listFiles() 方法时只会返回目录和按照这种方式写入的文件,不会返回通过 Documentfile 写入的文件,所以这个方法是行不通的。

如果使用 Documentfile 进行读写,该逻辑如下,

  kotlin 复制代码
val uriString = SPUtils.get().getString("__external_storage_path")
try {
    val uri = Uri.parse(uriString)
    val root = DocumentFile.fromTreeUri(this, uri)
    var doc = createOrExistsFile(root, "test_a", "application/txt", "${System.currentTimeMillis()}.txt")
    var ous = this.contentResolver.openOutputStream(doc!!.uri)
    var ret = writeToOutputStream(ous, "sample a")
} catch (e: Exception) {
    e.printStackTrace()
    toast("failed!")
}

private fun createOrExistsFile(
    root: DocumentFile?,
    directoryPath: String,
    mimeType: String,
    fileName: String
): DocumentFile? {
    if (root == null) return null
    val dir = createOrExistsDirectory(root, directoryPath)
    val file = dir?.findFile(fileName)
    return if (file != null && file.isFile) file else dir?.createFile(mimeType, fileName)
}

private fun createOrExistsDirectory(root: DocumentFile?, directoryPath: String): DocumentFile? {
    if (root == null) return null
    val parts = directoryPath.split(File.separator).toTypedArray()
    var dir = root
    parts.filter { it.isNotEmpty() }.forEach { part ->
        dir = dir?.listFiles()?.find {
            part == it.name && it.isDirectory
        } ?: dir?.createDirectory(part)
    }
    return dir
}

private fun writeToOutputStream(ous: OutputStream?, text: String): Boolean {
    return try {
        ous?.write(text.toByteArray())
        true
    } catch (e: IOException) {
        e.printStackTrace()
        false
    } finally {
        IOUtils.safeCloseAll(ous)
    }
}

这里的逻辑稍微复杂点,主要是处理了可能写入到子目录中的情况。从上面的代码也可以看出,这种读写方式是需要通过 listFiles() 获取所有文件并遍历,通过匹配文件名的方式来判断指定的文件是否存在的。而写入操作这是通过打开 OutputStream,然后使用 OutputStream 写入到流来实现的。

综合对比:显然使用 documentfile 进行读写逻辑更加复杂,而且可能需要在代码中同时存在 File 和 documentfile 两套逻辑,而使用老的方式进行读写的话,我们可以复用之前的读写逻辑。不过,按照上面对字符串处理获取相对路径的方式在生产的实际表现如何,仍然有待验证。

小结:通常,我们在开发应用的时候会在外部存储空间创建一个专属的目录并进行读写,但是之前的外部存储管理方式过于宽泛,特别是相册和外部存储混合的情况,导致用户不得不给予外部存储权限,而这很可能把用户暴露在危险中。按照新的分区规范,我们一样可以请求用户给予一个专门的文件夹供我们读写,不过用户拥有了更多的自主权,可以指定我们使用的目录。这对 Android 的安全和发展当然是一件好事,不过对开发而言就比较头疼了。

总结

这里记录的是升级目标版本到 30 遇到的一些问题以及实际解决办法,当然 AndroidR 上所做的变更比这更多,只是这里没有遇到。后续遇到升级问题会继续更新~

文件读写代码请参考:github.com/Shouheng88/…

标签: Android 本文收录于以下专栏 cover Android 开发文章合集 专栏目录 Android 相关的文章 19 订阅 · 45 篇文章   上一篇 使用 APT 开发组件化框架的若干细节问题 下一篇 较个真!Matirx 单例写法的线程安全问题 评论 3 avatar         0 / 1000 即可发布评论! 梦回少年的头像 梦回少年     感谢分享!最近在做兼容android11的草稿功能,需要保存文件uri。发现持久化了uri再次访问同一个uri会报权限异常。重点就是缺失了takePersistableUriPermission,需要获取系统提供的永久性 URI 访问权限 1年前 点赞 评论       Android开发苏同学的头像 Android开发苏同学创作等级LV.2 Android开发   最近刚好在做这个适配,不用sp保存授权的uri呀,有api查询。另外那个不用从root遍历查询到document,都知道路径可以构建试试singleuri 1年前 点赞 1     开发者如是说的头像 开发者如是说   作者  :  学习了   1年前 点赞 回复       avatar 创作等级LV.5   优秀作者 86 文章 315k 阅读 6.0k 粉丝 关注 已关注   私信 目录 收起 搜索建议     精选内容   究极逮虾户  ·  674阅读  ·  16点赞   野生的码农  ·  11k阅读  ·  322点赞   王晨彦  ·  1.1k阅读  ·  12点赞   程序员一鸣  ·  677阅读  ·  7点赞   张拭心  ·  329阅读  ·  3点赞 找对属于你的技术圈子 回复「进群」加入官方微信群 为你推荐       yoyo 更多登录后权益等你解锁 image 检测到系统启用深色模式 可在“我的设置-通用设置”中同步开启掘金深色模式 点击前往          

标签:Toast,java,自定义,app,uri,ActivityThread,android,Android,指针
From: https://www.cnblogs.com/xzylcf/p/17792521.html

相关文章

  • ?Mybatis多表查询(1:1、1:N、N:N),MP多表查询(自定义SQL)
    Mybatis多表关联查询Gitee地址:https://gitee.com/zhang-zhixi/mybatis-tables-query.git数据表:oracleCREATETABLE"T_ORDER"("ID"NUMBERNOTNULL,"F_ORDER_TIME"DATE,"F_TOTAL"VARCHAR2(255BYTE),"F_USER_ID"NU......
  • Revit 自定义事务进行自动管理事务DBTrans实现
    第一步:自定义事务对象自定义事务对象///<summary>///自定义事务///</summary>publicclassDBTrans:IDisposable{#region私有字段privatebooldisposedValue;privatebool_commit;///<summary>......
  • Fabric.js 自定义控件
    本文简介带尬猴,我是德育处主任虽然Fabric.js提供的基础功能已经很丰富了,但有时难免需要定制一些需求。比如本文要讲的『自定义控件』。掌握创建自定义控件这个功能,能够创建更加精美和实用的图形应用程序,提高用户体验和用户满意度。尽管Fabric.js的文档很一般,但demo还挺......
  • 自定义过滤器配置 Shiro 认证失败返回 json 数据
    byemanjusakafrom​https://www.emanjusaka.top/archives/11彼岸花开可奈何本文欢迎分享与聚合,全文转载请留下原文地址。Shiro权限框架认证失败默认是重定向页面的,这对于前后端分离的项目及其不友好,可能会造成请求404的问题。现在我们自定义过滤器实现认证失败返回json数......
  • c# winfom从0学习开发开发OA、BPM工作流程与自定义表单系统(十二)新建一个完整的工作流
     先设计一个表单 开始设计表单 设计一个表单例如请假表单 Tag十分的重要,再设计流程图节点的时候tag起到的作用是提示当前控件是谁,再设置可写字段环节十分重要 保存 设计流程图 设计请假流程图设计好请假的流程 设置每个节点的参数 所有部门下的人都......
  • 在不知带头节点地址的情况下删除和插入一个p指针指向的节点总结
    在不知带头节点地址的情况下删除和插入一个p指针指向的节点总结(p指向的不是第一个,也不是最后一个)A->B->C*p->B插入(在p结点之前插入q)解析:直接往p前插入q,由于没有头节点,不能遍历到p的位置,所以向p的后面插入q,在交换p、q的值q->next=p->next;p->next=q;swap(&p......
  • Mac电脑使用BetterAndBetter软件自定义的脚本
    新建文件tellapplication"Finder" setselectedItemstoselection if(countofselectedItems)is1then setselectedItemtoitem1ofselectedItems ifclassofselectedItemisfolderthen displaydialog"请输入文件名:"defaultansw......
  • 初阶指针(Pointer)---【C语言】
    ⛩️博主主页:@威化小餅干......
  • 易混知识 | 数组指针 VS 指针数组
    ⛩️博主主页:@威化小餅干......
  • 18.3 NPCAP自定义数据包过滤
    NPCAP库是一种用于在Windows平台上进行网络数据包捕获和分析的库。它是WinPcap库的一个分支,由Nmap开发团队开发,并在Nmap软件中使用。与WinPcap一样,NPCAP库提供了一些API,使开发人员可以轻松地在其应用程序中捕获和处理网络数据包。NPCAP库可以通过WinPcapAPI进行编程,因此现有的Win......