Android M 運行時權限

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

發表於 2015-08-30    |   分類於 android那些事兒    |   14條評論

android M 的名字官方剛發佈不久,最終正式版即未來臨!
android在不斷髮展,最近的更新 M 很是不一樣,一些主要的變化例如運行時權限將有顛覆性影響。驚訝的是android社區鮮有談論這事兒,儘管這事很重要或許在不遠的未來會引起很嚴重的問題。
這是今天我寫這篇博客的緣由。這裏有一切關於android運行時權限你須要知道的,包括如何在代碼中實現。如今亡羊補牢還不晚。 android

新運行時權限

android的權限系統一直是首要的安全概念,由於這些權限只在安裝的時候被詢問一次。一旦安裝了,app能夠在用戶絕不知曉的狀況下訪問權限內的全部東西。
難怪一些壞蛋利用這個缺陷惡意收集用戶數據用來作壞事了!
android小組也知道這事兒。7年了!權限系統終於被從新設計了。在android6.0棉花糖,app將不會在安裝的時候授予權限。取而代之的是,app不得不在運行時一個一個詢問用戶授予權限。
git

注意權限詢問對話框不會本身彈出來。開發者不得不本身調用。若是開發者要調用的一些函數須要某權限而用戶又拒絕受權的話,函數將拋出異常直接致使程序崩潰。 github

另外,用戶也能夠隨時在設置裏取消已經受權的權限。 api

你或許已經感受到背後生出一陣寒意。。。若是你是個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

問題又來了,這時候你的app崩潰嗎?
善意的主把這事也告訴了android小組,當咱們在targetSdkVersion 低於23的app調用一個須要權限的函數時,這個權限若是被用戶取消受權了的話,不拋出異常。可是他將啥都不幹,結果致使函數返回值是null或者0. ide

別高興的太早。儘管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 and targetSdkVersion爲 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。別的就沒啥特別的了。下面是代碼:
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正式發佈的時候沒有問題。
但願本文對你有用,快樂編碼!艹

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

相關文章
相關標籤/搜索