Android6.0動態權限


翻譯自:

http://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en

https://developer.android.com/training/permissions/declaring.html

歡迎大家指正討論


使用markdown編寫的效果發佈出來和預覽的不一樣啊,圖片還無法顯示,希望知道的同學指點一下.

另外前後字體怎麼無法統一啊 ,看起來很糾結,希望達人指點!!!

在Android6.0中,有一個重要的變化,就是對權限系統進行了重新的設計,引入了動態權限,即Runtime Permission的概念.這一變化很重要,我們之前的代碼可能需要比較大的改變才能做出適配.國外有個哥們針對這個主題發表了一篇博客,而我剛好遇到這個事情,就翻譯一下,做個記錄.


1.新的動態權限系統


一直以來,Android的權限系統都是最大的安全問題之一,因爲在進行安裝的時候,所有的權限都會統一進行請求,你必須允許這些權限請求才能進行安裝.應用安裝後,就可以在用戶毫不知情的情況下,訪問這些權限.所以有很多應用利用這一漏洞,偷偷地進行蒐集用戶個人信息或者其他進行其他用途,可以自行腦補一下.

Android開發團隊顯然也意識到這個問題了,終於在七年後,重新設計了權限系統.在最新的Android6.0 Marshmallow中,安裝應用的時候不再需要允許任何權限的請求了,相反,應用必須在運行的時候獲取用戶的權限許可.(注:這樣用戶就會意識到應用正在獲取權限,如果是敏感權限,就會引起用戶的注意,而不再是之前的情況,用戶對應用獲取敏感權限毫無察覺)


請注意,上圖顯示的權限請求對話框並不會自動的加載,而是需要開發者手動的調用.當開發者嘗試調用的方法需要某個權限,而用戶並沒有允許應用使用該權限的時候,你猜會怎麼樣... !

另外,用戶可以在任何時候,通過設置,來撤銷之前給應用賦予的權限.

作爲一名程序猿,這個時候你可能意識到出大事了,你不再可以像之前那樣,只需要簡單地調用某個方法來完成一項工作,現在,你必須要在每一次涉及相關權限的操作進行之前,檢查該權限是否被用戶允許.否則,很簡單,崩潰.

對於用戶來說,這是一種進步,然而對於程序猿們來說,這真是一場噩夢.我們必須對源代碼進行適配工作.

好消息是,動態權限系統只有在應用將targetSdkVersion設爲23之後纔會奏效,而且這一特性必須是在Android6.0上才能運行,這爲我們的適配工作贏得了時間.


2.已有應用會遇到什麼問題

新的動態權限系統可能會立刻引起你的恐慌,"我3年前的應用怎麼辦,如果把它安裝在Android6.0的機器上,它是否能正常運行,還是會崩潰?!?"

不必擔心,Android開發團隊已經考慮過這個問題了.如果應用的targetSdkVersion小於23,那麼它就會被假定爲並未通過動態權限系統的測試,它依然會使用之前的權限系統:在安裝的時候向用戶請求所有需要的權限,用戶必須接受這些權限請求才能進行安裝. 結果就是,應用依然會像之前那樣運行良好.但是請注意,此時用戶仍然可以在安裝之後撤銷賦予應用的權限,雖然系統會在用戶這麼做的時候發出警告.

下一刻,你可能擔心自己的應用在用戶撤銷權限之後發生崩潰了!!!

Android開發組團隊給我們送了一個福利,在應用的targetSdkVersion小於23,用戶撤銷了某項權限之後,我們調用需要該項權限的方法後,不會拋出任何異常.調用的方法什麼也不會做,而返回值則是看情況返回null或者0.

不要高興的太早,儘管調用方法不會產生異常,但是你很可能在處理返回值的時候發生問題.(注:這個時候可以體現出對返回值檢查判斷的重要性了) 目前來說,由於新版本的普及問題,發生這種事的機率很小.用戶撤銷權限後,可能就會意識到將會產生問題,就像系統警告的那樣.但是將來肯定會發生很多用戶撤銷權限許可的問題,我們必須對應用進行適配以便在新的手機上進行使用.


在你完成對Android6.0動態權限的適配之前,切勿將應用的targetSdkVersion設爲23,否則將會使你陷入到適配問題中去.

注意在你使用Android Studio創建新工程的時候,它會自動地將工程的targetSdkVersion設爲最新的版本,因此在你的應用支持動態權限之前,建議你修改一下targetSdkVersion


3.默認許可權限

還有一些權限是在安裝的時候默認許可並且不可撤銷的,我們把它們叫做普通權限,這些權限如下:

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

這些權限和以前一樣使用,你無需檢查它們是否被撤銷了


4.使應用支持動態權限

現在進入正題,如果使我們的應用完美的支持動態權限這一新特性呢?首先,將應用的compileSdkVersiontargetSdkVersion都設爲23

android {
    compileSdkVersion 23
    ...
 
    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }
     接下來以添加一個聯繫人舉慄說明

private static final String TAG = "Contacts";
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList operations = new ArrayList(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的權限,如果該權限沒有被許可,那麼會導致崩潰.

    接下來我們在清單文件中聲明該權限,就像以前做的那樣.

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

    然後我們需要創建一個方法來檢查該權限是否得到了許可.如果沒有獲得許可,那麼我們使用dialog來詢問用戶是否許可,如果權限已經獲得許可的話,我們就直接創建一個新的聯繫人即可.



    現在,權限按照類別分成了不同的組別,如下表:

        如果一個權限獲得了許可,那麼和它同組的其他權限也會自動的獲得許可.比如說,你得到了WRITE_CONTACTS的權限許可,那麼該組裏面的READ_CONTACTS和GET_ACCOUNTS也會同時自動獲得許可.


       在Android6.0的源碼中,Activity的源碼添加了checkSelfPermission和requestPermissions方法用來檢查和請求權限

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

      在上面的代碼中,首先會檢查WRITE_CONTACTS的權限是否獲得了許可,如果得到許可的話,那麼將會執行聯繫人創建的反覆,否則的話會調用requestPermissions方法來加載一個權限請求的dialog,如下圖所示:  

       當用戶做出選擇後,我們可以在Activity的onRequestPermissionsResult回調中,通過第三個參數
grantResults來得到用戶的選擇結果:允許或者是拒絕.
@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);
    }
}
       這就是動態權限工作的整個過程,有些複雜,但是你需要習慣這樣做.要想你的應用可以完美的使用動態權限這一特性,你必須在每次使用權限的時候都這麼來一遍.

5.處理Never Ask Again

如果用戶拒絕了某個權限的請求,在再次請求該權限的許可的時候,dialog中會有一個"Never Ask Again"的選項選中以防止應用將來再次重複請求這一個權限的許可.


如果Never Ask Again選項被選中了而且用戶拒絕了權限的許可,當下一次我們調用requestPermissions方法來請求同一權限(對同組的權限呢?未測試)的許可的時候,這個dialog將不會出現了.

當用戶做了交互之後,卻毫無反應,這是相當糟糕的用戶體驗,因此我們要在這種情況下做一下特別的處理:當我們調用requestPermissions之前,我們需要先確定一下是否需要通過Activity的shouldShowRequestPermissionRationale方法來說明一下應用需要權限許可的理由.使用方法如下:

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

該方法的調用效果就是,會在第一次請求某個權限的許可或者用戶在該權限的請求中選中"Nerver Ask Again"後拒絕許可的情況下,顯示一個請求權限許可的dialog.在後一種情況下,onRequestPermissionsResult會被調用並返回PERMISSION_DENIED,並且不會出現權限請求的dialog.

搞定!


6.一次請求多個權限許可

在一次請求多個權限的許可時,有特定的處理方式.你可以通過上面的方法同時請求多個權限,當然,別忘了對每個權限都需要確認一下是否被選中了"Never Ask Again"選項.下面是修改後的同時請求多個權限的代碼:
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
 
private void insertDummyContactWrapper() {
    List permissionsNeeded = new ArrayList();
 
    final List permissionsList = new ArrayList();
    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 permissionsList, String permission) {
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        permissionsList.add(permission);
        // Check for Rationale Option
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }
    return true;
}
當每個權限的請求被處理之後,處理結果將會被髮送到onRequestPermissionsResult回調中.
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
            {
            Map perms = new HashMap();
            // 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);
    }
}
這裏的情況可能比較複雜.有時候,可能有一個權限被拒絕了,你的功能就無法正常工作了,也有些情況下,你的功能可以在受限制的情況下工作,在獲得部分權限許可的情況下.這就需要你自己根據具體的情況來處理了.


7.使用support library來適配代碼

上面的代碼可以在Android6.0上面完美的運行,但是,在Android6.0之前的版本,將會發生崩潰,因爲這些方法是在SDK23之後添加的.
比較直接的是方式通過判斷當前運行的版本,來進行不同的處理:
if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}
這樣做的話,代碼會比較複雜,而在support library v4中,已經爲我們準備好了更簡單的方法,我們只需要採用library包中的方法來替換上面的方法即可:
ContextCompat.checkSelfPermission()
不管當前運行的Android版本是多少,該方法都可以正確的方法權限的許可情況,允許或者拒絕.


ActivityCompat.requestPermissions() 
當該方法在6.0之前被調用時,onRequestPermissionsResult回調方法會立即被調用,並返回給你正確的PERMISSION_GRANTED 或者
  PERMISSION_DENIED結果.


- ActivityCompat.shouldShowRequestPermissionRationale() 

在6.0之前調用該方法,將總會返回false.

切記,你應該總是使用這些方法來取代Activity自身的checkSelfPermission,requestPermissions和shouldShowRequestPermissionsRationale方法.這樣,在不同的Android版本中,你的代碼將總會完美的運行.注意,這些方法需要一個Context或者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();
}
另外,在Support Library v13中提供了兩個方法FragmentCompat.requestPermissions()和FragmentCompat.shouldShowRequestPermissionRationale(),這兩個方法可以在Fragment中使用,作用和Activity中的方法像對應.


8.使用第三方包來適配

通過上面的閱讀,可以看到使用動態權限的代碼比較複雜,因此,有很多第三方的框架試圖爲我們帶來更方便簡潔的方式來解決這個問題.作者在這裏推薦了hotchemi的 PermissionsDispatcher,有需要的同學可以試試看.

9.當應用使用時撤銷權限會怎樣

上面說過,用戶可以在任何時候撤銷對權限的許可





那麼當應用在使用的時候,去撤銷權限的許可會發生什麼呢? 作者試了一下之後,發現應用的進程會立即終止,這可真是程序猿們得噩夢啊.(在此,在撤銷權限的時候,當前界面至少會進入onPause,onStop生命週期方法,因此我們的檢查權限許可,請求權限的時機應該在我們返回到該界面的時候,選擇一個適當的時機,因爲權限許可可能在返回界面之前被撤銷了)


10.總結和建議

現在你應該很清楚整個新的權限系統了,同時你也應該知道我們面臨的問題了.在Android6.0中動態權限系統已經取代了原有的權限系統,我們別無選擇,退無可退,唯一能做的就是去做好適配工作.
好消息是需要進行動態申請的權限是少量的,大部分常用的權限是默認獲得許可,並不需要你處理的.總而言之,你只需要修改一小部分代碼來完成適配工作.
在這裏給出兩個建議:
1.優先對重要的部分進行適配,確保不會出現崩潰問題.
2.在完成適配工作前,不要將應用的targetSdkVersion設爲23,尤其是在使用AndroidStudio創建新工程的時候,記得手動修改一下工程的targetSdkVersion,因爲AndroidStudio會默認使用最新的SDK版本.

說到修改代碼,我必須承認工作量很大.如果之前的架構設計不合理的話,你可能需要花費一些時間來重新設計了,就像之前說的,我們別無選擇,除了去做好它.

我建議你列出應用的功能所依賴的所有的權限,然後考慮如果權限被拒絕,怎樣使你的應用的功能儘可能可用,並考慮好如果部分權限被拒絕的情況下,你應該怎麼做.恩,這也很複雜,最好能記錄好各種情況.

更多詳情請參考google文檔,點擊這裏 

原作者簡介:



Good Luck!

發佈了29 篇原創文章 · 獲贊 14 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章