Android系統權限
- Android 是一個權限分隔的操作系統,其中每個應用都有其獨特的系統標識(Linux 用戶 ID 和組 ID)。系統各部分也分隔為不同的標識。Linux 據此將不同的應用以及應用與系統分隔開來。在默認情況下任何應用都沒有權限執行對其他應用、操作系統或用戶有不利影響的任何操作。包括讀取或寫入用戶的私有數據(例如聯系人或電子郵件)、讀取或寫入其他應用程序的文件、執行網絡訪問、使設備保持喚醒狀態等。
- 在舊的權限管理系統中,權限僅僅在App安裝時詢問用戶一次,用戶同意了這些權限App才能被安裝(某些深度定制系統另說),App一旦安裝后后授權不可取消。
- Android6.0引入了新的權限模式,將系統權限區分為正常權限和危險權限。開發者在使用到危險權限相關的功能時,不僅需要在Manifest文件中配置,還需要在代碼中動態獲取權限,如果沒有確認獲取到權限而直接執行相應所需權限的代碼,將導致App崩潰。另外,Android6.0 以上系統,App 退到后臺,修改應用權限,再次 App 回到前臺,會出現應用新開進程重啟。
權限的使用
Android App 默認無任何權限,如果需要使用系統權限必須在AndroidManifest.xml文件中聲明權限。
//聲明網絡使用權限
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.permission">
<uses-permission android:name="android.permission.INTERNE"/>
<application ...>
...
</application>
</manifest>
如果所需權限為正常權限(即不會對用戶隱私或設備操作造成很大風險的權限),系統會自動授予這些權限。
如果所需權限為危險權限(即可能影響用戶隱私或設備正常操作的權限),系統會要求用戶明確授予這些權限。Android 發出請求的方式取決于系統版本(即targetSdkVersion):
- 如果運行在 Android 6.0 及以上版本,App targetSdkVersion 大于23,則需要在運行時向用戶請求權限,并且需要在 App 使用相關的權限之前檢查自身是否已被授予該權限。
- 如果運行在 Android 6.0 以下版本,或App targetSdkVersion 小于23(此時設備可以是Android 6.0 (API level 23)或者更高),則系統會在用戶安裝則系統會在用戶安裝App時要求用戶授予權限,系統就告訴用戶App需要什么權限組。如果App將新權限添加到更新的應用版本,系統會在用戶更新應用時要求授予該權限。用戶一旦安裝應用,他們撤銷權限的唯一方式是卸載應用。
正常權限
正常權限涵蓋應用需要訪問其沙盒外部數據或資源,但對用戶隱私或其他應用操作風險很小的區域。例如,設置時區的權限就是正常權限。此類權限都是正常保護的權限,只需要在Manifest文件中簡單聲明,安裝即授權。
危險權限
危險權限涵蓋應用需要涉及用戶隱私信息的數據或資源,或者可能對用戶存儲的數據或其他應用的操作產生影響的區域。例如,能夠讀取用戶的聯系人屬于危險權限。
特殊權限
有許多權限其行為方式與正常權限及危險權限都不同。SYSTEM_ALERT_WINDOW
和 WRITE_SETTINGS
特別敏感,因此大多數應用不應該使用它們。如果某應用需要其中一種權限,必須在清單中聲明該權限,并且發送請求用戶授權的 intent。系統將向用戶顯示詳細信息,以響應該 intent。
權限組
所有危險權限都擁有對應權限組,如果運行在 Android 6.0 及以上版本,App targetSdkVersion 大于23,則當用戶請求危險權限時系統會發生以下行為:
- 如果應用未擁有Manifest列出的危險權限所在的權限組任一權限,則系統會向用戶顯示一個對話框,描述應用要訪問的權限組。對話框不描述該組內的具體權限。例如,如果應用請求 READ_CONTACTS 權限,系統對話框只說明該應用需要訪問設備的聯系信息。如果用戶批準,系統將向應用授予其請求的權限。
- 如果應用為擁有Manifest列出的危險權限所在的權限組其他任一權限,則系統會立即授予該權限,而無需與用戶進行任何交互。例如,如果某應用已經請求并且被授予了 READ_CONTACTS 權限,然后它又請求 WRITE_CONTACTS,系統將立即授予該權限。
Android O的運行時權限策略變化
- 在 Android O 之前,如果應用在運行時請求權限并且被授予該權限,系統會錯誤地將屬于同一權限組并且在清單中注冊的其他權限也一起授予應用。對于針對Android O的應用,此行為已被糾正。系統只會授予應用明確請求的權限。然而一旦用戶為應用授予某個權限,則所有后續對該權限組中權限的請求都將被自動批準。
- 例如,假設某個應用在其清單中列出READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。應用請求READ_EXTERNAL_STORAGE,并且用戶授予了該權限,如果該應用針對的是API級別24或更低級別,系統還會同時授予WRITE_EXTERNAL_STORAGE,因為該權限也屬于STORAGE權限組并且也在清單中注冊過。如果該應用針對的是Android O,則系統此時僅會授予READ_EXTERNAL_STORAGE,不過在該應用以后申請WRITE_EXTERNAL_STORAGE權限時,系統會立即授予該權限,而不會提示用戶。
- 我們申請了WRITE_EXTERNAL_STORAGE權限,在Android O之前,我們同時會得到READ_EXTERNAL_STORAGE權限,我們在其它地方涉及到讀取存儲卡的操作時只需要判斷有WRITE_EXTERNAL_STORAGE權限就去讀取了。此時應用如果安裝在Android O的系統中我們會發現,判斷了有WRITE_EXTERNAL_STORAGE權限后去讀取存儲卡內容時應用崩潰了,原因就是我們沒有申請READ_EXTERNAL_STORAGE權限。
運行時請求權限
檢查權限及兼容
(1)對于運行在 Android 6.0及以上 App targetSdkVersion 大于23的應用
- 如果App需要用到危險權限,需要這一權限的操作時都必須檢查自己是否擁有該權限。檢查權限代碼如下所示:
// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.WRITE_CALENDAR);
- 如果應用已經具有了該權限,此方法將返回
PackageManager.PERMISSION_GRANTED
,并且應用可以繼續操作。如果應用不具有此權限,方法將返回PERMISSION_DENIED
,此時應用應當進行權限申請。
(2)對于運行在 Android 6.0及以上 App targetSdkVersion 大于23的應用
- 在App安裝時會詢問AndroidManifest.xml文件中的權限,用戶也可以在設置列表中手動關閉/開啟相關權限。
(3)對于運行在 Android 6.0以下 App targetSdkVersion 大于23的應用
對于運行在 Android 6.0以下 App targetSdkVersion 大于23的應用,默認情況下是會采取舊的權限機制,然而,一些國產手機在6.0之前就引入了權限管理系統,所以必須對其進行兼容。
-
下面我們來看
ActivityCompat.requestPermissions()
方法。public static void requestPermissions(final @NonNull Activity activity, final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) { if (Build.VERSION.SDK_INT >= 23) { ActivityCompatApi23.requestPermissions(activity, permissions, requestCode); } else if (activity instanceof OnRequestPermissionsResultCallback) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { final int[] grantResults = new int[permissions.length]; PackageManager packageManager = activity.getPackageManager(); String packageName = activity.getPackageName(); final int permissionCount = permissions.length; for (int i = 0; i < permissionCount; i++) { grantResults[i] = packageManager.checkPermission( permissions[i], packageName); } ((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult( requestCode, permissions, grantResults); } }); } }
系統版本小于23時,使用
packageManager.checkPermission()
對權限進行校驗,而當App在AndroidManifest.xml中聲明權限時,會返回PERMISSION_GRANTED
,顯然,這一校驗方法在Android 6.0以下將失效。PermissionChecker.checkSelfPermission()
PermissionChecker
是 Support V4 包下一個專門檢查權限的工具類
public static int checkPermission(@NonNull Context context, @NonNull String permission,
int pid, int uid, String packageName) {
if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) {
return PERMISSION_DENIED;
}
String op = AppOpsManagerCompat.permissionToOp(permission);
if (op == null) {
return PERMISSION_GRANTED;
}
if (packageName == null) {
String[] packageNames = context.getPackageManager().getPackagesForUid(uid);
if (packageNames == null || packageNames.length <= 0) {
return PERMISSION_DENIED;
}
packageName = packageNames[0];
}
if (AppOpsManagerCompat.noteProxyOp(context, op, packageName)
!= AppOpsManagerCompat.MODE_ALLOWED) {
return PERMISSION_DENIED_APP_OP;
}
return PERMISSION_GRANTED;
}
checkSelfPermission
通過上述四個判斷語句進行權限校驗
-
context.checkPermission()
實際上調用的還是上述packageManager.checkPermission()
方法進行校驗。 -
AppOpsManagerCompat.permissionToOp()
調用了IMPL.permissionToOp(permission)
方法
private static final AppOpsManagerImpl IMPL;
static {
if (Build.VERSION.SDK_INT >= 23) {
IMPL = new AppOpsManager23();
} else {
IMPL = new AppOpsManagerImpl();
}
}
- 可以看到,在Android6.0以下設備中,會使用
AppOpsManagerImpl
,其permissionToOp()
方法進行權限檢查,其默認返回null,所以PermissionChecker.checkSelfPermission()
同樣會失效。
private static class AppOpsManagerImpl {
AppOpsManagerImpl() {
}
public String permissionToOp(String permission) {
return null;
}
public int noteOp(Context context, String op, int uid, String packageName) {
return MODE_IGNORED;
}
public int noteProxyOp(Context context, String op, String proxiedPackageName) {
return MODE_IGNORED;
}
}
- 此外,Google官方還提供了AppOpsManager類來檢查權限,
public int checkOp(String op, int uid, String packageName) {
return checkOp(strOpToOp(op), uid, packageName);
}
@hide
public int checkOp(int op, int uid, String packageName) {
try {
int mode = mService.checkOperation(op, uid, packageName);
if (mode == MODE_ERRORED) {
throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
}
return mode;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
- 為了更好的兼容不同廠家的國產手機,建議針對不同的系統版本使用不同的權限校驗策略,對于 Android 6.0以上設備,使用PermissionChecker.checkSelfPermission()進行權限校驗,對于Android 6.0 以下設備,可通過觸發
try catch
后的危險權限代碼檢查是否有權限,以期準確校驗權限,對用戶進行引導,具體可參考AndPermission等第三方庫。
請求權限
對于App targetSdkVersion 大于23的應用,在應用需要使用的危險權限,必須要進行動態權限申請。Android 提供了多種權限請求方式。調用這些方法將顯示一個標準的 Android 對話框(不允許開發者自定義)。
調用requestPermissions() 方法,可進行動態權限申請,該方法是異步的。在用戶響應對話框之后,系統會回調onRequestPermissionsResult
。代碼實例如下:
// 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.
}
}
處理權限請求回調
在用戶響應對話框之后,系統會回調 onRequestPermissionsResult() 方法。代碼實例如下:
@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
}
}
權限申請被拒絕
如果用戶拒絕了某項權限請求,應用應采取適當的操作進行引導,應用應當顯示一個對話框,解釋應用為什么需要此權限,以及使用該權限有何影響。
當系統要求用戶授予權限時,用戶可以選擇指統不再要求提供該權限。這種情況下,無論應用在什么時候使用 requestPermissions()
請求該權限,系統都會立即拒絕此請求。系統會調用您的 onRequestPermissionsResult()
回調方法,并傳遞 PERMISSION_DENIED
,如果用戶再次明確拒絕了權限的請求,系統將采用相同方式操作。這意味著當調用 requestPermissions()
時,不一定會出現系統權限請求彈窗。
此時可借助shouldShowRequestPermissionRationale()
這個回調方法,如果應用之前請求過此權限但用戶拒絕了請求,此方法將返回 true。如果用戶拒絕了權限請求,并在權限請求系統對話框中選擇了不再詢問選項,此方法將返回 false。如果設備默認禁止應用具有該權限,此方法也會返回 false。
shouldShowRequestPermissionRationale()
- 望文生義,是否應該顯示請求權限的說明。
- 第一次請求權限時,用戶拒絕了,調用shouldShowRequestPermissionRationale()后返回true,應該顯示一些為什么需要這個權限的說明。
- 用戶在第一次拒絕某個權限后,下次再次申請時,授權的dialog中將會出現“不再提醒”選項,一旦選中勾選了,那么下次申請將不會提示用戶。
- 第二次請求權限時,用戶拒絕了,并選擇了“不再提醒”的選項,調用shouldShowRequestPermissionRationale()后返回false。
- 設備的策略禁止當前應用獲取這個權限的授權:shouldShowRequestPermissionRationale()返回false 。
- 加這個提醒的好處在于,用戶拒絕過一次權限后我們再次申請時可以提醒該權限的重要性,免得再次申請時用戶勾選“不再提醒”并決絕,導致下次申請權限直接失敗。
關于運行時權限的一些建議
(1)只請求需要的權限,減少請求的次數,或用隱式Intent來讓其他的應用來處理。
- 使用Intent,你不需要設計界面,由第三方的應用來完成所有操作。比如打電話、選擇圖片等。
- 如果請求權限,你可以完全控制用戶體驗,自己定義UI。但是用戶也可以拒絕權限,就意味著你的應用不能執行這個特殊操作。
(2)防止一次請求太多的權限或請求次數太多,用戶可能對你的應用感到厭煩,在應用啟動的時候,最好先請求應用必須的一些權限,非必須權限在使用的時候才請求,建議整理并按照上述分類管理自己的權限:
- 普通權限(Normal PNermissions):只需要在Androidmanifest.xml中聲明相應的權限,安裝即許可。
- 需要運行時申請的權限(Dangerous Permissions):
必要權限:最好在應用啟動的時候,進行請求許可的一些權限(主要是應用中主要功能需要的權限)。
附帶權限:不是應用主要功能需要的權限(如:選擇圖片時,需要讀取SD卡權限)。
解釋你的應用為什么需要這些權限:在你調用requestPermissions()之前,你為什么需要這個權限。
例如,一個攝影的App可能需要使用定位服務,因為它需要用位置標記照片。一般的用戶可能會不理解,他們會困惑為什么他們的App想要知道他的位置。所以在這種情況下,所以你需要在requestpermissions()之前告訴用戶你為什么需要這個權限。
(3)使用兼容庫support-v4中的方法
- PermissionChecker.checkSelfPermission() 或者 ContextCompat.checkSelfPermission()
- ActivityCompat.requestPermissions()
- ActivityCompat.shouldShowRequestPermissionRationale()
(4)將動態權限區分為必須和非必須授予
- 依照自身APP需求,區分成必須授予(如SD卡,設備,定位等)和非必須授予(如相機,通訊錄等)
- 啟動頁先行提示用戶,需要授予哪些權限,然后下一步就開始檢查必須授予的權限,如果拒絕了必須授予權限,彈框解釋為何APP必須授予此權限,如果用戶取消,就退出AP,如果同意,跳轉設置頁面授予權限。
國產機權限問題整理
- 部分中國廠商生產手機(例如小米某型號)的Rationale功能,在第一次拒絕后,第二次申請時不會返回true,并且會回調申請失敗,也就是說在第一次拒絕后默認勾選了不再提示。
- 部分中國廠商生產手機(例如小米、華為某型號)在申請權限時,用戶點擊確定授權后,還是回調我們申請失敗,這個時候其實我們是擁有權限的,所以我們可以在失敗的方法中使用AppOpsManager進行權限判斷。
- 部分中國廠商生產手機(例如vivo、Oppo某型號)在用戶允許權限,并且回調了權限授權成功的方法,但是實際執行代碼時并沒有這個權限,建議開發者在回調成功的方法中也利用AppOpsManager判斷下。
- 在某些手機的Setting中授權后實際檢查時還是沒有權限,部分執行代碼也是沒有權限。
從系統版本看國產機型的權限申請特點
- 5.0:此時 google 還未著手處理動態權限申請這么個東西,但是我們的小米、魅族等廠商就開始提前設置了強大的權限管理,所以 6.0 權限申請代碼在 5.0 上壓根不管用,但是說來也簡單,5.0 的權限申請對話框激活就是靠觸發危險權限代碼,然后根據返回值來判斷權限是否獲取到了(不同手機的返回值判斷方式不同,此處需要一一定制)。
- 6.0:國產大部分機型手機的申請權限實際上應該細致地分為申請權限和應用權限 。它們的 ContextCompat.checkSelfPermission(Context, String) 判斷是根據是否 AndroidManifest.xml 中聲明了該權限來決定返回值,在 AndroidManifest.xml 中聲明了權限就返回 true,當然也會有一些會返回 false,這個是申請權限的過程。而真正對話框的彈出是在開發者應用權限的過程中,什么叫做應用權限?就是調用了會觸發權限的代碼,這個時候就會激活對話框,但是如果僅到這里那就 too young too simple 了,當用戶點擊拒絕授權時,還是可能會回調授權成功的方法。另外,國產機大部分權限是有三個狀態——詢問、允許、拒絕——大部分權限都是詢問狀態,但是有些權限默認是允許狀態,有些是拒絕狀態,這就導致了調用 ContextCompat.checkSelfPermission(Context, String) 方法時會更畸形,例如小米手機的獲取 READ_PHONE_STATE 狀態,默認是授予狀態。
Tanks
- https://developer.android.google.cn/training/permissions/requesting.html
- https://mp.weixin.qq.com/s/OQRHEufCUXBA3d3DMZXMKQ
- http://blog.csdn.net/yanzhenjie1003/article/details/52503533
- http://blog.csdn.net/yanzhenjie1003/article/details/76719487
- https://github.com/jokermonn/permissions4m
- https://testerhome.com/topics/5181