我的GitHub | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|
baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
對運行時權限的一些理解
在舊的權限管理系統中,權限僅僅在App安裝時詢問用戶一次
,用戶同意了這些權限App才能被安裝,App一旦安裝后就可以偷偷的做一些不為人知的事情了。
從Android6.0
開始,App可以直接安裝
,App在運行時
由開發者決定在任一適當的時機一個一個詢問用戶是否授予權限,系統會彈出一個對話框讓用戶選擇是否授權某個權限給App,這個Dialog是系統定義的,開發者不能定制,當App要求用戶授予不恰當的權限的時候,用戶可以拒絕
(然而,很多APP可能會在請求權限失敗后直接退出,所以你往往無法拒絕),用戶即使授予或拒絕后也可以在設置頁面
對每個App的權限進行重新管理。
新的權限策略將權限分為兩類,第一類是不涉及用戶隱私的,只需要在Manifest中聲明即可,比如網絡、藍牙、NFC
等;第二類是涉及到用戶隱私信息的,需要用戶授權后才可使用,比如SD卡讀寫、聯系人、短信讀寫
等。
不需要運行時申請的權限
此類權限都是正常保護的權限,只需要在AndroidManifest.xml
中簡單聲明這些權限即可,安裝即授權
,不需要每次使用時都檢查權限,而且用戶不能取消以上授權
,除非用戶卸載App。
需要運行時申請的權限
所有危險的Android系統權限屬於權限組,如果APP運行在Android 6.0(API level 23)
或者更高級別的設備中,並且targetSdkVersion>=23
時,系統將會自動采用動態權限管理策略,如果你在涉及到特殊權限操作時沒有申請權限而直接調用了相關代碼,你的App可能就崩潰了。
綜上所述,你需要注意:
- 此類權限也必須在Manifest中申明,否則申請時不提示用戶,直接回調開發者權限被拒絕。
- 同一個權限組的任何一個權限被授權了,這個權限組的其他權限也自動被授權。例如一旦WRITE_CONTACTS被授權了,App也有READ_CONTACTS和GET_ACCOUNTS了。
- 申請某一個權限的時候系統彈出的Dialog是對整個權限組的說明,而不是單個權限。例如我申請READ_EXTERNAL_STORAGE,系統會提示"允許xxx訪問設備上的照片、媒體內容和文件嗎?"。
其他情景:
1、targetSdkVersion小於等於22,但是設備系統版本大於等於6.0:
- app使用舊的權限管理策略
- 注冊文件列出的權限將會在安裝時要求用戶授予權限
用戶可以在設置列表中編輯相關權限,這對app能否正常運行有很大影響
2、targetSdkVersion大於等於23,但是設備系統版本小於6.0:
- app使用舊的權限管理策略
- 注冊文件列出的權限將會在安裝時要求用戶授予權限
運行時權限使用案例
public class MainActivity extends ListActivity {
private static final int REQUEST_CODE = 20094;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] array = {"完整的授權過程演示"};
setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, array));
createFile();
}
private void createFile() {
try {
//如果將targetSdkVersion為22或以下,可以成功創建文件;如果改為23或以上,則失敗(會拋異常)
File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".txt");
Toast.makeText(this, "結果:" + file.createNewFile(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "異常了", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
requestPermissions();
}
private void requestPermissions() {
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
int permissionState = ContextCompat.checkSelfPermission(this, permissions[0]); //檢查權限
if (permissionState != PackageManager.PERMISSION_GRANTED) { //沒有權限,申請權限
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) { //是否應該顯示請求權限的說明
new AlertDialog.Builder(this)
.setTitle("通過彈一個自定義的對話框告訴用戶,我們為什么需要這個權限")
.setMessage("請求SD卡權限,作用是給你保存妹子圖片,點擊【好嘞】會重新嘗試請求用戶授權")
.setPositiveButton("好嘞", (dialog, which) -> ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE))
.create()
.show();
} else {
ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE); //請求用戶授權
}
} else {
Toast.makeText(this, "有權限了", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
switch (requestCode) {
case REQUEST_CODE: {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "有權限了", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "你拒絕了權限,我們已經沒法愉快的玩耍了!", Toast.LENGTH_SHORT).show();
}
}
}
}
}
}
開源庫:PermissionsDispatcher
PermissionsDispatcher provides a simple annotation-based
API to handle runtime permissions.
This library lifts the burden 解除了負擔
that comes with writing a bunch of 一系列
check statements whether a permission has been granted or not from you, in order to keep your code clean and safe.
特性:
- Kotlin support
- Special Permissions support
- 100% reflection-free 無反射
注解
@RuntimePermissions
和 @NeedsPermission
是必須的,其余注解均為可選。
注意:被注解的方法不能是私有方法,因為這些方法會被自動生成的類調用。
Annotation | Required | Description |
---|---|---|
@RuntimePermissions |
✓ | Register an Activity or Fragment to handle permissions |
@NeedsPermission |
✓ | Annotate a method which performs the action that requires one or more permissions 當用戶給予權限時會執行該方法 |
@OnShowRationale |
optional | Annotate a method which explains why the permissions are needed. |
@OnPermissionDenied |
optional | Annotate a method which is invoked if the user doesn't grant the permissions |
@OnNeverAskAgain |
optional | Annotate a method which is invoked if the user chose to have the device "never ask again" about a permission |
OnShowRationale 方法的細節
It passes in aPermissionRequest
object which can be used tocontinue or abort
the current permission request upon user input. If you don't specify any argument for the method compiler will generateprocess{方法名}ProcessRequest
andcancel{方法名}ProcessRequest
. You can use those methods in place ofPermissionRequest
(ex: withDialogFragment
)
使用案例
使用步驟
1、聲明權限
<uses-permission android:name="android.permission.CAMERA" />
2、引入框架
//注意:要將targetSdkVersion設為23或以上,否則這個庫是無效的
//NOTE: 4.x only supports Jetpack. If you still use appcompat 3.x is the way to go.
implementation 'com.github.hotchemi:permissionsdispatcher:3.3.1'
annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.4.0'
3、添加注解
4、點擊 菜單欄 → Build → Make Project 重新編譯整個項目
編譯完成后,會在 app\build\intermediates\classes\debug
目錄下生成一個與被注解的Activity同一個包下的輔助類,名稱為被注解的Activity名稱+PermissionsDispatcher
,例如MainActivityPermissionsDispatcher
。
測試代碼
@RuntimePermissions
public class MainActivity extends ListActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] array = {"開源庫 PermissionsDispatcher"};
setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, array));
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
MainActivityPermissionsDispatcher.performActionWithCheck(this);
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void performAction() {
Toast.makeText(this, "有權限了", Toast.LENGTH_SHORT).show();
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void doOnPermissionDenied() {
Toast.makeText(this, "你拒絕了權限,我們已經沒法愉快的玩耍了!", Toast.LENGTH_SHORT).show();
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void doOnShowRationale(final PermissionRequest request) {
new AlertDialog.Builder(this)
.setTitle("通過彈一個自定義的對話框告訴用戶,我們為什么需要這個權限")
.setMessage("請求SD卡權限,作用是給你保存妹子圖片,點擊【好嘞】會重新嘗試請求用戶授權")
.setPositiveButton("我知道了,繼續吧", (dialog, which) -> request.proceed())
.setNegativeButton("我不需要,不要再請求了", (dialog, which) -> request.cancel())
.create()
.show();
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void doOnNeverAskAgain() {
Toast.makeText(this, "你拒絕了權限,並勾選了不再提醒,已經沒法愉快的玩耍了!", Toast.LENGTH_SHORT).show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}
}
自動生成的類
//final 類型的工具類
final class MainActivityPermissionsDispatcher {
private static final int REQUEST_PERFORMACTION = 0; //請求碼
private static final String[] PERMISSION_PERFORMACTION = new String[]{"android.permission.WRITE_EXTERNAL_STORAGE"}; //需要的權限
private MainActivityPermissionsDispatcher() {} //私有構造方法
//注解處理器會檢索所有帶 @NeedsPermission 的方法名,生成相應的名為【方法名+WithCheck】的工具方法
//所有帶注解的方法都不能是私有方法,不然只能通過反射調用,而這個庫是100%不使用反射的
static void performActionWithCheck(MainActivity target) {
if (PermissionUtils.hasSelfPermissions(target, PERMISSION_PERFORMACTION)) { //檢查是否已經有權限
target.performAction(); //有權限時執行
} else {
if (PermissionUtils.shouldShowRequestPermissionRationale(target, PERMISSION_PERFORMACTION)) {
//判斷是否需要顯示提示,根據結果執行不同的回調
target.doOnShowRationale(new PerformActionPermissionRequest(target));
} else {
//不需要顯示提示,所以開始通過系統API請求權限
ActivityCompat.requestPermissions(target, PERMISSION_PERFORMACTION, REQUEST_PERFORMACTION);
}
}
}
static void onRequestPermissionsResult(MainActivity target, int requestCode, int[] grantResults) {
switch (requestCode) { //處理授權結果
case REQUEST_PERFORMACTION:
if (PermissionUtils.verifyPermissions(grantResults)) { //授予了權限
target.performAction();
} else { //拒絕了權限
if (!PermissionUtils.shouldShowRequestPermissionRationale(target, PERMISSION_PERFORMACTION)) {
target.doOnNeverAskAgain(); //拒絕了權限,並勾選了不再提醒
} else {
target.doOnPermissionDenied(); //用戶拒絕了權限
}
}
break;
default:
break;
}
}
private static final class PerformActionPermissionRequest implements PermissionRequest {
private final WeakReference<MainActivity> weakTarget; //弱引用,不影響GC
private PerformActionPermissionRequest(MainActivity target) {
this.weakTarget = new WeakReference<MainActivity>(target);
}
@Override
public void proceed() {
MainActivity target = weakTarget.get();
if (target == null) return;
//再次請求權限
ActivityCompat.requestPermissions(target, PERMISSION_PERFORMACTION, REQUEST_PERFORMACTION);
}
@Override
public void cancel() {
MainActivity target = weakTarget.get();
if (target == null) return;
target.doOnPermissionDenied();//結束請求
}
}
}
官方文檔:請求權限
Every Android app runs in a limited-access sandbox
. If an app needs to use resources or information
outside of its own sandbox, the app has to request the appropriate 相應的
permission. You declare
that your app needs a permission by listing the permission in the app manifest
and then requesting that the user approve 批准
each permission at runtime
(on Android 6.0 and higher).
This page describes how to use the Android Support Library
to check for
and request permissions. The Android framework
provides similar methods as of Android 6.0 (API level 23), but using the support library makes it easier to provide compatibility
with older versions of Android.
Add permissions to the manifest
On all versions of Android, to declare that your app needs a permission, put a <uses-permission>
element in your app manifest, as a child of the top-level <manifest>
element.
The system's behavior after you declare a permission depends on how sensitive 敏感程度
the permission is. Some permissions are considered "normal" so the system immediately
grants them upon installation. Other permissions are considered "dangerous" so the user must explicitly 明確
grant your app access. For more information about the different kinds of permissions, see 保護級別.
Check for permissions
If your app needs a dangerous permission, you must check whether you have that permission every time
you perform an operation
that requires that permission. Beginning with Android 6.0 (API level 23), users can revoke 撤銷
permissions from any app at any time, even if the app targets a lower API level. So even if the app used the camera yesterday, it can't assume 確認
it still has that permission today.
To check if you have a permission, call the ContextCompat.checkSelfPermission()
method. For example, this snippet
shows how to check if the activity has permission to write to the calendar:
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
}
If the app has the permission, the method returns PERMISSION_GRANTED
, and the app can proceed 繼續
with the operation. If the app does not have the permission, the method returns PERMISSION_DENIED
, and the app has to explicitly
ask the user for permission.
Request permissions
When your app receives PERMISSION_DENIED
from checkSelfPermission()
, you need to prompt
the user for that permission. Android provides several methods you can use to request a permission, such as requestPermissions()
, as shown in the code snippet below. Calling these methods brings up 顯示
a standard Android dialog, which you cannot customize
.
How this is displayed to the user depends on the device Android version as well as the target version of your application, as described in the 權限概覽.
Explain why the app needs permissions
In some circumstances
, you want to help the user understand why your app needs a permission. For example, if a user launches a photography
app, the user probably won't be surprised that the app asks for permission to use the camera, but the user might not understand why the app wants access to the user's location or contacts. Before your app requests a permission, you should consider providing an explanation
to the user. Keep in mind that you don't want to overwhelm 不勝其煩
the user with explanations; if you provide too many explanations, the user might find the app frustrating 很麻煩
and remove it.
One approach 方法
you might use is to provide an explanation only if the user has already denied
that permission request. Android provides a utility
method, shouldShowRequestPermissionRationale()
, that returns true if the user has previously 之前
denied the request, and returns false if a user has denied a permission and selected the Don't ask again option
in the permission request dialog, or if a device policy 政策
prohibits 禁止
the permission.
If a user keeps trying to use functionality
that requires a permission, but keeps denying the permission request, that probably means the user doesn't understand why the app needs the permission to provide that functionality. In a situation
like that, it's probably a good idea to show an explanation 解釋
.
More advice
about how to create a good user experience when asking for permission is provided in 應用權限最佳做法.
Request the permissions you need
If your app doesn't already have the permission it needs, the app must call one of the requestPermissions()
methods to request the appropriate 相應
permissions. Your app passes the permissions it wants and an integer request code
that you specify 指定
to identify 識別
this permission request. This method functions asynchronously 異步
. It returns right away, and after the user responds to the prompt 響應提示
, the system calls the app's callback method with the results, passing the same request code that the app passed to requestPermissions()
.
The following code checks if the app has permission to read the user's contacts. If it does not have permission it checks if it should show an explanation for needing the permission, and if no explanation is needed, it requests the permission:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
// Permission is not granted,Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) {
// Show an explanation 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; request the permission
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE);
// The callback method gets the result of the request.
}
} else {
// Permission has already been granted
}
The prompt 提示
shown by the system describes the 權限組 your app needs access to, not the specific permission.
Note: When your app calls
requestPermissions()
, the system shows astandard
dialog box to the user. Your app cannot configure or alter that dialog box. If you need to provide any information or explanation to the user, you should do that before you callrequestPermissions()
, as described in Explain why the app needs permissions.
Handle the permissions request response
When the user responds to your app's permission request, the system invokes 調用
your app's onRequestPermissionsResult()
method, passing it the user response. Your app has to override that method to find out whether the permission was granted. The callback is passed the same request code
you passed to requestPermissions()
. For example, if an app requests READ_CONTACTS access it might have the following callback method:
@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;
}
// other 'case' lines to check for other permissions this app might request.
}
}
The dialog box shown by the system describes the permission group
your app needs access to; it does not list the specific permission. For example, if you request the READ_CONTACTS
permission, the system dialog box just says your app needs access to the device's contacts
. The user only needs to grant permission once for each permission group.
If your app requests any other permissions in that group (that are listed in your app manifest), the system automatically
grants them. When you request the permission, the system calls your onRequestPermissionsResult()
callback method and passes PERMISSION_GRANTED, the same way it would if the user had explicitly 明確
granted your request through the system dialog box.
如果您的應用請求該組中的其他任何權限(已在您的應用清單中列出),系統會自動授予這些權限。當您請求權限時,系統會調用您的 onRequestPermissionsResult() 回調方法並傳遞 PERMISSION_GRANTED,就像用戶通過系統對話框明確同意了您的請求時的處理方式一樣。
Note: Your app still needs to explicitly request every permission it needs, even if the user has already granted another permission in the same group.
In addition 此外
, the grouping of permissions into groups may change in future Android releases. Your code should not rely on theassumption 假設
thatparticular 特定
permissions are or are not in the same group.
For example, suppose you list both READ_CONTACTS and WRITE_CONTACTS in your app manifest. If you request READ_CONTACTS and the user grants the permission, and you then request WRITE_CONTACTS, the system immediately grants you that permission without interacting with 交互
the user.
If the user denies a permission request, your app should take appropriate
action. For example, your app might show a dialog explaining why it could not perform the user's requested action that needs that permission.
When the system asks the user to grant a permission, the user has the option 選擇
of telling the system not to ask for that permission again
. In that case, any time an app uses requestPermissions() to ask for that permission again, the system immediately
denies the request. The system calls your onRequestPermissionsResult() callback method and passes PERMISSION_DENIED, the same way
it would if the user had explicitly rejected
your request again. The method also returns false if a device policy
prohibits
the app from having that permission. This means that when you call requestPermissions(), you cannot assume that any direct interaction with
the user has taken place 發生
.
To provide the best user experience
when asking for app permissions, also see 應用權限最佳做法.
Declare permissions by API level
To declare a permission only on devices that support runtime permissions, that is, devices running Android 6.0 (API level 23) or higher, include the uses-permission-sdk-23
tag instead of the uses-permission
tag.
When using either of these tags, you can set the maxSdkVersion
attribute to specify that, on devices running a higher version, a particular
permission isn't needed.
所有危險權限(組)
權限組:Manifest.permission_group
權限:Manifest.permission
中文解釋參考
使用以下 adb 命令可以查看所有需要授權的權限組:
adb shell pm list permissions -d -g
不同系統可能稍有差異,不過基本都一樣,以下為一個案例:
Dangerous Permissions:
group:com.google.android.gms.permission.CAR_INFORMATION 汽車資料
permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
permission:com.google.android.gms.permission.CAR_MILEAGE
permission:com.google.android.gms.permission.CAR_FUEL
group:android.permission-group.CONTACTS 聯系人
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE 【電話相關,高頻使用,經常會變】
permission:android.permission.ANSWER_PHONE_CALLS ---API26新增的
permission:android.permission.READ_PHONE_NUMBERS ---API26新增的
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.ACCEPT_HANDOVER ---API28新增的
permission:android.permission.USE_SIP
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALL_LOG【電話相關,API28中從PHONE權限組抽離出來的】
permission:android.permission.READ_CALL_LOG
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.PROCESS_OUTGOING_CALLS
group:android.permission-group.CALENDAR 日歷
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA 相機
permission:android.permission.CAMERA
group:android.permission-group.SENSORS 傳感器
permission:android.permission.BODY_SENSORS
group:android.permission-group.LOCATION 【位置,高頻使用】
permission:android.permission.ACCESS_FINE_LOCATION
permission:com.google.android.gms.permission.CAR_SPEED
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE 【存儲,高頻使用】
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE 麥克風
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS 短信
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
//省略了一小部分未分組的以及第三方應用(例如微博)定義的
2019-11-16