版權聲明:本文為HaiyuKing原創文章,轉載請注明出處!
前言
在6.0以前的系統,都是權限一刀切的處理方式,只要用戶安裝,Manifest申請的權限都會被賦予,並且安裝后權限也撤銷不了。
Android 6.0 采用新的權限模型,只有在需要權限的時候,才告知用戶是否授權;是在runtime時候授權,而不是在原來安裝的時候 ,同時默認情況下每次在運行時打開頁面時候,需要先檢查是否有所需要的權限申請。
判斷是否是需要運行時權限的標記就是targetSDKVersion。
當targetSDKVersion<23的時候,僅在安裝時賦予權限,使用時將不被提醒;
當targetSDKVersion≥23的時候才會使用新的運行時權限規則。
運行時權限未適配可能會導致應用崩潰或者在SD卡中創建目錄和文件不成功。
注意:
在 Android 8.0 之前,如果應用在運行時請求權限並且被授予該權限,系統會錯誤地將屬於同一權限組並且在清單中注冊的其他權限也一起授予應用。
對於針對 Android 8.0 的應用,此行為已被糾正。系統只會授予應用明確請求的權限。然而,一旦用戶為應用授予某個權限,則所有后續對該權限組中權限的請求都將被自動批准。
例如,假設某個應用在其清單中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。應用請求 READ_EXTERNAL_STORAGE,並且用戶授予了該權限。如果該應用針對的是 API 級別 24 或更低級別,系統還會同時授予 WRITE_EXTERNAL_STORAGE,因為該權限也屬於同一 STORAGE 權限組並且也在清單中注冊過。如果該應用針對的是 Android 8.0,則系統此時僅會授予 READ_EXTERNAL_STORAGE;不過,如果該應用后來又請求 WRITE_EXTERNAL_STORAGE,則系統會立即授予該權限,而不會提示用戶。
解決方案:
一、將targetSDKVersion人為地降到小於23,這樣就變成了還是默認使用權限,但是這種並不是Google所推薦使用的。
二、實現APP支持運行時權限
那么,哪些權限屬於運行時權限呢?
權限的分組
Android中有很多權限,但並非所有的權限都是敏感權限,於是6.0系統就對權限進行了分類,一般為下述幾類
- 正常(Normal Protection)權限
- 危險(Dangerous)權限
- 特殊(Particular)權限
- 其他權限(一般很少用到)
正常權限
正常權限具有如下的幾個特點
- 對用戶隱私沒有較大影響或者不會帶來安全問題。
- 安裝后就賦予這些權限,不需要顯示提醒用戶,用戶也不能取消這些權限。
上述的權限基本設計的是關於網絡,藍牙,時區,快捷方式等方面,只要在AndroidManifest.xml指定了這些權限,就會被授予,並且不能撤銷。
注意:直接在AndroidManifest.xml文件中聲明權限即可。
特殊權限
這里講特殊權限提前講一下,因為這個相對來說簡單一些。
特殊權限,顧名思義,就是一些特別敏感的權限,在Android系統中,主要由兩個
- SYSTEM_ALERT_WINDOW(設置懸浮窗,進行一些黑科技)
- WRITE_SETTINGS (修改系統設置)
關於上面兩個特殊權限的授權,做法是使用startActivityForResult啟動授權界面來完成。
注意:關於這兩個特殊權限,一般不建議應用申請。
請求SYSTEM_ALERT_WINDOW
private static final int REQUEST_CODE = 1; private void requestAlertWindowPermission() { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE) { if (Settings.canDrawOverlays(this)) { Log.i(LOGTAG, "onActivityResult granted"); } } }
上述代碼需要注意的是
- 使用Action
Settings.ACTION_MANAGE_OVERLAY_PERMISSION啟動隱式Intent - 使用
"package:" + getPackageName()攜帶App的包名信息 - 使用
Settings.canDrawOverlays方法判斷授權結果
請求WRITE_SETTINGS
private static final int REQUEST_CODE_WRITE_SETTINGS = 2; private void requestWriteSettings() { Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS ); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_WRITE_SETTINGS) { if (Settings.System.canWrite(this)) { Log.i(LOGTAG, "onActivityResult write settings granted" ); } } }
上述代碼需要注意的是
- 使用Action
Settings.ACTION_MANAGE_WRITE_SETTINGS啟動隱式Intent - 使用
"package:" + getPackageName()攜帶App的包名信息 - 使用
Settings.System.canWrite方法檢測授權結果
危險權限
危險權限實際上才是運行時權限主要處理的對象,這些權限可能引起隱私問題或者影響其他程序運行。
Android中的危險權限可以歸為以下幾個分組:
| 權限組 |
權限列表 |
| android.permission-group.CALENDAR |
android.permission.READ_CALENDAR (允許程序讀取用戶的日程信息) |
| android.permission-group.CAMERA |
android.permission.CAMERA (允許訪問攝像頭進行拍照) |
| android.permission-group.CONTACTS |
android.permission.READ_CONTACTS (允許應用訪問聯系人通訊錄信息) android.permission.WRITE_CONTACTS (寫入聯系人,但不可讀取) android.permission.GET_ACCOUNTS (訪問GMail賬戶列表) |
| android.permission-group.LOCATION |
android.permission.ACCESS_COARSE_LOCATION (通過WiFi或移動基站的方式獲取用戶錯略的經緯度信息,定位精度大概誤差在30~1500米) android.permission.ACCESS_FINE_LOCATION (通過GPS芯片接收衛星的定位信息,定位精度達10米以內) |
| android.permission-group.MICROPHONE |
android.permission.RECORD_AUDIO (錄制聲音通過手機或耳機的麥克) |
| android.permission-group.PHONE |
android.permission.READ_PHONE_STATE (訪問電話狀態) android.permission.CALL_PHONE (允許程序從非系統撥號器里輸入電話號碼) android.permission.READ_CALL_LOG (允許應用程序讀取用戶的通話記錄) android.permission.WRITE_CALL_LOG (允許一個程序寫入(但不讀取)用戶的通話記錄資料) com.android.voicemail.permission.ADD_VOICEMAIL (允許應用程序添加語音郵件進入系統) android.permission.USE_SIP (允許程序使用SIP視頻服務) android.permission.PROCESS_OUTGOING_CALLS (允許程序監視,修改或放棄播出電話) |
| android.permission-group.SENSORS |
android.permission.BODY_SENSORS (允許從傳感器,用戶使用來衡量什么是他/她的身體內發生的事情,如心臟速率訪問數據的應用程序) |
| android.permission-group.SMS |
android.permission.SEND_SMS (發送短信) android.permission.RECEIVE_SMS (接收短信) android.permission.READ_SMS (讀取短信內容) android.permission.RECEIVE_WAP_PUSH (接收WAP PUSH信息) android.permission.RECEIVE_MMS (接收彩信) android.permission.READ_CELL_BROADCASTS () |
| android.permission-group.STORAGE |
android.permission.READ_EXTERNAL_STORAGE (允許程序讀取外部存儲,如SD卡讀文件) android.permission.WRITE_EXTERNAL_STORAGE (允許程序寫入外部存儲,如SD卡上寫文件) |
效果圖

代碼分析
- 基於Android Studio開發環境。
- 基於RxPermission開源庫。
- 項目的最低sdk版本號(minSdkVersion)必須>=11。
- 分為以下四種情況分析:
A) 只有一個運行時權限申請的情況
B) 同時請求多個權限(合並結果)的情況
C) 同時請求多個權限(分別獲取結果)的情況
D) 條件觸發獲取權限(結合RxBinding使用)的情況
使用步驟
一、檢查minSdkVersion值
當前項目的app/ build.gradle文件中的minSdkVersion值必須>=11

二、添加依賴開源庫
在當前項目的app/ build.gradle文件中的dependencies{}里面添加以下代碼:
普通情況【這個暫時不會用】
//運行時權限
compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
項目中使用了RxJava2的的情況【建議使用這個--不論項目中是否使用了RxJava2】
//運行時權限
compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
compile 'io.reactivex.rxjava2:rxjava:2.0.2'
如果想要實現條件觸發獲取權限(結合RxBinding使用)的情況,則還需要依賴RxBinding開源庫
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
例如,Demo中的依賴配置如下:

三、在AndroidManifest.xml文件中聲明權限列表
<!-- ======================授權獲取設備ANDROID_ID========================== --> <!-- 訪問電話狀態 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- 允許程序讀取外部存儲文件 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- 允許程序寫入外部存儲,如SD卡上寫文件 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <uses-permission android:name="android.permission.CAMERA" />
四、運行時權限申請代碼
注意:需要import的相關類如下:
import com.jakewharton.rxbinding2.view.RxView; import com.tbruyelle.rxpermissions2.Permission; import com.tbruyelle.rxpermissions2.RxPermissions; import io.reactivex.functions.Action; import io.reactivex.functions.Consumer;
4.1 只有一個運行時權限申請的情況
/**只有一個運行時權限申請的情況*/ private void onePermission(){ RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance rxPermissions.request(Manifest.permission.READ_PHONE_STATE) //權限名稱,多個權限之間逗號分隔開 .subscribe(new Consumer<Boolean>() { @Override public void accept(Boolean granted) throws Exception { Log.e(TAG, "{accept}granted=" + granted);//執行順序——1【多個權限的情況,只有所有的權限均允許的情況下granted==true】 if (granted) { // 在android 6.0之前會默認返回true // 已經獲取權限 Toast.makeText(MainActivity.this, "已經獲取權限", Toast.LENGTH_SHORT).show(); String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根據不同的手機設備返回IMEI,MEID或者ESN碼 Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show(); } else { // 未獲取權限 Toast.makeText(MainActivity.this, "您沒有授權該權限,請在設置中打開授權", Toast.LENGTH_SHORT).show(); } } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { Log.e(TAG,"{accept}");//可能是授權異常的情況下的處理 } }, new Action() { @Override public void run() throws Exception { Log.e(TAG,"{run}");//執行順序——2 } }); }
在Activity的onCreate方法中調用即可:

打印的日志如下:

4.2 同時請求多個權限(合並結果)的情況
/**同時請求多個權限(合並結果)的情況*/ private void MultPermission(){ RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance rxPermissions.request(Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_EXTERNAL_STORAGE)//權限名稱,多個權限之間逗號分隔開 .subscribe(new Consumer<Boolean>() { @Override public void accept(Boolean granted) throws Exception { Log.e(TAG, "{accept}granted=" + granted);//執行順序——1【多個權限的情況,只有所有的權限均允許的情況下granted==true】 if (granted) { // 在android 6.0之前會默認返回true // 已經獲取權限 Toast.makeText(MainActivity.this, "已經獲取權限", Toast.LENGTH_SHORT).show(); } else { // 未獲取權限 Toast.makeText(MainActivity.this, "您沒有授權該權限,請在設置中打開授權", Toast.LENGTH_SHORT).show(); } } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { Log.e(TAG,"{accept}");//可能是授權異常的情況下的處理 } }, new Action() { @Override public void run() throws Exception { Log.e(TAG,"{run}");//執行順序——2 } }); }
在Activity的onCreate方法中調用即可:

打印的日志如下:

4.3 同時請求多個權限(分別獲取結果)的情況
/**同時請求多個權限(分別獲取結果)的情況*/ private void MultPermission2(){ RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance rxPermissions.requestEach(Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_EXTERNAL_STORAGE)//權限名稱,多個權限之間逗號分隔開 .subscribe(new Consumer<Permission>(){ @Override public void accept(Permission permission) throws Exception { Log.e(TAG, "{accept}permission.name=" + permission.name); Log.e(TAG, "{accept}permission.granted=" + permission.granted); if(permission.name.equals(Manifest.permission.READ_PHONE_STATE) && permission.granted){ // 已經獲取權限 Toast.makeText(MainActivity.this, "已經獲取權限", Toast.LENGTH_SHORT).show(); String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根據不同的手機設備返回IMEI,MEID或者ESN碼 Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show(); } } }); }
在Activity的onCreate方法中調用即可:

打印的日志如下:

4.4 條件觸發獲取權限(結合RxBinding使用)的情況
/**條件觸發獲取權限(結合RxBinding使用)的情況*/ private void clickPermission(View view){ RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance RxView.clicks(view) .compose(rxPermissions.ensure(Manifest.permission.CAMERA)) .subscribe(new Consumer<Boolean>() { @Override public void accept(Boolean granted) { Log.e(TAG, "{accept}granted=" + granted);//【多個權限的情況,只有所有的權限均允許的情況下granted==true】 if (granted) { // 在android 6.0之前會默認返回true // 已經獲取權限 Toast.makeText(MainActivity.this, "已經獲取CAMERA權限", Toast.LENGTH_SHORT).show(); } else { // 未獲取權限 Toast.makeText(MainActivity.this, "您沒有授權該權限,請在設置中打開授權", Toast.LENGTH_SHORT).show(); } } }); }
在activity的onCreate方法中調用:

打印的日志如下:

其中,點擊事件對應的控件如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.why.project.runtime.MainActivity"> <Button android:id="@+id/btn_getpermission" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="手動獲取權限" android:layout_centerInParent="true"/> </RelativeLayout>
4.5、跳轉到應用權限設置界面(參考《XXPermissions》)【待完善】
package com.why.project.runtime; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.provider.Settings; /** * Created by HaiyuKing * Used 打開應用權限設置界面 * 參考資料:https://github.com/getActivity/XXPermissions */ public class PermissionSettingPage { private static final String MARK = Build.MANUFACTURER.toLowerCase(); /** * 跳轉到應用權限設置頁面 * * @param context 上下文對象 * @param newTask 是否使用新的任務棧啟動 */ static void start(Context context, boolean newTask) { Intent intent; if (MARK.contains("huawei")) { intent = huawei(context); } else if (MARK.contains("xiaomi")) { intent = xiaomi(context); } else if (MARK.contains("oppo")) { intent = oppo(context); } else if (MARK.contains("vivo")) { intent = vivo(context); } else if (MARK.contains("meizu")) { intent = meizu(context); } else { intent = google(context); } if (newTask) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } try { context.startActivity(intent); } catch (Exception e) { intent = google(context); context.startActivity(intent); } } private static Intent google(Context context) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts("package", context.getPackageName(), null)); return intent; } private static Intent huawei(Context context) { Intent intent = new Intent(); intent.putExtra("packageName", context.getPackageName()); intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity")); if (hasIntent(context, intent)) return intent; intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity")); if (hasIntent(context, intent)) return intent; intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity")); return intent; } private static Intent xiaomi(Context context) { Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); intent.putExtra("extra_pkgname", context.getPackageName()); if (hasIntent(context, intent)) return intent; intent.setPackage("com.miui.securitycenter"); if (hasIntent(context, intent)) return intent; intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); if (hasIntent(context, intent)) return intent; intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity"); return intent; } private static Intent oppo(Context context) { Intent intent = new Intent(); intent.putExtra("packageName", context.getPackageName()); intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity"); if (hasIntent(context, intent)) return intent; intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity"); if (hasIntent(context, intent)) return intent; intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.PermissionAppListActivity"); return intent; } private static Intent vivo(Context context) { Intent intent = new Intent(); intent.setClassName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.FloatWindowManager"); intent.putExtra("packagename", context.getPackageName()); if (hasIntent(context, intent)) return intent; intent.setComponent(new ComponentName("com.iqoo.secure", "com.iqoo.secure.safeguard.SoftPermissionDetailActivity")); return intent; } private static Intent meizu(Context context) { Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); intent.putExtra("packageName", context.getPackageName()); intent.setComponent(new ComponentName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity")); return intent; } private static boolean hasIntent(Context context, Intent intent) { return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; } }
findViewById(R.id.btn_opensetting).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //打開應用權限設置界面 PermissionSettingPage.start(MainActivity.this,false); } });
混淆配置
無
參考資料
Android權限管理之Android 6.0運行時權限及解決辦法
