Android Q 越來越近了,最近 Google 又發(fā)布了 Android Q Beta 的第五個(gè)版本,眼瞅著這進(jìn)度,在今年?Q3 季度,Android Q 就正式和用戶見面了,在此之前,開發(fā)者必然又是面臨的一波讓人頭疼的適配。
了解新特性,首推應(yīng)該去看官方文檔,官方已經(jīng)給出了一份完整的新特性文檔,在發(fā)布的這段時(shí)間,也一直在保持同步的更新。而作為開發(fā)者,我們更關(guān)心的是如何解決在我們現(xiàn)有的 App 上,保證 Android Q 的兼容性問題。
今天就給推薦給大家一份適配文檔,以開發(fā)者的角度列一份適配清單,在 Android Q 還沒來之前,先了解需要做什么,以及怎么做,到時(shí)候才不至于措手不及。
這份文檔的出自 OPPO 開放平臺(tái),可能有人會(huì)覺得是 KPI 工程,但是你想想這些廠商每年耗巨資研發(fā)的旗艦機(jī),用著最新的硬件,當(dāng)然要搭配最新的系統(tǒng),而用戶在旗艦機(jī)上的體驗(yàn),也是他們最關(guān)心的,所以每次 Android 發(fā)布新系統(tǒng),這些廠商也在推進(jìn)自己應(yīng)用市場上 App 的適配工作。
你只需要想想他們做這件事的動(dòng)機(jī),就能知道這份文檔肯定是花了心思的。文檔我看過一遍,從場景出發(fā)來分析原因,并附上解決方案,很有參考意義。
文檔比較長,大家可以先收藏,再跳躍閱讀看自己關(guān)注的點(diǎn)。
一. 背景說明
本文檔是基于谷歌安卓 Q 的 beta4 版本的變更輸出的兼容性整改指導(dǎo),如果后續(xù) beta 版本有新的變更和新的特性,我們也會(huì)刷新文檔的相關(guān)章節(jié)內(nèi)容,請(qǐng)開發(fā)者持續(xù)關(guān)注。
二. 存儲(chǔ)空間限制
2.1 背景
為了讓用戶更好地控制自己的文件,并限制文件混亂的情況,Android Q 修改了 APP 訪問外部存儲(chǔ)中文件的方法。外部存儲(chǔ)的新特性被稱為 Scoped Storage。
Android Q 仍然使用 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 作為面向用戶的存儲(chǔ)相關(guān)運(yùn)行時(shí)權(quán)限,但現(xiàn)在即使獲取了這些權(quán)限,訪問外部存儲(chǔ)也受到了限制。APP 需要這些運(yùn)行時(shí)權(quán)限的情景發(fā)生了變化,且各種情況下外部存儲(chǔ)對(duì) APP 的可見性也發(fā)生了變化。
在 Scoped Storage 新特性中,外部存儲(chǔ)空間被分為兩部分:
●?公共目錄?:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones 等
公共目錄下的文件在 APP 卸載后,不會(huì)刪除。
APP 可以通過 SAF(System Access Framework)、MediaStore 接口訪問其中的文件。
●?App-specific 目錄
APP 卸載后,數(shù)據(jù)會(huì)清除。
APP 的私密目錄,APP 訪問自己的 App-specific 目錄時(shí)無需任何權(quán)限。
Android Q 規(guī)定了 APP 有兩種外部存儲(chǔ)空間視圖模式:Legacy View、Filtered View。
●?Filtered View
App 可以直接訪問 App-specific 目錄,但不能直接訪問 App-specific 外的文件。訪問公共目錄或其他 APP 的 App-specific 目錄,只能通過 MediaStore、SAF、或者其他 APP 提供的 ContentProvider、FileProvider 等訪問。
●?Legacy View
兼容模式。與 Android Q 以前一樣,申請(qǐng)權(quán)限后 App 可訪問外部存儲(chǔ),擁有完整的訪問權(quán)限。
在 Android Q 上,target SDK 大于或等于 29 的 APP 默認(rèn)被賦予 Filtered View,反之則默認(rèn)被賦予 Legacy View。APP 可以在?AndroidManifest.xml?中設(shè)置新屬性?requestLegacyExternalStorage?來修改外部存儲(chǔ)空間視圖模式,true 為 Legacy View,false 為 Filtered View。可以使用?Environment.isExternalStorageLegacy()?這個(gè) API 來檢查 APP 的運(yùn)行模式。APP 開啟 Filtered View 后,Scoped Storage 新特性對(duì) APP 生效。
Android Q 除了劃分外部存儲(chǔ)和定義 Filtered View,還在查詢、讀寫文件的一些細(xì)節(jié)上做了改進(jìn)或限制,例如圖片文件中的地理位置信息將不再默認(rèn)提供、查詢 MediaProvider 獲得的 DATA 字段不再可靠、新增了文件的 Pending 狀態(tài)等等。這些細(xì)節(jié)的具體內(nèi)容請(qǐng)參考適配方案章節(jié)。
2.2 ?兼容性影響
Scoped Storage 對(duì)于 APP 訪問外部存儲(chǔ)方式、APP 數(shù)據(jù)存放以及 APP 間數(shù)據(jù)共享,都產(chǎn)生很大影響。請(qǐng)開發(fā)者注意以下的兼容性影響事項(xiàng)。
2.2.1 無法新建文件
問題原因:直接使用自身 App-specific 目錄以外的路徑新建文件。
問題分析:在 Android Q 上,APP 只允許在自身 App-specific 目錄以內(nèi)通過路徑生成的文件。
解決方案:APP 自身 App-specific 目錄下新建文件的方法與文件路徑,請(qǐng)參見 2.3.1;如果要在公共目錄下新建文件,使用 MediaStore 接口,請(qǐng)參見 2.3.2;如果要在任意目錄下新建文件,需要使用 SAF,請(qǐng)參見 2.3.3。
2.2.2 無法訪問存儲(chǔ)設(shè)備上的文件
問題原因 1:直接使用路徑訪問公共目錄文件。
問題分析 1:在 Android Q 上,APP 默認(rèn)只能訪問外部存儲(chǔ)設(shè)備上的 App-specific 目錄。
解決方法 1:參見 2.3.2 和 2.3.3,使用 MediaStore 接口訪問公共目錄中的多媒體文件,或者使用 SAF 訪問公共目錄中的任意文件。注意:從 MediaStore 接口中查詢到的 DATA 字段將在 Android Q 開始廢棄,不應(yīng)該利用它來訪問文件或者判斷文件是否存在;從 MediaStore 接口或者 SAF 獲取到文件 Uri 后,請(qǐng)利用 Uri 打開 FD 或者輸入輸出流,而不要轉(zhuǎn)換成文件路徑去訪問。
問題原因 2:使用 MediaStore 接口訪問非多媒體文件。
問題分析 2:在 Android Q 上,使用 MediaStore 接口只能訪問公共目錄中的多媒體文件。
解決方法 2:使用 SAF 向用戶申請(qǐng)文件或目錄的讀寫權(quán)限,請(qǐng)參見 2.3.3。
2.2.3 無法正確分享文件
問題原因:APP 將 App-specific 目錄中的私有文件分享給其他 APP 時(shí),使用了?file://?類型的 Uri。
問題分析:在 Android Q 上,由于 App-specific 目錄中的文件是私有受保護(hù)的,其他 APP 無法通過文件路徑訪問。
解決方案:參見 2.3.4,使用 FileProvider,將?content://?類型的 Uri 分享給其他 APP。
2.2.4 無法修改存儲(chǔ)設(shè)備上的文件
問題原因 1:直接使用路徑訪問公共目錄文件。
問題分析 1:同 2.2.2。
解決方案 1:同 2.2.2,請(qǐng)使用正確的公共目錄文件訪問方式。
問題原因 2:使用 MediaStore 接口獲取公共目錄多媒體文件的 Uri 后,直接使用該 Uri 打開 OutputStream 或文件描述符。
問題分析 2:在 Android Q 上,修改公共目錄文件,需要用戶授權(quán)。
解決方案 2:從 MediaStore 接口獲取公共目錄多媒體文件 Uri 后,打開 OutputStream 或 FD 時(shí),注意 catch RecoverableSecurityException,然后向用戶申請(qǐng)?jiān)摱嗝襟w文件的刪改權(quán)限,請(qǐng)參見 2.3.2.6;使用 SAF 獲取到文件或目錄的 Uri 時(shí),用戶已經(jīng)授權(quán)讀寫,可以直接使用,但要注意 Uri 權(quán)限的時(shí)效,請(qǐng)參見 2.3.3.6。
2.2.5 應(yīng)用卸載后文件意外刪除
問題原因:將想要保留的文件保存在外部存儲(chǔ)的 App-specific 目錄下。
問題分析:在 Android Q 上,卸載 APP 默認(rèn)刪除 App-specific 目錄下的數(shù)據(jù)。
解決方案:APP 應(yīng)該將想要保留的文件通過 MediaStore 接口保存到公共目錄下,請(qǐng)參見 2.3.2。默認(rèn)情況下,MediaStore 接口會(huì)將非媒體類文件保存到 Downloads 目錄下,推薦 APP 指定一級(jí)目錄為 Documents。如果 APP 想要在卸載時(shí)保留 App-specific 目錄下的數(shù)據(jù),要在 AndroidManifest.xml 中聲明 android:hasFragileUserData="true",這樣在 APP 卸載時(shí)就會(huì)有彈出框提示用戶是否保留應(yīng)用數(shù)據(jù)。
2.2.6 無法訪問圖片文件中的地理位置數(shù)據(jù)
問題原因:直接從圖片文件輸入流中解析地理位置數(shù)據(jù)。
問題分析:由于圖片的地理位置信息涉及用戶隱私,Android Q 上默認(rèn)不向 APP 提供該數(shù)據(jù)。
解決方案:申請(qǐng) ACCESS_MEDIA_LOCATION 權(quán)限,并使用 MediaStore.setRequireOriginal() 接口更新文件 Uri,請(qǐng)參見 2.3.5.1 。
2.2.7 Fota 升級(jí)問題
問題原因:Fota 升級(jí)后,APP 被卸載,重新安裝后無法訪問到 APP 數(shù)據(jù)。
問題分析:Scoped Storage 新特性只對(duì) Android Q 上新安裝的 APP 生效。設(shè)備從 Android Q 之前的版本升級(jí)到 Android Q,已安裝的 APP 獲得 Legacy View 視圖。這些 APP 如果直接通過路徑的方式將文件保存到了外部存儲(chǔ)上,例如外部存儲(chǔ)的根目錄,那么 APP 被卸載后重新安裝,新的 APP 獲得 Filtered View 視圖,無法直接通過路徑訪問到舊數(shù)據(jù),導(dǎo)致數(shù)據(jù)丟失。
解決方案:APP 應(yīng)該修改保存文件的方式,不再使用路徑的方式直接保存,而是采用 MediaStore 接口將文件保存到對(duì)應(yīng)的公共目錄下。在 Fota 升級(jí)前,可以將 APP 的用戶歷史數(shù)據(jù)通過 MediaStore 接口遷移到公共目錄下。此外,APP 應(yīng)當(dāng)改變?cè)L問 App-specific 目錄以外的文件的方式,請(qǐng)使用 MediaStore 接口或者 SAF。
2.3 適配指導(dǎo)
Android Q Scoped Storage 新特性谷歌官方適配文檔:
https://developer.android.google.cn/preview/privacy/scoped-storage
OPPO 適配指導(dǎo)如下,分為:訪問 APP 自身 App-specific 目錄文件、使用 MediaStore 訪問公共目錄、使用 SAF 訪問指定文件和目錄、分享 App-specific 目錄下文件和其他細(xì)節(jié)適配。
2.3.1 訪問 APP 自身 App-specific 目錄文件
無需任何權(quán)限,APP 即可直接使用文件路徑來讀寫自身 App-specific 目錄下的文件。獲取 App-specific 目錄路徑的接口如下表所示。
如下,以新建并寫入文件為例。
//set"Documents"assubDirfinal File[] dirs = getExternalFilesDirs("Documents");File primaryDir =null;if(dirs !=null&& dirs.length >0) {? ? primaryDir = dirs[0];}if(primaryDir ==null) {return;}File newFile =newFile(primaryDir.getAbsolutePath(),"MyTestDocument");OutputStream fileOS =null;try{? ? fileOS =newFileOutputStream(newFile);if(fileOS !=null) {? ? ? ? fileOS.write("file is created".getBytes(StandardCharsets.UTF_8));? ? ? ? fileOS.flush();? ? }}catch(IOException e) {? ? LogUtil.log("create file fail");}finally{try{if(fileOS !=null) {? ? ? ? ? ? fileOS.close();? ? ? ? }? ? }catch(IOException e1) {? ? ? ? LogUtil.log("close stream fail");? ? }}
2.3.2 使用 MediaStore 訪問公共目錄
APP 無法直接訪問公共目錄下的文件。MediaStore 為 APP 提供了訪問公共目錄下媒體文件的接口。APP 在有適當(dāng)權(quán)限時(shí),可以通過 MediaStore 查詢到公共目錄文件的 Uri,然后通過 Uri 讀寫文件。
MediaStore 相關(guān)的 Google 官方文檔:
https://developer.android.google.cn/reference/android/provider/MediaStore
2.3.2.1 MediaStore 的 Uri 和路徑對(duì)照表
MediaStore 提供了下列幾種類型的訪問 Uri,通過查詢對(duì)應(yīng) Uri 數(shù)據(jù)(在 MediaProvider 中),達(dá)到訪問的目的。
下列每種類型又分為三種 Uri:Internal、External、可移動(dòng)存儲(chǔ)。
在 Android Q 上,所有的外部存儲(chǔ)設(shè)備,包括內(nèi)置卡、SD 卡等,都會(huì)被命名,即設(shè)備的 Volume Name。?MediaStore 可以通過 Volume Name 獲取對(duì)應(yīng)存儲(chǔ)設(shè)備的 Uri。
for(StringvolumeName : MediaStore.getExternalVolumeNames(this)){? ? MediaStore.Images.Media.getContentUri(volumeName);}
MediaProvider 對(duì)于 APP 新建到公共目錄的文件,通過 ContentResolver.insert 方法中的 Uri 來確定具體存放目錄。其中下表中
content://media//>
2.3.2.2 APP 通過 MediaStore 訪問文件所需要的權(quán)限
通過 MediaStore 提供的 Uri,使用 ContentResolver 的 insert 接口,將文件保存到公共目錄下。不同的 Uri,可以保存到不同的公共目錄中,詳見 2.3.2.1。
ContentValues values =newContentValues();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();Uri insertUri = resolver.insert(external, values);LogUtil.log("insertUri: "+ insertUri);OutputStream os =null;try{if(insertUri !=null) {? ? ? ? os = resolver.openOutputStream(insertUri);? ? }if(os !=null) {finalBitmap 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.3.2.4 使用 MediaStore 查詢文件
用 MediaStore 提供的 Uri 指定設(shè)備,selection 參數(shù)指定過濾條件,通過 ContentResolver.query 接口查詢文件 Uri。
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;ContentResolver resolver = context.getContentResolver();Stringselection = MediaStore.Images.Media.TITLE +"=?";String[] args =newString[] {"Image"};String[] projection =newString[] {MediaStore.Images.Media._ID};Cursor cursor = resolver.query(external, projection, selection, args,null);Uri imageUri =null;if(cursor !=null&& cursor.moveToFirst()) {? ? imageUri = ContentUris.withAppendedId(external, cursor.getLong(0));? ? cursor.close();}
2.3.2.5 使用 MediaStore 讀取文件
通過以上查詢方式得到 Uri 之后,通過以下方式讀取文件:
1)通過 ContentResolver openFileDescriptor 接口,選擇對(duì)應(yīng)的打開方式。例如”r” 表示讀,”w” 表示寫,返回 ParcelFileDescriptor 類型的文件描述符。
ParcelFileDescriptor pfd =null;if(imageUri !=null) {try{? ? ? ? pfd = context.getContentResolver().openFileDescriptor(imageUri,"r");if(pfd !=null) {? ? ? ? ? ? Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());//show the bitmap,ordosomethingelse.? ? ? ? }? ? }catch(IOException e) {? ? ? ? LogUtil.log("fail: "+ e.getCause());? ? }finally{try{if(pfd !=null) {? ? ? ? ? ? ? ? pfd.close();? ? ? ? ? ? }? ? ? ? }catch(IOException e) {? ? ? ? ? ? LogUtil.log("fail in close: "+ e.getCause());? ? ? ? }? ? }}
2)訪問 Thumbnail,使用 ContentResolver.loadThumbnail 接口。通過傳入 size 參數(shù),MediaProvider 返回指定大小的 Thumbnail。
3)Native 代碼訪問文件
如果 Native 代碼需要訪問文件,可以參考下面方式:
通過 openFileDescriptor 返回 ParcelFileDescriptor
通過 ParcelFileDescriptor.detachFd() 讀取 FD
將 FD 傳遞給 Native 層代碼
通過 close 接口關(guān)閉 FD
String fileOpenMode ="r";ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);if(parcelFd !=null) {intfd = parcelFd.detachFd();// Pass the integer value "fd" into your native code. Remember to call// close(2) on the file descriptor when you're done using it.}
2.3.2.6 使用 MediaStore 修改文件
根據(jù)查詢得到的文件 Uri,使用 MediaStore 修改其他 APP 新建的多媒體文件,需要 catch?RecoverableSecurityException?,由 MediaProvider 彈出彈框給用戶選擇是否允許 APP 修改或刪除圖片 / 視頻 / 音頻文件。用戶操作的結(jié)果,將通過 onActivityResult 回調(diào)返回到 APP。如果用戶允許,APP 將獲得該 Uri 的修改權(quán)限,直到設(shè)備下一次重啟。
根據(jù)文件 Uri,通過下列接口,獲取需要修改文件的 FD 或者 OutputStream:
1)getContentResolver().openOutputStream(contentUri)
獲取對(duì)應(yīng)文件的 OutputStream。
2)getContentResolver().openFile 或者 getContentResolver().openFileDescriptor
通過 openFile 或者 openFileDescriptor 打開文件,需要選擇 Mode 為”w”,表示寫權(quán)限。這些接口返回一個(gè) ParcelFileDescriptor。
OutputStream os =null;try{if(imageUri !=null) {? ? ? ? os = resolver.openOutputStream(imageUri);? ? }}catch(IOException e) {? ? LogUtil.log("open image fail");}catch(RecoverableSecurityException e1) {? ? LogUtil.log("get RecoverableSecurityException");try{? ? ? ? ((Activity) context).startIntentSenderForResult(? ? ? ? ? ? ? ? e1.getUserAction().getActionIntent().getIntentSender(),100,null,0,0,0);? ? }catch(IntentSender.SendIntentException e2) {? ? ? ? LogUtil.log("startIntentSender fail");? ? }}
2.3.2.7 使用 MediaStore 刪除文件
刪除其他 APP 新建的媒體文件,與修改類似,需要用戶授權(quán)。刪除文件使用 ContentResolver.delete 接口。
getContentResolver().delete(imageUri,null,null);
2.3.3 使用 SAF 訪問指定文件和目錄
SAF,即 Storage Access Framework。根據(jù)當(dāng)前系統(tǒng)中存在的 DocumentsProvider,讓用戶選擇特定的文件或文件夾,使調(diào)用 SAF 的 APP 獲取它們的讀寫權(quán)限。APP 通過 SAF 獲得文件或目錄的讀寫權(quán)限,無需申請(qǐng)任何存儲(chǔ)相關(guān)的運(yùn)行時(shí)權(quán)限。
SAF 相關(guān)的 Google 官方文檔:
https://developer.android.com/guide/topics/providers/document-provider
使用 SAF 獲取文件或目錄權(quán)限的過程:
APP 通過特定 Intent 調(diào)起 DocumentUI -> 用戶在 DocumentUI 界面上選擇要授權(quán)的文件或目錄 ?-> APP 在回調(diào)中解析文件或目錄的 Uri,最后根據(jù)這一 Uri 可進(jìn)行讀寫刪操作。
2.3.3.1 使用 SAF 選擇單個(gè)文件
使用 Intent.ACTION_OPEN_DOCUMENT 調(diào)起 DocumentUI 的文件選擇頁面,用戶可以選擇一個(gè)文件,將它的讀寫權(quán)限授予 APP。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);// you cansettypetofilter filestoshowintent.setType("*/*");startActivityForResult(intent, REQUEST_CODE_FOR_SINGLE_FILE);
2.3.3.2 使用 SAF 修改文件
通過 2.3.3.1 的方式,用戶選擇文件授權(quán)給 APP 后,在 APP 的 onActivityResult 回調(diào)中收到返回結(jié)果,解析出對(duì)應(yīng)文件的 Uri。然后使用該 Uri,用戶可以獲取可寫的 ParcelFileDescriptor 或者打開 OutputStream 進(jìn)行修改。
if(requestCode == REQUEST_CODE_FOR_SINGLE_FILE && resultCode == Activity.RESULT_OK) {? ? Uri fileUri =null;if(data !=null) {? ? ? ? fileUri = data.getData();? ? }if(fileUri !=null) {? ? ? ? OutputStream os =null;try{? ? ? ? ? ? os = getContentResolver().openOutputStream(fileUri);? ? ? ? ? ? os.write("something".getBytes(StandardCharsets.UTF_8));? ? ? ? }catch(IOException e) {? ? ? ? ? ? LogUtil.log("modify document fail");? ? ? ? }finally{if(os !=null) {try{? ? ? ? ? ? ? ? ? ? os.close();? ? ? ? ? ? ? ? }catch(IOException e1) {? ? ? ? ? ? ? ? ? ? LogUtil.log("close fail");? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }}
2.3.3.3 使用 SAF 刪除文件
類似修改文件,在回調(diào)中解析出文件 Uri,然后使用 DocumentsContract.deleteDocument 接口進(jìn)行刪除操作。
if(requestCode == REQUEST_CODE_FOR_SINGLE_FILE && resultCode == Activity.RESULT_OK) {? ? Uri fileUri =null;if(data !=null) {? ? ? ? fileUri = data.getData();? ? }if(fileUri !=null) {try{? ? ? ? ? ? DocumentsContract.deleteDocument(getContentResolver(), fileUri);? ? ? ? }catch(FileNotFoundException e) {? ? ? ? ? ? LogUtil.log("delete document fail");? ? ? ? }? ? }}
2.3.3.4 使用 SAF 新建文件
APP 通過 Intent.ACTION_CREATE_DOCUMENT 調(diào)起 DocumentUI 界面,由用戶決定文件命名,以及存放位置。
Intent intent =newIntent(Intent.ACTION_CREATE_DOCUMENT);intent.addCategory(Intent.CATEGORY_OPENABLE);// you can set file mimetypeintent.setType("*/*");// default file nameintent.putExtra(Intent.EXTRA_TITLE,"myFileName");startActivityForResult(intent, REQUEST_CODE_FOR_CREATE_FILE);
在用戶確定后,操作結(jié)果將返回到 APP 的 onActivityResult 回調(diào)中,APP 解析出文件 Uri,之后就可以利用這一 Uri 對(duì)文件進(jìn)行讀寫刪操作。
if(requestCode == REQUEST_CODE_FOR_CREATE_FILE && resultCode == Activity.RESULT_OK) {? ? Uri fileUri =null;if(data !=null) {? ? ? ? fileUri = data.getData();? ? }//read/update/deletebythe uri got here.? ? LogUtil.log("uri: "+ fileUri);}
2.3.3.5 使用 SAF 選擇目錄
通過 Intent.ACTION_OPEN_DOCUMENT_TREE 調(diào)起 DocumentUI 界面,用戶可以選擇任意文件夾,將它及其子文件夾的讀寫權(quán)限授予 APP。
Intent intent =newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE);startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
在右上角的菜單中選擇 show internal storage,可以在左側(cè)菜單中選擇內(nèi)置存儲(chǔ)設(shè)備,接著用戶可以選擇內(nèi)置存儲(chǔ)設(shè)備中的任意文件夾。
在用戶確定后,APP 的 onActivityResult 回調(diào)收到操作結(jié)果,解析出被選文件夾的 uriTree。根據(jù)這一 uriTree ,進(jìn)一步可以生成表示被選文件夾的 DocumentFile,利用 DocumentFile 提供的 API 可以對(duì)目錄下的文件進(jìn)行各種操作。
if(requestCode == REQUEST_CODE_FOR_DIR && resultCode == Activity.RESULT_OK) {? ? Uri uriTree =null;if(data !=null) {? ? ? ? uriTree = data.getData();? ? }if(uriTree !=null) {// create DocumentFile which represents the selected directoryDocumentFile root = DocumentFile.fromTreeUri(this, uriTree);// list all sub dirs of rootDocumentFile[] files = root.listFiles();// do anything you want with APIs provided by DocumentFile// ...}}
2.3.3.6 永久保存獲取的目錄權(quán)限
在 2.3.3.5 中,通過 SAF 獲取了用戶指定目錄的讀寫權(quán)限,直至設(shè)備下一次重啟。APP 可以通過 takePersistableUriPermission 接口獲取該 uriTree 的永久權(quán)限,并將 uriTree 以 SharedPreferences 等形式持久化保存,以備之后隨時(shí)使用。
if(uriTree !=null) {// get persistable uri permissionfinalinttakeFlags = data.getFlags()? ? ? ? ? ? & (Intent.FLAG_GRANT_READ_URI_PERMISSION? ? ? ? ? ? | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);? ? getContentResolver().takePersistableUriPermission(uriTree, takeFlags);// save uriTree to sharedPreferenceSharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);? ? SharedPreferences.Editor editor = sp.edit();? ? editor.putString("uriTree", uriTree.toString());? ? editor.commit();}
在使用保存的 uriTree 時(shí),首先檢查是否順利從 SharedPreferences 中獲取到 uriTree,然后通過 takePersistableUriPermission 接口是否拋異常來判斷權(quán)限是否仍存在。如果權(quán)限不存在,則重新通過 SAF 申請(qǐng)權(quán)限。
SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);String uriTree = sp.getString("uriTree","");if(TextUtils.isEmpty(uriTree)) {? ? startSafForDirPermission();}else{try{? ? ? ? Uri uri = Uri.parse(uriTree);? ? ? ? finalinttakeFlags = getIntent().getFlags()? ? ? ? ? ? ? ? & (Intent.FLAG_GRANT_READ_URI_PERMISSION? ? ? ? ? ? ? ? | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);? ? ? ? getContentResolver().takePersistableUriPermission(uri, takeFlags);// uri tree permission is granted, do what you want with this uriLogUtil.log("uri is granted");? ? ? ? DocumentFile root = DocumentFile.fromTreeUri(this, uri);? ? }catch(SecurityException e) {? ? ? ? LogUtil.log("uri is not granted");? ? ? ? startSafForDirPermission();? ? }}
APP 申請(qǐng)到目錄的永久權(quán)限后,用戶可以在該 APP 的設(shè)置頁面取消目錄的訪問權(quán)限,即點(diǎn)擊如下圖的 “Clear access” 按鈕。
2.3.4 分享 App-specific 目錄下文件
APP 可以選擇以下的方式,將自身 App-specific 目錄下的文件分享給其他 APP 讀寫。
2.3.4.1 使用 FileProvider
APP 可以使用 FileProvider 將私有文件的讀寫權(quán)限賦給其他 APP。這種方式十分適用于 APP 主動(dòng)發(fā)起事件的情況,例如從 APP 將某個(gè)私有文件分享給其他 APP。
FileProvider 相關(guān)的 Google 官方文檔:
https://developer.android.google.cn/reference/androidx/core/content/FileProvider
https://developer.android.com/training/secure-file-sharing/setup-sharing
自定義 FileProvider 及使用的基本步驟:
1)在 AndroidManifest.xml 中聲明 App 的 FileProvider
android:authorities="com.oppo.whoops.fileprovider"android:? ? android:grantUriPermissions="true"android:exported="false">? ? ? ? ? ? android:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/filepaths"/>
2)根據(jù) FileProvider 聲明中的 meta data,在 res/xml 中新建 filepaths.xml ,用于定義分享的路徑。
name represents what other apps seeinthe shared uriassubdir. -->
3)在 APP 邏輯代碼中生成要分享的 uri,設(shè)置權(quán)限,然后發(fā)送 uri。
StringfilePath = getExternalFilesDir("Documents") +"/MyTestImage.PNG";Uri uri = FileProvider.getUriForFile(this,"com.oppo.whoops.fileprovider",newFile(filePath));Intent intent =newIntent(Intent.ACTION_SEND);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(uri, getContentResolver().getType(uri));startActivity(Intent.createChooser(intent,"File Provider share"));
4)接收方 APP 的組件設(shè)置對(duì)應(yīng)的 intent-filter。
5)接收方 APP 的組件收到 intent,解析獲得 uri,通過 uri 獲取文件的 FD。
Uri uri = getIntent().getData();ParcelFileDescriptor pdf =null;try{if(uri !=null) {? ? ? ? LogUtil.log("Uri: "+ uri);? ? ? ? pdf = getContentResolver().openFileDescriptor(uri,"r ");? ? ? ? LogUtil.log("Pdf: "+ pdf);? ? }}catch(FileNotFoundException e) {? ? LogUtil.log("open file fail
? ? ? ? ? ? ? ? ? ? ? ? ");}finally{try{if(pdf !=null) {? ? ? ? ? ? pdf.close();? ? ? ? }? ? }catch(IOException e1) {? ? ? ? LogUtil.log("close fd fail ");? ? }}
2.3.4.2 使用 ContentProvider
APP 可以實(shí)現(xiàn)自定義 ContentProvider 來向外提供 APP 私有文件。這種方式十分適用于內(nèi)部文件分享,不希望有 UI 交互的情況。
ContentProvider 相關(guān)的 Google 官方文檔:
https://developer.android.google.cn/guide/topics/providers/content-providers
2.3.4.3 使用 DocumentsProvider
Android 默認(rèn)提供的 ExternalStorageProvider、DownloadStorageProivder 和 MediaDocumentsProvider 會(huì)顯示在 SAF 調(diào)起的 DocumentUI 界面中。ExternalStorageProvider 展示了所有外部存儲(chǔ)設(shè)備的所有目錄及文件,包括 App-specific 目錄,所以 App-specific 目錄下的文件也可以通過 SAF 授權(quán)給其他 APP。
APP 也可以自定義 DocumentsProvider 來提供向外授權(quán)。自定義的 DocumentsProivder 將作為第三方 DocumentsProvider 展示在 SAF 調(diào)起的界面中。DocumentsProvider 的使用方法請(qǐng)參考官方文檔。
DocumentsProvider 相關(guān)的 Google 官方文檔:
https://developer.android.google.cn/reference/kotlin/android/provider/DocumentsProvider
2.3.5 細(xì)節(jié)適配
2.3.5.1 圖片的地理位置信息
Android Q 上,默認(rèn)情況下 APP 不能獲取圖片的地理位置信息。如果 APP 需要訪問圖片上的 Exif Metadata,需要完成以下步驟:
1)申請(qǐng) ACCESS_MEDIA_LOCATION 權(quán)限。
2)通過 MediaStore.setRequireOriginal 返回新 Uri。
Uri photoUri = Uri.withAppendedPath(? ? ? ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI,? ? ? ? cursor.getString(idColumnIndex));finaldouble[] latLong;// Get location data from the ExifInterface class.photoUri = MediaStore.setRequireOriginal(photoUri);InputStream stream = getContentResolver().openInputStream(photoUri);if(stream !=null) {? ? ExifInterface exifInterface =newExifInterface(stream);double[] returnedLatLong = exifInterface.getLatLong();// If lat/long is null, fall back to the coordinates (0, 0).latLong = returnedLatLong !=null? returnedLatLong :newdouble[2];// Don't reuse the stream associated with the instance of "ExifInterface".stream.close();}else{// Failed to load the stream, so return the coordinates (0, 0).latLong =newdouble[2];}
2.3.5.2 DATA 字段數(shù)據(jù)不再可靠
MediaStore 中,DATA(即_data)字段,在 Android Q 中開始廢棄,不再表示文件的真實(shí)路徑。讀寫文件或判斷文件是否存在,不應(yīng)該使用 DATA 字段,而要使用 openFileDescriptor。
同時(shí)也無法直接使用路徑訪問公共目錄的文件。
2.3.5.3 MediaStore.Files 接口自過濾
通過 MediaStore.Files 接口訪問文件時(shí),只展示多媒體文件(圖片、視頻、音頻)。其他文件,例如 PDF 文件,無法訪問到。
2.3.5.4 文件的 Pending 狀態(tài)
Android Q 上,MediaStore 中添加了一個(gè) IS_PENDING Flag,用于標(biāo)記當(dāng)前文件是 Pending 狀態(tài)。
其他 APP 通過 MediaStore 查詢文件,如果沒有設(shè)置 setIncludePending 接口,就查詢不到設(shè)置為 Pending 狀態(tài)的文件,這就能使 APP 專享此文件。
這個(gè) flag 在一些應(yīng)用場景下可以使用,例如在下載文件的時(shí)候:下載中,文件設(shè)置為 Pending 狀態(tài);下載完成,把文件 Pending 狀態(tài)置為 0。
ContentValues values =newContentValues();values.put(MediaStore.Images.Media.DISPLAY_NAME,"myImage.PNG");values.put(MediaStore.Images.Media.MIME_TYPE,"image/png");values.put(MediaStore.Images.Media.IS_PENDING,1);ContentResolver resolver = context.getContentResolver();Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;Uri item = resolver.insert(uri, values);try{? ? ParcelFileDescriptor pfd = resolver.openFileDescriptor(item,"w",null);// write data into the pending image.}catch(IOException e) {? ? LogUtil.log("write image fail");}// clear IS_PENDING flag after writing finished.values.clear();values.put(MediaStore.Images.Media.IS_PENDING,0);resolver.update(item, values,null,null);
2.3.5.5 使用 MediaStore 接口定義好的 Columns
在使用 MediaStore 接口時(shí),如果用到 Projection,Column Name 要使用在 MediaStore 中定義好的。如果 APP 引用的庫使用了其他 Column Name,需要由 APP 做好 Column Name 映射。
2.3.5.6 設(shè)置相對(duì)路徑
Android Q 上,通過 MediaStore 存儲(chǔ)到公共目錄的文件,除了 Uri 跟公共目錄關(guān)系中規(guī)定的每一個(gè)存儲(chǔ)空間的一級(jí)目錄外,可以通過 MediaColumns.RELATIVE_PATH 來指定存儲(chǔ)的次級(jí)目錄,這個(gè)目錄可以使多級(jí),具體方法如下:
1)ContentResolver insert 方法
通過 values.put(Media.RELATIVE_PATH, "Pictures/album/family") 指定存儲(chǔ)目錄。其中,Pictures 是一級(jí)目錄,album/family 是子目錄。
2)ContentResolver update 方法
通過 values.put(Media.RELATIVE_PATH, "Pictures/album/family") 指定存儲(chǔ)目錄。通過 update 方法,可以移動(dòng)文件的存儲(chǔ)地方。
2.3.5.7 卸載應(yīng)用
如果 APP 在 AndroidManifest.xml 中聲明:android:hasFragileUserData="true",卸載應(yīng)用會(huì)有提示是否保留 APP 數(shù)據(jù)。默認(rèn)應(yīng)用卸載時(shí) App-specific 目錄下的數(shù)據(jù)被刪除,但用戶可以選擇保留。
2.3.5.8 新建虛擬可移動(dòng)存儲(chǔ)
APP 適配時(shí),如果一個(gè)設(shè)備沒有可移動(dòng)存儲(chǔ),可以使用下面的方法新建虛擬存儲(chǔ)設(shè)備:
1)adb shell sm set-virtual-disk true
2)在設(shè)置 -> 存儲(chǔ) -> Virtual SD,進(jìn)行初始化
三. 禁止應(yīng)用讀取 Device ID
3.1 背景
Android Q 對(duì)設(shè)備標(biāo)識(shí) (Device Identifier) 做了訪問限制。App 必須擁有系統(tǒng)簽名權(quán)限:READ_PRIVILEGED_PHONE_STATE,才能訪 Device ID,包括 IMEI、Serial Number,這意味著第三方應(yīng)用無法獲取 Device ID。
3.2 兼容性影響
(1)TargetSdkVersion 并且沒有申請(qǐng) READ_PHONE_STATE 權(quán)限,或者 TargetSdkVersion>=Q,獲取 device id 會(huì)拋異常 SecurityException;
(2)TargetSdkVersion 并且申請(qǐng)了 READ_PHONE_STATE,通過 getDeviceId 接口讀取的值為 Null;
(3)無法獲取到 device id,會(huì)對(duì)應(yīng)用依賴 device id 的功能產(chǎn)生影響。
3.3 適配指導(dǎo)
● 谷歌提供的適配指導(dǎo)文檔:
唯一標(biāo)識(shí)符最佳做法:https://developer.android.google.cn/training/articles/user-data-ids
官方文檔:
● 避免使用硬件 ID
● 使用 Advertising, ID 表示用戶資料或者廣告用途需要依賴于 GMS 包里面的 AdvertisingIdClient
● 使用 Google FirebaseInstanceId,但是會(huì)在下列情況下不同:
App 刪除 Instance ID
App 在新設(shè)備恢復(fù)
用戶卸載或者重新安裝 App
用戶清除數(shù)據(jù)
● 使用 IDs 跟 GUIDs
StringuniqueID = UUID.randomUUID().toString();
● 使用 Android ID,恢復(fù)出廠,這個(gè)值會(huì)改變
Settings.System.getString(getContentResolver(),Settings.Secure.ANDROID_ID);
● 接入 OPPO OPEN_ID
目前方案正在開發(fā)中,開發(fā)完成后再補(bǔ)充適配信息
四. Mac 地址隨機(jī)化
4.1 背景
為了進(jìn)一步保護(hù)用戶的隱私,Android Q 在連接 Wi-Fi 時(shí),默認(rèn)啟用了 Mac 地址隨機(jī)化的特性,如果 APP 不進(jìn)行適配,使用原來方式獲取到的 Mac 地址可能是隨機(jī)生成的,并不是真實(shí)的 Mac 地址。
4.2 兼容性影響
如果您的 APP 需要使用 Mac 地址作為設(shè)備的標(biāo)識(shí),無論您的 Target SDK 是否設(shè)置為 Q,只要運(yùn)行在 Android Q 上,您就需要進(jìn)行適配。
4.3 適配 指導(dǎo)
請(qǐng)參考谷歌適配指導(dǎo):
https://developer.android.com/preview/privacy/data-identifiers#randomized-mac-addresses
五. 禁止后臺(tái)應(yīng)用啟動(dòng) Activity
5.1 背景
安卓 Q 版本限制了應(yīng)用后臺(tái)啟動(dòng) Activity,該變更的目的是最大限度減少后臺(tái)應(yīng)用彈界面對(duì)用戶的打擾,在 Android Q 上運(yùn)行的應(yīng)用只有在滿足以下一個(gè)或多個(gè)條件時(shí)才能啟動(dòng) Activity
應(yīng)用處于前臺(tái);
桌面 widget 點(diǎn)擊啟動(dòng) Activity;
由桌面點(diǎn)擊啟動(dòng)應(yīng)用;
由 Recent 點(diǎn)擊啟動(dòng)應(yīng)用;
前臺(tái)應(yīng)用啟動(dòng)后臺(tái)應(yīng)用;
臨時(shí)白名單機(jī)制,不攔截通過通知拉起的應(yīng)用。
(a)應(yīng)用通過通知,在 pendingintent 中啟動(dòng) activity;
(b)應(yīng)用通過通知,在 pendingintent 中發(fā)送廣播,接收廣播后啟動(dòng) activity,加入臨時(shí)白名單不攔截。
(c)應(yīng)用通過通知,在 pendingintent 中啟動(dòng) service,在 service 中啟動(dòng) activity,加入臨時(shí)白名單不攔截。
5.2 兼容性 影響
影響所有應(yīng)用的后臺(tái)啟動(dòng) Activity,需全面排查及整改。
5.3 適配 指導(dǎo)
請(qǐng)參考谷歌適配指導(dǎo):
https://developer.android.com/preview/privacy/background-activity-starts
谷歌在 Q 的 beta 版本并未真正打開該管控限制,但是如果應(yīng)用的頁面存在被管控的場景,系統(tǒng)會(huì)通過一個(gè) Toast 告警提示,提示開發(fā)者需要整改,否則應(yīng)用的某些頁面在谷歌的后續(xù)版本會(huì)被攔截,具體的告警文字內(nèi)容:
This background activity start from "packageName" will be blocked in future Q builds. See g.co/dev/bgblock.
App 需要測(cè)試這個(gè)特性,需要在打開這個(gè)限制,使用下面任一步驟開啟即可:
● Settings > Developer options > Allow background activity starts 設(shè)置為關(guān)閉
● adb shell settings put global background_activity_starts_enabled 0
六. 后臺(tái)應(yīng)用地理位置權(quán)限
6.1 背景
Android Q 針對(duì)位置信息新增了 ACCESS_BACKGROUND_LOCATION 權(quán)限,以管控應(yīng)用是否可以在后臺(tái)訪問位置信息。原有的 ACCESS_COARSE_LOCATION 和 ACCESS_FINE_LOCATION 權(quán)限用于管控應(yīng)用在前臺(tái)是否可以獲取位置信息。
6.2 兼容性影響
地圖類應(yīng)用在后臺(tái)獲取位置信息時(shí)將受到影響。
6.3 適配指導(dǎo)
Google 官方適配指導(dǎo)鏈接:
https://developer.android.com/preview/privacy/device-location
1、Target Sdk Version 兼容
當(dāng)應(yīng)用的 Target Sdk Version < Q
● 請(qǐng)求 ACCESS_COARSE_LOCATION 或者 ACCESS_FINE_LOCATION 權(quán)限,系統(tǒng)會(huì)自動(dòng)同時(shí)請(qǐng)求 Q 新增的 ACCESS_BACKGROUND_LOCATION(圖 ?6-3-1)。
當(dāng)應(yīng)用的 Target Sdk Version >= Q
● 只請(qǐng)求 ACCESS_COARSE_LOCATION 權(quán)限或 ACCESS_FINE_LOCATION 權(quán)限,系統(tǒng)將不會(huì)提供 “始終允許” 的選擇按鈕,應(yīng)用只能在使用時(shí)獲取位置信息(圖 6-3-2)。
Google 建議:如果應(yīng)用不需要在后臺(tái)獲取位置信息,不要請(qǐng)求 ACCESS_BACKGROUND_LOCATION 權(quán)限。
● 請(qǐng)求 ACCESS_COARSE_LOCATION 權(quán)限或 ACCESS_FINE_LOCATION 權(quán)限,并同時(shí)請(qǐng)求 ACCESS_BACKGROUND_LOCATION 權(quán)限,則系統(tǒng)提供 “僅在使用該應(yīng)用期間允許” 選擇按鈕(圖 6-3-3 )。
● Android Q 不允許在沒有請(qǐng)求 ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 權(quán)限(或者 ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 沒有被授權(quán))的情況下單獨(dú)請(qǐng)求 ACCESS_BACKGROUND_LOCATION 權(quán)限。
● 在 ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 已經(jīng)授權(quán)的情況下,請(qǐng)求 ACCESS_BACKGROUND_LOCATION 權(quán)限,則彈出如圖 6-3-4 的權(quán)限說明框。
2、僅在使用該應(yīng)用時(shí)允許
在 Q 上,選擇 “僅在使用該應(yīng)用時(shí)允許”,應(yīng)用只有在可見或者使用前臺(tái)服務(wù)情況下才能獲取到位置信息。
使用前臺(tái)服務(wù),應(yīng)用需要參考以下步驟:
a. 在應(yīng)用的 Manifest 中的對(duì)應(yīng) service 中添加值為 location 的 foregroundServiceType:
android:name="MyNavigationService"android:foregroundServiceType="location"... >? ? ...
b. 啟動(dòng)前臺(tái)服務(wù)前檢查是否具有獲取位置信息的權(quán)限:
booleanpermissionAccessCoarseLocationApproved =? ? ActivityCompat.checkSelfPermission(this,? ? ? ? permission.ACCESS_COARSE_LOCATION) ==? ? ? ? PackageManager.PERMISSION_GRANTED;if(permissionAccessCoarseLocationApproved) {// App has permission to access location in the foreground. Start your// foreground service that has a foreground service type of "location".}else{// Make a request for foreground-only location access.ActivityCompat.requestPermissions(this,newString[] {? ? ? ? Manifest.permission.ACCESS_COARSE_LOCATION},? ? ? your-permission-request-code);}
3、始終允許
在某些場景下,比如共享位置信息,應(yīng)用需要始終獲取位置信息。
當(dāng)用戶選擇了 “始終允許”,應(yīng)用無論在前臺(tái)還是后臺(tái)都可以獲取到位置信息。但需要注意的是,用戶選擇“始終允許” 后可以手動(dòng)撤銷掉后臺(tái)訪問的權(quán)限!(當(dāng)用戶選擇 “始終允許” 后,系統(tǒng)會(huì)周期在通知欄提示用戶選擇了“始終允許”,點(diǎn)擊通知會(huì)進(jìn)入位置權(quán)限詳情界面,用戶可以重新選擇位置權(quán)限的授權(quán))。因此,應(yīng)用在進(jìn)入后臺(tái)獲取位置信息前需要判斷當(dāng)前是否依然具有權(quán)限,參考以下步驟:
a. 定義后臺(tái)服務(wù),無需添加 location 的 type
access to the device's location"all the time"in order to run successfully.-->? ? android: ... >? ? ...
b. 進(jìn)入后臺(tái)前判斷是否具有后臺(tái)訪問位置信息的權(quán)限
booleanpermissionAccessCoarseLocationApproved =? ? ActivityCompat.checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION)? ? ? ? == PackageManager.PERMISSION_GRANTED;if(permissionAccessCoarseLocationApproved) {booleanbackgroundLocationPermissionApproved =? ? ? ? ? ActivityCompat.checkSelfPermission(this,? ? ? ? ? ? ? permission.ACCESS_BACKGROUND_LOCATION)? ? ? ? ? ? ? == PackageManager.PERMISSION_GRANTED;if(backgroundLocationPermissionApproved) {// App can access location both in the foreground and in the background.// Start your service that doesn't have a foreground service type// defined.}else{// App can only access location in the foreground. Display a dialog// warning the user that your app must have all-the-time access to// location in order to function properly. Then, request background// location.ActivityCompat.requestPermissions(this,newString[] {? ? ? ? ? Manifest.permission.ACCESS_BACKGROUND_LOCATION},? ? ? ? ? your-permission-request-code);? }}else{// App doesn't have access to the device's location at all. Make full request// for permission.ActivityCompat.requestPermissions(this,newString[] {? ? ? ? Manifest.permission.ACCESS_COARSE_LOCATION,? ? ? ? Manifest.permission.ACCESS_BACKGROUND_LOCATION? ? ? ? },? ? ? ? your-permission-request-code);}
如果應(yīng)用進(jìn)入后臺(tái)前,發(fā)現(xiàn)后臺(tái)訪問位置信息的權(quán)限被撤銷即 backgroundLocationPermissionApproved 為 false,需要重新申請(qǐng) ACCESS_BACKGROUND_LOCATION 權(quán)限,這時(shí)會(huì)彈出如圖 6-3-4 的權(quán)限說明框,提示用戶重新選擇。
7. 非 SDK 接口管控
7.1 背景
Google 認(rèn)為非公開接口可能在不同版本之間進(jìn)行變動(dòng)從而導(dǎo)致應(yīng)用兼容性問題,因此從 Android P 開始強(qiáng)制約定三方應(yīng)用只能使用 Android SDK 公開的類和接口;對(duì)于非公開的 API,Google 按照不同名單類型進(jìn)行不同程度的限制使用。
a) 從原生的 android.jar 中能夠看到的就是 SDK 接口,也可以從 Google 的開發(fā)者網(wǎng)站 https://developer.android.com/reference/packages 進(jìn)行查詢。
b) 非公開 API 的分類和限制
7.2 兼容性影響
所有三方應(yīng)用都可能會(huì)受到影響,Android Q 版本由于名單生成規(guī)則變化了,導(dǎo)致增加很多黑名單接口;同時(shí)有很多非 SDK 接口被刪除,這些都會(huì)導(dǎo)致應(yīng)用出現(xiàn)兼容性問題。
由于非公開接口的管控是在運(yùn)行時(shí)進(jìn)行管控的,因此用使用反射、JNI 調(diào)用和正常的空實(shí)現(xiàn)封裝這些深灰和黑名單的接口都不會(huì)繞過 Google 的限制,應(yīng)用會(huì)出現(xiàn)異常。
具體異常信息如下:
7.3 ?適配指導(dǎo)
Google 的官方文檔中的適配指導(dǎo)。
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces
https://android-developers.googleblog.com/2018/06/an-update-on-non-sdk-restrictions-in.html
a) Google 原生最新的名單見下面的鏈接
https://android.googlesource.com/platform/prebuilts/runtime/+/refs/heads/master/appcompat/
hiddenapi-flags.csv 文件包含了所有的類和接口,第二列顯示的就是名單類型。
三方應(yīng)用要重點(diǎn)關(guān)注 max-o、max-p 和 blacklist 接口(private 類型的接口會(huì)是 blacklist)。這些接口不可用。
b) 靜態(tài)掃描 APK 文件,獲得應(yīng)用使用非公開接口的情況。
使用下面鏈接的 veridex 工具,建議在 linux 環(huán)境下使用
https://android.googlesource.com/platform/prebuilts/runtime/+/refs/heads/master/appcompat/
README.txt 給出了詳細(xì)的使用步驟
備注:該方法不能檢測(cè)出使用反射方式調(diào)用黑名單接口的情況。
c) 通過運(yùn)行應(yīng)用,獲得應(yīng)用使用非公開接口的情況。
通過執(zhí)行 adb shell settings put global hidden_api_policy ?1
將系統(tǒng)的 Hidden API 強(qiáng)制策略禁用掉,這樣應(yīng)用就可以在 AndroidQ 設(shè)備上運(yùn)行。查看日志是否有如下關(guān)鍵日志信息。
log 中會(huì)有關(guān)鍵字打印:
Accessing hidden field Landroid/content/pm/PackageParser;->mParseError:I (blacklist, reflection)
應(yīng)用行為異常有報(bào)錯(cuò),有類似如下的報(bào)錯(cuò):
java.lang.NoSuchMethodError: No virtual method getActivityIconCache(Landroid/content/ComponentName;)Landroid/graphics/drawable/Drawable; in class Landroid/content/pm/PackageManager; or its super classes (declaration of 'android.content.pm.PackageManager' appears in /system/framework/framework.jar)
當(dāng)沒有發(fā)現(xiàn) Accessing hidden 日志信息時(shí),需要確認(rèn)調(diào)用的類或者方法是否由于 Android 版本升級(jí)有改變了。
建議開發(fā)者采用上述方法進(jìn)行應(yīng)用的非公開 SDK 接口的檢查。
d) 總的適配原則是針對(duì) max-o 、max-p 和 blacklist 接口, 嘗試在公開的 SDK 中找替代方案。?例如 android.util.FloatMath 的 sqrt 方法,可用 java.lang.Math 類 return (float) Math.sqrt(value); 方式來替換。
e) 若應(yīng)用無法找到可替代的 SDK 接口,但是又要使用這個(gè)非 SDK 接口,建議開發(fā)者直接給谷歌反饋,申請(qǐng)新的公共 API,申請(qǐng)鏈接:https://partnerissuetracker.corp.google.com/issues/new?component=328403&template=1027267
八. API Level 要求
8.1 背景
增加對(duì)于上架谷歌 Play 商店應(yīng)用要求:
a) 新開發(fā)的應(yīng)用:2019-8-1 之后,上架谷歌 Play 商店要求應(yīng)用的 TargetSdkVersion>=28 ;
b) 更新的應(yīng)用:2019-11-1 之后,上架谷歌 Play 商店要求應(yīng)用的 TargetSdkVersion>=28 。
最小 TargetSdkVersion 要求:當(dāng)用戶首次運(yùn)行 API 低級(jí)低于 ?23 (Android Marshmallow) 的應(yīng)用時(shí),會(huì)受到來自 Android Q 的警告信息。
8.2 兼容性 影響
應(yīng)用升級(jí) TargetSdkVersion 之后,和應(yīng)用的 TargetSdkVersion 相關(guān)的變更就會(huì)影響,不適配很可能導(dǎo)致應(yīng)用出現(xiàn)兼容性問題或者功能問題。
8.3 適配 指導(dǎo)
谷歌適配指導(dǎo)鏈接:https://developer.android.google.cn/about/versions/pie/android-9.0-changes-28
重點(diǎn)關(guān)注以下特性變化:
非 SDK 接口管控,需要重點(diǎn)排查是否使用 P 版本的深灰名單接口和 Q 版本的 max-o 名單接口,請(qǐng)參考 7 章節(jié)進(jìn)行適配
針對(duì) Android 9 或更高版本并使用前臺(tái)服務(wù)的應(yīng)用必須請(qǐng)求 ?FOREGROUND_SERVICE 權(quán)限,否則會(huì)引發(fā) SecurityException,這是普通權(quán)限,因此,系統(tǒng)會(huì)自動(dòng)為請(qǐng)求權(quán)限的應(yīng)用授予此權(quán)限。
后臺(tái)執(zhí)行限制,請(qǐng)參考谷歌適配指導(dǎo):https://developer.android.google.cn/about/versions/oreo/background.html
九. 64 位兼容
9.1 背景
從 2019 年 8 月 1 日開始,在 Google Play 上發(fā)布的應(yīng)用必須支持 64 位架構(gòu)
9.2 兼容性影響
應(yīng)用需要自查是否具有 so 庫(native 代碼)。如果沒有則已支持 64 位架構(gòu)。如果有則需要自檢是否支持并采取對(duì)應(yīng)措施,否則將無法正常運(yùn)行。
9.3 適配 指導(dǎo)
Google 官方適配指導(dǎo)鏈接:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit
1、查找應(yīng)用中的 so 文件
a. 通過 Analyze Apk 查找,參考:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit#look_for_native_libraries_using_apk_analyzer
b. 通過解壓 apk 文件查找,參考:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit#look_for_native_libraries_by_unzipping_apks
2、確認(rèn)是否支持 64 位架構(gòu)
a. 如果沒有 so 文件,則已支持 64 位架構(gòu)
b. 根據(jù) ABI,so 文件對(duì)應(yīng)不同的目錄(圖 10-3-1 ),如果每個(gè)目錄下都存在 so 文件,則支持 64 位架構(gòu):
3、構(gòu)建 64 位架構(gòu)的 app
a. 應(yīng)用的 so 文件自己開發(fā),參考:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit#build_your_app_with_64-bit_libraries
b. Game 游戲開發(fā)者,需要使用支持 64 位的引擎:
Unreal since 2015
Cocos2d since 2015
Unity since 2018
c. Unity 開發(fā)者,參考:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit#unity_developers
d. 應(yīng)用使用三方的 sdk 庫文件,需通知三方開發(fā)者適配支持,然后應(yīng)用重新集成
10. 其他權(quán)限變更
10.1 USB 序列化
10.1.1 背景
Android Q 上獲取 USB 序列號(hào)需要 android.permission.MANAGE_USB 權(quán)限
10.1.2 兼容性影響
三方應(yīng)用無法獲取 USB 序列號(hào)
10.1.3 適配指導(dǎo)
Google 官方適配指導(dǎo)鏈接:https://developer.android.com/preview/privacy/data-identifiers
由于 android.permission.MANAGE_USB 是 signature|privileged 級(jí)別的權(quán)限,三方應(yīng)用無法獲取該權(quán)限,因此無法獲取 USB 序列號(hào)。
10.2 電話、WiFi 、藍(lán)牙 API 所需的精確位置權(quán)限
10.2.1 背景
為了管理 APP 對(duì)一些關(guān)鍵 API 的調(diào)用,Android Q 對(duì)電話、 Wi-Fi 以及藍(lán)牙的相關(guān) API 增加了 ACCESS_FINE_LOCATION 權(quán)限的限制。
10.2.2 兼容性影響
具體影響的各模塊接口如下:
10.2.3 適配指導(dǎo)
參考 Google 官方適配指導(dǎo)鏈接:https://developer.android.com/preview/privacy/camera-connectivity#fine-location-telephony-wifi-bt
如果應(yīng)用的 targetSdkVersion?= Q,那么必須申請(qǐng) ACCESS_FINE_LOCATION 權(quán)限,否則當(dāng) APP 運(yùn)行在 Android Q 平臺(tái)時(shí),將無法正常使用受影響的 API。
10.3 應(yīng)用無法后臺(tái)訪問剪切板數(shù)據(jù)
10.3.1 背景
Android P 非 Instant 應(yīng)用可以任何時(shí)刻獲取剪貼板內(nèi)容。
Android Q 上新增 READ_CLIPBOARD_IN_BACKGROUND 權(quán)限限制應(yīng)用后臺(tái)獲取剪貼板內(nèi)容。
10.3.2 兼容性影響
除非應(yīng)用是默認(rèn)輸入法編輯器(IME)或具有焦點(diǎn)的應(yīng)用程序,否則無法獲取剪貼板內(nèi)容
10.3.3 適配指導(dǎo)
Google 官方適配指導(dǎo)鏈接:https://developer.android.com/preview/privacy/data-identifiers
由于 READ_CLIPBOARD_IN_BACKGROUND 權(quán)限是簽名 signature 級(jí)別的權(quán)限,因此三方應(yīng)用無法通過授權(quán)該權(quán)限在后臺(tái)獲取剪貼板內(nèi)容。應(yīng)用只有在具有焦點(diǎn)時(shí)才能獲取剪貼板內(nèi)容。
10.4 訪問相機(jī)信息所需權(quán)限
10.4.1 背景
為了進(jìn)一步保護(hù)用戶的隱私,從 Android Q 開始,google 更改了默認(rèn)情況下 getCameraCharacteristics() ?方法返回的設(shè)備信息的粒度,部分屬性將受到權(quán)限限制,這些屬性可能用戶運(yùn)動(dòng)跟蹤等涉及用戶隱私的領(lǐng)域。因此,需要特別注意的是,當(dāng)您的應(yīng)用需要嘗試獲取以下列出的包含設(shè)備特定信息的元數(shù)據(jù)時(shí),您的應(yīng)用必須具有 android.permission.CAMERA 權(quán)限才能獲取此 Key 對(duì)應(yīng)的返回值, 否則將返回 null。
關(guān)于如何獲取 android.permission.CAMERA 權(quán)限,請(qǐng)?jiān)L問 https://developer.android.google.cn/training/permissions/requesting
如果您的應(yīng)用沒有 CAMERA 權(quán)限,則無法訪問以下字段:
10.4.2 ?兼容性影響
即使您的應(yīng)用的 target 在 Android 9(API 級(jí)別 28)或更低級(jí)別,如果沒有 CAMERA 權(quán)限,在 Q 版本的 Android 系統(tǒng)上運(yùn)行您的應(yīng)用,通過 cameraCharacteristics.get(CameraCharacteristics.LENS_POSE_ROTATION) 獲取對(duì)應(yīng)屬性時(shí),返回值仍將為 null。
10.4.3 適配指導(dǎo)
要獲取以上的屬性,請(qǐng)動(dòng)態(tài)申請(qǐng) android.permission.CAMERA 的權(quán)限,參照:https://developer.android.google.cn/training/permissions/requesting
10.5 限制 SMS/Call Log 訪問
10.5.1 背景
Google Play Store 中限制一些高危、高敏感的權(quán)限,包括 SMS、Call Log 權(quán)限。如果 App 沒有滿足 Google Play Store 的要求,會(huì)從 Google Play 移除。
10.5.2 兼容性影響
在 Google Play 上架的 App,需要注意影響。
https://support.google.com/googleplay/android-developer/answer/9047303
10.5.3 適配指導(dǎo)
適配指導(dǎo)請(qǐng)參考:https://play.google.com/about/privacy-security-deception/permissions/
● 如果應(yīng)用不具備默認(rèn)短信、電話或輔助處理程序功能,就不得在清單中(包括清單中的占位文本)聲明需要使用上述權(quán)限。
● 只有在用戶主動(dòng)將應(yīng)用注冊(cè)為默認(rèn)短信、電話或輔助處理程序的情況下,應(yīng)用才能提示用戶接受上述任何權(quán)限請(qǐng)求;當(dāng)應(yīng)用不再是默認(rèn)處理程序時(shí),則必須立即停止使用相應(yīng)權(quán)限。
● 應(yīng)用只能將權(quán)限(及其衍生數(shù)據(jù))用于提供已獲批準(zhǔn)的關(guān)鍵核心應(yīng)用功能(例如應(yīng)用說明中記錄并宣傳的應(yīng)用現(xiàn)有關(guān)鍵功能)。您絕不能出售此類數(shù)據(jù)。您只能基于提供應(yīng)用關(guān)鍵核心功能或服務(wù)的目的,轉(zhuǎn)移、分享或許可使用此類數(shù)據(jù),不能將此類數(shù)據(jù)用于任何其他用途(例如改進(jìn)其他應(yīng)用或服務(wù)、投放廣告或營銷)。您不得使用其他方法(包括其他權(quán)限、API 或第三方來源)從上述權(quán)限中衍生數(shù)據(jù)。
● 通話記錄和短信默認(rèn)處理程序限制的例外情
上述限制是為了保護(hù)用戶隱私。如果應(yīng)用不是默認(rèn)處理程序,但符合以上所有要求,并清楚明確地提供極具吸引力的功能或關(guān)鍵功能,而該功能目前只有在獲得相關(guān)權(quán)限后才能實(shí)現(xiàn),則我們可能會(huì)允許少數(shù)特例。我們會(huì)評(píng)估相應(yīng)功能在隱私權(quán)或安全性方面對(duì)用戶可能造成的影響。這類特例十分少見,并不適用于所有開發(fā)者。
11. WIFI 相關(guān)接口變更
11.1 背景
Android Q 為了更好的保護(hù)用戶的隱私,讓用戶知曉應(yīng)用對(duì) Wi-Fi 配置的改動(dòng),其限制了應(yīng)用對(duì) WifiManager 重要接口的調(diào)用,三方應(yīng)用將無法正常使用這些接口。此外,針對(duì) Wi-Fi Direct 相關(guān)的廣播以及接口也做了調(diào)整。
11.2 兼容性影響
11.2.1 WifiManager 相關(guān)接口變更
如下接口,應(yīng)用需要進(jìn)行適配。
11.2.2 Wi-Fi Direct 相關(guān)變更
在 Android Q 中,以下與 Wi-Fi Direct 相關(guān)的廣播不再具有黏性。如果您的應(yīng)用依賴于在注冊(cè)的時(shí)候接收這些廣播,需要進(jìn)行適配。
● WIFI_P2P_CONNECTION_CHANGED_ACTION
● WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
11.3 適配指導(dǎo)
兼容如上接口變更,可以參照:
https://developer.android.com/preview/privacy/camera-connectivity#wifi-network-config-restrictions
https://developer.android.com/preview/behavior-changes-all#wifi-direct-broadcasts
11.3.1 開關(guān)或關(guān)閉 Wi-Fi
Android Q 提供了 Pannel 的方式打開或者關(guān)閉 Wi-Fi,若只希望開關(guān) Wi-Fi,可以通過如下方式。
Intent panelIntent =newIntent(Settings.Panel.ACTION_WIFI);startActivity(panelIntent);
若希望是連接互聯(lián)網(wǎng),可以通過如下方式。
Intent panelIntent =newIntent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY);startActivity(panelIntent);
11.3.2 使用 NetworkRequest 連接網(wǎng)絡(luò)
Android Q 限制應(yīng)用使用連接或斷開網(wǎng)絡(luò)的接口,并推薦使用 NetworkRequest 的方式連接網(wǎng)絡(luò)。應(yīng)用通過配置 NetworkSpecifier 規(guī)則,并通過 NetworkRequst 下發(fā)到 Framework,F(xiàn)ramework 會(huì)彈出選擇框并觸發(fā)一次 Wi-Fi 掃描。掃描接收后,系統(tǒng)會(huì)根據(jù) NetworkSpecifier 規(guī)則將過濾后的掃描結(jié)果呈現(xiàn)在彈框中供用戶選擇。
PatternMatcher ssidMatcher =newPatternMatcher("OPPO", PatternMatcher.PATTERN_PREFIX);WifiNetworkSpecifier.Builder buidler =newWifiNetworkSpecifier.Builder();buidler.setSsidPattern(ssidMatcher);NetworkRequest.Builder networkRequestBuilder =newNetworkRequest.Builder();networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);networkRequestBuilder.setNetworkSpecifier(buidler.build());mConnectivityManager.requestNetwork(networkRequestBuilder.build(), mNetworkCallback);
11.3.3 使用 WifiNetworkSuggestion 連接網(wǎng)絡(luò)
應(yīng)用可以創(chuàng)建 NetworkSuggestions,將其添加到 Framework。待 Framework 收到掃描結(jié)果后,會(huì)首先從系統(tǒng)已保存的網(wǎng)絡(luò)中選網(wǎng)。如果沒有可選的網(wǎng)絡(luò),就會(huì)從添加的 NetworkSuggestions 中選網(wǎng)。若 Framework 連接上本應(yīng)用添加的 NetworkSuggestions 后,會(huì)發(fā)送 WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION 廣播到應(yīng)用內(nèi)。
WifiNetworkSuggestion.Builder networkSuggestionBuilder =newWifiNetworkSuggestion.Builder();networkSuggestionBuilder.setSsid("TEST");List networkSuggestionList =newArrayList();networkSuggestionList.add(networkSuggestionBuilder.build());if(mWifiManager.addNetworkSuggestions(networkSuggestionList) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {? ? Log.d(TAG,"Successfully add network suggestion.");}
11.3.4 Wi-Fi Direct 廣播適配
原依賴于注冊(cè)WIFI_P2P_CONNECTION_CHANGED_ACTION時(shí)接收廣播內(nèi)容的邏輯,可以使用WifiP2pManager.requestNetworkInfo() 接口查詢,具體使用方式如下:
p2pManager.requestNetworkInfo(p2pChannel,newWifiP2pManager.NetworkInfoListener() {@OverridepublicvoidonNetworkInfoAvailable(NetworkInfo networkInfo){? ? ? ? Log.d(TAG,"Receive network information "+ networkInfo);? ? }});
原依賴于注冊(cè) WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 時(shí)接收廣播內(nèi)容的邏輯,可以使用 WifiP2pManager.requestDeviceInfo() 接口查詢,注意該方法需要申請(qǐng) ACCESS_FINE_LOCATION 權(quán)限,具體使用方式如下:
p2pManager.requestDeviceInfo(p2pChannel,newWifiP2pManager.DeviceInfoListener() {@OverridepublicvoidonDeviceInfoAvailable(WifiP2pDevice wifiP2pDevice){? ? ? ? Log.d(TAG,"Receive deivce information "+ wifiP2pDevice);? ? }}
十二 電話 API 重要變更
12.1 背景
TelephonyManager.java 的 endCall()、answerRingingCall ()、silenceRinger () 方法已失效。
12.2 兼容性影響
調(diào)用如上方法將無法生效,不會(huì)有任何響應(yīng)。
12.3 適配指導(dǎo)
如需掛斷電話:
使用 android.telecom.TelecomManager#endCall(),該接口需申請(qǐng)權(quán)限 Manifest.permission.ANSWER_PHONE_CALLS ,但谷歌已不建議使用,
不久的將來存在廢棄的風(fēng)險(xiǎn)。
如需接聽電話:
使用 android.telecom.TelecomManager# acceptRingingCall (),該接口需申請(qǐng)權(quán)限 Manifest.permission.ANSWER_PHONE_CALLS ,但谷歌已不建議使用,
不久的將來存在廢棄的風(fēng)險(xiǎn)。
如需實(shí)現(xiàn)來電靜音:
請(qǐng)使用 android.telecom.TelecomManager# silenceRinger () 方法,但只能被用戶設(shè)為默認(rèn)撥號(hào)應(yīng)用的前提下使用,否則將會(huì)拋出權(quán)限異常。
Android Q 的適配文檔就到這里,本文檔是以 Beta 4 版本為基礎(chǔ)編寫,有些地方后續(xù)應(yīng)該還有一些微調(diào),大家可以持續(xù)關(guān)注 Android Q 各方面的更新消息。
后續(xù)我也會(huì)分享一些具有針對(duì)性的 Android Q 新特性和兼容問題,有興趣就持續(xù)關(guān)注吧。
「?聯(lián)機(jī)圓桌?」:point_left:推薦我的知識(shí)星球,一年 50 個(gè)優(yōu)質(zhì)問題,上桌聯(lián)機(jī)學(xué)習(xí)。
公眾號(hào)后臺(tái)回復(fù)成長『?成長?』,將會(huì)得到我準(zhǔn)備的學(xué)習(xí)資料,也能回復(fù)『?加群?』,一起學(xué)習(xí)進(jìn)步。