-
- Android權限适配全攻略
- 概述
- 運作時權限管理機制介紹
- 權限組
- 權限組的作用
- 權限适配
- Android 60 運作時動态權限适配
- 國産ROM自定義權限機制适配
- 特殊權限适配
- 不依賴Activity進行權限申請
- Android80權限适配
- 參考
- Android權限适配全攻略
Android權限适配全攻略
概述
在Android6.0(API 23)之前Android的權限機制太過于簡單粗暴:App安裝時會提示使用者APP所需權限,使用者同意安裝後就會永久授權應用所需全部權限。其中大部分使用者并不知道為什麼APP需要這些權限,也不知道這些權限意味着什麼。這樣很容易被“别有用心”的APP利用,對使用者的隐私及賬戶安全形成了很大的挑戰。
Google也開始意識到問題的嚴重性并且開始收緊權限,在6.0(API 23)做了大的改動,引入了動态權限管理概念。
運作時權限管理機制介紹
對于開發者而言,在6.0之前應用申請權限隻要将權限在Androidmanifest.xml中注冊即可擷取對應權限。在6.0之後Google将所有權限進行了整合分組,将系統權限分為幾個保護級别也就是權限組。
權限組
需要了解的兩個最重要保護級别是normal permissions和dangerous permissions:
normal permissions:涵蓋應用需要通路資料或資源,但對使用者隐私或其他應用操作風險很小的區域。例如,設定時區的權限就是正常權限。如果應用聲明其需要正常權限,系統會自動向應用授予該權限。
dangerous permissions:涵蓋應用需要涉及使用者隐私資訊的資料或資源,或者可能對使用者存儲的資料或其他應用的操作産生影響的區域。例如,能夠讀取使用者的聯系人屬于危險權限。如果應用聲明其需要危險權限,則使用者必須明确向應用授予該權限。
檢視dangerous permissions權限可以在perm-groups 或通過
adb shell pm list permissions -d -g
檢視
總結一下就是Google将權限進行了分組,申請屬于normal permissions的權限在manifest中聲明即可,對于dangerous permissions 權限必須運作時動态申請。
權限組的作用
開發者可能要問了:一個規模一般的APP需要的權限最少也有7,8個,難道要一個個申請麼?這開發工作量可不小啊!
Google也考慮到了對開發者的影響,上面介紹的權限組的作用也就展現出來了:
如果應用請求其清單中列出的危險權限,而應用目前在權限組中沒有任何權限,則系統會向使用者顯示一個對話框,描述應用要通路的權限組。對話框不描述該組内的具體權限。例如,如果應用請求 READ_CONTACTS 權限,系統對話框隻說明該應用需要通路裝置的聯系資訊。如果使用者準許,系統将向應用授予其請求的權限。
如果應用請求其清單中列出的危險權限,而應用在同一權限組中已有另一項危險權限,則系統會立即授予該權限,而無需與使用者進行任何互動。例如,如果某應用已經請求并且被授予了 READ_CONTACTS 權限,然後它又請求 WRITE_CONTACTS,系統将立即授予該權限。
總結一下就是開發者申請危險權限時,隻要獲得過屬于同一組的其他權限的授權,即可立即獲得系統對該權限的授權,無需重複申請。
權限适配
權限适配主要受APP運作環境及APP開發使用的
targetSdkVersion
有關,分為以下幾種情況:
-
targetSdkVersion >= 23 && App運作在 >= 6.0(api 23)系統的終端上
這種情況就會采用标準的Google**運作時動态權限機制**
-
targetSdkVersion >= 23 && App運作在 < 6.0(api 23)系統的終端上
這種情況就會采用原權限機制即安裝時擷取授權機制
-
targetSdkVersion < 23 && App運作在 >= 6.0(api 23)系統的終端上
這種情況就會采用相容模式,即在安裝時擷取授權。但是使用者可以在設定界面随時關閉/開啟 敏感權限。APP需做好權限關閉的邏輯處理。
-
targetSdkVersion < 23 && App運作在 < 6.0(api 23)系統的終端上
這種情況就會采用原權限機制即安裝時擷取授權機制
注意:部分國産ROM存在自定義權限機制導緻差異,下節詳述。
Android 6.0 運作時動态權限适配
适配權限按步驟可以分為四步:權限檢查,權限申請,授權結果處理,權限解釋。
- 權限檢查:可用的API較多,如下:
- 使用
進行檢查Activity#checkSelfPermission(String permission)
- 使用
進行檢查ContextCompat#checkSelfPermission(Context context, String permission)
ContextCompat#checkSelfPermission
進行檢查,這樣可以不依賴Activity。
已獲授權傳回
,未獲授權傳回PackageManager#PERMISSION_GRANTED
PackageManager#PERMISSION_DENIED
- 使用
- 權限申請:可以同時申請多個權限,requestCode辨別目前申請,會彈出系統權限申請彈窗。
- 使用
Activity#requestPermissions(String[] permissions, int requestCode)
- 使用
ActivityCompat#requestPermissions(Activity activity,String[] permissions, int requestCode)
ActivityCompat#requestPermissions
,會根據API版本使用最佳方式。
我們可以注意到,無論使用哪種方式進行權限申請都需要依賴Activity,因為系統在權限處理後是需要回調到Activity。
- 使用
- 授權結果處理:權限請求會通過接口回調的方式回調到Activity:
/**
* @param requestCode 權限申請request code
* @param permissions 申請的權限
* @param grantResults 授權結果
*/
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
}
- 權限解釋:Google提供了
API可以判斷使用者是否拒絕過目前申請權限,開發者可以彈出自定義框解釋申請權限原因ActivityCompat#shouldShowRequestPermissionRationale
public boolean shouldShowRequestPermissionRationale(permission)
{
、APP是第一次申請這個權限的話,傳回false
、使用者拒絕,勾選了不再提示的話,傳回false
、使用者拒絕,但是沒有勾選不再提示的話,傳回true
是以如果想在第一次就給使用者提示,需要記錄權限是否申請過,沒有申請過的話,強制彈窗提示,而不能根據這個方法的傳回值來。
最好在第一次給使用者提示,因為Google的權限申請文案本地化翻譯太差勁了,會導緻使用者很莫名其妙。
}
這幾步就是一個标準的權限申請處理步驟,我們可以看出步驟雖然不多,但是确實挺繁瑣的。權限一多,需要寫很多重複代碼。這裡我推薦大家一些處理的比較好的開源庫,可以讓權限申請更簡單:
1. EasyPermissions ,Google官方示例推薦。隻需關心權限處理結果即可,還可以結合注解使代碼更精簡。
2. PermissionsDispatcher ,封裝了大部分權限申請邏輯,使用友善。
3. AndPermission ,封裝的比較好,對國産ROM做了支援,相容Android8.0權限變化,API鍊式調用簡潔友善。
國産ROM自定義權限機制适配
相信很多開發者在機型适配或使用中會發現有些機型明明是6.0以下版本,但是也存在運作時權限管理機制。
這是因為部分國産ROM(MIUI等)在4.4以上自己使用了一套權限檢測機制,在這些機型上如果我們使用上一節介紹的6.0權限适配是無法适用的,具體的表現如下:
1. 使用
ContextCompat#checkSelfPermission
無法檢測出目前權限是否被授權,在API23以下manifest中注冊過就會傳回true,API23以上可能出現實際是被關閉但是傳回true的情況。
2. 不支援動态權限申請
那我們該如何檢測權限是否被正确授權?
雖然很坑爹,但是辦法還是有的。
查閱資料發現國産ROM中針對權限的開關可以被
AppOpsManager#noteOp(int op)
檢測出來,我們可以利用這個類來檢測權限授權狀态,這裡我們介紹這個個新的類
AppOpsManager
:
/**
* API for interacting with "application operation" tracking.
*
* <p>This API is not generally intended for third party application developers; most
* features are only available to system applications. Obtain an instance of it through
* {@link Context#getSystemService(String) Context.getSystemService} with
* {@link Context#APP_OPS_SERVICE Context.APP_OPS_SERVICE}.</p>
*/
public class AppOpsManager {
}
AppOpsManager中将權限與OP_*操作碼做了一一映射,比如:
android.Manifest.permission.READ_CONTACTS
public static final int OP_READ_CONTACTS = ;
這些操作碼與支援的權限是在不斷添加的,我們可以通過觀察
public static final int _NUM_OP = 62;
知道目前API版本支援的操作總數,需要做好相容。
但是通過注釋可以知道,這個類主要是給系統應用使用的,并不提供給第三方應用開發者。具體表現在類中基本所有方法都被@hide标記,開發者是無法直接使用的。那該怎麼辦呢?
我們搜尋AppOpsManager有關的類,發現Google貼心的在v4包中提供了一個相容類
AppOpsManagerCompat
,這個類對開發者來說是開放的,并且從API4向後相容。
/**
* 檢測權限,小米上使用
*
* @param context
* @param permission
* @return
*/
private static boolean hasSelfPermissionForXiaomi(Context context, String permission) {
// 将permission轉換成OP_*
String permissionToOp = AppOpsManagerCompat.permissionToOp(permission);
if (permissionToOp == null) {
// 不支援的權限,或者是normal permission
return true;
}
int noteOp = AppOpsManagerCompat.noteOp(context, permissionToOp, android.os.Process.myUid(), context.getPackageName());
// AppOpsManagerCompat 與 checkSelfPermission都檢測過則表明權限被開啟
return noteOp == AppOpsManagerCompat.MODE_ALLOWED && ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
我們使用這個方法就能對像小米這種自定義權限ROM進行權限檢查了,但在測試中發現這在< API 23上一直傳回true,這是什麼原因呢?我們看看
AppOpsManagerCompat
是如何實作的:
private static final AppOpsManagerImpl IMPL;
static {
if (Build.VERSION.SDK_INT >= ) {
IMPL = new AppOpsManager23();
} else {
IMPL = new AppOpsManagerImpl();
}
}
private static class AppOpsManagerImpl {
public String permissionToOp(String permission) {
// 直接傳回null,導緻檢測方法直接判斷為成功
return null;
}
public int noteOp(Context context, String op, int uid, String packageName) {
// 直接調用也會直接傳回MODE_IGNORED,不會進行檢測
return MODE_IGNORED;
}
public int noteProxyOp(Context context, String op, String proxiedPackageName) {
return MODE_IGNORED;
}
}
private static class AppOpsManager23 extends AppOpsManagerImpl {
@Override
public String permissionToOp(String permission) {
return AppOpsManagerCompat23.permissionToOp(permission);
}
@Override
public int noteOp(Context context, String op, int uid, String packageName) {
return AppOpsManagerCompat23.noteOp(context, op, uid, packageName);
}
@Override
public int noteProxyOp(Context context, String op, String proxiedPackageName) {
return AppOpsManagerCompat23.noteProxyOp(context, op, proxiedPackageName);
}
}
我們發現
AppOpsManagerCompat
在23以下的實作類中,一些方法直接傳回指定值并不會進行權限檢測。
這就尴尬了,在低版本
AppOpsManagerCompat
失效,而且
AppOpsManager
方法又是@hide無法直接使用,那隻能用反射去實作了:
/**
* 通過反射調用 AppOpsManager#noteOp
*
* @param context
* @param op
* @return
*/
public static boolean noteOp(Context context, int op) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
if (appOpsManager != null) {
try {
Method method = AppOpsManager.class.getDeclaredMethod("noteOp", Integer.TYPE, Integer.TYPE,
String.class);
int noteOp = (int) method.invoke(appOpsManager, op, Process.myUid(), context.getPackageName());
return noteOp == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
總結:
對于權限檢測:
1. Build.VERSION.SDK_INT >= 23時,直接使用我們上面的
hasSelfPermissionForXiaomi
方法
2. Build.VERSION.SDK_INT < 23時,使用我們上面的反射方法
noteOp
。
對于權限申請:
1. Build.VERSION.SDK_INT >= 23時,使用上節介紹的标準運作時權限申請方式
2. Build.VERSION.SDK_INT < 23時,沒有辦法申請API,隻能通過引導使用者跳轉權限設定頁進行配置。
特殊權限适配
有幾個權限其行為方式與正常權限及危險權限都不同。
SYSTEM_ALERT_WINDOW
和
WRITE_SETTINGS
特别敏感,是以大多數應用不應該使用它們。
SYSTEM_ALERT_WINDOW
我們接觸的比較多,有些功能像桌面懸浮窗,桌面歌詞等,隻要是通過向WindowManager裡面添加View方式的,都需要通過這個權限來實作。
這裡拿
SYSTEM_ALERT_WINDOW
來講解如何處理這種特殊權限:
// AndroidManifest中添權重限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
// 定義權限申請request code
public static int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE= ;
public void requestPermission() {
// 通過Settings.canDrawOverlays(this)可以檢測是否包含SYSTEM_ALERT_WINDOW權限
if (!Settings.canDrawOverlays(this)) {
// 申請權限,跳轉到系統權限設定頁面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 在onActivityResult中判斷是否授權成功
if (requestCode == ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
// 授權成功
}
}
}
Tips: 懸浮窗實作可以使用
LayoutParams.TYPE_TOAST
替換
LayoutParams.TYPE_SYSTEM_ALERT
,
LayoutParams.TYPE_TOAST
的使用不需要權限。(LayoutParams.TYPE_TOAST限制就是Android 4.4 (API level 18)及以下使用TYPE_TOAST無法接收觸摸事件)
“不依賴Activity”進行權限申請
我們在6.0運作時動态權限适配一節中說到了,目前API進行權限申請都需要依賴Activity,因為系統在權限處理後是需要回調到Activity做處理的。
大部分場景都是可以滿足的,但是對于沒有Activity或者需要在不存在具體Activity進行權限申請的情況,就有點尴尬了。
輸入法就遇到了這種尴尬的情況,輸入法是系統服務,鍵盤區展示出來的其實是個Dialog。
有個這種場景:使用者在輸入法上點選語言輸入,輸入法需要擷取錄音權限才能進一步操作,那如何在這種場景去申請權限呢?
這就是我們這節的重點了,如何“不依賴Activity”進行權限申請,我們的方案如下:
我們這裡的“不依賴Activity”指的是不依賴具體業務Activity,申請權限時啟動一個隻用于權限申請的Activity來完成申請流程。
這裡我結合easypermissions 實作的例子如下:
PermissionsController封裝了權限請求,通過統一API進行權限申請;具體申請交由EasyPermission(可替換任何庫),僅處理對權限申請結果監聽回調的處理
申請權限時僅需調用,并在回調中處理結果即可
PermissionsController.get(mContext).requestPermissions(this, null, RC_RECORD_PERM, Manifest.permission
.RECORD_AUDIO);
// PermissionsController.java
/**
* <pre>
* author : lee
* e-mail : [email protected]
* time : 2017/07/11
* desc : 權限控制類,用于處理權限請求時機控制等
* version: 1.0
* </pre>
*/
public class PermissionsController {
private static final String TAG = "PermissionsTag";
private static PermissionsController sInstance;
private final Context mContext;
// 權限回調監聽
private final SparseArray<PermissionsResultCallback> mRequestIdToCallback = new SparseArray<>();
public PermissionsController(Context context) {
mContext = context.getApplicationContext();
}
@Nonnull
public static synchronized PermissionsController get(@Nonnull Context context) {
if (sInstance == null) {
sInstance = new PermissionsController(context);
}
return sInstance;
}
/**
* 請求權限;封裝了鍵盤和正常請求
*
* @param callback 申請權限回調,不能為空
* @param activity 權限處理activity,鍵盤區傳null,其他非空
* @param permissionsToRequest 需要申請的權限,多個權限可組合同時申請
*/
public synchronized void requestPermissions(@Nonnull PermissionsResultCallback callback,
Activity activity,
int requestId,
String... permissionsToRequest) {
List<String> deniedPermissions = getDeniedPermissions(mContext, permissionsToRequest);
if (deniedPermissions.isEmpty()) {
// 被拒絕的權限為空,直接回調成功
callback.onPermissionsGranted(requestId, deniedPermissions);
return;
}
String[] permissionsArray = deniedPermissions.toArray(
new String[deniedPermissions.size()]);
if (activity != null) {
String rationale = activity.getString(R.string.permission_msg);
requestPermissions(activity, rationale, requestId, permissionsArray);
} else {
mRequestIdToCallback.put(requestId, callback);
PermissionsActivity.run(mContext, requestId, permissionsArray);
}
}
/**
* 正常請求方式,用于在activity中
*
* @param host 權限處理activity
* @param rationale 權限申請解釋文本,僅當使用者拒絕首次後才能彈出
* @param requestCode 權限申請request code
* @param perms 需要申請的權限,多個權限可組合同時申請
*/
public static void requestPermissions(
@NonNull Activity host, @NonNull String rationale,
int requestCode, @NonNull String... perms) {
EasyPermissions.requestPermissions(host, rationale, requestCode, perms);
}
/**
* 申請成功回調
*
* @param requestCode 權限申請request code
* @param perms 授權成功權限
*/
public synchronized void onPermissionsGranted(int requestCode, List<String> perms) {
PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
mRequestIdToCallback.remove(requestCode);
if (permissionsResultCallback != null) {
permissionsResultCallback.onPermissionsGranted(requestCode, perms);
}
}
/**
* 申請失敗回調
*
* @param requestCode 權限申請request code
* @param perms 授權失敗權限
*/
public synchronized void onPermissionsDenied(int requestCode, List<String> perms) {
PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
mRequestIdToCallback.remove(requestCode);
if (permissionsResultCallback != null) {
permissionsResultCallback.onPermissionsDenied(requestCode, perms);
}
}
/**
* 權限申請結果處理,需在activity onRequestPermissionsResult回調中調用
* @param requestCode 權限申請request code
* @param permissions 申請的權限
* @param grantResults 授權結果
* @param receivers 授權結果處理
*/
public static void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults,
@NonNull Object... receivers){
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, receivers);
}
/**
* 存在一些權限被永久的拒絕
*
* @param host
* @param deniedPermissions
* @return
*/
public static boolean somePermissionPermanentlyDenied(@NonNull Activity host,
@NonNull List<String> deniedPermissions) {
return EasyPermissions.somePermissionPermanentlyDenied(host, deniedPermissions);
}
/**
* 展示跳轉設定頁提示窗
*
* @param host
*/
public static void showAppSettingDialog(@NonNull Activity host) {
new AppSettingsDialog.Builder(host).build().show();
}
/**
* 是否已擷取目前權限
*
* @param context
* @param perms
* @return
*/
public static boolean hasPermissions(Context context, @NonNull String... perms) {
return EasyPermissions.hasPermissions(context, perms);
}
/**
* 擷取未授權權限
*
* @param context
* @param permissions
* @return
*/
public static List<String> getDeniedPermissions(Context context, String... permissions) {
final List<String> deniedPermissions = new ArrayList<String>();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permission);
}
}
return deniedPermissions;
}
/**
* 權限回調接口,直接繼承 EasyPermissions.PermissionCallbacks
*/
public interface PermissionsResultCallback extends EasyPermissions.PermissionCallbacks{
}
}
PermissionsActivity 統一處理權限申請Activity,因為本身隻是個載體,樣式透明并不設定任何ContentView。
// AndroidManifest.xml
<!-- 申請權限activity -->
<activity android:name=".permission.PermissionsActivity"
android:theme="@style/permissionTheme"
android:taskAffinity=""
android:configChanges="orientation|screenSize|keyboardHidden"/>
// styles.xml
<style name="permissionTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
<item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
</style>
// PermissionsActivity.java
/**
* <pre>
* author : lee
* e-mail : [email protected]
* time : 2017/07/11
* desc : 請求權限的Activity
* version: 1.0
* </pre>
*/
public class PermissionsActivity extends Activity implements PermissionsController.PermissionsResultCallback {
private static final String TAG = "PermissionsActivity";
// 需要請求的權限
public static final String EXTRA_PERMISSION_REQUESTED_PERMISSIONS = "requested_permissions";
// 請求權限的requestCode
public static final String EXTRA_PERMISSION_REQUEST_CODE = "request_code";
// 預設無效requestCode
private static final int INVALID_REQUEST_CODE = -;
private int mPendingRequestCode = INVALID_REQUEST_CODE;
/**
* 啟動一個權限Activity,并立即請求權限
*
* @param context
* @param requestCode
* @param permissionStrings
*/
public static void run(
@NonNull Context context, int requestCode, @NonNull String... permissionStrings) {
Intent intent = new Intent(context.getApplicationContext(), PermissionsActivity.class);
intent.putExtra(EXTRA_PERMISSION_REQUESTED_PERMISSIONS, permissionStrings);
intent.putExtra(EXTRA_PERMISSION_REQUEST_CODE, requestCode);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPendingRequestCode = (savedInstanceState != null)
? savedInstanceState.getInt(EXTRA_PERMISSION_REQUEST_CODE, INVALID_REQUEST_CODE)
: INVALID_REQUEST_CODE;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_PERMISSION_REQUEST_CODE, mPendingRequestCode);
}
@Override
protected void onResume() {
super.onResume();
if (mPendingRequestCode == INVALID_REQUEST_CODE) {
final Bundle extras = getIntent().getExtras();
final String[] permissionsToRequest =
extras.getStringArray(EXTRA_PERMISSION_REQUESTED_PERMISSIONS);
mPendingRequestCode = extras.getInt(EXTRA_PERMISSION_REQUEST_CODE);
PermissionsController.requestPermissions(this, getString(R.string.permission_msg), mPendingRequestCode,
permissionsToRequest);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[]
grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionsController.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
mPendingRequestCode = INVALID_REQUEST_CODE;
}
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
PermissionsController.get(this).onPermissionsGranted(requestCode, perms);
finish();
}
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
PermissionsController.get(this).onPermissionsDenied(requestCode, perms);
if (PermissionsController.somePermissionPermanentlyDenied(this, perms)) {
if (BuildConfig.DEBUG) {
Log.d("PermissionsTag", "存在一些權限被永久的拒絕");
}
PermissionsController.showAppSettingDialog(this);
}
finish();
}
}
以上代碼還存在優化空間,但是思路就是這樣。
Android8.0權限适配
我們之前在Android6.0權限申請時介紹過開發者申請危險權限時,隻要獲得過屬于同一組的其他權限的授權,即可立即獲得系統對該權限的授權,無需重複申請這一特性。
但是Android8.0對此行為發生了變化,具體表現為:系統隻會授予應用明确請求的權限。然而一旦使用者為應用授予某個權限,則所有後續對該權限組中權限的請求都将被自動準許,不會顯示彈框。
差別:
1. Android6.0在申請權限成功後,屬于同一組的其他權限會自動授權,無需申請。
2. Android8.0在申請權限成功後,在使用同一組權限時還是需要進行申請操作,隻是申請會被自動授權。不申請則不會獲得授權。
是以為了相容8.0的變化,開發者在申請權限時最好一同申請權限組内其他權限,做好權限檢查。更多内容可參考:Android8.0運作時權限政策變化和适配方案
參考
Android權限機制與适配經驗
Android 開發者必知必會的權限管理知識
Android8.0運作時權限政策變化和适配方案