Android R 新特性分析及適配指南


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_STORAGEWRITE_EXTERNAL_STORAGE 權限;

文件路徑的訪問權限

Android 11以上設備中,如果您的應用再次請求READ_EXTERNAL_STORAGE權限時,動態權限申請彈窗將變化為“您的應用正在請求訪問照片和媒體”

您的應用正在請求訪問照片和媒體

文件媒體訪問 官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage

1.3、共享文件

如果需要與其他應用共享單個文件或應用數據,可以使用API:

  • FileProvider(分享自己的一個或多個文件)
    如果應用需要將自己的一個或多個文件提供給其他應用,安全的做法是向接收方應用發送文件的內容 URI,並授予對該 URI 的臨時訪問權限。
    Android FileProvider 組件提供了 getUriForFile() 方法,用於生成文件的內容 URI
  • ContentProvider(獲取替他應用提供的數據)
    如果您需要向其他應用提供數據,可以使用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權限

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);

在Android10設備上,同時 申請前台、后台位置權限

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);

Android11設備上targetSdkVersion<=29的應用,申請前台、后台位置權限

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的應用,申請后台位置權限,直接跳轉到設置頁面。

// 在Android11設備上,targetSdkVersion=30的應用,申請后台位置權限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

Android11設備上,targetSdkVersion=30的應用,申請后台位置權限

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_SCREENINGRoleManager中的常量類,多用於通知用戶將我們的應用替換掉手機自帶的預搭載應用(短信、電話撥號);
  • 系統會自動向通過 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

veridex檢測截圖

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

========== THE END ==========

歡迎關注我的公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM