一、背景
客户使用了安卓11系统手机,发现应用安装闪退,于是分析,发现需要做升级适配。
二、遇到问题
应用中之前有提供一个手机唯一标识的功能,是通过调用安卓系统接口TelephoneManager 来获取IMEI的,然后再新的系统版本上发现此接口无法获取该信息,
才知道原来安卓也和苹果一样,对于这种信息不再提供给外部三方应用,个人猜测也是基于用户隐私安全考虑,市场上这么多三方应用,如果都通过该系统接口获取到唯一标识,多个三方应用之间再共享数据,则可以将每台手机的用户从各个维度进行用户画像,真的是底裤是什么颜色都可以分析出来。
于是需要自己来实现应用的唯一标识,想到的是应用自己生成uuid,然后再存储到手机中。如果是存储在应用自己的沙盒空间中,对于用户清除数据,或者卸载应用,则会一起清理掉,为了能最大可能地规避这种情况,想像以前一样存储到外部空间中。结果在调试的时候才发现已经有了 存储分区 机制, 很好地限制了三方应用随意乱写入数据,导致卸载应用后,各种残留问题。
参考网址说明:
https://open.oppomobile.com/wiki/doc#id=10724
2.1 存储
2.1.1 分区存储
1.1. 背景
Android 11 进一步增强了平台功能,为外部存储设备上的应用和用户数据提供了更好的保护。作为这项工作的一部分,平台引入了进一步的改进,以简化向分区存储的转换。
为了让用户更好地控制自己的文件,保护用户隐私数据,并限制文件混乱情况,Android 11在分区存储基础上限制了应用访问其他应用的文件。
分区存储将存储空间分为两部分:
● 公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
■ 公共目录的文件在App卸载后,不会删除
■ 可以通过SAF、MediaStore接口访问
■ 拥有权限,也能通过路径直接访问
● 应用专属目录
■ 应用专属目录只能自己直接访问
■ App卸载,数据会清除。
关于过度配置 requestLegacyExternalStorage 自己在应用中的调试结果如下:
targetSdkVersion = 29, requestLegacyExternalStorage=true, 存储访问表现还和以前一样,卸载应用后再次重装对于通过contentProvider中之前插入的数据,后面还是能访问到。
插入数据:
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Downloads.TITLE, TEMP_FILE_NAME);
contentValues.put(MediaStore.Downloads.MIME_TYPE, TEMP_FILE_NAME_MIME_TYPE);
contentValues.put(MediaStore.Downloads.DISPLAY_NAME, TEMP_FILE_NAME);
contentValues.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + TEMP_DIR);
Uri insert = contentResolver.insert(externalContentUri, contentValues);
读取访问数据:
Uri externalContentUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
ContentResolver contentResolver = context.getContentResolver();
String[] projection = new String[]{
MediaStore.Downloads._ID ,
MediaStore.Downloads.DISPLAY_NAME,
MediaStore.Downloads.TITLE
};
String selection = MediaStore.Downloads.TITLE + "=?";
String[] args = new String[]{
TEMP_FILE_NAME
};
// TODO: 先查询数据库
Cursor query = contentResolver.query(externalContentUri, projection, null, null, null);
if (null != query){
int count = query.getCount();
if (count > 0) {
boolean result = query.moveToFirst();
for(int i=0; i<count; i++){
long id = query.getLong(0);
String displayName = query.getString(1);
String title = query.getString(2);
Log.d(TAG, "id="+id+" displayName="+displayName
+" title="+title);
query.moveToNext();
}
}
}
targetSdkVersion = 29, requestLegacyExternalStorage=false, 这种情况,以及targetSdkVersion = 30 之后,存储分区机制生效,卸载应用再次重装无法访问之前写入到媒体库中的数据。
另外对于文件的访问方式也是如此,使用 Environment.getExternalStoragePublicDirectory 这个接口虽然不会抛出异常,文件和目录都存在,但是正式访问读取文件时
会报没有权限,最终导致FileNotFound异常:
尝试多次无果之后,最终无奈放弃,卸载重装的应用只能当新用户处理,虽然还是同一台手机。对于安卓这种机制的完善还是很欢迎的,毕竟应用是在不断吃磁盘空间的,如果卸载应用还有残留,那确实体验很不好。
—————————————–补充—————————-
在后续的适配中还遇到了其他问题,最终找到的原因还是和 存储分区机制有关。
1、当targetSdk 改为29 以及以上,即安卓10 开始,使用webview 通过 input 标签获取到 照片或者视频文件进行上传时,会出现如下报错:
Err_Access_Denied , 一看就是权限问题,但是实际上文件数据已经填充到 input 标签了,所以刚开始自己怀疑是否是和调用后台的接口由关系。但是调试过程中发现将targetSdk 调整到28 却能够正常,将问题还是聚焦在了安卓版本的适配上。但是刚开始一直在找webview 的问题,苦苦没有搜寻到解决方案。然后一个偶然发现有人说过类似问题还是存储分区机制造成的,然后自己就朝着这个方向去了解了。
首先将 argetSdkVersion = 29, requestLegacyExternalStorage=true,果然问题没有出现,这个就更加坚定了这个报错原因就是因为存储分区机制生效造成。
然后仔细回看了以前写的代码,在给webview回调设置文件数据的时候,居然是先从uri转换成path ,然后又将path转换成了uri 进行设置,也许是当初对于这种绝对路径比较痴迷直观,还是太菜造成的:
else if (null != uriParam) { //其次使用uri
AFLog.d(TAG, "uriParam not null mFilePathCallback="+mFilePathCallback
+" mFilePathCallbacks="+mFilePathCallbacks);
if (mFilePathCallback != null) {
// String path = UriUtils.getPath(getApplicationContext(), uriParam);
// Uri uri = Uri.fromFile(new File(path));
mFilePathCallback.onReceiveValue(uriParam);
}
最终直接将uri 设置给回调即正常,也不需要将存储分区机制关闭。 因为通过uri里面获取path 这种方式在该机制下已经不行了。
参考了网友的总结:
https://weichao.blog.csdn.net/article/details/105790720
但是此问题还有一点诡异的地方就是只要先安装低版本targetSdk 的 apk ,然后再安装安卓11 版本的apk 进行覆盖, 此错误也会消失。并且在安卓11系统的手机上也不会出现,这个现在还不明其意。另外对于手机存储的理解,可以参考下文:
https://blog.csdn.net/u010937230/article/details/73303034
————————————–关于媒体库的访问————————————–
/**
* 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
* @return 图片的uri
*/
public static Uri createMediaUri4Q(Context context, String type) {
//设置保存参数到ContentValues中
ContentValues contentValues = new ContentValues();
//设置文件名
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
Uri uri = null;
//兼容Android Q和以下版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//TODO RELATIVE_PATH 是相对路径不是绝对路径;照片存储的地方为:内部存储/Pictures/xxx
if ("photo".equals(type)) {
String imageFileName = "PIC_" + timeStamp;
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, imageFileName);
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/xxx");
//设置文件类型
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/JPEG");
//执行insert操作,向系统文件夹中添加文件
//EXTERNAL_CONTENT_URI代表外部存储器,该值不变
uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
}else if ("video".equals(type)){
String videoFileName = "VID_" + timeStamp;
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, videoFileName);
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/xxx");
contentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
}
}
return uri;
}
当在安卓10 系统中使用 uri 方式往input 标签放置文件数据时,此时能够正常方式,但是当前端在调用上传接口进行上传操作时,webview 会报错误:
2021-06-30 14:58:33.881 32521-32521/com.org.BaseWebviewApp.debug D/WebViewActivity: parse uri get path=/storage/emulated/0/DCIM/Camera/VID20210630145441.mp4
2021-06-30 14:58:35.327 32521-32521/com.org.BaseWebviewApp.debug E/WebViewActivity: onReceivedHttpError statusCode=401 reason=Unauthorized
2021-06-30 14:58:35.328 32521-32521/com.org.BaseWebviewApp.debug E/AFTag:WebViewActivity$8.onReceivedHttpError(Line:1156): onReceivedHttpError statusCode=401 reason=Unauthorized
另外在对于视频文件是使用绝对路径path ,还是使用uri 方式,这个需要根据版本来定,在安卓7.0 到 9.0 的时候可以直接使用path, 在安卓10 以上使用uri, 如果在安卓9 上面使用了uri,然后调用通用的根据uri转path 的接口会报如下错误:
2021-07-01 11:24:10.065 19727-19727/com.org.BaseWebviewApp.debug E/AFTag:MyCrashHandler.uncaughtException(Line:24): uncaughtException
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=5, result=-1, data=null} to activity {com.org.BaseWebviewApp.debug/com.hnac.hznet.WebViewActivity}: java.lang.IllegalArgumentException: column ‘_data’ does not exist. Available columns: []
可能是因为使用了如下代码:
public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; }
于是这边才去的策略就是在安卓10 以及以上使用uri 方式填充数据,安卓9以及以下使用原先的path 填充数据。