Android 10 和Android 11的適配
背景
最近在專案中著手做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的裝置上 會咋麼樣?
答: 如果按照上面正常適配,肯定完全沒得問題!
以上是自己適配經驗,難免有疏忽之處,如果文章有問題或者更好的建議,歡迎評論指正~
相關教程
Android基礎系列教程:
Android基礎課程UI-佈局_嗶哩嗶哩_bilibili
Android基礎課程UI-控制元件_嗶哩嗶哩_bilibili
Android基礎課程UI-動畫_嗶哩嗶哩_bilibili
Android基礎課程-activity的使用_嗶哩嗶哩_bilibili
Android基礎課程-Fragment使用方法_嗶哩嗶哩_bilibili
Android基礎課程-熱修復/熱更新技術原理_嗶哩嗶哩_bilibili
本文轉自https://juejin.cn/post/7032525748686553095,如有侵權,請聯絡刪除。