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