Android | 作用域存储适配

简介: Android | 作用域存储适配

前言

Android 10 已经发布了很长一段时间了,并且 Android 11 已经有很大一部分人在使用了,那么你的程序对他做了适配吗?


在 10.0 中,作用域存储变得非常重要,这个新的功能颠覆了我们一直惯用外置存储的方式,因此大量的 app 都面临着代码的适配


本篇文章对作用域存储,以及如何进行适配,做了比较详细的介绍


在 7.0 以前我们访问内存卡中的文件时可以通过 Uri.fromFile ,将 File 转换成 Uri 对象,这个 uri 对象表示这本地真实路径。


在 7.0 后,这种通过真实路径来获取的 Uri 被认为是不安全的,所以提供了一种新的解决方案,就是通过 FileProvide 来实现文件的访问,FileProvider 是一种比较特殊的内容提供器,他使用了类似于内容提供器的机制来对数据进行保护。


在7.0以前,访问一个图片如下所示:


String fileName = "defaultImage.jpg";


File file = new File("文件路径", fileName);

Uri uri = Uri.fromFile(file);


7.0后,访问如下所示:


File file = new File(CACHE_IMG, fileName);
Uri imageUri=FileProvider.getUriForFile(activity,"com.sandan.fileprovider", file);//这里进行替换uri的获得方式
 <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.sandan.fileprovider"//这里需要和上面部分字符串相同
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>


<resource xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="images"
        path="" />
        //path 表示共享的具体路径,这里为空表示整个SD卡进行共享
</resource>


然而上面这种真的好吗,对用开发者而且这算是好处吧,但是对用用户而言,上述的无疑一些流氓作用,因为开发者完全可以访问的内存中的所有位置,并作出一些改变,导致 SD 卡中的空间变得非常乱,即使卸载了 app,但是一些垃圾文件却还在内存中。


作用域存储


10.0 中,为了解决上述问题, google 在 Android 10 中加入了作用域功能


什么是作用域呢?就是 Android 系统对 SD 卡做了很大的限制,从 10.0 开始,每个程序只能有权在自己的外置存储空间关联的目录下读取和创建相应的文件,也称作沙箱。获取改目录的代码是:getExternalFilesDir() ,关联的目录路径大致如下:


/storage/emulated/0/Android/data/<包名>/files


将数据放在这个目录下,你可以使用之前的方法对文件进行读写,不需要作出任何变更和适配。但是这个文件夹中的文件会随着应用卸载而被随之删除。


那如果需要访问其他目录怎么办呢,比如获取相册中的图片,向相册中添加一张图片。为此,Android 系统针对系统文件类型进行了分类**:图片,音频,视频 这三类文件可以通过 MediaStore API 来进行访问,这种称为共享空间,其他的系统文件需要使用 系统的文件选择器来进行访问,**


另外,如果程序向媒体库写入图片,视频,音频,将会自动用于读写权限,不需要额外申请权限,如果你要读取其他程序向媒体贡献的图片,视频,音频,则必须要申请 READ_EXTERNAL_STORAGE 权限,WRITE_EXTERNAL_STORAGE 权限会在未来的版本中被废弃。


获取系统图片:


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()
}


适配要点


示例代码,以及Demo


打开相机


如果是10.0,需要根据共享文件创建一条图片地址的 Uri,用于保存拍照后的照片。


拍照完成后,拿到对应的 uri


如果要直接显示图片,则通过 uri 可直接加载


如果图片要上传,则需要将 uri 处理为一个 file 对象


在 10.0 中,只能访问沙箱文件和共享文件夹,需要注意的是:共享文件夹可以通过 uri 进行访问,如拿到输入/输出流等。但是不能将其转为 file。因为就算是共享文件夹,也不能直接通过 file 进行访问。


所以在图片上传的时候,需要通过 contentProider 将 uri 转为一个 inputStream,然后将数据读取出来,并且保存在沙箱文件中,然后在获取沙箱文件中的 file 即可。


注意,在拿到 uri 后可以对图片进行一些压缩处理。


打开相册


1,直接通过 intent 打开相册


2,拿到 返回的 uri 地址


3,如果是10.0,则需要进行和 “打开相机” 中 3,4,同样的操作。


下载文件


1,如果是 10.0,需要根据共享文件夹创建一条文件地址的 uri,用于保存文件


2,通过网络操作,拿到对应的 inputSteam


3,通过 contentProider 将 uri 转为一个 outputStream


4,input 读取数据,output 写入数即可。


需要注意的


只能在沙箱中操作 file 对象,切记。


在对 图片进行复制和压缩上传的时候,需要注意耗时,如果太耗时,需要放在子线程中。


上传文件


需要将文件复制到沙箱中,然后在进行上传操作


1,使用文件选择器,选择文件


val mimeTypes = arrayOf(
    FileIntentUtils.getMap("doc"),
    FileIntentUtils.getMap("pdf"), FileIntentUtils.getMap("ppt"),
    FileIntentUtils.getMap("xls"), FileIntentUtils.getMap("xlsx")
)
FileIntentUtils.openBle(this, REQUEST_CHOICE_FILE, mimeTypes)


/**
 * 选择文件
 */
fun openBle(activity: Activity, code: Int, types: Array<String>) {
    val intent = Intent(Intent.ACTION_GET_CONTENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "application/*";
    intent.putExtra(Intent.EXTRA_MIME_TYPES, types)
    activity.startActivityForResult(intent, code)
}


/**
 * 获取常见文件类型
 * @param key
 * @return
 */
fun getMap(key: String): String {
    val map: MutableMap<String, String> = HashMap()
    map["rar"] = "application/x-rar-compressed"
    map["jpg"] = "image/jpeg"
    map["png"] = "image/jpeg"
    map["jpeg"] = "image/jpeg"
    map["zip"] = "application/zip"
    map["pdf"] = "application/pdf"
    map["doc"] = "application/msword"
    map["docx"] = "application/msword"
    map["wps"] = "application/msword"
    map["xls"] = "application/vnd.ms-excel"
    map["et"] = "application/vnd.ms-excel"
    map["xlsx"] = "application/vnd.ms-excel"
    map["ppt"] = "application/vnd.ms-powerpoint"
    map["html"] = "text/html"
    map["htm"] = "text/html"
    map["txt"] = "text/html"
    map["mp3"] = "audio/mpeg"
    map["mp4"] = "video/mp4"
    map["3gp"] = "video/3gpp"
    map["wav"] = "audio/x-wav"
    map["avi"] = "video/x-msvideo"
    map["flv"] = "flv-application/octet-stream"
    map[""] = "*/*"
    return map[key] ?: "application/msword"
}


2,选择文件后,intent 会返回一个 uri,然后将 uri 转为 file


/**
 * uri 转 file
 */
fun uriToFile(context: Context, uri: Uri): File? = when (uri.scheme) {
    ContentResolver.SCHEME_FILE -> uri.toFile()
    ContentResolver.SCHEME_CONTENT -> {
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        cursor?.let { it ->
            if (it.moveToFirst()) {
                //如果是 10.0 以上
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    //保存到本地
                    val ois = context.contentResolver.openInputStream(uri)
                    val displayName =
                        it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
                    ois?.let { input ->
                        val file = File(
                            context.externalCacheDir?.absolutePath + File.separator,
                            displayName
                        )
                        if (file.exists()) file.delete()
                        file.createNewFile()
                        file.outputStream().use { input.copyTo(it) }
                        file
                    }
                } else {
                    //com.blankj:utilcodex:1.30.5
                    UriUtils.uri2File(uri)
                }
            } else {
                it.close()
                null
            }
        }
    }
    else -> null
}


通过以上步骤,就可以将 uri 转成一个 file 对象,并且支持上传。


如果添加了可以打开文件的需求,如何处理?


到此时,文件以及被复制到了沙箱中,你可以对他进行任意处理,但是如果要打开这个文件,则需要使用其他应用来打开,这个时候文件存储在沙箱下面就不行了,因为其他 app 无法获取当前 app 沙箱下的文件。


所以,在这里需要将文件复制到共享目录下面,然后生成对应的 uri,在通过别的 app 打开即可


//打开文件
data.fileData?.file?.also { file ->
    val index = file.name.lastIndexOf(".")
    val suffix = file.name.substring(index + 1, file.name.length)
    //android 10 之后,需要将文件复制到公有目录下,其他应用才可以打开
    showLoading()
    lifecycleScope.launch(Dispatchers.IO) {
        //10.0 以上则将文件复制到共享目录
        val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            FileIntentUtils.copyToDownloadAndroidQ(this@WorkReleaseActivity, suffix, file.inputStream(), file.name,"tidycar")
        } else {
            //否则直接转为 uri
            file.toUri()
        }
        launch(Dispatchers.Main) {
            dismissLoading()
            //打开文件
            FileIntentUtils.openFileEx( uri, suffix, this@WorkReleaseActivity )
        }
    }
}


/**
 * 复制或下载文件到公有目录
 *
 * @param context
 * @param mimeType 文件类型
 * @param input 输入流
 * @param fileName 文件名称
 * @param saveDirName 文件夹名称
 * @return
 */
@RequiresApi(api = Build.VERSION_CODES.Q)
fun copyToDownloadAndroidQ(  context: Context, mimeType: String?, input: InputStream, fileName: String,saveDirName: String): Uri? {
    val file = File(
        Environment.getExternalStorageDirectory().path + "/Download/$saveDirName",
        fileName
    )
    //如果公有目录中已经存在相同文件,则直接返回
    if (file.exists()) {
        return file.toUri()
    }
    if (!FileQUtils.isExternalStorageReadable()) {
        throw RuntimeException("External storage cannot be written!")
    }
    val values = ContentValues()
    //显示名称
    values.put(MediaStore.Downloads.DISPLAY_NAME, fileName)
    //存储文件的类型
    values.put(MediaStore.Downloads.MIME_TYPE, mimeType)
    //公有文件路径
    values.put(
        MediaStore.Downloads.RELATIVE_PATH,
        "Download/" + saveDirName.replace("/".toRegex(), "") + "/"
    )
    //生成一个Uri
    val external = MediaStore.Downloads.EXTERNAL_CONTENT_URI
    val resolver = context.contentResolver
    //写入
    val insertUri = resolver.insert(external, values) ?: return null
    val fos: OutputStream?
    try {
        //输出流
        fos = resolver.openOutputStream(insertUri)
        if (fos == null)  return null
        var read: Int
        val buffer = ByteArray(1444)
        while (input.read(buffer).also { read = it } != -1) {
            //写入uri中
            fos.write(buffer, 0, read)
        }
    } catch (e: java.lang.Exception) {
        e.printStackTrace()
    }
    return insertUri
}


在共享目录中,创建一个文件夹,然后将文件复制进去,最后返回 uri 即可


/**
 * 打开文件
 */
fun openFileEx(uri: Uri?, fileType: String, context: Context) {
    try {
        val intent = Intent()
        intent.action = Intent.ACTION_VIEW
        intent.addCategory("android.intent.category.DEFAULT")
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        // 判断版本大于等于7.0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val builder = VmPolicy.Builder()
            StrictMode.setVmPolicy(builder.build())
        }
        //getMap 在最上面有代码
        intent.setDataAndType(uri, getMap(fileType))
        context.startActivity(intent)
    } catch (e: Exception) {
    }
}


相关文章
|
6月前
|
Web App开发 移动开发 小程序
"项目中mpaas升级到10.2.3 适配Android 14之后 app中的H5以及小程序都访问不了,
"项目中mpaas升级到10.2.3 适配Android 14之后 app中的H5以及小程序都访问不了,显示“网络不给力,请稍后再试”,预发内网版本不能使用,线上版本可以正常使用,这个是什么原因啊,是某些参数没有配置吗,还是说是一些参数改错了?
108 2
|
5月前
|
存储 API 文件存储
47. 【Android教程】SharedPreferences 存储
47. 【Android教程】SharedPreferences 存储
63 2
|
2月前
|
调度 Android开发 UED
Android经典实战之Android 14前台服务适配
本文介绍了在Android 14中适配前台服务的关键步骤与最佳实践,包括指定服务类型、请求权限、优化用户体验及使用WorkManager等。通过遵循这些指南,确保应用在新系统上顺畅运行并提升用户体验。
199 6
|
3月前
|
存储 安全 API
Android经典实战之存储方案对比:SharedPreferences vs MMKV vs DataStore
本文介绍了 Android 开发中常用的键值对存储方案,包括 SharedPreferences、MMKV 和 DataStore,并对比了它们在性能、并发处理、易用性和稳定性上的特点。通过实际代码示例,帮助开发者根据项目需求选择最适合的存储方案,提升应用性能和用户体验。
100 1
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
61 8
|
4月前
|
IDE API Android开发
安卓与iOS开发环境的差异及适配策略
在移动应用开发的广阔舞台上,Android和iOS两大操作系统各据一方,各自拥有独特的开发环境和工具集。本文旨在深入探讨这两个平台在开发环境上的关键差异,并提供有效的适配策略,帮助开发者优化跨平台开发流程。通过比较Android的Java/Kotlin和iOS的Swift/Objective-C语言特性、IDE的选择、以及API和系统服务的访问方式,本文揭示了两个操作系统在开发实践中的主要分歧点,并提出了一套实用的适配方法,以期为移动开发者提供指导和启示。
|
3月前
|
安全 Java Android开发
Android 14适配Google play截止时间临近,适配注意点和经验
本文介绍了Android 14带来的关键更新,包括性能优化、定制化体验、多语言支持、多媒体与图形增强等功能。此外,还强调了适配时的重要事项,如targetSdkVersion升级、前台服务类型声明、蓝牙权限变更等,以及安全性与用户体验方面的改进。开发者需按官方指南更新应用,以充分利用新特性并确保兼容性和安全性。
273 0
|
6月前
|
存储 监控 Java
Android Service之设备存储空间监控 DeviceStorageMonitorService
Android Service之设备存储空间监控 DeviceStorageMonitorService
133 2
|
6月前
|
存储 缓存 安全
Android系统 应用存储路径与权限
Android系统 应用存储路径与权限
333 0
Android系统 应用存储路径与权限
|
6月前
|
编解码 人工智能 测试技术
安卓适配性策略:确保应用在不同设备上的兼容性
【4月更文挑战第13天】本文探讨了提升安卓应用兼容性的策略,包括理解平台碎片化、设计响应式UI(使用dp单位,考虑横竖屏)、利用Android SDK的兼容工具(支持库、资源限定符)、编写兼容性代码(运行时权限、设备特性检查)以及优化性能以适应低端设备。适配性是安卓开发的关键,通过这些方法可确保应用在多样化设备上提供一致体验。未来,自动化测试和AI将助力应对设备碎片化挑战。
687 4
下一篇
无影云桌面