注:本文只針對Google原生Android系統有效, 小米魅族等手機有自己的權限機制, 可能不適用
新的權限機制更好的保護了用戶的隱私,Google將權限分為兩類,一類是Normal Permissions,這類權限一般不涉及用戶隱私,是不需要用戶進行授權的,比如手機震動、訪問網絡等;另一類是Dangerous Permission,一般是涉及到用戶隱私的,需要用戶進行授權,比如讀取sdcard、訪問通訊錄等。
運行時權限的特點是,實時性,用戶可以隨時取消授權,所以每次調用運行時權限的方法都需要判斷或者請求一次運行時權限。
If the device is running Android 5.1 (API level 22) or lower, or the app's targetSdkVersion
is 22 or lower, the system asks the user to grant the permissions at install time. And the system just tells the user what permission groups the app needs, not the individual(個別的) permissions.
If the device is running Android 6.0 (API level 23) and the app's targetSdkVersion
is 23 or higher, the following system behavior applies when your app requests a dangerous permission:
- If an app requests a dangerous permission listed in its manifest, and the app does not currently have any permissions in the permission group, the system shows a dialog box to the user describing the permission group that the app wants access to. The dialog box does not describe the specific permission within that group. For example, if an app requests the
READ_CONTACTS
permission, the system dialog box just says the app needs access to the device's contacts. If the user grants approval, the system gives the app just the permission it requested. - If an app requests a dangerous permission listed in its manifest, and the app already has another dangerous permission in the same permission group, the system immediately grants the permission without any interaction with the user. For example, if an app had previously requested and been granted the
READ_CONTACTS
permission, and it then requestsWRITE_CONTACTS
, the system immediately grants that permission.
Permission groups
Table 1. Permissions and permission groups.
Permission Group | Permissions |
---|---|
android.permission-group.CALENDAR |
|
android.permission-group.CAMERA |
|
android.permission-group.CONTACTS |
|
android.permission-group.LOCATION |
|
android.permission-group.MICROPHONE |
|
android.permission-group.PHONE |
|
android.permission-group.SENSORS |
|
android.permission-group.SMS |
|
android.permission-group.STORAGE |
|
二、相關API
API的講解就跟着申請權限步驟一起了:
1. 在AndroidManifest文件中添加需要的權限。這個步驟和我們之前的開發並沒有什么變化,試圖去申請一個沒有聲明的權限可能會導致程序崩潰。
2. 檢查權限
-
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { }else{ // }
這里涉及到一個API,ContextCompat.checkSelfPermission
,主要用於檢測某個權限是否已經被授予,方法返回值為PackageManager.PERMISSION_DENIED
或者PackageManager.PERMISSION_GRANTED
。當返回DENIED就需要進行申請授權了。
3.申請授權
ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS);
該方法是異步的,第一個參數是Context;第二個參數是需要申請的權限的字符串數組;第三個參數為requestCode,主要用於回調的時候檢測。可以從方法名requestPermissions
以及第二個參數看出,是支持一次性申請多個權限的,系統會通過對話框逐一詢問用戶是否授權。
4. 處理權限申請回調
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! Do the // contacts-related task you need to do. } else { // permission denied, boo! Disable the // functionality that depends on this permission. } return; } } }
ok,對於權限的申請結果,首先驗證requestCode定位到你的申請,然后驗證grantResults對應於申請的結果,這里的數組對應於申請時的第二個權限字符串數組。
如果你同時申請兩個權限,那么grantResults的length就為2,分別記錄你兩個權限的申請結果。如果申請成功,就可以做你的事情了~
5.當然,到此我們的權限申請的不走,基本介紹就如上述。不過還有個API值得提一下:
// Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) // Show an expanation to the user *asynchronously* -- don't block // this thread waiting for the user's response! After the user // sees the explanation, try again to request the permission. }
這個API主要用於給用戶一個申請權限的解釋,該方法只有在用戶在上一次已經拒絕過你的這個權限申請時才用得到。也就是說,用戶已經拒絕一次了,你又彈個授權框,你需要給用戶一個解釋,為什么要授權,則使用該方法。
shouldShowRequestPermissionRationale()返回值
的說明:
1. 第一次請求權限時,用戶拒絕了,下一次:shouldShowRequestPermissionRationale()
返回 true,應該顯示一些為什么需要這個權限的說明
2.第二次請求權限時,用戶拒絕了,並選擇了“不在提醒”的選項時:shouldShowRequestPermissionRationale()
返回 false
3. 設備的策略禁止當前應用獲取這個權限的授權:shouldShowRequestPermissionRationale()
返回 false
處理 “不再提醒”
如果用戶拒絕某授權, 下一次彈框,用戶會有一個“不再提醒”的選項的來防止app以后繼續請求授權。如果這個選項在拒絕授權前被用戶勾選了, 下次為這個權限請求requestPermissions時,對話框就不彈出來了,結果就是,app啥都不干。這將是很差的用戶體驗,用戶做了操作卻得不到響應, 這種情況需要好好處理一下。在請求requestPermissions前,我們需要檢查是否需要展示請求權限的提示.
那么將上述幾個步驟結合到一起就是:
// Here, thisActivity is the current activity if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) { // Show an expanation to the user *asynchronously* -- don't block // this thread waiting for the user's response! After the user // sees the explanation, try again to request the permission. } else { // No explanation needed, we can request the permission. ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an // app-defined int constant. The callback method gets the // result of the request. } }
三、Fragment中運行時權限的特殊處理
- 在Fragment中申請權限,不要使用ActivityCompat.requestPermissions, 直接使用Fragment的requestPermissions方法,否則會回調到Activity的 onRequestPermissionsResult
- 如果在Fragment中嵌套Fragment,在子Fragment中使用requestPermissions方 法,onRequestPermissionsResult不會回調回來,建議使用 getParentFragment().requestPermissions方法,這個方法會回調到父Fragment中的onRequestPermissionsResult,加入以下代碼可以把回調透傳到子Fragment
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); List<Fragment> fragments = getChildFragmentManager().getFragments(); if (fragments != null) { for (Fragment fragment : fragments) { if (fragment != null) { fragment.onRequestPermissionsResult(requestCode,permissions,grantResults); } } } }
四、簡單的例子
這里寫一個簡單的例子,針對於運行時權限。相信大家在最開始接觸Android的時候,都利用Intent實驗了打電話、發短信等功能。
我們看看直接撥打電話在Android 6.x的設備上權限需要如何處理。當然代碼非常簡單:
package com.wytiger.permissiondemo;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
public class MainActivity extends Activity implements OnClickListener {
private static final int REQUEST_CALL_PHONE = 100;
private static final String TAG = "testPermission";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btnCall).setOnClickListener(this);
}
@Override
public void onClick(View v) {
testPermission();
}
private void testPermission() {
// 檢查權限是否允許
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "沒有權限");
// 沒有權限,考慮是否需要解釋為什么需要這個權限
/*申請權限的解釋,該方法在用戶上一次已經拒絕過你的這個權限申請時使用。
* 也就是說,用戶已經拒絕一次了,你又彈個授權框,你需要給用戶一個解釋,為什么要授權*/
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
Log.e(TAG, "沒有權限,用戶上次已經拒絕該權限,解釋為什么需要這個權限");
// Show an expanation to the user *asynchronously* -- don't block this thread
//waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
Toast.makeText(MainActivity.this, "我需要權限才能撥打電話", Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "沒有權限,申請權限");
// 申請權限
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.CALL_PHONE },
REQUEST_CALL_PHONE);
}
} else {
Log.e(TAG, "有權限,執行相關操作");
// 有權限,執行相關操作
callPhone();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "權限同意");
callPhone();
} else {
Log.e(TAG, "權限拒絕");
Toast.makeText(this, "權限拒絕", Toast.LENGTH_SHORT).show();
}
return;
}
}
private void callPhone() {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + "10086");
intent.setData(data);
startActivity(intent);
}
}
在Android 6.x上運行是,點擊testCall,即會彈出授權窗口,如何你Allow則直接撥打電話,如果Denied則Toast彈出”權限拒絕”。
例子很簡單,不過需要注意的是,對於Intent這種方式,很多情況下是不需要
授權的甚至權限都不需要的,比如:你是到撥號界面而不是直接撥打電話,就不需要去申請權限;打開系統圖庫去選擇照片;調用系統相機app去拍照等。當然,上例也說明了並非所有的通過Intent的方式都不需要申請權限。一般情況下,你是通過Intent打開另一個app,讓用戶通過該app去做一些事情,你只關注結果(onActivityResult),那么權限是不需要你處理的,而是由打開的app去處理。
舊版本app兼容問題
那么問題來了,是不是所有以前發布的app都會出現問題呢?答案是不會,只有那些targetSdkVersion 設置為23和23以上的應用才會出現異常,在使用危險權限的時候系統必須要獲得用戶的同意才能使用,要不然應用就會崩潰,出現類似java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider
的崩潰日志。所以targetSdkVersion如果沒有設置為23版本或者以上,系統還是會使用舊規則:在安裝的時候賦予該app所申請的所有權限, 所以app當然可以和以前一樣正常使用了. 但是還有一點需要注意的是6.0的系統里面,用戶可以手動將該app的權限關閉,如下圖
那么問題又來了,雖然會彈出提示,但是如果以前的老應用申請的權限被用戶不管提示強行手動關閉了怎么辦,應用會崩潰么?
我們來試一試
好吧,可以慶幸了一下了,不會拋出異常,不會崩潰(只針對原生android系統),只不過調用那些被用戶禁止權限的api接口返回值都為null或者0,所以我們只需要做一下判空操作就可以了,不判空當然還是會崩潰的嘍。有些權限被關閉, 是會拋異常的(小米魅族等定制rom).
注意事項:
1,權限判斷在某些機型上不可靠,會出現明明拒絕了權限,但是打印出來的還是同意,比如小米5. 本文只針對原生android系統有效
2,特殊權限:Special Permissions
There are a couple of permissions that don't behave like normal and dangerous permissions. SYSTEM_ALERT_WINDOW
andWRITE_SETTINGS
are particularly sensitive, so most apps should not use them. If an app needs one of these permissions, it must declare the permission in the manifest, and send an intent requesting the user's authorization. The system responds to the intent by showing a detailed management screen to the user.
For details on how to request these permissions, see theSYSTEM_ALERT_WINDOW
andWRITE_SETTINGS
reference entries.
參考:
android permission權限與安全機制解析(上)
android permission權限與安全機制解析(下)
Android API23 M權限管理機制:Runtime Permission簡介
Android API19 K原生權限管理:AppOps