Android M 新的運行時權限開發者需要知道的一切


本文轉自:https://www.jianshu.com/p/e1ab1a179fbb

翻譯的國外一篇文章。

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

ROTECTION_NORMAL類權限

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

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 and targetSdkVersion 為 23開始吧.

android {
    compileSdkVersion 23
    ...
 
    defaultConfig {
        ...
        targetSdkVersion 23
        ...
}

例子,我想用一下方法添加聯系人。

 1 private static final String TAG = "Contacts";
 2 private void insertDummyContact() {
 3     // Two operations are needed to insert a new contact.
 4     ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);
 5  
 6     // First, set up a new raw contact.
 7     ContentProviderOperation.Builder op =
 8             ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
 9                     .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
10                     .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
11     operations.add(op.build());
12  
13     // Next, set the name for the contact.
14     op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
15             .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
16             .withValue(ContactsContract.Data.MIMETYPE,
17                     ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
18             .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
19                     "__DUMMY CONTACT from runtime permissions sample");
20     operations.add(op.build());
21  
22     // Apply the operations.
23     ContentResolver resolver = getContentResolver();
24     try {
25         resolver.applyBatch(ContactsContract.AUTHORITY, operations);
26     } catch (RemoteException e) {
27         Log.d(TAG, "Could not add a new contact: " + e.getMessage());
28     } catch (OperationApplicationException e) {
29         Log.d(TAG, "Could not add a new contact: " + e.getMessage());
30     }
31 }

上面代碼需要WRITE_CONTACTS權限。如果不詢問授權,app就崩了。
下一步像以前一樣在AndroidManifest.xml添加聲明權限。

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

下一步,不得不再寫個方法檢查有沒有權限。如果沒有彈個對話框詢問用戶授權。然后你才可以下一步創建聯系人。
權限被分組了,如下表:

 

同一組的任何一個權限被授權了,其他權限也自動被授權。例如,一旦WRITE_CONTACTS被授權了,app也有READ_CONTACTSGET_ACCOUNTS了。
源碼中被用來檢查和請求權限的方法分別是Activity的checkSelfPermissionrequestPermissions。這些方法api23引入。
 1 final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
 2  
 3 private void insertDummyContactWrapper() {
 4     int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
 5     if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
 6         requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
 7                 REQUEST_CODE_ASK_PERMISSIONS);
 8         return;
 9     }
10     insertDummyContact();
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(); }


作者:微涼一季
鏈接:https://www.jianshu.com/p/e1ab1a179fbb
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
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(); }


作者:微涼一季
鏈接:https://www.jianshu.com/p/e1ab1a179fbb
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

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

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

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

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

處理 “不再提醒”

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

如果這個選項在拒絕授權前被用戶勾選了。下次為這個權限請求requestPermissions時,對話框就不彈出來了,結果就是,app啥都不干。
這將是很差的用戶體驗,用戶做了操作卻得不到響應。這種情況需要好好處理一下。在請求requestPermissions前,我們需要檢查是否需要展示請求權限的提示通過activity的shouldShowRequestPermissionRationale,代碼如下:
 1 final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
 2  
 3 private void insertDummyContactWrapper() {
 4     int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
 5     if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
 6             if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
 7                 showMessageOKCancel("You need to allow access to Contacts",
 8                         new DialogInterface.OnClickListener() {
 9                             @Override
10                             public void onClick(DialogInterface dialog, int which) {
11                                 requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
12                                         REQUEST_CODE_ASK_PERMISSIONS);
13                             }
14                         });
15                 return;
16             }
17         requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
18                 REQUEST_CODE_ASK_PERMISSIONS);
19         return;
20     }
21     insertDummyContact();
22 }
23  
24 private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
25     new AlertDialog.Builder(MainActivity.this)
26             .setMessage(message)
27             .setPositiveButton("OK", okListener)
28             .setNegativeButton("Cancel", null)
29             .create()
30             .show();
31 }

當一個權限第一次被請求和用戶標記過不再提醒的時候,我們寫的對話框被展示。
后一種情況,onRequestPermissionsResult 會收到PERMISSION_DENIED ,系統詢問對話框不展示。

搞定!

一次請求多個權限

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

 1 final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
 2  
 3 private void insertDummyContactWrapper() {
 4     List<String> permissionsNeeded = new ArrayList<String>();
 5  
 6     final List<String> permissionsList = new ArrayList<String>();
 7     if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
 8         permissionsNeeded.add("GPS");
 9     if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
10         permissionsNeeded.add("Read Contacts");
11     if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
12         permissionsNeeded.add("Write Contacts");
13  
14     if (permissionsList.size() > 0) {
15         if (permissionsNeeded.size() > 0) {
16             // Need Rationale
17             String message = "You need to grant access to " + permissionsNeeded.get(0);
18             for (int i = 1; i < permissionsNeeded.size(); i++)
19                 message = message + ", " + permissionsNeeded.get(i);
20             showMessageOKCancel(message,
21                     new DialogInterface.OnClickListener() {
22                         @Override
23                         public void onClick(DialogInterface dialog, int which) {
24                             requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
25                                     REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
26                         }
27                     });
28             return;
29         }
30         requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
31                 REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
32         return;
33     }
34  
35     insertDummyContact();
36 }
37  
38 private boolean addPermission(List<String> permissionsList, String permission) {
39     if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
40         permissionsList.add(permission);
41         // Check for Rationale Option
42         if (!shouldShowRequestPermissionRationale(permission))
43             return false;
44     }
45     return true;
46 }

如果所有權限被授權,依然回調onRequestPermissionsResult,我用hashmap讓代碼整潔便於閱讀。

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

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

用兼容庫使代碼兼容舊版

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

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

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

  • ContextCompat.checkSelfPermission()
    被授權函數返回PERMISSION_GRANTED,否則返回PERMISSION_DENIED ,在所有版本都是如此。
  • ActivityCompat.requestPermissions()
    這個方法在M之前版本調用,OnRequestPermissionsResultCallback 直接被調用,帶着正確的 PERMISSION_GRANTED或者 PERMISSION_DENIED 。
  • ActivityCompat.shouldShowRequestPermissionRationale()
    在M之前版本調用,永遠返回false。
    用v4包的這三方法,完美兼容所有版本!這個方法需要額外的參數,Context or Activity。別的就沒啥特別的了。下面是代碼:
 1 private void insertDummyContactWrapper() {
 2     int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
 3             Manifest.permission.WRITE_CONTACTS);
 4     if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
 5         if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
 6                 Manifest.permission.WRITE_CONTACTS)) {
 7             showMessageOKCancel("You need to allow access to Contacts",
 8                     new DialogInterface.OnClickListener() {
 9                         @Override
10                         public void onClick(DialogInterface dialog, int which) {
11                             ActivityCompat.requestPermissions(MainActivity.this,
12                                     new String[] {Manifest.permission.WRITE_CONTACTS},
13                                     REQUEST_CODE_ASK_PERMISSIONS);
14                         }
15                     });
16             return;
17         }
18         ActivityCompat.requestPermissions(MainActivity.this,
19                 new String[] {Manifest.permission.WRITE_CONTACTS},
20                 REQUEST_CODE_ASK_PERMISSIONS);
21         return;
22     }
23     insertDummyContact();
24 }

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

第三方庫簡化代碼

以上代碼真尼瑪復雜。為解決這事,有許多第三方庫已經問世了,真屌真有速度。我試了很多最終找到了個滿意的hotchemi's PermissionsDispatcher
他和我上面做的一樣,只是簡化了代碼。靈活易擴展,試一下吧。如果不滿足你可以找些其他的。

如果我的app還開着呢,權限被撤銷了,會發生生么

權限隨時可以被撤銷。

當app開着的時候被撤消了會發生什么呢?我試過了發現這時app會突然終止 terminated。app中的一切都被簡單粗暴的停止了,因為terminated!對我來說這可以理解,因為系統如果允許它繼續運行(沒有某權限),這會召喚弗雷迪到我的噩夢里。或許更糟...

結論建議

我相信你對新權限模型已經有了清晰的認識。我相信你也意識到了問題的嚴峻。
但是你沒得選擇。新運行時權限已經在棉花糖中被使用了。我們沒有退路。我們現在唯一能做的就是保證app適配新權限模型.
欣慰的是只有少數權限需要運行時權限模型。大多數常用的權限,例如,網絡訪問,屬於Normal Permission 在安裝時自動會授權,當然你要聲明,以后無需檢查。因此,只有少部分代碼你需要修改。
兩個建議:
1.嚴肅對待新權限模型
2.如果你代碼沒支持新權限,不要設置targetSdkVersion 23 。尤其是當你在Studio新建工程時,不要忘了修改!

說一下代碼修改。這是大事,如果代碼結構被設計的不夠好,你需要一些很蛋疼的重構。每個app都要被修正。如上所說,我們沒的選擇。。。
列出所有你需要請求的權限所有情形,如果A被授權,B被拒絕,會發生什么。blah,blah。
祝重構順利。把它列為你需要做的大事,從現在就開始着手做,以保證M正式發布的時候沒有問題。
希望本文對你有用。


免責聲明!

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



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