隨着Android 6.0發佈以及普及,咱們開發者所要應對的主要就是新版本SDK帶來的一些變化,首先關注的就是權限機制的變化。html
對於6.0如下的權限及在安裝的時候,根據權限聲明產生一個權限列表,用戶只有在贊成以後才能完成app的安裝,形成了咱們想要使用某個app,就要默默忍受其一些沒必要要的權限(好比是個app都要訪問通信錄、短信等)。而在6.0之後,咱們能夠直接安裝,當app須要咱們授予不恰當的權限的時候,咱們能夠予以拒絕(好比:單機的象棋對戰,請求訪問任何權限,我都是不一樣意的)。固然你也能夠在設置界面對每一個app的權限進行查看,以及對單個權限進行受權或者解除受權。java
新的權限機制更好的保護了用戶的隱私,Google將權限分爲兩類,一類是Normal Permissions,這類權限通常不涉及用戶隱私,是不須要用戶進行受權的,好比手機震動、訪問網絡等;另外一類是Dangerous Permission,通常是涉及到用戶隱私的,須要用戶進行受權,好比讀取sdcard、訪問通信錄等。android
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_INSTALL_PACKAGES SET_ALARM SET_TIME_ZONE SET_WALLPAPER SET_WALLPAPER_HINTS TRANSMIT_IR UNINSTALL_SHORTCUT USE_FINGERPRINT VIBRATE WAKE_LOCK WRITE_SYNC_SETTINGS
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.READ_CALL_LOG permission:android.permission.READ_PHONE_STATE permission:android.permission.CALL_PHONE permission:android.permission.WRITE_CALL_LOG permission:android.permission.USE_SIP permission:android.permission.PROCESS_OUTGOING_CALLS permission:com.android.voicemail.permission.ADD_VOICEMAIL 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: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
能夠經過adb shell pm list permissions -d -g
進行查看。git
看到上面的dangerous permissions,會發現一個問題,好像危險權限都是一組一組的,恩,沒錯,的確是這樣的,github
那麼有個問題:分組對咱們的權限機制有什麼影響嗎?shell
的確是有影響的,若是app運行在Android 6.x的機器上,對於受權機制是這樣的。若是你申請某個危險的權限,假設你的app早已被用戶受權了同一組的某個危險權限,那麼系統會當即受權,而不須要用戶去點擊受權。好比你的app對READ_CONTACTS
已經受權了,當你的app申請WRITE_CONTACTS
時,系統會直接受權經過。此外,對於申請時彈出的dialog上面的文本說明也是對整個權限組的說明,而不是單個權限(ps:這個dialog是不能進行定製的)。數組
不過須要注意的是,不要對權限組過多的依賴,儘量對每一個危險權限都進行正常流程的申請,由於在後期的版本中這個權限組可能會產生變化。網絡
好在運行時相關的API也比較簡單,因此適配起來並不會很是痛苦。app
API的講解就跟着申請權限步驟一塊兒了:框架
在AndroidManifest文件中添加須要的權限。
這個步驟和咱們以前的開發並無什麼變化,試圖去申請一個沒有聲明的權限可能會致使程序崩潰。
檢查權限
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { }else{ // }
這裏涉及到一個API,ContextCompat.checkSelfPermission
,主要用於檢測某個權限是否已經被授予,方法返回值爲PackageManager.PERMISSION_DENIED
或者PackageManager.PERMISSION_GRANTED
。當返回DENIED就須要進行申請受權了。
申請受權
ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS);
該方法是異步的,第一個參數是Context;第二個參數是須要申請的權限的字符串數組;第三個參數爲requestCode,主要用於回調的時候檢測。能夠從方法名requestPermissions
以及第二個參數看出,是支持一次性申請多個權限的,系統會經過對話框逐一詢問用戶是否受權。
處理權限申請回調
@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; } } }
ok,對於權限的申請結果,首先驗證requestCode定位到你的申請,而後驗證grantResults對應於申請的結果,這裏的數組對應於申請時的第二個權限字符串數組。若是你同時申請兩個權限,那麼grantResults的length就爲2,分別記錄你兩個權限的申請結果。若是申請成功,就能夠作你的事情了~
固然,到此咱們的權限申請的不走,基本介紹就如上述。不過還有個API值得提一下:
// 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. }
這個API主要用於給用戶一個申請權限的解釋,該方法只有在用戶在上一次已經拒絕過你的這個權限申請。也就是說,用戶已經拒絕一次了,你又彈個受權框,你須要給用戶一個解釋,爲何要受權,則使用該方法。
那麼將上述幾個步驟結合到一塊兒就是:
// 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. } }
這裏寫一個簡單的例子,針對於運行時權限。相信你們在最開始接觸Android的時候,都利用Intent實驗了打電話、發短信等功能。
咱們看看直接撥打電話在Android 6.x的設備上權限須要如何處理。
固然代碼很是簡單:
package com.zhy.android160217; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private static final int MY_PERMISSIONS_REQUEST_CALL_PHONE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void testCall(View view) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, MY_PERMISSIONS_REQUEST_CALL_PHONE); } else { callPhone(); } } public void callPhone() { Intent intent = new Intent(Intent.ACTION_CALL); Uri data = Uri.parse("tel:" + "10086"); intent.setData(data); startActivity(intent); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == MY_PERMISSIONS_REQUEST_CALL_PHONE) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { callPhone(); } else { // Permission Denied Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show(); } return; } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } }
在Android 6.x上運行是,點擊testCall,即會彈出受權窗口,如何你Allow則直接撥打電話,若是Denied則Toast彈出」Permission Denied」。
例子很簡單,不過須要注意的是,對於Intent這種方式,不少狀況下是不須要
受權的甚至權限都不須要的,好比:你是到撥號界面而不是直接撥打電話,就不須要去申請權限;打開系統圖庫去選擇照片;調用系統相機app去牌照等。更多請參考Consider Using an Intent
.
固然,上例也說明了並不是全部的經過Intent的方式都不須要申請權限。通常狀況下,你是經過Intent打開另外一個app,讓用戶經過該app去作一些事情,你只關注結果(onActivityResult),那麼權限是不須要你處理的,而是由打開的app去處理。
雖然權限處理並不複雜,可是須要編寫不少重複的代碼,因此目前也有不少庫對用法進行了封裝,你們能夠在github首頁搜索:android permission
,對比了幾個庫的使用方式,發現https://github.com/lovedise/PermissionGen這個庫據我所見相比較而言使用算是比較簡單的,那麼就以這個庫的源碼爲基礎來說解,你們有興趣能夠多看幾個庫的源碼。
封裝的代碼很簡單,正如你們的對上面的權限代碼所見,沒有特別複雜的地方。
對於申請權限的代碼,本來的編寫爲:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, MY_PERMISSIONS_REQUEST_CALL_PHONE); } else { callPhone(); }
你們能夠看到,對於其餘的權限,其實申請的邏輯是相似的;惟一不一樣的確定就是參數,那麼看上面的代碼,咱們須要3個參數:Activity|Fragment
、權限字符串數組
、int型申請碼
。
也就是說,咱們只須要寫個方法,接受這幾個參數便可,而後邏輯是統一的。
public static void needPermission(Fragment fragment, int requestCode, String[] permissions) { requestPermissions(fragment, requestCode, permissions); } @TargetApi(value = Build.VERSION_CODES.M) private static void requestPermissions(Object object, int requestCode, String[] permissions) { if (!Utils.isOverMarshmallow()) { doExecuteSuccess(object, requestCode); return; } List<String> deniedPermissions = Utils.findDeniedPermissions(getActivity(object), permissions); if (deniedPermissions.size() > 0) { if (object instanceof Activity) { ((Activity) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); } else if (object instanceof Fragment) { ((Fragment) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); } else { throw new IllegalArgumentException(object.getClass().getName() + " is not supported"); } } else { doExecuteSuccess(object, requestCode); } }
Utils.findDeniedPermissions
其實就是check沒有受權的權限。
#Utils @TargetApi(value = Build.VERSION_CODES.M) public static List<String> findDeniedPermissions(Activity activity, String... permission) { List<String> denyPermissions = new ArrayList<>(); for (String value : permission) { if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED) { denyPermissions.add(value); } } return denyPermissions; }
那麼上述的邏輯就很清晰了,須要的3種參數傳入,先去除已經申請的權限,而後開始申請權限。
ok,我相信上面代碼,你們掃一眼就能夠了解了。
對於回調,由於要根據是否受權去執行不一樣的事情,因此不少框架也須要些一連串的代碼,或者和前面的申請代碼耦合。不過這個框架仍是比較方便的,也是我選擇它來說解的緣由。
回調主要作的事情:
對於第一條邏輯都同樣;對於第二條,由於涉及到兩個分支,每一個分支執行不一樣的方法。
對於第二條,不少框架選擇將兩個分支的方法在申請權限的時候進行註冊,而後在回調中根據requestCode進行匹配執行,不過這樣須要在針對每次申請進行對象管理。
不過這個框架採起了一種頗有意思的作法,它利用註解去肯定須要執行的方法,存在兩個註解:
@PermissionSuccess(requestCode = 100) @PermissionFail(requestCode = 100)
利用反射根據受權狀況+requestCode便可找到註解標註的方法,而後直接執行便可。
大體的代碼爲:
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { PermissionGen.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } private static void requestResult(Object obj, int requestCode, String[] permissions, int[] grantResults) { List<String> deniedPermissions = new ArrayList<>(); for (int i = 0; i < grantResults.length; i++) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { deniedPermissions.add(permissions[i]); } } if (deniedPermissions.size() > 0) { doExecuteFail(obj, requestCode); } else { doExecuteSuccess(obj, requestCode); } }
首先根據grantResults進行判斷成功仍是失敗,對於成功則:
private static void doExecuteSuccess(Object activity, int requestCode) { Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(), PermissionSuccess.class, requestCode); executeMethod(activity, executeMethod); } #Utils public static <A extends Annotation> Method findMethodWithRequestCode(Class clazz, Class<A> annotation, int requestCode) { for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(annotation)) { if (isEqualRequestCodeFromAnntation(method, annotation, requestCode)) { return method; } } } return null; }
根據註解和requestCode找到方法,而後反射執行便可。失敗的邏輯相似,不貼代碼了。
ok,到此咱們的運行時權限相對於早起版本的變化、特色、以及如何處理和封裝都介紹完了。
不過對於上面講解的庫,確定有人會說:運行時反射會影響效率,沒錯,不過我已經在上述代碼的基礎上將運行時註解改爲Annotation Processor的方式了,即編譯時註解,這樣的話,就不存在反射損失效率的問題了。原本準備fork修改,而後PR,結果寫完,改動太大,估計PR是不可能經過了,因此另起項目了,也方便後面的作一些擴展和維護。
詳見庫:https://github.com/hongyangAndroid/MPermissions.
對外的接口和PermissionGen基本一致,由於申請只須要三個參數,拋棄了使用本來類庫的單例的方式,直接一個幾個靜態方法,簡單整潔暴力。
貼一個用法:
public class MainActivity extends AppCompatActivity { private Button mBtnSdcard; private static final int REQUECT_CODE_SDCARD = 2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtnSdcard = (Button) findViewById(R.id.id_btn_sdcard); mBtnSdcard.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MPermissions.requestPermissions(MainActivity.this, REQUECT_CODE_SDCARD, Manifest.permission.WRITE_EXTERNAL_STORAGE); } }); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { MPermissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @PermissionGrant(REQUECT_CODE_SDCARD) public void requestSdcardSuccess() { Toast.makeText(this, "GRANT ACCESS SDCARD!", Toast.LENGTH_SHORT).show(); } @PermissionDenied(REQUECT_CODE_SDCARD) public void requestSdcardFailed() { Toast.makeText(this, "DENY ACCESS SDCARD!", Toast.LENGTH_SHORT).show(); } }
是否是簡單明瞭~~對於onRequestPermissionsResult全部的Activity都是一致的,因此能夠放到BaseActivity中去。此外,在Fragment中使用的方式一致,詳見demo。
詳見庫:https://github.com/hongyangAndroid/MPermissions.
至於爲何不直接介紹MPermission的源碼,由於主要涉及到Annotation Processor,因此以這種方式引出,後面考慮單篇博文介紹下我目前所會的編譯時註解的相關作法以及API的使用。