一.概述
爲了保護系統的完整性和用戶隱私權,android6.0(API 級別 23)引入了運行時權限的概念。
1.概念:
android6.0運行時權限是使用戶可直接在運行時管理應用權限的一種新的權限模式。
2.變化:
對象\系統版本 | Android6.0以下 | Android6.0以後 |
---|---|---|
用戶 | 用戶只有在同意權限列表之後才能完成應用的安裝 | 用戶可爲所安裝的各個應用分別授予或撤銷權限,對應用的功能進行更多控制 |
開發者 | 只需在Manifest中申明所需要的權限 | 在Manifest申明權限後,還需在運行時檢查和請求權限,用戶允許後方可使用 |
Android6.0以後,用戶可以隨時進入應用的“Settings”屏幕調用權限。
所以對於開發者,要求以 Android 6.0(API 級別 23)或更高版本爲目標平臺的應用,必須在運行時檢查和請求權限,否則會發生崩潰。
3.意義:
這種模式下用戶可爲所安裝的各個應用分別授予或撤銷權限,用戶能夠更好地瞭解和控制權限,同時爲應用開發者精簡了安裝和自動更新過程。
4.使用 adb 工具從命令行管理權限:
按組列出權限和狀態:
$ adb shell pm list permissions -d -g
授予或撤銷一項或多項權限:
$ adb shell pm [grant|revoke] …
二.權限分類
系統權限分爲幾個保護級別。其中兩個最重要保護級別是正常權限(Normal Permissions)和危險權限(Dangerous Permissions):
1.Normal Permissions:
(1)概念:
這類權限是指一般不涉及用戶隱私或其他應用操作,系統自動向應用授予的權限。比如手機震動、藍牙,訪問網絡等。
(2)特點:
只需要在Manifest中聲明,不需要每次使用時都檢查權限,用戶不能取消以上授權,除非用戶卸載App。
(3)正常權限列表:
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS
2.Dangerous Permissions:
(1)概念:
這類權限是指一般涉及到用戶隱私的,需要用戶進行授權後纔可使用的權限。比如讀取sdcard、訪問通訊錄等。
(2)特點:
對於此類權限,如果設備運行的是 Android 6.0(API 級別 23),並且應用的 targetSdkVersion 是 23 或更高版本,除了在Manifest中申明,還需要動態申請權限。
(3)權限組:
任何權限都可屬於一個權限組,包括正常權限和應用定義的權限。但權限組僅當權限危險時才影響用戶體驗。可以忽略正常權限的權限組。
同一個權限組的任何一個權限被授權了,這個權限組的其他權限也自動被授權。例如:當WRITE_CONTACTS被授權了,當App申請READ_CONTACTS權限時,系統會直接授權通過。
申請權限的時候系統彈出的Dialog上面的文本是對整個權限組的說明,而不是單個權限。
(4) 危險權限和權限組:
三.相關API解析
以最簡單的電話權限申請步驟爲例解析相關API。
1.聲明權限:
需要在AndroidManifest.xml中聲明所需要的權限。
<uses-permission android:name="android.permission.CALL_PHONE"/>
2.檢查權限:
調用ContextCompat.checkSelfPermission方法來檢查用戶是否授權。
if (ContextCompat.checkSelfPermission(this,
android.Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
//用戶未授權
} else {
//用戶已經授權
}
返回值 | 返回值說明 |
---|---|
PackageManager.PERMISSION_GRANTED | 表示應用具有此權限 |
PackageManager.PERMISSION_DENIED | 表示應用不具有此權限 |
3.請求權限:
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission
.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {//用戶未授權
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.CALL_PHONE)) {
//給用戶一個申請權限的解釋
} else {
//請求權限
ActivityCompat.requestPermissions(this, new
String[]{android.Manifest.permission.CALL_PHONE},
REQUEST_PERMISSION_CALL_PHONE);
}
} else {//用戶已經授權
}
(1)requestPermissions方法:
調用requestPermissions(Context context, String[] permissions, int requestCode)方法用來向用戶請求一個或多個權限。
參數 | 參數說明 |
---|---|
Context | Context對象 |
String[] permissions | 需要申請的權限數組(支持一次一個或多個權限的請求) |
requestCode | 請求碼,主要用戶回調的時候進行判斷 |
(2)shouldShowRequestPermissionRationale方法:
主要用於給用戶一個申請權限的解釋。即用戶在上一次已經拒絕過此權限申請,應用又彈框申請此權限,使用該方法來向用戶解釋需要授權的原因,避免用戶勾選“不再詢問”拒絕權限,導致應該中一些功能無法正常使用。
方法返回值 | 情況 |
---|---|
true | 第一次請求權限時,用戶拒絕了。 |
false | 第二次請求權限時,用戶拒絕了,並選擇了“不再詢問”的選項 |
4.請求權限的回調:
根據onRequestPermissionsResult回調方法中的結果來判斷用戶是否授權。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSION_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//權限被用戶同意
} else {
//權限被用戶拒絕
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
其中從int[] grantResults中可以得到授權結果。
在Fragment中申請權限API和在Activity中申請權限API不完全相同,但大同小異,此處不再進行說明。
四.完整案例
電話權限申請的完整代碼:當用戶點擊“不再詢問”選項後,跳轉至設置頁面提示用戶手動授權。以在Activity中申請權限爲例:
public class MainActivity extends AppCompatActivity {
private static int REQUEST_PERMISSION_CALL_PHONE = 100;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView )findViewById(R.id.textView);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestPermission();
}
});
}
/**
* 申請電話權限
*/
private void requestPermission(){
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission
.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {//用戶未授權
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.CALL_PHONE)) {
//給用戶一個申請權限的解釋
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("說明")
.setMessage("需要使用電話權限,撥打電話")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//繼續請求權限
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL_PHONE);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "未授權", Toast.LENGTH_LONG).show();
return;
}
})
.create()
.show();
} else {
//請求權限
ActivityCompat.requestPermissions(this, new
String[]{android.Manifest.permission.CALL_PHONE},
REQUEST_PERMISSION_CALL_PHONE);
}
} else {//用戶已經授權
callPhone();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSION_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//權限被用戶同意
callPhone();
} else {
//權限被用戶拒絕
if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, android.Manifest.permission.CALL_PHONE)) {
//用戶點擊了“不在詢問”選項,則跳到設置頁提醒用戶手動設置權限
Toast.makeText(MainActivity.this, "需要手動打開權限才能使用此功能", Toast.LENGTH_LONG).show();
intentSetting();
} else {
Toast.makeText(MainActivity.this, "未授權", Toast.LENGTH_LONG).show();
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/**
* 撥打電話
*/
private void callPhone(){
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + "10086");
intent.setData(data);
startActivity(intent);
}
/**
* 跳到設置頁面
*/
private void intentSetting(){
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, 0);
}
}
五.代碼封裝
一個應用可能需要多次申請權限,雖然權限處理並不複雜,但是需要編寫很多重複的代碼,比較繁瑣,故需要代碼封裝。
1.相關開源項目:
開源項目名稱 | 特點 |
---|---|
PermissionsDispatcher | 使用標註的方式,動態生成類處理運行時權限 |
RxPermissions | 基於RxJava的運行時權限檢測框架 |
PermissionGen | 使用起來很簡單,但是運行時反射會影響效率 |
MPermissions | 在PermissionGen基礎上使用編譯時註解提高效率 |
Grant | 簡化運行時權限的處理,比較靈活 |
android-RuntimePermissions | 由Google官方提供 |
2.自己封裝工具類:
此工具類支持在Activity和Fragment中申請權限:
運行效果:
因上傳gif大小限制,在fragment中申請權限未錄製,如有需要,請下載源碼進行查看。
(1)工具類:
public class PermissionUtil {
public static int testCode = 100;
public static int fragmentCode = 101;
private PermissionCallback callback;
public int code;
private static final String TAG = "PermissionUtil";
private static final String NO_LONGER_ASK = "no_longer_ask";
private Activity activity;
private Fragment fragment;
public static <T extends Object> PermissionUtil getInstance(T t) {
PermissionUtil permission = new PermissionUtil();
if (t == null) {
throw new NullPointerException("NullPointerException");
} else if (t instanceof Activity) {
permission.activity = (Activity) t;
} else if (t instanceof Fragment) {
permission.fragment = (Fragment) t;
permission.activity = permission.fragment.getActivity();
} else {
throw new IllegalArgumentException("IllegalArgumentException");
}
return permission;
}
private PermissionUtil() {
}
/**
* @param isNeedShowRequestPermissionRationale 當用戶第一次拒絕申請權限後再次申請權限是否彈出解釋彈框
* @param permissions 申請的權限數組
* @param requestCode 請求碼
* @param permissionCallback 檢查權限回調
*/
public void requestPermissions(boolean isNeedShowRequestPermissionRationale, String[] permissions, int requestCode, PermissionCallback permissionCallback) {
callback = permissionCallback;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (permissions != null && permissions.length > 0) {
String[] denyPermissions = getDenyPermissions(permissions);
if (denyPermissions.length > 0) {
if (isNeedShowRequestPermissionRationale) {
if (shouldShowRequestPermissionRationale(denyPermissions)) {
showRequestPermissionRationale(denyPermissions, callback, requestCode);
} else {
handlerPermission(requestCode, denyPermissions);
}
} else {
handlerPermission(requestCode, denyPermissions);
}
} else {
callback.permittedPermissions();
}
} else {
Log.d(TAG, "requestPermissions:permissions is null");
}
} else {
callback.permittedPermissions();
}
}
private void handlerPermission(int requestCode, String[] denyPermissions) {
if (fragment != null) {
fragment.requestPermissions(denyPermissions, requestCode);
} else {
ActivityCompat.requestPermissions(activity, denyPermissions, requestCode);
}
}
/**
* 請求權限結果處理
*
* @param requestCode
* @param permissions
* @param grantResults
* @param callback
*/
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, PermissionCallback callback) {
if (requestCode == code) {
String[] denyPermissions = getNotVerifyPermissions(grantResults, permissions);
if (denyPermissions.length > 0) {
callback.rejectPermission(denyPermissions);
intentSetting(denyPermissions);
} else {
callback.permittedPermissions();
}
}
}
/**
* 根據申請權限結果,得到未授權的權限數組
*
* @param grantResults 存儲權限結果數組
* @param permissions 存儲權限數組
* @return 未授權的權限數組
*/
public String[] getNotVerifyPermissions(int[] grantResults, String[] permissions) {
List<String> denyPermissions = new ArrayList<>();
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
denyPermissions.add(permissions[i]);
}
}
}
return denyPermissions.toArray(new String[denyPermissions.size()]);
}
/**
* 檢查權限得到未授權的權限數組
*
* @param permissions 申請授權的權限數組
* @return 未授權的權限數組
*/
private String[] getDenyPermissions(String[] permissions) {
List<String> denyPermissions = new ArrayList<>();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
denyPermissions.add(permission);
}
}
return denyPermissions.toArray(new String[denyPermissions.size()]);
}
/**
* 判斷是否有權限需要顯示解釋彈框
*
* @param permissions
* @return
*/
private boolean shouldShowRequestPermissionRationale(String[] permissions) {
if (fragment != null) {
for (String permission : permissions) {
if (fragment.shouldShowRequestPermissionRationale(permission)) {
return true;
}
}
} else {
for (String permission : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
return true;
}
}
}
return false;
}
/**
* 顯示解釋彈框
*
* @param callback
* @param requestCode
*/
private void showRequestPermissionRationale(final String[] permissions, final PermissionCallback callback, final int requestCode) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage("需要開啓權限才能使用此功能")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//繼續請求權限
handlerPermission(requestCode, permissions);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if(callback != null){
callback.rejectPermission(permissions);
}
}
})
.create()
.show();
}
/**
* 申請權限的結果回調接口
*/
public interface PermissionCallback {
void permittedPermissions(); //允許申請的權限
void rejectPermission(String[] rejectPermissions);//申請的權限中有未授權的權限,參數爲未授權的權限集合
}
/**
* 當用戶點擊不再詢問之後,再次請求權限提示用戶進入設置頁手動授權
*
* @param permissions
*/
public void intentSetting(String[] permissions) {
if (!shouldShowRequestPermissionRationale(permissions)) {
if (getFlag()) {
showGoToSettingDialog(permissions);
}
saveFlag(true);
}
}
/**
* 跳到系統的設置頁面
*/
private void gotoSetting() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivityForResult(intent, 0);
}
/**
* 用戶點擊不再詢問後,再次申請權限彈框
*
* @param permissions 請求的權限
*/
private void showGoToSettingDialog(final String[] permissions) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage("需要在設置頁面手動設置權限纔可使用此功能")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
gotoSetting();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (callback != null) {
callback.rejectPermission(permissions);
}
}
})
.create()
.show();
}
private void saveFlag(boolean flag) {
SharedPreferences sharedPreferences = activity.getSharedPreferences(
"userInfo", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(NO_LONGER_ASK, flag);
editor.commit();
}
private boolean getFlag() {
SharedPreferences sharedPreferences = activity.getSharedPreferences(
"userInfo", Context.MODE_PRIVATE);
return sharedPreferences.getBoolean(NO_LONGER_ASK, false);
}
public void showRejectMsg() {
if (fragment != null) {
Toast.makeText(fragment.getActivity(), "授權後,才能使用此功能", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(activity, "授權後,才能使用此功能", Toast.LENGTH_SHORT).show();
}
}
}