背景
最近在项目中着手做Android10
和Android11
适配时候,期间遇到了不少的坑。之前有专门写过qq、微信分享的适配。但是此次在针对偏业务侧适配工作的时候还是碰到了一些新的问题。记录下来,方便以后查阅,希望能帮到碰到此问题的相关同学。
一、 私有目录下资源访问
存在这样一个场景:我们要分享一张图片到qq或者微信,首先第一步是要是得到这个bitmap(通过本地生成或者网络加载),然后存储到本地sd卡上,最后把存储的图片的绝对路径传给qq或者微信即可。
在以上的场景中,涉及到了这些关键点:
- 把图片存储到sd卡
- 把绝对路径path传递给qq或者微信
1.1 直接访问sd卡的根目录
通过FileOutPutStream来完成,在Android10以下都没问题。路径如下:
/storage/emulated/0/demo/sharePicture/1637048769163_share.jpg
但是在Android10及以上
,就会存在会报错:
java.io.FileNotFoundException: /storage/emulated/0/demo/sharePicture/1637048769163_share.jpg: open failed: EACCES (Permission denied)
//其实存储权限是同意了的
这是因为,我们被存储分区限制了,不能直接访问外部目录。因此,我们需要修改存储路径为scope的App-specific目录。
1.2 改为App-specific私有目录
该目录自己访问不需要权限,如果第三方访问需要权限! 因此,我们后面通过FileProvider
去临时授权即可。 如果对 FileProvider
不熟悉,可参考篇头的文章。
/storage/emulated/0/Android/data/com.demo.test/files
当你再通过FileOutPutStream
来存储图片时候,是成功的。
private fun saveImage(bitmap: Bitmap, storePath: String, filePath: String): Boolean {
val appDir = File(storePath)
if (!appDir.exists()) {
appDir.mkdirs()
}
val file = File(filePath)
if (file.exists()) {
file.delete()
}
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
return true
} catch (e: IOException) {
e.printStackTrace()
} catch (e: FileNotFoundException) {
e.printStackTrace()
} finally {
fos?.close()
}
return false
}
经过测试,在29的下和29 的设备下,分享qq、微信都成功了。
1.3 分享原理总结
分享的本质就是把图片路径
给qq或微信
访问,让他们能够访问到我们的图片。分区之前是存储在外部sd卡,都没有问题。
分区后,qq或微信
没法访问的我们的私有目录App-specific
。因此,我们需要通过 fileprovider
转换成 content:// 格式
去分享,临时授权给 qq或微信
来访问我们的图片。
qq是内部自己做了 fileprovider
适配,因此,我们只需要传入绝对路径 file://
格式即可,而微信是需要接收 content://
格式,所以需要我们外部自己来转换。
具体的适配逻辑参考篇头的文章~
二、公共目录下资源访问
Google建议我们采用 mediaStore
或者 SAF
去访问。在Android10
上公共目录下的图片无法通过file:// 格式
去访问,提示找不到路径。如glide加载、图片选择库、裁剪框架等等都会收到影响。
但是,这里有个坑: 在Android10上不行,在Android11上又可以!!为什么?
因为Google改回来了,让Android11支持file://
格式了。。。。 (wtf? 我谢谢你啊~~)
**我这里说的 Android10
和 android 11
是指 targetSdkVersion
哦 **
2.1 往公共目录插入一张图片
只能通过mediaStore方式:
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.TITLE, "Image.png");
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
//这里就能拿到这个insertUri
Uri insertUri = resolver.insert(external, values);
LogUtil.log("insertUri: " + insertUri);
OutputStream os = null;
try {
if (insertUri != null) {
os = resolver.openOutputStream(insertUri);
}
if (os != null) {
final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, os);
// write what you want
}
} catch (IOException e) {
LogUtil.log("fail: " + e.getCause());
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
LogUtil.log("fail in close: " + e.getCause());
}
}
2.2 content uri转file格式路径
public static String getFilePathFromContentUri(Uri selectedVideoUri,
ContentResolver contentResolver) {
String filePath;
String[] filePathColumn = {MediaStore.MediaColumns.DATA};
Cursor cursor = contentResolver.query(selectedVideoUri, filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
filePath = cursor.getString(columnIndex);
cursor.close();
return filePath;
}
2.3 根据图片名来获取file格式路径
String imageName="test";
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = BaseApp.getContext().getContentResolver();
String selection = MediaStore.Images.Media.TITLE + "=?";
String[] args = new String[] {imageName};
String[] projection = new String[] {MediaStore.Images.Media._ID};
Cursor cursor = resolver.query(external, projection, selection, args, null);
// 这里的得到content 格式的uri
Uri imageUri = null;
//content://media/external/images/media/318952
if (cursor != null && cursor.moveToFirst()) {
imageUri = ContentUris.withAppendedId(external, cursor.getLong(0));
cursor.close();
}
拿到绝对路径后,在Android11上都 glide、qq分享、第三方的图片选择框架等都可以正常访问。
三、终极适配方案
- 在Android10上
开启标志位 :android:requestLegacyExternalStorage="true"
来开启兼容模式,关闭分区适配,相当于targetSdkVersion=29
的时候还是以旧的方式运行,完全没问题。完美避开无法访问公共目录的坑!!!
- 在Android11上
以上标志会自动失效。因此,应用存储的东西还在放在App-specific目录下。分享私有目录可以通过fileprovider
方式适配。 要分享公共目录,因为支持File api
直接访问公共目录,因此,可以直接把content格式
转成file格式
即可,具体可回看文中的第二部分。
最后,我还想问两个问题:
1. targetSdk=30,android:requestLegacyExternalStorage="false"运行在Android10的设备上 会咋么样?
答: 肯定会碰到权限问题。因为,Android10
的设备还是以Android10
的兼容模式运行的。所以要改成true
。
2. targetSdk=30,android:requestLegacyExternalStorage="false"运行在Android11的设备上 会咋么样?
答: 如果按照上面正常适配,肯定完全没得问题!
以上是自己适配经验,难免有疏忽之处,如果文章有问题或者更好的建议,欢迎评论指正~
本文转自 https://juejin.cn/post/7032525748686553095,如有侵权,请联系删除。