1. 程式人生 > >Android 6.0許可權機制及開發流程詳解

Android 6.0許可權機制及開發流程詳解

許可權機制變更的背景 

在Android6.0之前,app在安裝時會提示使用者此app需要使用哪些許可權,但使用者只能選擇同意或拒絕安裝,而不能單獨對某項許可權進行授予或拒絕。只要使用者選擇了安裝,即表示使用者接受了app對這些許可權的使用,如果使用者不希望app獲取某些涉及隱私的資訊,例如讀取通訊錄,讀取簡訊,獲取地理位置等,只能選擇不安裝。

在這套許可權機制下,使用者只能在安裝應用和拒絕許可權之間二選一,選擇拒絕許可權就意味著不能使用此應用,這樣做的代價太大,和使用者下載此應用的初衷相違背,大多數時候使用者只能選擇妥協,而安裝了應用則意味著將個人隱私資訊完全暴露給了應用。當用戶習慣了這種方式之後,在應用安裝時基本都不會再關注提示的許可權資訊,因此Android的這套許可權機制並沒有真正的起到許可權管理和保護資訊的作用。

新的許可權管理機制 

從Android6.0開始,Android引入了新的許可權管理機制,將應用可使用的許可權劃分成了兩類,一類是normal permissions,也就是普通許可權,例如訪問網路,建立快捷方式,開啟閃光燈等 ,這類許可權一般不涉及使用者隱私,另一類是dangerous permissions,例如撥打電話,讀取通訊錄,讀取簡訊,獲取地理位置等。對normal permissions,仍然和以前一樣,開發者只需要在AndroidManifest中配置即可,應用安裝時提示使用者所需的許可權,使用者同意安裝即表示授權應用使用這些許可權。對dangerous permissions這類涉及使用者隱私的許可權,不僅需要在AndroidManifest中配置,還需要在執行時請求使用者授權,使用者這時可以單獨允許或拒絕某項許可權。當用戶選擇了拒絕某項許可權時,應用將無法執行需要對應許可權的api。

通過引入這套新的許可權管理機制,使用者在許可權管理上有了更高的自由度,使用者不再需要為了限制某項資訊不被獲取而捨棄整個應用的使用權。對涉及使用者隱私的這類操作,使用者可以選擇拒絕,而應用的其他功能又不受影響。

執行時許可權申請流程 

dangerous permissions執行時的許可權申請主要用到如下幾個API。

  1. Context.checkSelfPermission(String permission) 檢查是否被授予了某個許可權
  2. Activity.requestPermissions(String[] permissions, int requestCode) 申請一組許可權
  3. Activity.shouldShowRequestPermissionRationale(String permission) 判斷是否需要顯示申請此許可權的原因,在應用第一次申請某個許可權,或者使用者對該許可權請求授權介面選擇了不再顯示時此方法返回false,否則返回true。
  4. Activity.onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 許可權申請結果回撥

這四個都是從Android 6.0系統 (API Level 23)才開始有的new API,因此使用前都需要判斷當前系統的版本是否是Android 6.0以上。 

完整的許可權申請流程如下,虛線表示這是一個非同步的過程。

這裡寫圖片描述

執行時許可權申請注意事項

  1. requestPermissions()的第一個引數是一個數組,可以一次申請多個許可權。
  2. 如果一次申請了多個許可權,許可權請求對話方塊的彈出順序是按照陣列的順序來的,陣列前面的許可權會先讓使用者確認。一般來說,將必要的許可權放在陣列前面,輔助的許可權放在陣列後面。這樣可以增加必要許可權申請的成功率。
  3. 如果一次申請了多個許可權,只有所有的許可權被使用者處理(拒絕或接受)後,onRequestPermissionsResult()才會被回撥,不是處理一個回撥一次。
  4. 如果一次申請的許可權中,部分許可權沒有在AndroidManifest.xml中宣告,則不會彈出該許可權的請求對話方塊,只會彈出那些在AndroidManifest.xml中宣告過的許可權,等使用者處理完後onRequestPermissionsResult()被回撥,那些未在AndroidManifest.xml中宣告的許可權請求結果一定是PERMISSION_DENIED。特別的,如果一次申請的許可權中,所有的許可權都沒有在AndroidManifest.xml中宣告,則不會彈出任何請求對話方塊,回撥onRequestPermissionsResult()會被立刻執行(這裡立刻的含義是指整個過程中沒有需要使用者互動的地方,不是指onRequestPermissionsResult()會在執行requestPermissions()時就被呼叫,onRequestPermissionsResult()回撥仍然是一個非同步的過程),且所有許可權請求結果都是PERMISSION_DENIED。
  5. 如果一次申請的許可權中,部分許可權已經被授予,對已經授予的許可權並不是忽略,而是仍然會彈出請求對話方塊,不同的是沒有下次不再提醒的複選框。如果使用者此次選擇了拒絕,則應用將會失去該許可權。所以在申請許可權前一定要先判斷哪些許可權是已經獲得的,已經授予的許可權不要再次申請。特別是一次申請多個許可權的時候,一定要每次都判斷哪些許可權已經獲得了,只申請哪些未被授予的許可權。 
    例如,想要一次性申請READ_PHONE_STATE,READ_EXTERNAL_STORAGE,RECORD_AUDIO,ACCESS_COARSE_LOCATION,GET_ACCOUNTS五個許可權,以下是正確的處理方式

    String [] permissions = {Manifest.permission.READ_PHONE_STATE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.GET_ACCOUNTS};
    // getUngrantedPermissions():遍歷permissions,返回其中未被授予的許可權,如果所有許可權都被授予,則返回空的陣列。
    String [] unGranted = getUngrantedPermissions(permissions);
    if (unGranted.length != 0) {
        requestPermissions(unGranted);
    }

    以下是錯誤的處理方式

    String [] permissions = {Manifest.permission.READ_PHONE_STATE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.GET_ACCOUNTS};
    // checkPermissions()會遍歷permissions,如果存在未被授予的許可權,返回false,所有許可權都被授予,返回true
    boolean isGranted = checkPermissions(permissions);
    if (!isGranted) {
        requestPermissions(permissions);
    }
  6. 如果一個app的targetSdkVersion設定為23以下,當這個app在Android 6.0系統上執行時,系統會自動為它授予dangerous的許可權。不過使用者仍然可以通過系統設定來取消某項許可權。在系統設定取消授權時,和targetSdkVersion設定為23的app不同的是,會多出一個警告提示,告知使用者取消授權可能會導致應用異常。

  7. 如果一個app的targetSdkVersion設定為23以下,在Android 6.0系統上執行checkSelfPermission()檢查是否有某項許可權時,只要在AndroidManifest.xml中聲明瞭該許可權,無論當前是否被授予了該許可權,返回結果都是PERMISSION_GRANTED。也就是說如果該許可權沒有在AndroidManifest.xml中宣告,則checkSelfPermission()返回PERMISSION_DENIED,如果該許可權在AndroidManifest.xml中聲明瞭,即使使用者手動禁止了該許可權,checkSelfPermission()也會返回PERMISSION_GRANTED。所以,無法通過後checkSelfPermission()來判斷使用者是否禁止了某項許可權。
  8. 如果一個app的targetSdkVersion設定為23以下,在Android 6.0系統上執行requestPermissions(),結果和targetSdkVersion設定為23的app差不多,唯一不同的對話方塊中沒有下次不再提醒的複選框。這點和targetSdkVersion設定為23時申請已經被授予的許可權的效果相同。原因應該也是系統認為所有申請的許可權都已經被授予了。
  9. 如果一個app的targetSdkVersion設定為23以下,在Android 6.0系統上呼叫一個需要許可權的api時,如果這個許可權被使用者手動取消了,不會丟擲異常。但是該api將什麼也不做,如果有返回值的話會返回null或者0。
  10. requestPermissions()的第二個引數requestCode是一個int型別的整數,用來標識一次請求過程,在onRequestPermissionsResult()中可以通過requestCode來區分此次返回的是哪一次請求的結果。如果在一個Activity類中有多次請求不同許可權的操作,則需要區分requestCode,一般來說可以隨意取一個整數。需要注意的是,requestCode必須是一個大於等於0的整數。如果傳入了一個小於0的整數,雖然不會有異常,但是也不會有任何效果。不會彈出請求對話方塊,onRequestPermissionsResult()也不會被執行。例如,用0xFF000001作為requestCode是不會有任何效果的。

WRITE_SETTINGS和SYSTEM_ALERT_WINDOW許可權申請

從normal permissions (https://developer.android.com/guide/topics/security/normal-permissions.html) 和dangerous permissions (https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous) 列表中可以看到,這兩個列表中都沒有包含WRITE_SETTINGS和SYSTEM_ALERT_WINDOW這兩個許可權,也就是說這兩個許可權既不屬於normal permission,也不屬於dangerous permission。這是因為Android認為這兩個許可權非常敏感,已經超出了dangerous permissions的程度,一般app中都不應該使用這兩個許可權,因此將這兩個許可權單獨分成一類,稱為special permissions。

這兩個許可權在Android6.0系統上同樣需要在執行時申請,不過上述針對dangerous permissions的執行時許可權申請方法對這兩個許可權是不適用的,Android提供了額外的api來檢查和申請這兩個許可權。

special permissions執行時的許可權申請主要用到如下幾個api。

  1. Settings.System.canWrite(Context context) 檢查是否被授予了WRITE_SETTINGS許可權
  2. Settings.canDrawOverlays(Context context) 檢查是否被授予了SYSTEM_ALERT_WINDOW許可權
  3. startActivityForResult(Intent intent, in requestCode) 開啟使用者授權介面
  4. onActivityResult(int requestCode, int resultCode, Intent data) 許可權申請結果回撥

此外還用到兩個字串常量

  1. Settings.ACTION_MANAGE_WRITE_SETTINGS申請WRITE_SETTINGS許可權對應的intent action
  2. Settings.ACTION_MANAGE_OVERLAY_PERMISSION申請SYSTEM_ALERT_WINDOW許可權對應的intent action

前兩個API和兩個字串常量同樣是從Android 6.0系統(API Level 23)才開始有的,因此使用前都需要判斷當前系統的版本是否是Android 6.0以上。 

申請WRITE_SETTINGS許可權示例程式碼如下。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    // 判斷是否有WRITE_SETTINGS許可權
    if(!Settings.System.canWrite(this)) {
        // 申請WRITE_SETTINGS許可權
        Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, 
                                Uri.parse("package:" + getPackageName()));
        // REQUEST_CODE1是本次申請的請求碼
        startActivityForResult(intent, REQUEST_CODE1);
    } else {
        dosomething();
    }
} else {
    dosomething();
}

判斷WRITE_SETTINGS許可權申請結果流程示例程式碼如下。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 請求碼是REQUEST_CODE1,表示本次結果是申請WRITE_SETTINGS許可權的結果
    if (requestCode == REQUEST_CODE1) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // 判斷是否有WRITE_SETTINGS許可權
            if (Settings.System.canWrite(this)) {
                dosomething();
            }
        }
    }
    super.onActivityResult(requestCode, resultCode, data);
}

申請SYSTEM_ALERT_WINDOW許可權示例程式碼如下。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    // 判斷是否有SYSTEM_ALERT_WINDOW許可權
    if(!Settings.canDrawOverlays(this)) {
        // 申請SYSTEM_ALERT_WINDOW許可權
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 
                                Uri.parse("package:" + getPackageName()));
        // REQUEST_CODE2是本次申請的請求碼
        startActivityForResult(intent, REQUEST_CODE2);
    } else {
        dosomething();
    }
} else {
    dosomething();
}

判斷SYSTEM_ALERT_WINDOW許可權申請結果流程示例程式碼如下。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 請求碼是REQUEST_CODE2,表示本次結果是申請SYSTEM_ALERT_WINDOW許可權的結果
    if (requestCode == REQUEST_CODE2) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // 判斷是否有SYSTEM_ALERT_WINDOW許可權
            if (Settings.canDrawOverlays(this)) {
                  dosomething();
            }
        }
    }
    super.onActivityResult(requestCode, resultCode, data);
}

這裡同樣有幾個需要注意的地方

  1. special permission同樣需要先在AndroidManifest中配置,如果未在AndroidManifest中配置,執行startActivityForResult()後仍然會顯示使用者授權介面,不過文字和按鈕都是灰色的,使用者無法更改。
  2. 申請的許可權如果已經被授予,執行startActivityForResult()後仍然會顯示使用者授權介面,使用者可以選擇取消授權,所以在執行startActivityForResult()前一定要先判斷是否已經授予了許可權。
  3. 多次執行startActivityForResult(),會按照startActivityForResult()的執行順序依次彈出多個使用者授權介面。即使申請的是同一個許可權,也是如此。
  4. 和onRequestPermissionsResult()中的第三個引數grantResults的作用不同,onActivityResult()中的resultCode不能用來判斷許可權申請的結果,無論使用者是否授予了許可權,resultCode始終為0。
  5. 對這兩個special permission,無論是否已經授予許可權,checkSelfPermission()返回的都是PERMISSION_DENIED,如果試圖使用requestPermissions()來申請許可權,不會彈出任何許可權確認的介面,不過onRequestPermissionsResult()回撥方法仍然會被執行,grantResults的結果始終都是PERMISSION_DENIED。因此,試圖用申請dangerous permission的方法來檢查和申請這兩個special permission是沒有任何效果的。

一些額外的話題

Android裝置的唯一標識問題

很多app都需要獲取Android裝置的唯一標識(UDID),用來作為臨時的身份認證,後臺的日誌記錄,資料統計等。但由於Android版本和機型眾多,同時又有數量龐大的各種非官方ROM,通過某個單一特徵難以唯一標識一臺裝置。因此,通常會獲取多個特徵,然後整合在一起作為該裝置標識。裝置的IMEI碼是其中的一個重要特徵,對有電話功能的裝置,IMEI碼都是唯一的。國內大量的app都會使用IMEI碼作為標記使用者身份的一個關鍵資訊。 
然而獲取IMEI碼需要READ_PHONE_STATE許可權,此許可權屬於dangerous permission,所以在Android 6.0之後需要在執行時申請。這帶來了如下兩個問題。 
1. 獲取裝置唯一標識通常都在進入應用後立刻執行的,而申請許可權則是一個非同步的過程,使用者可能很長時間後才會處理,這使得所有需要此資訊的地方都需要放到onRequestPermissionsResult()回撥後才能執行。 
2. 由於使用者可以選擇允許和拒絕此許可權,如果使用者選擇了允許此許可權,則可以獲取到IMEI碼,如果使用者選擇了拒絕,則無法獲取。這意味著同一個裝置,使用者的不同選擇會產生兩個不同的裝置識別碼(注意:使用者選擇後可以隨時在系統裡面更改是否授予此許可權)。如果裝置識別碼只是用來記錄一些使用者日誌,可能不會有太大問題,只是同一個使用者產生了兩份使用者日誌。但是如果將裝置識別碼作為登陸時的身份認證(例如很多app都有的遊客登入功能),則可能會產生一些問題。使用者選擇允許或拒絕許可權就會變成兩個不同的使用者。

為了避免此類問題,建議調整裝置識別碼的計算方式,對Android 6.0及以上版本的裝置不再將IMEI碼作為裝置識別碼(或裝置識別碼的一部分)。另一個常用的作為裝置識別碼的資訊是ANDROID ID,這也是Google官方推薦的裝置標識。不過在早期的Android版本中,ANDROID ID的設定存在一些bug,此外幾年前有些國內手機廠家出廠時會將同一個批次的所有手機用同一個ANDROID ID。這些問題使得ANDROID ID在早期的版本上不是很可靠,不過目前這些問題應該都已得到解決。ANDROID ID的bug Google早已修復,國內幾個大廠應該也不會再犯這種錯誤。因此,對Android 6.0及以上版本使用ANDROID ID已經完全可以唯一標識一個裝置。對Android 6.0以下版本,仍然可以使用原先的混合多個資訊的方式。

第三方SDK的問題

一個功能完整的app通常需要接入多個第三方sdk,如地圖,推送,統計,社交,廣告,渠道等。目前大量的第三方sdk仍然是在低版本上開發,沒有相容Android 6.0。當sdk中的程式碼需要使用某個許可權時,沒有經過許可權檢查,許可權申請的流程。當集成了這些sdk的app在Android 6.0系統上執行時, 如果此時應用沒有被授予對應的許可權,就會導致程式異常。

對app開發者來說。可以嘗試以下幾個方法。 
1. 將targetSDKVersion設定為Android 23以下。由於Android 6.0系統會為targetSDKVersion為23以下的app自動授予dangerous許可權,這樣就可以避免由於沒有許可權導致的異常。不過這種方法並不是萬能的,部分應用市場對新發布APP的targetSDKVersion有最低版本的要求,如果要將應用釋出到這些應用市場, 就不能這樣設定了。此外,雖然Android 6.0系統會為targetSDKVersion為23以下的app自動授予dangerous許可權,但是使用者仍然可以通過系統設定來禁止某項許可權。如果使用者手動禁止了某項許可權,仍然會導致程式異常。 
2. app代替sdk申請許可權。在sdk api呼叫之前,在app程式碼中增加許可權申請流程。待許可權申請通過後再去呼叫sdk的api。這種方法並不能完全解決問題,sdk中很多程式碼都是在後臺執行的,不是通過某個api,而是通過某些後臺事件來觸發,沒有辦法在app中知道何時需要申請該許可權,即使是在app啟動時就立刻申請許可權,也無法保證sdk中程式碼執行時,使用者已經授予了許可權。

對sdk開發者來說,應當儘快升級sdk版本,支援Android 6.0的許可權機制。

通過intent使用相機的許可權問題

通常我們會使用如下程式碼來使用系統相機,將拍照儲存到指定的檔案中。

Uri uri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE);

無論是Android6.0之前的版本,還是Android6.0及之後的版本, 通過這種方式使用相機都不需要在AndroidManifest.xml中宣告CAMERA許可權(https://developer.android.com/training/permissions/best-practices.html#perms-vs-intents)。也就是說,如果沒有在AndroidManifest.xml中宣告CAMERA許可權,那麼這段程式碼執行是沒有問題的。但是反過來,如果在AndroidManifest.xml中聲明瞭CAMERA許可權(這可能是其他地方程式碼或者接入的某些第三方SDK中需要用到CAMERA許可權),則這段程式碼在Android6.0系統上執行時,會檢查app此時是否已經被授予了使用相機的許可權,如果沒有,則會產生SecurityException。

這個設定看起來相當怪異,沒有宣告許可權可以正常執行,聲明瞭許可權卻有可能導致應用崩潰。但是Google就是這樣設計的,只能在程式碼中做相容了。

對app開發者來說,如果app的AndroidManifest.xml中沒有宣告CAMERA許可權,則不需要修改。如果app的AndroidManifest.xml中聲明瞭CAMERA許可權,則在通過intent啟動相機前需要先判斷是否已經被授予了CAMERA許可權,如果沒有,則需要先通過requestPermissions()申請拍照許可權,申請到許可權後再執行上述程式碼。

對sdk開發者來說,由於不知道接入sdk的app是否需要CAMERA許可權,所以必須先判斷AndroidManifest.xml中是否聲明瞭CAMERA許可權,如果沒有宣告,則直接啟動相機,如果聲明瞭,則同樣是先判斷是否已經被授予了CAMERA許可權,如果沒有授予許可權,則需要再通過requestPermissions()申請拍照許可權。這個流程是通用的,對app開發者來說也是適用的。建議app開發者也採用這個流程,避免出現剛開始專案中沒有用到CAMERA許可權,但是後來由於接入第三方sdk等原因加了CAMERA許可權,但是卻忘記修改之前的呼叫相機程式碼的情況。

判斷AndroidManifest.xml中是否聲明瞭某項許可權的方法如下。

public boolean hasPermissionInManifest(Context context, String permissionName) {
    final String packageName = context.getPackageName();
    try {
        final PackageInfo packageInfo = context.getPackageManager()
                .getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
        final String[] declaredPermisisons = packageInfo.requestedPermissions;
        if (declaredPermisisons != null && declaredPermisisons.length > 0) {
            for (String p : declaredPermisisons) {
                if (p.equals(permissionName)) {
                    return true;
                }
            }
        }
    } catch (NameNotFoundException e) {

    }
    return false;
}

WIFI和藍芽掃描問題

Android 6.0增加了對附近裝置掃描的許可權限制,如下三個API在呼叫前都需要先獲取ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION許可權。 
1. WifiManager.getScanResults() 
2. BluetoothDevice.ACTION_FOUND 
3. BluetoothLeScanner.startScan() 

例如,通過BluetoothAdapter.startDiscovery()來搜尋附近的藍芽裝置,在Android 6.0之前只需要在AndroidManifest中宣告BLUETOOTH和BLUETOOTH_ADMIN許可權即可,從Android 6.0之後還需要在AndroidManifest中宣告ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION許可權。由於這兩個許可權屬於dangerous permission,所以還需要在執行時申請該許可權,等使用者授權後才可以通過BluetoothAdapter.startDiscovery()來搜尋附近的藍芽裝置。如果沒有在AndroidManifest中宣告ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION許可權,或者沒有得到使用者授權就呼叫BluetoothAdapter.startDiscovery(),那麼定義的BroadcastReceiver在Android 6.0系統上中是不會收到任何訊息的。

轉載自:https://blog.csdn.net/ccpat/article/details/51151863