1. 程式人生 > >Android M 新的執行時許可權開發者需要知道的一切

Android M 新的執行時許可權開發者需要知道的一切

http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/

android M 的名字官方剛釋出不久,最終正式版即將來臨!
android在不斷髮展,最近的更新 M 非常不同,一些主要的變化例如執行時許可權將有顛覆性影響。驚訝的是android社群鮮有談論這事兒,儘管這事很重要或許在不遠的將來會引發很嚴重的問題。
這是今天我寫這篇部落格的原因。這裡有一切關於android執行時許可權你需要知道的,包括如何在程式碼中實現。現在亡羊補牢還不晚。

新執行時許可權

android的許可權系統一直是首要的安全概念,因為這些許可權只在安裝的時候被詢問一次。一旦安裝了,app可以在使用者毫不知曉的情況下訪問許可權內的所有東西。
難怪一些壞蛋利用這個缺陷惡意收集使用者資料用來做壞事了!
android小組也知道這事兒。7年了!許可權系統終於被重新設計了。在android6.0棉花糖,app將不會在安裝的時候授予許可權。取而代之的是,app不得不在執行時一個一個詢問使用者授予許可權。

注意許可權詢問對話方塊不會自己彈出來。開發者不得不自己呼叫。如果開發者要呼叫的一些函式需要某許可權而使用者又拒絕授權的話,函式將丟擲異常直接導致程式崩潰。

另外,使用者也可以隨時在設定裡取消已經授權的許可權。

你或許已經感覺到背後生出一陣寒意。。。如果你是個android開發者,意味著要完全改變你的程式邏輯。你不能像以前那樣直接呼叫方法了,你不得不為每個需要的地方檢察許可權,否則app就崩潰了!
是的。我不能哄你說這是簡單的事兒。儘管這對使用者來說是好事,但是對開發者來說就是噩夢。我們不得不修改編碼不然不論短期還是長遠來看都是潛在的問題。
這個新的執行時許可權僅當我們設定targetSdkVersion to 23

(這意味著你已經在23上測試通過了)才起作用,當然還要是M系統的手機。app在6.0之前的裝置依然使用舊的許可權系統。

已經發布了的app會發生什麼

新執行時許可權可能已經讓你開始恐慌了。“hey,夥計!我三年前釋出的app可咋整呢。如果他被裝到android 6.0上,我的app會崩潰嗎?!?”
莫慌張,放輕鬆。android小隊又不傻,肯定考慮到了這情況。如果app的targetSdkVersion 低於 23,那將被認為app沒有用23新許可權測試過,那將被繼續使用舊有規則:使用者在安裝的時候不得不接受所有許可權,安裝後app就有了那些許可權咯!

然後app像以前一樣奔跑!注意,此時使用者依然可以取消已經同意的授權!

使用者取消授權時,android 6.0系統會警告,但這不妨礙使用者取消授權。

問題又來了,這時候你的app崩潰嗎?
善意的主把這事也告訴了android小組,當我們在targetSdkVersion 低於23的app呼叫一個需要許可權的函式時,這個許可權如果被使用者取消授權了的話,不丟擲異常。但是他將啥都不幹,結果導致函式返回值是null或者0.

別高興的太早。儘管app不會呼叫這個函式時崩潰,返回值null或者0可能接下來依然導致崩潰。
好訊息(至少目前看來)是這類取消許可權的情況比較少,我相信很少使用者這麼搞。如果他們這麼辦了,後果自負咯。
但從長遠看來,我相信還是會有大量使用者會關閉一些許可權。我們app不能再新裝置完美執行這是不可接受的。
怎樣讓他完美執行呢,你最好修改程式碼支援最新的許可權系統,而且我建議你立刻著手搞起!
程式碼沒有成功改為支援最新執行時許可權的app,不要設定targetSdkVersion 23 釋出,否則你就有麻煩了。只有當你測試過了,再改為targetSdkVersion 23 。
警告:現在你在android studio新建專案,targetSdkVersion 會自動設定為 23。如果你還沒支援新執行時許可權,我建議你首先把targetSdkVersion 降級到22

PROTECTION_NORMAL類許可權

當用戶安裝或更新應用時,系統將授予應用所請求的屬於 PROTECTION_NORMAL 的所有許可權(安裝時授權的一類基本許可權)。這類許可權包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

只需要在AndroidManifest.xml中簡單宣告這些許可權就好,安裝時就授權。不需要每次使用時都檢查許可權,而且使用者不能取消以上授權。

讓你的app支援新執行時許可權

是時候讓我們的app支援新許可權模型了,從設定compileSdkVersion andtargetSdkVersion 為 23開始吧.

1
2
3
4
5
6
7
8
9
android {
    compileSdkVersion 23
    ...

    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }

例子,我想用一下方法新增聯絡人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static final String TAG = "Contacts";
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);

    // First, set up a new raw contact.
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());

    // Next, set the name for the contact.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
                    "__DUMMY CONTACT from runtime permissions sample");
    operations.add(op.build());

    // Apply the operations.
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

上面程式碼需要WRITE_CONTACTS許可權。如果不詢問授權,app就崩了。
下一步像以前一樣在AndroidManifest.xml新增宣告許可權。

1
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

下一步,不得不再寫個方法檢查有沒有許可權。如果沒有彈個對話方塊詢問使用者授權。然後你才可以下一步建立聯絡人。
許可權被分組了,如下表:

同一組的任何一個許可權被授權了,其他許可權也自動被授權。例如,一旦WRITE_CONTACTS被授權了,app也有READ_CONTACTSGET_ACCOUNTS了。
原始碼中被用來檢查和請求許可權的方法分別是Activity的checkSelfPermissionrequestPermissions。這些方法api23引入。

1
2
3
4
5
6
7
8
9
10
11
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

如果已有許可權,insertDummyContact()會執行。否則,requestPermissions被執行來彈出請求授權對話方塊,如下:

不論使用者同意還是拒絕,activity的onRequestPermissionsResult會被回撥來通知結果(通過第三個引數),grantResults,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_PERMISSIONS:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

這就是新許可權模型工作過程。程式碼真複雜但是隻能去習慣它。。。為了讓app很好相容新許可權模型,你不得不用以上類似方法處理所有需要的情況。
如果你想捶牆,現在是時候了。。。

處理 “不再提醒”

如果使用者拒絕某授權。下一次彈框,使用者會有一個“不再提醒”的選項的來防止app以後繼續請求授權。

如果這個選項在拒絕授權前被使用者勾選了。下次為這個許可權請求requestPermissions時,對話方塊就不彈出來了,結果就是,app啥都不幹。
這將是很差的使用者體驗,使用者做了操作卻得不到響應。這種情況需要好好處理一下。在請求requestPermissions前,我們需要檢查是否需要展示請求許可權的提示通過activity的shouldShowRequestPermissionRationale,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                                        REQUEST_CODE_ASK_PERMISSIONS);
                            }
                        });
                return;
            }
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(MainActivity.this)
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show();
}

當一個許可權第一次被請求和使用者標記過不再提醒的時候,我們寫的對話方塊被展示。
後一種情況,onRequestPermissionsResult 會收到PERMISSION_DENIED ,系統詢問對話方塊不展示。

搞定!

一次請求多個許可權

當然了有時候需要好多許可權,可以用上面方法一次請求多個許可權。不要忘了為每個許可權檢查“不再提醒”的設定。
修改後的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;

private void insertDummyContactWrapper() {
    List<String> permissionsNeeded = new ArrayList<String>();

    final List<String> permissionsList = new ArrayList<String>();
    if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
        permissionsNeeded.add("GPS");
    if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
        permissionsNeeded.add("Read Contacts");
    if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
        permissionsNeeded.add("Write Contacts");

    if (permissionsList.size() > 0) {
        if (permissionsNeeded.size() > 0) {
            // Need Rationale
            String message = "You need to grant access to " + permissionsNeeded.get(0);
            for (int i = 1; i < permissionsNeeded.size(); i++)
                message = message + ", " + permissionsNeeded.get(i);
            showMessageOKCancel(message,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                        }
                    });
            return;
        }
        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
        return;
    }

    insertDummyContact();
}

private boolean addPermission(List<String> permissionsList, String permission) {
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        permissionsList.add(permission);
        // Check for Rationale Option
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }
    return true;
}

如果所有許可權被授權,依然回撥onRequestPermissionsResult,我用hashmap讓程式碼整潔便於閱讀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
            {
            Map<String, Integer> perms = new HashMap<String, Integer>();
            // Initial
            perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
            // Fill with results
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);
            // Check for ACCESS_FINE_LOCATION
            if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                // All Permissions Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

條件靈活的,你自己設定。有的情況,一個許可權沒有授權,就不可用;但是也有情況,能工作,但是表現的是有所限制的。對於這個我不做評價,你自己設計吧。

用相容庫使程式碼相容舊版

以上程式碼在android 6.0以上執行沒問題,但是23 api之前就不行了,因為沒有那些方法。
粗暴的方法是檢查版本

1
2
3
4
5
if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

但是太複雜,我建議用v4相容庫,已對這個做過相容,用這個方法代替:

  • ContextCompat.checkSelfPermission()
    被授權函式返回PERMISSION_GRANTED,否則返回PERMISSION_DENIED ,在所有版本都是如此。
  • ActivityCompat.requestPermissions()
    這個方法在M之前版本呼叫,OnRequestPermissionsResultCallback 直接被呼叫,帶著正確的 PERMISSION_GRANTED或者 PERMISSION_DENIED 。
  • ActivityCompat.shouldShowRequestPermissionRationale()
    在M之前版本呼叫,永遠返回false。
    用v4包的這三方法,完美相容所有版本!這個方法需要額外的引數,Context or Activity。別的就沒啥特別的了。下面是程式碼:
private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.WRITE_CONTACTS)) {
            showMessageOKCancel("You need to allow access to Contacts",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[] {Manifest.permission.WRITE_CONTACTS},
                                    REQUEST_CODE_ASK_PERMISSIONS);
                        }
                    });
            return;
        }
        ActivityCompat.requestPermissions(MainActivity.this,
                new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

後兩個方法,我們也可以在Fragment中使用,用v13相容包:FragmentCompat.requestPermissions() and FragmentCompat.shouldShowRequestPermissionRationale().和activity效果一樣。

第三方庫簡化程式碼

以上程式碼真尼瑪複雜。為解決這事,有許多第三方