Android M 新運(yùn)行時權(quán)限開發(fā)者需要知道的一切
android M 的名字官方剛發(fā)布不久,最終正式版即將來臨!
android在不斷發(fā)展,最近的更新 M 非常不同,一些主要的變化例如運(yùn)行時權(quán)限將有顛覆性影響。驚訝的是android社區(qū)鮮有談?wù)撨@事兒,盡管這事很重要或許在不遠(yuǎn)的將來會引發(fā)很嚴(yán)重的問題。
這是今天我寫這篇博客的原因。這里有一切關(guān)于android運(yùn)行時權(quán)限你需要知道的,包括如何在代碼中實(shí)現(xiàn)。現(xiàn)在亡羊補(bǔ)牢還不晚。
新運(yùn)行時權(quán)限
android的權(quán)限系統(tǒng)一直是首要的安全概念,因?yàn)檫@些權(quán)限只在安裝的時候被詢問一次。一旦安裝了,app可以在用戶毫不知曉的情況下訪問權(quán)限內(nèi)的所有東西。
難怪一些壞蛋利用這個缺陷惡意收集用戶數(shù)據(jù)用來做壞事了!
android小組也知道這事兒。7年了!權(quán)限系統(tǒng)終于被重新設(shè)計(jì)了。在android6.0棉花糖,app將不會在安裝的時候授予權(quán)限。取而代之的是,app不得不在運(yùn)行時一個一個詢問用戶授予權(quán)限。
注意權(quán)限詢問對話框不會自己彈出來。開發(fā)者不得不自己調(diào)用。如果開發(fā)者要調(diào)用的一些函數(shù)需要某權(quán)限而用戶又拒絕授權(quán)的話,函數(shù)將拋出異常直接導(dǎo)致程序崩潰。
另外,用戶也可以隨時在設(shè)置里取消已經(jīng)授權(quán)的權(quán)限。
你或許已經(jīng)感覺到背后生出一陣寒意。。。如果你是個android開發(fā)者,意味著要完全改變你的程序邏輯。你不能像以前那樣直接調(diào)用方法了,你不得不為每個需要的地方檢察權(quán)限,否則app就崩潰了!
是的。我不能哄你說這是簡單的事兒。盡管這對用戶來說是好事,但是對開發(fā)者來說就是噩夢。我們不得不修改編碼不然不論短期還是長遠(yuǎn)來看都是潛在的問題。
這個新的運(yùn)行時權(quán)限僅當(dāng)我們設(shè)置targetSdkVersion to 23(這意味著你已經(jīng)在23上測試通過了)才起作用,當(dāng)然還要是M系統(tǒng)的手機(jī)。app在6.0之前的設(shè)備依然使用舊的權(quán)限系統(tǒng)。
已經(jīng)發(fā)布了的app會發(fā)生什么
新運(yùn)行時權(quán)限可能已經(jīng)讓你開始恐慌了。“hey,伙計(jì)!我三年前發(fā)布的app可咋整呢。如果他被裝到android 6.0上,我的app會崩潰嗎?!?”
莫慌張,放輕松。android小隊(duì)又不傻,肯定考慮到了這情況。如果app的targetSdkVersion 低于 23,那將被認(rèn)為app沒有用23新權(quán)限測試過,那將被繼續(xù)使用舊有規(guī)則:用戶在安裝的時候不得不接受所有權(quán)限,安裝后app就有了那些權(quán)限咯!
然后app像以前一樣奔跑!注意,此時用戶依然可以取消已經(jīng)同意的授權(quán)!用戶取消授權(quán)時,android 6.0系統(tǒng)會警告,但這不妨礙用戶取消授權(quán)。
問題又來了,這時候你的app崩潰嗎?
善意的主把這事也告訴了android小組,當(dāng)我們在targetSdkVersion 低于23的app調(diào)用一個需要權(quán)限的函數(shù)時,這個權(quán)限如果被用戶取消授權(quán)了的話,不拋出異常。但是他將啥都不干,結(jié)果導(dǎo)致函數(shù)返回值是null或者0.
別高興的太早。盡管app不會調(diào)用這個函數(shù)時崩潰,返回值null或者0可能接下來依然導(dǎo)致崩潰。
好消息(至少目前看來)是這類取消權(quán)限的情況比較少,我相信很少用戶這么搞。如果他們這么辦了,后果自負(fù)咯。
但從長遠(yuǎn)看來,我相信還是會有大量用戶會關(guān)閉一些權(quán)限。我們app不能再新設(shè)備完美運(yùn)行這是不可接受的。
怎樣讓他完美運(yùn)行呢,你最好修改代碼支持最新的權(quán)限系統(tǒng),而且我建議你立刻著手搞起!
代碼沒有成功改為支持最新運(yùn)行時權(quán)限的app,不要設(shè)置targetSdkVersion 23 發(fā)布,否則你就有麻煩了。只有當(dāng)你測試過了,再改為targetSdkVersion 23 。
警告:現(xiàn)在你在android studio新建項(xiàng)目,targetSdkVersion 會自動設(shè)置為 23。如果你還沒支持新運(yùn)行時權(quán)限,我建議你首先把targetSdkVersion 降級到22
PROTECTION_NORMAL類權(quán)限
當(dāng)用戶安裝或更新應(yīng)用時,系統(tǒng)將授予應(yīng)用所請求的屬于 PROTECTION_NORMAL 的所有權(quán)限(安裝時授權(quán)的一類基本權(quán)限)。這類權(quán)限包括:
- 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中簡單聲明這些權(quán)限就好,安裝時就授權(quán)。不需要每次使用時都檢查權(quán)限,而且用戶不能取消以上授權(quán)。
#p#
讓你的app支持新運(yùn)行時權(quán)限
是時候讓我們的app支持新權(quán)限模型了,從設(shè)置compileSdkVersion andtargetSdkVersion 為 23開始吧.
- android {
- compileSdkVersion 23
- ...
- defaultConfig {
- ...
- targetSdkVersion 23
- ...
- }
例子,我想用一下方法添加聯(lián)系人。
- 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權(quán)限。如果不詢問授權(quán),app就崩了。
下一步像以前一樣在AndroidManifest.xml添加聲明權(quán)限。
- <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
下一步,不得不再寫個方法檢查有沒有權(quán)限。如果沒有彈個對話框詢問用戶授權(quán)。然后你才可以下一步創(chuàng)建聯(lián)系人。
權(quán)限被分組了,如下表:
同一組的任何一個權(quán)限被授權(quán)了,其他權(quán)限也自動被授權(quán)。例如,一旦WRITE_CONTACTS被授權(quán)了,app也有READ_CONTACTS和GET_ACCOUNTS了。
源碼中被用來檢查和請求權(quán)限的方法分別是Activity的checkSelfPermission和requestPermissions。這些方法api23引入。
- 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();
- }
如果已有權(quán)限,insertDummyContact()會執(zhí)行。否則,requestPermissions被執(zhí)行來彈出請求授權(quán)對話框,如下:
不論用戶同意還是拒絕,activity的onRequestPermissionsResult會被回調(diào)來通知結(jié)果(通過第三個參數(shù)),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);
- }
- }
這就是新權(quán)限模型工作過程。代碼真復(fù)雜但是只能去習(xí)慣它。。。為了讓app很好兼容新權(quán)限模型,你不得不用以上類似方法處理所有需要的情況。
如果你想捶墻,現(xiàn)在是時候了。。。
處理 “不再提醒”
如果用戶拒絕某授權(quán)。下一次彈框,用戶會有一個“不再提醒”的選項(xiàng)的來防止app以后繼續(xù)請求授權(quán)。
如果這個選項(xiàng)在拒絕授權(quán)前被用戶勾選了。下次為這個權(quán)限請求requestPermissions時,對話框就不彈出來了,結(jié)果就是,app啥都不干。
這將是很差的用戶體驗(yàn),用戶做了操作卻得不到響應(yīng)。這種情況需要好好處理一下。在請求requestPermissions前,我們需要檢查是否需要展示請求權(quán)限的提示通過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();
- }
當(dāng)一個權(quán)限第一次被請求和用戶標(biāo)記過不再提醒的時候,我們寫的對話框被展示。
后一種情況,onRequestPermissionsResult 會收到PERMISSION_DENIED ,系統(tǒng)詢問對話框不展示。
搞定!
#p#
一次請求多個權(quán)限
當(dāng)然了有時候需要好多權(quán)限,可以用上面方法一次請求多個權(quán)限。不要忘了為每個權(quán)限檢查“不再提醒”的設(shè)置。
修改后的代碼:
- 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;
- }
如果所有權(quán)限被授權(quán),依然回調(diào)onRequestPermissionsResult,我用hashmap讓代碼整潔便于閱讀。
- @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);
- }
- }
條件靈活的,你自己設(shè)置。有的情況,一個權(quán)限沒有授權(quán),就不可用;但是也有情況,能工作,但是表現(xiàn)的是有所限制的。對于這個我不做評價,你自己設(shè)計(jì)吧。
用兼容庫使代碼兼容舊版
以上代碼在android 6.0以上運(yùn)行沒問題,但是23 api之前就不行了,因?yàn)闆]有那些方法。
粗暴的方法是檢查版本
- if (Build.VERSION.SDK_INT >= 23) {
- // Marshmallow+
- } else {
- // Pre-Marshmallow
- }
但是太復(fù)雜,我建議用v4兼容庫,已對這個做過兼容,用這個方法代替:
- ContextCompat.checkSelfPermission()
被授權(quán)函數(shù)返回PERMISSION_GRANTED,否則返回PERMISSION_DENIED ,在所有版本都是如此。 - ActivityCompat.requestPermissions()
這個方法在M之前版本調(diào)用,OnRequestPermissionsResultCallback 直接被調(diào)用,帶著正確的 PERMISSION_GRANTED或者 PERMISSION_DENIED 。 - ActivityCompat.shouldShowRequestPermissionRationale()
在M之前版本調(diào)用,永遠(yuǎn)返回false。
用v4包的這三方法,完美兼容所有版本!這個方法需要額外的參數(shù),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效果一樣。
第三方庫簡化代碼
以上代碼真尼瑪復(fù)雜。為解決這事,有許多第三方庫已經(jīng)問世了,真屌真有速度。我試了很多最終找到了個滿意的hotchemi’s PermissionsDispatcher。
他和我上面做的一樣,只是簡化了代碼。靈活易擴(kuò)展,試一下吧。如果不滿足你可以找些其他的。
如果我的app還開著呢,權(quán)限被撤銷了,會發(fā)生生么
權(quán)限隨時可以被撤銷。
當(dāng)app開著的時候被撤消了會發(fā)生什么呢?我試過了發(fā)現(xiàn)這時app會突然終止 terminated。app中的一切都被簡單粗暴的停止了,因?yàn)閠erminated!對我來說這可以理解,因?yàn)橄到y(tǒng)如果允許它繼續(xù)運(yùn)行(沒有某權(quán) 限),這會召喚弗雷迪到我的噩夢里。或許更糟…
結(jié)論建議
我相信你對新權(quán)限模型已經(jīng)有了清晰的認(rèn)識。我相信你也意識到了問題的嚴(yán)峻。
但是你沒得選擇。新運(yùn)行時權(quán)限已經(jīng)在棉花糖中被使用了。我們沒有退路。我們現(xiàn)在唯一能做的就是保證app適配新權(quán)限模型.
欣慰的是只有少數(shù)權(quán)限需要運(yùn)行時權(quán)限模型。大多數(shù)常用的權(quán)限,例如,網(wǎng)絡(luò)訪問,屬于Normal Permission 在安裝時自動會授權(quán),當(dāng)然你要聲明,以后無需檢查。因此,只有少部分代碼你需要修改。
兩個建議:
1.嚴(yán)肅對待新權(quán)限模型
2.如果你代碼沒支持新權(quán)限,不要設(shè)置targetSdkVersion 23 。尤其是當(dāng)你在Studio新建工程時,不要忘了修改!
說一下代碼修改。這是大事,如果代碼結(jié)構(gòu)被設(shè)計(jì)的不夠好,你需要一些很蛋疼的重構(gòu)。每個app都要被修正。如上所說,我們沒的選擇。。。
列出所有你需要請求的權(quán)限所有情形,如果A被授權(quán),B被拒絕,會發(fā)生什么。blah,blah。
祝重構(gòu)順利。把它列為你需要做的大事,從現(xiàn)在就開始著手做,以保證M正式發(fā)布的時候沒有問題。
希望本文對你有用,快樂編碼!