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_CONTACTS
和GET_ACCOUNTS
了。
原始碼中被用來檢查和請求許可權的方法分別是Activity的checkSelfPermission
和requestPermissions
。這些方法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。別的就沒啥特別的了。下面是程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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效果一樣。
第三方庫簡化程式碼
以上程式碼真尼瑪複雜。為解決這事,有許多第三方庫已經問世了,真屌真有速度。我試了很多最終找到了個滿意的hotchemi’s PermissionsDispatcher。
他和我上面做的一樣,只是簡化了程式碼。靈活易擴充套件,試一下吧。如果不滿足你可以找些其他的。
如果我的app還開著呢,許可權被撤銷了,會發生生麼
許可權隨時可以被撤銷。
當app開著的時候被撤消了會發生什麼呢?我試過了發現這時app會突然終止 terminated。app中的一切都被簡單粗暴的停止了,因為terminated!對我來說這可以理解,因為系統如果允許它繼續執行(沒有某許可權),這會召喚弗雷迪到我的噩夢裡。或許更糟…
結論建議
我相信你對新許可權模型已經有了清晰的認識。我相信你也意識到了問題的嚴峻。
但是你沒得選擇。新執行時許可權已經在棉花糖中被使用了。我們沒有退路。我們現在唯一能做的就是保證app適配新許可權模型.
欣慰的是隻有少數許可權需要執行時許可權模型。大多數常用的許可權,例如,網路訪問,屬於Normal Permission 在安裝時自動會授權,當然你要宣告,以後無需檢查。因此,只有少部分程式碼你需要修改。
兩個建議:
1.嚴肅對待新許可權模型
2.如果你程式碼沒支援新許可權,不要設定targetSdkVersion 23 。尤其是當你在Studio新建工程時,不要忘了修改!
說一下程式碼修改。這是大事,如果程式碼結構被設計的不夠好,你需要一些很蛋疼的重構。每個app都要被修正。如上所說,我們沒的選擇。。。
列出所有你需要請求的許可權所有情形,如果A被授權,B被拒絕,會發生什麼。blah,blah。
祝重構順利。把它列為你需要做的大事,從現在就開始著手做,以保證M正式釋出的時候沒有問題。
希望本文對你有用,快樂編碼!