Android11系统应用兼容适配

  • Post author:
  • Post category:其他


一、背景

客户使用了安卓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 填充数据。



版权声明:本文为yangwubolwg原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。