天天看點

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_STORAGE

WRITE_EXTERNAL_STORAGE

權限;

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

Android 11以上裝置中,如果您的應用再次請求

READ_EXTERNAL_STORAGE

權限時,動态權限申請彈窗将變化為

“您的應用正在請求通路照片和媒體”

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

檔案媒體通路 官方描述:

1.3、共享檔案

如果需要與其他應用共享單個檔案或應用資料,可以使用API:

  • FileProvider

    (分享自己的一個或多個檔案)

如果應用需要将自己的一個或多個檔案提供給其他應用,安全的做法是向接收方應用發送檔案的内容 URI,并授予對該 URI 的臨時通路權限。

Android

FileProvider

元件提供了

getUriForFile()

方法,用于生成檔案的内容

URI

  • ContentProvider

    (擷取替他應用提供的資料)

    如果您需要向其他應用提供資料,可以使用

    ContentProvider

    ContentProvider

    是一種标準接口,可将一個程序中的資料與另一個程序中運作的代碼進行連。
    Android R 新特性分析及适配指南

Android 11 共享檔案官方描述:

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

    資訊;

相關資訊參考

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

    的應用,同時

    申請前台、背景位置權限

    時,對話框不再提示始終允許字樣,而是提供了位置權限的設定入口,需要

    使用者在設定頁面選擇始終允許

    才能獲得背景位置權限。
  • targetSdkVersion=30(Android 11)

    申請前台、背景位置權限

    時,系統會忽略該請求,無任何響應(

    需首先擷取前台位置權限,再次申請背景位置權限

    )。
  • targetSdkVersion=30(Android 11)

    的應用,

    先申請前台位置權限,後申請背景位置權限

背景通路位置權限 官方描述:

https://developer.android.google.cn/training/location/background

a、Android10裝置

申請前台、背景位置權限

始終允許

// 在Android10裝置上,同時 申請前台、背景位置權限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);           
Android R 新特性分析及适配指南

b、Android11裝置 targetSdkVersion<=29

targetSdkVersion<=29(Android 10)

申請前台、背景位置權限

使用者在設定頁面選擇始終允許

// 在Android11裝置上,targetSdkVersion<=29的應用,同時 申請前台、背景位置權限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);           
Android R 新特性分析及适配指南

c、Android11裝置 targetSdkVersion=30 同時申請前台、背景位置權限

  • 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 依次申請前台、背景位置權限

targetSdkVersion=30(Android 11)

先申請前台位置權限,後申請背景位置權限

// 在Android11裝置上,targetSdkVersion=30的應用,申請前台位置權限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION}, 101);           
Android R 新特性分析及适配指南

Android11裝置上,targetSdkVersion=30的應用,申請背景位置權限,直接跳轉到設定頁面。

// 在Android11裝置上,targetSdkVersion=30的應用,申請背景位置權限
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);           
Android R 新特性分析及适配指南

2.3、使用者

多次針對某項特定的權限請求

拒絕

在 Android 11 中,使用者

多次針對某項特定的權限請求

點選了

拒絕

,那麼應用再次請求該項權限時,使用者将不會看到系統權限彈窗,該操作表示使用者希望

不再詢問

2.4、長時間未使用,自動重置已授予敏感權限

在 Android 11 中,當targetSdkVersion>=30時,

應用在一段時間内未使用

,系統會通過

自動重置使用者已授予應用的運作時敏感權限

來保護使用者資料;

2.5、新增“僅限這一次”授權按鈕

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

從 Android 11(API 級别 30)開始,當應用請求與

位置、麥克風、攝像頭

相關權限時,面向使用者的授權對話框會包含

僅限這一次

選項;如果使用者在對話框中選擇

僅限這一次

,系統會向應用授予臨時的單次授權。

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

權限申請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           
Android R 新特性分析及适配指南

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 ==========

文章首發于公衆号”CODING技術小館“,如果文章對您有幫助,可關注我的公衆号。

上一篇: ECS使用體驗
下一篇: ECS使用有感

繼續閱讀