Android R(Android 11 API 30)於2020年9月9日正式釋出,隨國內各終端廠商在售Android裝置的版本更新升級,應用軟體對Android R 版本的相容適配已迫在眉睫。
對於Android R的新特性,這裡按照以下幾個方面進行了歸納:分割槽儲存、許可權、隱私、效能、安全
。
官方文件描述:https://developer.android.google.cn/about/versions/11
一、分割槽儲存
從Android 10(API 29)開始,Android預設開啟分割槽儲存
功能,不過Android 10 可通過增加android:requestLegacyExternalStorage="true"
配置停用分割槽儲存
;
從Android 11(API 30)開始,強制執行分割槽儲存
,對於Android 11及以上裝置,android:requestLegacyExternalStorage="true"
配置將不再有效。
Android 11 分割槽儲存官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
Android 10 預設開啟分割槽儲存:
https://xiaxl.blog.csdn.net/article/details/103125117
1.1、訪問目錄
開啟分割槽儲存後,應用預設情況下只能訪問應用專屬目錄(內部儲存、外部儲存應用專屬目錄)
,以及本應用所建立的特定型別的媒體檔案
。
-
應用專屬目錄
包括內部儲存
、外部儲存專屬目錄
(若應用包名com.xiaxl.demo):
/data/data/com.xiaxl.demo/files,
/sdcard/Android/data/com.xiaxl.demo/files
分別採用以下API進行訪問:
File appFile = new File(context.getFilesDir(), filename);
File appExternalFile = new File(context.getExternalFilesDir(), filename);
-
共享儲存目錄
包括媒體、文件和其他檔案。例如DCIM、Pictures、Movies、Download等目錄;
注:
Android 10(Android Q)中共享儲存目錄使用MediaStore API訪問;
Android 11(Android R)中共享儲存目錄支援MediaStore API與File API訪問。
為保證應用在Android 10、Android 11裝置中,使用File API對共享儲存目錄具有相同的檔案訪問許可權
。建議在應用 AndroidManifest配置檔案中,增加requestLegacyExternalStorage="true"
標識,以關閉Android 10裝置上的分割槽儲存功能
,使分割槽儲存只對Android 11以上裝置生效
:
1.2、訪問所需許可權
- 應用專屬目錄
應用專屬目錄(內部儲存
、外部儲存專屬目錄
)的讀寫,Android 4.4以上裝置不需要任何許可權; - 共享儲存目錄
共享儲存路徑的讀寫,需要READ_EXTERNAL_STORAGE
與WRITE_EXTERNAL_STORAGE
許可權;
Android 11以上裝置中,如果您的應用再次請求READ_EXTERNAL_STORAGE
許可權時,動態許可權申請彈窗將變化為“您的應用正在請求訪問照片和媒體”
。
檔案媒體訪問 官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
1.3、共享檔案
如果需要與其他應用共享單個檔案或應用資料,可以使用API:
FileProvider
(分享自己的一個或多個檔案)
如果應用需要將自己的一個或多個檔案提供給其他應用,安全的做法是向接收方應用傳送檔案的內容 URI,並授予對該 URI 的臨時訪問許可權。
AndroidFileProvider
元件提供了getUriForFile()
方法,用於生成檔案的內容URI
。ContentProvider
(獲取替他應用提供的資料)
如果您需要向其他應用提供資料,可以使用ContentProvider
。
ContentProvider
是一種標準介面,可將一個程式中的資料與另一個程式中執行的程式碼進行連。
Android 11 共享檔案官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
1.4、所有檔案的訪問許可權
有一些應用需要獲取所有檔案的訪問許可權,例如:檔案管理器軟體。
獲取所有檔案的訪問許可權,可申請MANAGE_EXTERNAL_STORAGE
許可權。
// 許可權配置
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
// 是否擁有MANAGE_EXTERNAL_STORAGE許可權判斷
Environment.isExternalStorageManager();
// 跳轉到設定頁,請求使用者授權
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
MANAGE_EXTERNAL_STORAGE
相關官方描述:
https://developer.android.google.cn/training/data-storage/manage-all-files
二、許可權
Android 11 中對許可權進行了如下更改:
- 新增
READ_PHONE_NUMBERS
許可權,獲取手機號碼; 後臺訪問位置
許可權調整;- 使用者
多次針對某項特定的許可權請求
點拒絕
,表示使用者希望不再詢問
; - 應用
長時間未使用
,系統會自動重置使用者已授予敏感許可權
; - 針對
位置、麥克風、攝像頭
授權彈窗新增僅限這一次
授權按鈕; SYSTEM_ALERT_WINDOW
許可權授權方式改變為系統自動授權;
參考 Android 11 許可權更新官方文件:
https://developer.android.google.cn/about/versions/11/privacy/permissions#one-time
2.1、新增 READ_PHONE_NUMBERS 許可權
當應用的 targetSdkVersion>=30
時,使用以下API獲取手機號碼
時,需要申請READ_PHONE_NUMBERS
許可權,而不再是READ_PHONE_STATE
許可權。
TelephonyManager
類和TelecomManager
類中的getLine1Number()
方法。TelephonyManager
類中不受支援的getMsisdn()
方法。
在Android 10及之前的裝置,可以繼續使用READ_PHONE_STATE
獲取手機號;
對Android11及以上裝置,需獲取READ_PHONE_NUMBERS
許可權,才能獲取手機號;
<manifest>
<!-- 僅在Android 10及以下裝置獲取READ_PHONE_STATE許可權,以獲取終端手機號碼-->
<uses-permission android:name="READ_PHONE_STATE"
android:maxSdkVersion="29" />
<!-- Android 11及以上裝置獲取READ_PHONE_NUMBERS許可權,以獲取終端手機號碼-->
<uses-permission android:name="READ_PHONE_NUMBERS" />
</manifest>
對於READ_PHONE_STATE
許可權
- Android 10 開始
普通應用
已經不能再讀取裝置的硬體ID
資訊;
相關資訊參考 https://xiaxl.blog.csdn.net/article/details/103125117; - Android 11 開始
獲取手機號
相關API更換為READ_PHONE_NUMBERS
許可權;
READ_PHONE_NUMBERS
許可權官方API描述:
https://developer.android.google.cn/reference/android/Manifest.permission#READ_PHONE_NUMBERS
2.2、後臺訪問位置許可權調整
- 在Android10裝置上,同時
申請前臺、後臺位置許可權
時,並在使用者選擇始終允許
後,才能獲得後臺位置許可權。 - 在Android11裝置上,對於
targetSdkVersion<=29(Android 10)
的應用,同時申請前臺、後臺位置許可權
時,對話方塊不再提示始終允許字樣,而是提供了位置許可權的設定入口,需要使用者在設定頁面選擇始終允許
才能獲得後臺位置許可權。 - 在Android11裝置上,對於
targetSdkVersion=30(Android 11)
的應用,同時申請前臺、後臺位置許可權
時,系統會忽略該請求,無任何響應(需首先獲取前臺位置許可權,再次申請後臺位置許可權
)。 - 在Android11裝置上,對於
targetSdkVersion=30(Android 11)
的應用,先申請前臺位置許可權,後申請後臺位置許可權
。
後臺訪問位置許可權 官方描述:
https://developer.android.google.cn/training/location/background
a、Android10裝置
在Android10裝置上,同時申請前臺、後臺位置許可權
時,並在使用者選擇始終允許
後,才能獲得後臺位置許可權。
// 在Android10裝置上,同時 申請前臺、後臺位置許可權
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
b、Android11裝置 targetSdkVersion<=29
在Android11裝置上,對於targetSdkVersion<=29(Android 10)
的應用,同時申請前臺、後臺位置許可權
時,對話方塊不再提示始終允許字樣,而是提供了位置許可權的設定入口,需要使用者在設定頁面選擇始終允許
才能獲得後臺位置許可權。
// 在Android11裝置上,targetSdkVersion<=29的應用,同時 申請前臺、後臺位置許可權
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
c、Android11裝置 targetSdkVersion=30 同時申請前臺、後臺位置許可權
- 在Android11裝置上,對於
targetSdkVersion=30(Android 11)
的應用,同時申請前臺、後臺位置許可權
時,系統會忽略該請求,無任何響應(需首先獲取前臺位置許可權,再次申請後臺位置許可權
)。
// 在Android11裝置上,targetSdkVersion=30的應用,同時 申請前臺、後臺位置許可權
// 請求無反應,此為錯誤寫法
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
d、Android11裝置 targetSdkVersion=30 依次申請前臺、後臺位置許可權
在Android11裝置上,對於targetSdkVersion=30(Android 11)
的應用,先申請前臺位置許可權,後申請後臺位置許可權
。
// 在Android11裝置上,targetSdkVersion=30的應用,申請前臺位置許可權
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION}, 101);
Android11裝置上,targetSdkVersion=30的應用,申請後臺位置許可權,直接跳轉到設定頁面。
// 在Android11裝置上,targetSdkVersion=30的應用,申請後臺位置許可權
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);
2.3、使用者多次針對某項特定的許可權請求
點拒絕
在 Android 11 中,使用者多次針對某項特定的許可權請求
點選了拒絕
,那麼應用再次請求該項許可權時,使用者將不會看到系統許可權彈窗,該操作表示使用者希望不再詢問
;
2.4、長時間未使用,自動重置已授予敏感許可權
在 Android 11 中,當targetSdkVersion>=30時,應用在一段時間內未使用
,系統會通過自動重置使用者已授予應用的執行時敏感許可權
來保護使用者資料;
2.5、新增“僅限這一次”授權按鈕
從 Android 11(API 級別 30)開始,當應用請求與位置、麥克風、攝像頭
相關許可權時,面向使用者的授權對話方塊會包含僅限這一次
選項;如果使用者在對話方塊中選擇僅限這一次
,系統會嚮應用授予臨時的單次授權。
許可權申請API使用方式不變:
private void showCameraPreview() {
// 判斷是否擁有Camera許可權
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
// 進入Camera頁面
// startCamera();
} else {
// 請求Camera許可權
requestCameraPermission();
}
}
private void requestCameraPermission() {
// 判斷Camera許可權,之前是否已被使用者"拒絕"
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 彈窗告訴使用者,為什麼需要Camera許可權
Snackbar.make(mLayout, R.string.camera_access_required,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
// 請求Camera許可權
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
PERMISSION_REQUEST_CAMERA);
}
}).show();
} else {
// 請求Camera許可權
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CAMERA) {
// 使用者授權Camera(使用者選擇"使用使用時允許"、"僅這一次允許")
if (grantResults.length == 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission has been granted. Start camera preview Activity.
Snackbar.make(mLayout, R.string.camera_permission_granted,
Snackbar.LENGTH_SHORT)
.show();
startCamera();
}
// 使用者選擇"拒絕"
else {
// Permission request was denied.
Snackbar.make(mLayout, R.string.camera_permission_denied,
Snackbar.LENGTH_SHORT)
.show();
}
}
}
原始碼參考:
https://github.com/android/permissions-samples/tree/main/RuntimePermissionsBasic;
2.6、SYSTEM_ALERT_WINDOW 許可權授權方式
在 Android 11 中,SYSTEM_ALERT_WINDOW
許可權授權方式更改為:根據請求自動向某些應用授予 SYSTEM_ALERT_WINDOW 許可權
。
- 系統會自動向具有
ROLE_CALL_SCREENING
且請求SYSTEM_ALERT_WINDOW
的所有應用授予該許可權。如果應用失去ROLE_CALL_SCREENING
,就會失去該許可權。
ROLE_CALL_SCREENING
為RoleManager
中的常量類,多用於通知使用者將我們的應用替換掉手機自帶的預搭載應用(簡訊、電話撥號); - 系統會自動向通過
MediaProjection
擷取螢幕且請求SYSTEM_ALERT_WINDOW
的所有應用授予該許可權,除非使用者已明確拒絕嚮應用授予該許可權。當應用停止擷取螢幕時,就會失去該許可權。此用例主要用於遊戲直播應用。
SYSTEM_ALERT_WINDOW許可權 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/permissions#system-alert
三、隱私保護
主要更改涉及以下幾個方面:
- 軟體包可見性:獲取其他應用資訊需在
AndroidManifest
中增加<queries>
標籤; - 前臺服務:訪問位置資訊、攝像頭、麥克風限制;
- 永久 SIM 卡識別符號 ICCID 獲取受限;
- 新增
AppOpsManager.OnOpNotedCallback
監聽危險許可權的呼叫,從而保護使用者的私密資料;
這樣對於第三方依賴庫的許可權使用申請可以做一個監控
3.1、軟體包可見性
- 在 Android 11 及更高版本裝置中,當應用的
targetSdkVersion>=30
時,如果應用希望獲取其他應用的資訊(比如:包名、軟體名稱),原有方式將無法獲取到。 - 如需獲取其他應用資訊,需要在
AndroidManifest
中增加<queries>
元素標籤,告知系統希望獲取哪些應用的資訊或者哪一類應用的資訊。 - 如果需要獲取所有應用的資訊(比如:Launcher應用、裝置管理器應用):這種情況只需要在
AndroidManifest
中新增QUERY_ALL_PACKAGES
許可權即可。
QUERY_ALL_PACKAGES
許可權為普通許可權,不需要進行動態申請。但提交應用市場後,應用市場可能會進行稽核
軟體包可見性 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/package-visibility
<manifest package="com.xiaxl.myapp">
// 1、若知道具體應用的包名
<queries>
<package android:name="com.xiaxl.otherapp01" />
<package android:name="com.xiaxl.otherapp01" />
</queries>
// 2、不知道包名,但想知道某一類App的應用資訊
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
</queries>
</manifest>
3.2、前臺服務:訪問位置資訊、攝像頭、麥克風限制
當應用的 targetSdkVersion>=30
時,前臺服務
訪問位置資訊、攝像頭、麥克風
時,需新增foregroundServiceType
。
<manifest>
// 前臺服務訪問:位置資訊、攝像頭、麥克風
<service
android:foregroundServiceType="location|camera|microphone" />
</manifest>
前臺服務 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/foreground-services
3.3、永久 SIM 卡識別符號 ICCID 獲取受限
在 Android 11 及更高版本中,使用 SubscriptionInfo.getIccId()
方法訪問不可重置的 ICCID 受到限制。
SubscriptionInfo.getIccId()
方法會返回一個非null的空字串
。
如需唯一標識裝置上安裝的 SIM 卡,請改用 getSubscriptionId()
方法。SubscriptionId
會提供一個索引值,用於唯一識別已安裝的 SIM 卡(包括實體 SIM 卡和電子 SIM 卡),除非裝置恢復出廠設定,否則此識別符號的值對於給定 SIM 卡是保持不變的。
3.4、監聽危險許可權的呼叫
Android 11新增AppOpsManager.OnOpNotedCallback
為開發者提供對應用危險許可權的使用監聽,從而保護使用者的私密資料
。
當應用以及應用的依賴包中,申請某項危險許可權時,AppOpsManager.OnOpNotedCallback
的對應回撥方法將會被呼叫,從而列印申請的許可權
與對應的API呼叫棧
。
舉例:
使用位置許可權獲取位置資訊
時,將會回撥AppOpsManager.OnOpNotedCallback
中的onNoted
方法,並列印使用的許可權
與對應的API呼叫棧
。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//
AppOpsManager.OnOpNotedCallback appOpsCallback =
new AppOpsManager.OnOpNotedCallback() {
private void logPrivateDataAccess(String opCode, String trace) {
Log.i("xiaxl: ", "opCode: " + opCode + "\n trace: " + trace);
}
@Override
public void onNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
}
@Override
public void onSelfNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onSelfNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
}
@Override
public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncNotedAppOp) {
Log.i("xiaxl: ", "---onAsyncNoted---");
logPrivateDataAccess(asyncNotedAppOp.getOp(),
asyncNotedAppOp.getMessage());
}
};
AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);
if (appOpsManager != null) {
appOpsManager.setOnOpNotedCallback(getMainExecutor(), appOpsCallback);
}
}
public void getLocation() {
// 建立歸因
Context attributionContext = createAttributionContext("shareLocation");
// 獲取位置資訊
LocationManager locationManager =
attributionContext.getSystemService(LocationManager.class);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
列印日誌如下:
---onNoted---
opCode: android:coarse_location
trace:
[com.xiaxl.android_test.MainActivity$1.onNoted(MainActivity.java:42),
android.app.AppOpsManager.readAndLogNotedAppops(AppOpsManager.java:8204),
android.os.Parcel.readExceptionCode(Parcel.java:2304),
android.os.Parcel.readException(Parcel.java:2279),
android.location.ILocationManager$Stub$Proxy.getLastLocation(ILocationManager.java:1225),
android.location.LocationManager.getLastKnownLocation(LocationManager.java:648),
com.xiaxl.android_test.MainActivity.getLocation(MainActivity.java:87),
com.xiaxl.android_test.MainActivity$2.onClick(MainActivity.java:70),
android.view.View.performClick(View.java:7448),
com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:967),
android.view.View.performClickInternal(View.java:7425),
android.view.View.access$3600(View.java:810),
android.view.View$PerformClick.run(View.java:28305),
android.os.Handler.handleCallback(Handler.java:938),
android.os.Handler.dispatchMessage(Handler.java:99),
android.os.Looper.loop(Looper.java:223),
android.app.ActivityThread.main(ActivityThread.java:7656),
java.lang.reflect.Method.invoke(Native Method),
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592),
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)]
從以上日誌可以看出,當應用申請ACCESS_COARSE_LOCATION
許可權並獲取位置資訊時
,列印了應用申請的許可權
與對應的API呼叫棧
。
AppOpsManager 相關官方描述:
https://developer.android.google.cn/guide/topics/data/audit-access#audit-by-attribution-tag
四、效能
- JobScheduler使用頻率進行限制
4.1、JobScheduler使用頻率進行限制
Android 11 為對JobScheduler
使用頻率進行一定限制。
對於 debuggable 清單屬性設定為 true 的應用,過多的呼叫 JobScheduler
API 將返回 RESULT_FAILURE
。
JobScheduler
主要用於在未來某個時間下滿足一定條件時觸發執行某項任務,例如:當裝置在空閒狀態, 並且使用wifi時, 自動下載Apk
。
JobScheduler
典型的使用舉例如下:
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(this, MyJobService.class);
//任務Id等於123
JobInfo jobInfo = new JobInfo.Builder(123, jobService)
// 任務最少延遲時間
.setMinimumLatency(5000)
// 任務deadline,當到期沒達到指定條件也會開始執行
.setOverrideDeadline(60000)
// 網路條件,網路無需付費時執行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
// 是否充電
.setRequiresCharging(true)
// 是否在空閒時執行
.setRequiresDeviceIdle(true)
// 裝置重啟後是否繼續執行
.setPersisted(true)
// 設定退避/重試策略
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR)
.build();
scheduler.schedule(jobInfo);
官方描述參考:
https://developer.android.google.cn/about/versions/11/behavior-changes-all
官方Demo參考:
https://github.com/googlearchive/android-JobScheduler
五、安全
- 非 SDK 介面限制
5.1、非 SDK 介面限制
官方從 Android 9(API 級別 28)開始,對應用使用的非 SDK 介面實施了限制。
如果你的APP通過引用非 SDK 介面
或嘗試使用反射或 JNI 來獲取控制程式碼
,這些限制就會起作用。官方給出的解釋是為了提升使用者體驗、降低應用崩潰風險
。
a、非SDK介面檢測工具
官方給出了一個檢測工具,下載地址:veridex
veridex使用方法:
appcompat.sh --dex-file=apk.apk
b、blacklist、greylist、greylist-max-o、greylist-max-p含義
以上截圖中,blacklist、greylist、greylist-max-o、greylist-max-p含義如下:
- blacklist 黑名單:禁止使用的非SDK介面,執行時直接Crash(因此必須解決)
- greylist 灰名單:即當前版本仍能使用的非SDK介面,但在下一版本中可能變成被限制的非SDK介面
- greylist-max-o: 在targetSDK<=O中能使用,但是在targetSDK>=P中被禁止使用的非SDK介面
- greylist-max-p: 在targetSDK<=P中能使用,但是在targetSDK>=Q中被禁止使用的非SDK介面
非SDK介面限制 官方描述:
https://developer.android.google.cn/about/versions/11/non-sdk-11