問題來源于最近的一個項目中,原本好好的程序,在一臺Nexus 6(Android6.0)上測試發現所有需要保存圖片到本地的都不行,看報錯:
其實不僅僅是保存圖片這里報這個錯,還有打開相機的時候也會報錯。好了問題就這么開始了!
看到這里都知道是權限的問題所引起的原因,對,確實是因為權限的問題引起的,但是程序明明已經在Manifest中聲明了以上操作需要的權限,而且在以前測試的系統機型中都是沒問題的,為什么!突然間想到了Marshmallow發布的時候提及到的Requesting Permissions at Run Time這東西,對就是他的原因!
Requesting Permissions at Run Time究竟是啥,官方對他是如下介紹滴:
從Android6.0(API >= 23)開始,用戶在APP運行的時候授予其權限而不是像以前安裝的時候就通通授予了(以前授權方式好像沒什么卵用)。由于不在需要在安裝或更新APP的時候授予相關權限,這就簡化了APP的安裝過程。這也提高了用戶對APP功能的控制,比如:用戶可以選擇讓一個Camera APP使用Camera,用戶可以在任何時候在設置面板撤銷這個權限。。。
看完是不是有點像我們在國產ROM中常見到的每個應用運行時權限授予。
系統權限也被分成normal
和dangerous
兩類:
- Normal類的權限不會直接涉及到用戶隱私風險。如果APP在Manifest文件中聲明了Normal類的權限,系統會自動授予這些權限。
-
Dangerous類的權限可能會讓APP涉及到用戶機密的數據。如果APP在Manifest文件中聲明了Normal類的權限,系統會自動授予這些權限。如果在Manifest文件中添加了Dangerous類的權限,用戶必須明確的授予對應的權限后APP才具有這些權限。
關于哪些權限屬于Normal類,哪些屬于Dangerous類,如下圖:
normal-dangerous-permissions.png
更詳細請看normal-dangerous-permissions文檔。
現在我們知道了在APP中normal和dangerous類的權限都需要Manifest文件中聲明,但是在不同的版本系統和target SDK效果是不一樣的,有如下幾點需要注意:
- 系統Android 5.1以下或target SDK 22以下,只要在Manifest文件中聲明了需要的權限,用戶在安裝APP的時候就會授予相應的權限;如果不授予當然APP也無法安裝。
- 如果設備運行在6.0以上并且你的應用的target SDK版本>=23,APP除了需要在Manifest文件中聲明相應的權限之外,還要在APP運行時向用戶進行請求每個dangerous類的權限。用戶可以選擇授予或不授予該權限,即使用戶不授予該權限APP也可以繼續運行,但是相關的需要權限的操作是沒法進行的。
使用系統提供的API檢查并請求權限
a. 檢查權限
以為用戶可以隨時撤銷對APP的授權,所以在每次準備進行需要dangerous類權限操作的時候,需要檢查APP是否具有對應的權限。使用[ContextCompat.checkSelfPermission()](http://developer.android.com/reference/android/support/v4/content/ContextCompat.html#checkSelfPermission(android.content.Context, java.lang.String))檢查權限,代碼如下:
// 檢查activity是否有寫日程的權限
// Assume thisActivity is the current activity
intpermissionCheck=ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.WRITE_CALENDAR);
上面代碼,如果APP有該權限返回PackageManager.PERMISSION_GRANTED,APP接著可以進行對應操作;如果沒有權限,以上方法返回PERMISSION_DENIED,APP需要明確的向用戶請求授權。
b. 請求權限
如何使用系統的API向用戶請求dangerous類的權限,也很簡單,support-library都基本上幫我們做好了。首先這個權限已經在Manifest文件中聲明(廢話么),然后向用戶請求該權限,同意了你就用,否則想都別想(用戶是上帝)。Google是這么解釋why the app needs permissions的:
在某些情況下,你可能想讓用戶理解為啥你的APP需要這個權限。比如用戶打開了一個拍攝APP(好吧,又是Camera),用戶不會對這個APP需要使用Camera感到奇怪,但是用戶可能不能理解為什么這破APP還需要使用我的位置和聯系人數據(不能忍了)!所以在你請求某個權限之前,你應該考慮提供一段解釋性的話給用戶。請記住你不是要靠這點解釋性的話就讓用戶徹底的明白你為什么需要這個權限,如果你解釋過多,用戶覺得沒卵用直接卸了APP。只有當用戶拒絕了你權限請求之后才是使用那段解釋性的話最好的時候,因為如果用戶一直嘗試使用某個需要權限的功能,但是卻又一直不授予該權限,這就表明用戶真不知道為什么這個功能需要這個這個權限,在這種情況下,你的解釋性的話就派上用場了。
Android提供[shouldShowRequestPermissionRationale()](http://developer.android.com/reference/android/support/v4/app/ActivityCompat.html#shouldShowRequestPermissionRationale(android.app.Activity, java.lang.String))方法求向用戶展示為啥你需要這個權限,當用戶之前已經請求過該權限并且拒絕了授權這個方法返回true。
注意:如果用戶拒絕權限請求的時候選擇了Don’t ask again選項,上面的方法返回false,當然如果設備本身就不允許有這個權限也是返回false。
看看代碼:
// 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,newString[{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.
}
}
其中[requestPermissions()](http://developer.android.com/reference/android/support/v4/app/ActivityCompat.html#requestPermissions(android.app.Activity, java.lang.String[], int))就是請求權限方法,異步方法。看看這個方法需要三個參數:
@param activity The target activity
@param permissions The requested permissions(這里就是你需要申請的權限,在Manifest類中可以找到你需要的權限)
@param requestCode Application specific request code to match with a result reported to onRequestPermissionsResult(int, String[], int[])}(標志這次授權的唯一請求碼,當用戶進行授權操作后在回調方法中可以根據這個標識符進行區分不同的授權操作)
有個缺點就是使用系統請求授權API你不能自定義樣式,請求授權彈出來的是標準的Android Dialog(如下圖,遵循Android標準蠻好的),如果你希望提供一些信息之類的應該在[requestPermissions()](http://developer.android.com/reference/android/support/v4/app/ActivityCompat.html#requestPermissions(android.app.Activity, java.lang.String[], int))之前進行操作。
c. 處理授權請求回調
使用onRequestPermissionsResult(int ,String , int[])
方法處理回調,上面說到了根據[requestPermissions()](http://developer.android.com/reference/android/support/v4/app/ActivityCompat.html#requestPermissions(android.app.Activity, java.lang.String[], int))方法中的requestCode,我們就可以在回調方法中區分授權請求,看代碼:
@Override
publicvoidonRequestPermissionsResult(intrequestCode,Stringpermissions[],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
}
}
最后注意以下兩點:
- 當在Manifest文件中聲明了同一個permission group中的權限后,請求權限時不會列出具體的權限,只是將這個permission group權限進行說明。對于permission group中的所有權限,用戶只需要授權一次,以后里面的所有其他權限都不需要在進行授權(撤銷后等操作除外),系統會自動認為是授權并以PERMISSION_GRANTED為參數調用onRequestPermissionsResult方法,和彈出系統請求授權對話框點擊授權是一樣的效果。雖然這樣,對于每個授權還是需要進行單獨請求授權操作。
-
Requesting Permissions at Run Time是當target API >= 23并且系統為Android 6.0 (API level 23)及以上才有的,如不屬于這種情況APP還是像以前一樣在安裝的時候回提示所有需要的權限并授予這些權限。
看看簡單的demo效果;
compare.png
代碼:
https://gitlab.com/lujun/RuntimePermissionDemo
參考:
[http://developer.android.com/training/permissions/requesting.html#perm-request][1]
[][2]http://stackoverflow.com/questions/23527767/open-failed-eacces-permission-denied
[1]:http://developer.android.com/training/permissions/requesting.html#perm-request
[2]:http://stackoverflow.com/questions/23527767/open-failed-eacces-permission-denied