背景
> 經過檢測,我們識別到您的應用,目前未適配安卓11(API30),請您關注適配截止時間,盡快開展適配工作,避免影響應用正常發布和經營。
> targetSdkVersion30 升級適配工作參考文檔:(小米開放平臺)[TargetSdk
> 30上架適配指南](https://dev.mi.com/distribute/doc/details?pId=1737)
> 截至日期為2023年12月4日,請問您可以如期適配嘛?
適配內容
主要處理內容
- 請求運行時權限
- 存儲機制更新(強制執行分區存儲)
其他
隱私設置
1.1強制執行分區存儲
1.2閑置應用權限自動重置
1.3后臺位置訪問
1.4應用包可見性 二、安全
2.1堆指針標記
2.2消息框的更新 網絡連接
3.1限制對APN數據庫的讀取訪問 、無障礙服務
4.1在清單文件中聲明與TTS引擎
4.2在元數據文件中聲明"無障礙"… 五、相機
5.1媒體intent操作需要系統默認 應用打包和安裝
6.1壓縮的資源文件
6.2現在需要APK簽名方案v2 ……
十、非SDK接口限制
十一、Google Android 11適配信息…
升級版本
直接從26改到30。
請求運行時權限
- 需要注意的點:閑置應用權限自動重置
- 背景:如果應用以 Android 11 或更高版本為目標平臺并且數月未使用,系統會通過自動重置用戶已授予應用的運行時敏感權限來保護用戶數據。
此操作與用戶在系統設置中查看權限并將應用的訪問權限級別更改為拒絕的做法效果一樣。 - 兼容性影響:如果您的應用以Android 11為目標平臺,若用戶長時間不使用,當用戶再次使用時,若應用沒有權限校驗邏輯則會導致與回收權限相關的業務失效。
- 第二個注意的點:權限對話框的可見性
從 Android 11 開始,在應用安裝到設備上后,如果用戶在使用過程中多次針對某項特定的權限點按拒絕,那么在您的應用再次請求該權限時,用戶將不會看到系統權限對話框。 - 該操作表示用戶希望“不再詢問”。在之前的版本中,除非用戶先前已選中“不再詢問”對話框或選項,否則每當您的應用請求權限時,用戶都會看到系統權限對話框。
- Android 11 中的這一行為變更旨在避免重復請求用戶已選擇拒絕的權限。
權限優化
- 補充項目中各處具體業務權限遺漏申請的地方。
- 將一些會提前申請權限的操作后置,在真正使用的地方再按需調用
- 補充了權限使用說明彈窗和授權提示彈窗。
- 將項目中多個權限申請方式統一,改成用XPermission。
原先項目中權限庫的不足
- 無法監控權限是否曾經被禁止過。
- 因為:它是基于新增一個Activity,然后這個Activity攔截了申請回調,并將回調很局限的區分成拒絕和通過。它所現有的暴露的api無法滿足當前的需求。
- 這個庫比較舊,沒有繼續更新,后續Android版本適配的高版本容易存在風險。
新庫的選擇
- 它本身一直在穩定地更新。
- 它更輕量。
- 它能滿足舊庫所不能滿足的。
- 它能夠將權限從說明到跳轉設置到成功失敗等一系列操作全部考慮到并鏈式配置。
fun requestPermission(context: FragmentActivity, sureBlock: ()->Unit, permissions: List<String>){PermissionX.init(context).permissions(permissions).onForwardToSettings { scope, deniedList ->scope.showForwardToSettingsDialog(PermissionToSettings(context, deniedList))}.request { allGranted, _, _ ->if (allGranted) {sureBlock.invoke()}}}
關于新庫的版本問題
implementation 'com.guolindev.permissionx:permissionx:1.5.0'
直接執行新庫,會報錯
:app:checkPpDebugAarMetadata 7 errors
1 sec, 436 ms
org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: occurred while executing com.android.build.gradle.internal.tasks.CheckDuplable
java.lang.RuntimeException: Duplicate class androidx.lifecycle.ViewModelLamodules jetified-lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycleodel-ktx:2.3.1) and lifecycle-viewmodel-2.5.0-runtime (androidx.lifecycle:
org.gradle.apii.internal.tasks.execution.ExecuteActionsTaskExecuter$MultipFailures: Multiple task action failures occurred:
java.lang.RuntimeException: The minCompileSdk (32) specified in a
java.lang.RuntimeException: The minCompileSdk (31) specified in a
java.lang.RuntimeException: The minCompileSdk (32) specified in a
java lang RuntimeException: The minCompileSdk (31) specified in a
分析原因是項目所適配的最低版本和sdk的不適配。
因此,不能直接用1.7.1;改用了1.5.0.
后續等項目適配到Android13之后,可將新庫的版本更新到最新即可。
存儲機制更新
1.1 強制執行分區存儲
背景
為了讓用戶更好地管理自己的文件并減少混亂,以 Android 10(API 級別
29)及更高版本為目標平臺的應用在默認情況下被授予了對外部存儲空間的分區訪問權限(即分區存儲)。此類應用只能訪問外部存儲空間上的應用專屬目錄,以及本應用所創建的特定類型的媒體文件。在 Android 11 上運行但以 Android 10(API 級別 29)為目標平臺的應用仍可請求
requestLegacyExternalStorage
屬性。應用可以利用此標記暫時停用與分區存儲相關的變更,例如授予對不同目錄和不同類型的媒體文件的訪問權限。當您將應用更新為以 Android
11 為目標平臺后,系統會忽略 requestLegacyExternalStorage 標記。
兼容影響
當您將應用更新為以 Android 11 為目標平臺后,您將無法使用requestLegacyExternalStorage,而且也沒有其他標記可以提供停用分區存儲。
分區存儲對于App訪問存儲方式、App數據存放以及App間數據共享,都產生很大影響。
而Environment.getExternalStorageDirectory() 在 API Level 29 開始已被棄用,開發者應遷移至 Context#getExternalFilesDir(String), MediaStore, 或Intent#ACTION_OPEN_DOCUMENT。
適配建議
如果您將應用專屬文件存儲在外部存儲空間中,則可以將這些文件存放在外部存儲空間中的應用專屬目錄內,以便更加輕松地采用分區存儲。這樣,在啟用分區存儲后,您的應用將可以繼續訪問這些文件。
如需讓您的應用適合分區存儲,請參閱存儲用例和最佳實踐指南。
具體適配參考:
- https://developer.android.google.cn/training/data-storage#scoped-storage
- https://developer.android.google.cn/preview/privacy/storage
- https://developer.android.google.cn/training/data-storage/use-cases?hl=zh-cn
----------------------------------------------------------------------------------------------------.
需要重點適配的內容:
- 檢查使用到的第三方庫是否存在適配上的問題。(比如PictureSelector、Matisse、ShareSDK等第三方庫就需要做適配處理)
- 鴻蒙系統的測試機在瀏覽相冊功能有偶現異常現象
- 處理下載的圖片視頻資源同步更新到系統相冊。
- 拍照、圖片上傳和壓縮緩存處理。
- 驗證了不同系統的測試機(Android 9、10、11、13。HarmonyOS 3.00。)
關鍵步驟
涉及到文件儲存的功能。
通過官方推薦的方式來替換掉關鍵代碼:
Environment.getExternalStorageDirectory()
如果直接使用以上代碼進行存儲的話,會報一下錯誤。
FileNotFoundException xxxxxxxxxx open failed: EPERM (Operation not permitted)
可以使用
context.getExternalFilesDir(null)?.absolutePath)
來存儲,目錄為:/storage/emulated/0/Android/data/com.xxx/files
關于鴻蒙系統比較特殊的點
resizeBmp = BitmapFactory.decodeFile(file.getPath(), opts);
場景:當通過相冊拿到外部文件路徑之后,使用以上代碼獲取bitmap,會偶現失敗。檢查權限相關無異常,但是會偶爾報這個錯誤
Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/DCIM/com.xxx/1699410185196.jpg: open failed: EACCES (Permission denied)
通過調試和看日志,沒能找到相關獲取bitmap失敗的其他日志。根據以上的這條error日志,判斷是否鴻蒙系統對這塊的適配有問題。因此,在華為的包中,添加一下代碼:
<manifest ... ><application android:requestLegacyExternalStorage="true" ...>...</application>
</manifest>
雖然在官方文檔中,表示:
當您將應用更新為以 Android 11
為目標平臺后,您將無法使用requestLegacyExternalStorage,而且也沒有其他標記可以提供停用分區存儲。
但是,通過這種方式進行設置,確實能避免鴻蒙系統會存在偶現失敗的場景發生。
Permission to access file: /storage/emulated/0/Mob/comm/locks/.dhlock is denied uid = 10242java.lang.SecurityException: com.yishouapp.fumi has no access to content://media/external_primary/file/1000000118at com.android.providers.media.MediaProvider.enforceCallingPermissionInternal(MediaProvider.java:10152)at com.android.providers.media.MediaProvider.enforceCallingPermission(MediaProvider.java:10049)at com.android.providers.media.MediaProvider.checkAccess(MediaProvider.java:10177)at com.android.providers.media.MediaProvider.checkIfFileOpenIsPermitted(MediaProvider.java:9068)
項目中一些第三方庫的調整:
圖片選擇器PictureSelector
報錯日志:
Unable to decode stream: java.io.FileNotFoundException:
/storage/emulated/0/DCIM/com.xxx/1699410185196.jpg: open
failed: EACCES (Permission denied)
- 檢查權限申請代碼等細節,一切正常。
這個庫需要更新到高版本才能適配
'com.github.LuckSiege.PictureSelector:picture_library:v2.6.0'
因為項目中用到了camera-view這個庫。因此,這個庫的相關代碼調用,也需要做代碼上的適配,會有一些api上的改動。
知乎圖片選擇器Matisse
com.android.providers.media.module Permission to access file:
/storage/emulated/0/Mob/comm/lockss/.dhlock is denied uid = 10242
java.lang.SecurityException: com.yishouapp.fumi has no access to
conttent://media/external_primary/file/100000000000118 at
com.android.providers.media.MediaProvider.enforceCallingPermissiionInternal(MediaProvider.java:10152)
at
com.android.providers.media.MediaProvider.enforceCallingPermission(MediaProvider.java:10049)
at
com.android.providers.media.MediaProvider.checkAccess(MediaProvider.java:10177)
at com.android.
roviders.media.MediaProvider.checkIfFileOpenIsPermitted(MediaProvider.java:9068)
at
com.android.providers.media.MediaProvider.onFileOpenForFusse(MediaProvider.java:9181)
FATAL EXCEPTION: ModernAsyncTask #1 com.yishouapp.fumi Process:
com.yishouapp.fumi, PID:30137 java.lang.RuntimeException: An error
occurred while executing doInBackground( at
androidx.loader.content.ModernAsyncTask 3. d o n e ( M o d e r n A s y n c T a s k . j a v a : 164 ) C a u s e d b y : j a v a . l a n g . I l l e g a l A r g u m e n t E x c e p t i o n : I n v a l i d c o l u m n C O U N T ( ? ) A S c o u n t < 6 i n t e r n a l l i n e s > a t a n d r o i d . d a t a b a s e . D a t a b a s e U t i l s . r e a d E x c e p t i o n F r o m P a r c e l ( D a t a b a s e u t i l s . j a v a : 172 ) a t a n d r o i d . d a t a b a s e . D a t a b a s e U t i l s . r e a d E x c e p t i o n F r o m P a r c e l ( D a t a b a s e u t i l s . j a v a : 142 a t a n d r o i d . c o n t e n t . C o n t e n t P r o v i d e r P r o x y . q u e r y ( C o n t e n t P r o v i d e r N a t i v e j a v a : 481 ) a t a n d r o i d . c o n t e n t . C o n t e n t R e s o l v e r . q u e r y ( C o n t e n t R e s o l v e r . j a v a : 1221 a t a n d r o i d . c o n t e n t . C o n t e n t R e s o l v e r . q u e r y ( C o n t e n t R e s o l v e r . j a v a : 1152 a t a n d r o i d x . c o r e . c o n t e n t . C o n t e n t R e s o l v e r C o m p a t . q u e r y ( C o n t e n t R e s o l v e r C o m p a . j a v a : 81 a t a n d r o i d x . l o a d e r . c o n t e n t . C u r s o r L o a d e r . l o a d I n B a c k g r o u n d ( C u r s o r L o a d e r . j a v a : 63 ) a t c o m . z h i h u . m a t i s s e . i n t e r n a l . l o a d e r . A l b u m L o a d e r . l o a d I n B a c k g r r o u n d ( A l b u m L o a d e r . j a v a : 98 a t c o m . z h i h u . m a t i s s e . i n t e r n a l . l o a d e r . A l b u m L o a d e r . l o a d I i n B a c k g r o u n d ( A l b u m L o a d e r . j a v a : 34 ) a t a n d r o i d x . l o a d e r . c o n t e n t . A s y n c T a s k L o a d e r . o n L o a d I n B a c k g r o u n d ( A s y n c T a s k L o a d e r . j a v a : 307 ) a t a n d r o i d x . l o a d e r . c o n t e n t . A s y n c T a s k L o a d e r 3.done(ModernAsyncTask.java:164) Caused by: java.lang.IllegalArgumentException: Invalid column COUNT(*) AS count <6 internal lines> at android.database.DatabaseUtils.readExceptionFromParcel(Databaseutils.java:172) at android.database.DatabaseUtils.readExceptionFromParcel(Databaseutils. java:142 at android.content.ContentProviderProxy.query (ContentProviderNativejava:481) at android.content.ContentResolver.query(ContentResolver.java:1221 at android.content.ContentResolver.query(ContentResolver.java:1152 at androidx.core.content.ContentResolverCompat.query (ContentResolverCompa.java:81 at androidx.loader.content.CursorLoader.loadInBackground(CursorLoader.java:63) at com.zhihu.matisse.internal.loader.AlbumLoader.loadInBackgrround(AlbumLoader.java:98 at com.zhihu.matisse.internal.loader.AlbumLoader.loadIinBackground(AlbumLoader.java:34) at androidx.loader.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:307) at androidx.loader.content.AsyncTaskLoader 3.done(ModernAsyncTask.java:164)Causedby:java.lang.IllegalArgumentException:InvalidcolumnCOUNT(?)AScount<6internallines>atandroid.database.DatabaseUtils.readExceptionFromParcel(Databaseutils.java:172)atandroid.database.DatabaseUtils.readExceptionFromParcel(Databaseutils.java:142atandroid.content.ContentProviderProxy.query(ContentProviderNativejava:481)atandroid.content.ContentResolver.query(ContentResolver.java:1221atandroid.content.ContentResolver.query(ContentResolver.java:1152atandroidx.core.content.ContentResolverCompat.query(ContentResolverCompa.java:81atandroidx.loader.content.CursorLoader.loadInBackground(CursorLoader.java:63)atcom.zhihu.matisse.internal.loader.AlbumLoader.loadInBackgrround(AlbumLoader.java:98atcom.zhihu.matisse.internal.loader.AlbumLoader.loadIinBackground(AlbumLoader.java:34)atandroidx.loader.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:307)atandroidx.loader.content.AsyncTaskLoaderLoadTask.doInBackground(AsyncTaskLoader.java:60
at
androidx.loader.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:48
at
androidx.loader.content.ModernAsyncTask$2.call(ModernAsyndTask.java:141)
<4 internal lines
該庫已經從19年開始就不再維護了。因此,本次調整,將替換掉這個庫。
微信分享問題排查
- ShareSDK使用權限情況 ShareSDK使用權限情況
- 棄用這個庫,改用自己寫。
- 具體參考:分享與收藏-Android開發手冊
- 特殊需求,上面官方的開發手冊中,很詳細地講解了各種分享方式和場景,但是并不支持分享隊長圖片到微信中來。因為業務需求需要,需要實現分享多張圖片,因此通過下面這種Intent跳轉攜帶Extra的方式來實現。
/分享的圖片集合
val imageUris = ArrayList<Uri>()
bitmaps.forEach {imageUris.add(SaveUtils.saveBitmapToAlbumForShare(activity, it))
}//分享到微信好友
activity.startActivity(Intent().apply {component = ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareImgUI")action = Intent.ACTION_SEND_MULTIPLEtype = "image/*"putExtra(Intent.EXTRA_STREAM, imageUris)
})
升級騰訊云orcsdk
- 需要更新庫的升級,并且需要添加混淆。
api調用方面,也需要做同步更改:
// 啟動 ocr 識別,識別類型為身份證正面
OcrSDKKit.getInstance().startProcessOcr(MainActivity.this, OcrType.IDCardOCR_FRONT, customConfigUi, new ISDKKitResultListener() {@Overridepublic void onProcessSucceed(String response, String srcBase64Image, String requestId) {popTip(response, "Succeed"); // 展示 ocr 識別結果}@Overridepublic void onProcessFailed(String errorCode, String message, String requestId) {popTip(message, errorCode); // 展示 ocr 識別錯誤信息}
});
總結:存儲這塊,很多項目中使用到的第三方庫都沒有做很好兼容。需要去仔細排查然后去做適配或者廢棄替換工作。整體還是存在風險的,因此不會全量覆蓋所有渠道,逐步覆蓋了。
文件路徑統一整理
- 兼容高低版本,統一處理保存主路徑。
- 整理統一圖片、視頻、文件、h5緩存下載保存等路徑。
關于共享
注意一個點,如果要處理保存圖片/視頻,并同步到相冊的話。則不能直接將文件資源存儲在私有目錄中,私有目錄的文件無法共享到相冊中。可以通過MediaStore來操縱,具體可以看文檔:
訪問共享存儲空間中的媒體文件
心得總結
- 本次適配,涉及了整體app的權限申請優化,識別、分享、相冊、存儲方案的統一調整,以及第三方庫版本依賴相關的內容。改動代碼量比較大,涉及到的業務頁面也較多,還發現了一些隱藏至今的歷史遺留bug。
- 項目比較久了,經歷的人手多了,避免不了存在一些臟代碼。重構代碼不僅在做技術優化需求中進行,平時的迭代甚至改bug的過程中,也可以及時執行。所以說,平時代碼的review是必不可少的,這樣可以規范很多。
- 另外,一些能統一封裝的代碼也需要規整,像項目中有三種權限方式,兩個權限申請第三方庫這種,還是需要避免的,最好統一入口封裝,如果避免不了需要使用不同方案處理,建議使用策略模式來設計,而當一個功能的實現涉及到需要相冊、存儲、識別、分享等多個方案的話,也可以采用外觀模式來進行設計,代碼設計規范,后續改動、迭代、優化都會事半功倍。
- 而且保存文件的入口和工具類也比較雜,隨處放一點,需要整改的時候,排查起來也比較費勁并且容易遺漏。再一個是第三方庫版本需要統一,避免不同module引入統一個庫不同版本的問題。