最近在項目上因為6.0運行時權限吃了虧,發現之前對運行時權限的理解不足,決定回爐重造,重新學習一下Android Permission。
進入正題:
Android權限
在Android系統中,權限分為三種:正常權限、危險權限和特殊權限:
- 正常權限:不會直接給用戶隱私權帶來風險。如果您的應用在其清單中列出了正常權限,系統將自動授予該權限。
- 危險權限:涵蓋應用需要涉及用戶隱私信息的數據或資源,或者可能對用戶存儲的數據或其他應用的操作產生影響的區域。例如,能夠讀取用戶的聯系人屬於危險權限。如果應用聲明其需要危險權限,則用戶必須明確向應用授予該權限。
- 特殊權限:主要指
SYSTEM_ALERT_WINDOW
和WRITE_SETTINGS,從官方文檔和實際應用可以知道,這兩個權限必須在清單中聲明,在需要使用的時候發送用戶授權的intent,並在指定的方法中回調授權申請情況。(這里在后面詳細分析)
權限組:
- 當應用請求AndroidManifest中列出的危險權限,而應用目前在權限組中沒有任何權限,則系統會向用戶顯示一個對話框,描述應用要訪問的權限組。對話框不描述該組內的具體權限。例如,如果應用請求
READ_CONTACTS
權限,系統對話框只說明該應用需要訪問設備的聯系信息。如果用戶批准,系統將向應用授予其請求的權限。 - 如果應用請求其清單中列出的危險權限,而應用在同一權限組中已有另一項危險權限,則系統會立即授予該權限,而無需與用戶進行任何交互。例如,如果某應用已經請求並且被授予了
READ_CONTACTS
權限,然后它又請求WRITE_CONTACTS
,系統將立即授予該權限。 - 任何權限都可屬於一個權限組,包括正常權限和應用定義的權限。但權限組僅當權限危險時才影響用戶體驗。可以忽略正常權限的權限組。
- 如果設備運行的是 Android 5.1(API 級別 22)或更低版本,並且應用的
targetSdkVersion
是 22 或更低版本,則系統會在安裝時要求用戶授予權限。
(參考自https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous)
危險權限和權限組-->https://developer.android.com/guide/topics/security/permissions.html#perm-groups
正常權限----------->https://developer.android.com/guide/topics/security/normal-permissions.html
所有Android版本的應用都需要在Androidmanifest.xml中聲明正常權限和危險權限。當然在不同的版本中聲明的影響會有所不同。
聲明權限:
將<uses-permission>作為頂級<manifest>元素的子項加入到Androidmanifest.xml中:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.snazzyapp"> <uses-permission android:name="android.permission.SEND_SMS"/> <application ...> ... </application> </manifest>
根據系統的不同,對於危險權限有不同的處理方式:
- 若設備運行的是Android5.1或更低版本,或者應用的targetSDK為22或更低:如果您在清單中列出了危險權限,則用戶必須在安裝應用時授予此權限;如果他們不授予此權限,系統根本不會安裝應用。
- 若設備運行的是Android6.0或更高版本,或者應用的targetSDK為23或更高:應用必須在清單中列出權限,並且它必須在運行時請求其需要的每項危險權限。用戶可以授予或拒絕每項權限,且即使用戶拒絕權限請求,應用仍可以繼續運行有限的功能。
那么在6.0下該怎么處理危險權限的申請呢?
6.0下的權限處理
可以分為三步:
1.檢查權限
2.請求權限
3.請求回調處理
1.檢查權限
為了兼容舊版本,這里使用ContextCompat的API,詳細的信息看http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0830/3387.html的“用兼容庫使代碼兼容舊版”部分
// Assume thisActivity is the current activity int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);
2.請求權限
如果應用尚未所需的權限,則需要調用requestPermissions()方法請求權限。這個方法是異步執行的,它會調用一個系統標准Android對話框。會在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. } }
對於shouldShowRequsetPermissionRationale()方法的使用解釋:
- 這里需要指出的是當第一次被用戶拒絕之后,在第二次申請權限的時候可以使用shouldShowRequsetPermissionRationale()來幫助判斷用戶是否需要解釋授權,若應用之前請求過此權限但用戶拒絕了請求,該方法將返回true。
對於這個方法的使用場景,官方文檔原話:
您可以采用的一個方法是僅在用戶已拒絕某項權限請求時提供解釋。如果用戶繼續嘗試使用需要某項權限的功能,但繼續拒絕權限請求,則可能表明用戶不理解應用為什么需要此權限才能提供相關功能。對於這種情況,比較好的做法是顯示解釋。
- 在第二次請求權限時系統對話框會有Don't ask again的選項,若用戶選擇了這個選項,那么在下次請求時shouldShowRequsetPermissionRationale()也會返回false。
3.申請結果回調處理
當用戶響應時,系統將調用應用的 onRequestPermissionsResult()
方法,向其傳遞用戶響應。您的應用必須重寫該方法,以了解是否已獲得相應權限。回調會將您傳遞的相同請求代碼傳遞給 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; } // other 'case' lines to check for other // permissions this app might request } }
總結:到這里,需要說的是,當用戶拒絕了權限申請后,應用應該采取適當的操作,例如彈出一個對話框解釋為什么需要該權限。
當用戶指示系統不再要求提供該權限時,這種情況下,應用使用requestPermissions()再次請求權限時,系統都會立即拒絕請求,並回調onRequestPermissionsResult()回調方法,並傳遞PERMISSION_DENIED。這意味着當您調用 requestPermissions()
時,您不能假設已經發生與用戶的任何直接交互。
封裝后的基類代碼:BaseActivity
public class BaseActivity extends AppCompatActivity { private HashMap<Integer,RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>(); private List<Integer> requestCodes=new ArrayList<>(); /** * 申請成功的回調接口 */ public interface RequestPermissionCallback{ void onRequestCallback(); } private String[] findDeniedPermissions(String...permissions){ List<String> permissionList=new ArrayList<>(); for (String perm:permissions){ if (ContextCompat.checkSelfPermission(this,perm)!= PackageManager.PERMISSION_GRANTED){ permissionList.add(perm); } } return permissionList.toArray(new String[permissionList.size()]); } protected void hasPermissions(int requestCode,RequestPermissionCallback callback,String... permissions){ permissionCallbackHashMap.put(requestCode,callback); requestCodes.add(requestCode); String[] deniedPermissions=findDeniedPermissions(permissions); if (deniedPermissions.length>0){ ActivityCompat.requestPermissions(this,permissions,requestCode); }else { callback.onRequestCallback(); } } private boolean verifyPermissions(int [] grantResults){ for (int result:grantResults){ if (result!=PackageManager.PERMISSION_GRANTED){ return false; } } return true; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); boolean grandResult=verifyPermissions(grantResults); for (int code:requestCodes){ if (code==requestCode){ if (grandResult){ permissionCallbackHashMap.get(code).onRequestCallback(); }else { ActivityCompat.requestPermissions(this,permissions, requestCode); } } } } }
BaseFragment:
public abstract class BaseFragment extends Fragment { private HashMap<Integer,BaseActivity.RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>(); private List<Integer> requestCodes=new ArrayList<>(); private Context mContext; @Override public void onAttach(Context context) { super.onAttach(context); this.mContext=mContext; } public Context getmContext() { return mContext; } /** * 申請成功的回調接口 */ public interface RequestPermissionCallback{ void onRequestCallback(); } private String[] findDeniedPermissions(String...permissions){ List<String> permissionList=new ArrayList<>(); for (String perm:permissions){ if (ContextCompat.checkSelfPermission(getmContext(),perm)!= PackageManager.PERMISSION_GRANTED){ permissionList.add(perm); } } return permissionList.toArray(new String[permissionList.size()]); } protected void hasPermissions(int requestCode, BaseActivity.RequestPermissionCallback callback, String... permissions){ permissionCallbackHashMap.put(requestCode,callback); requestCodes.add(requestCode); String[] deniedPermissions=findDeniedPermissions(permissions); if (deniedPermissions.length>0){ /** * 這里要直接用Fragment的requestPermissions(), * 使用ActivityCompat的requestPermissions()會回調到Activity的onRequestPermissionsResult() */ requestPermissions(permissions,requestCode); }else { callback.onRequestCallback(); } } private boolean verifyPermissions(int [] grantResults){ for (int result:grantResults){ if (result!=PackageManager.PERMISSION_GRANTED){ return false; } } return true; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); boolean grandResult=verifyPermissions(grantResults); for (int code:requestCodes){ if (code==requestCode){ if (grandResult){ permissionCallbackHashMap.get(code).onRequestCallback(); }else { requestPermissions(permissions, requestCode); } } } } }
※這里有一個坑:在Fragment中申請權限要用Fragment自帶的requestPermissions,否則會導致結果回調到activity的onRequestPermissionsResult而收不到申請結果。
關於Fragment嵌套的情況看:http://blog.csdn.net/qfanmingyiq/article/details/52561658
如果應用非要申請某個權限的話,可以用以下代碼:(慎用,有引起用戶反感的可能):
public abstract class BaseActivity extends AppCompatActivity { private HashMap<Integer,RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>(); private List<Integer> requestCodes=new ArrayList<>(); /** * 申請成功的回調接口 */ public interface RequestPermissionCallback{ void onRequestCallback(); } private String[] findDeniedPermissions(String...permissions){ List<String> permissionList=new ArrayList<>(); for (String perm:permissions){ if (ContextCompat.checkSelfPermission(this,perm)!= PackageManager.PERMISSION_GRANTED){ permissionList.add(perm); } } return permissionList.toArray(new String[permissionList.size()]); } protected void hasPermissions(int requestCode, RequestPermissionCallback callback, String... permissions){ permissionCallbackHashMap.put(requestCode,callback); requestCodes.add(requestCode); String[] deniedPermissions=findDeniedPermissions(permissions); if (deniedPermissions.length>0){ ActivityCompat.requestPermissions(this,permissions,requestCode); }else { callback.onRequestCallback(); } } private boolean verifyPermissions(int [] grantResults){ for (int result:grantResults){ if (result!=PackageManager.PERMISSION_GRANTED){ return false; } } return true; } /** * 當被用戶拒絕授權並且出現不再提示時(小米在第一拒絕后再次申請就不會出現請求對話框了,不過對於call phone權限不同,這點再看), * shouldShowRequestPermissionRationale也會返回false,若實在必須申請權限時可以使用方法檢測, * 可能會引起用戶的厭惡感,慎用 * @param permissions * @return */ protected boolean verifyShouldShowRequestPermissions(String[] permissions){ for (String permission:permissions){ if (!ActivityCompat.shouldShowRequestPermissionRationale(this,permission)){ return false; } } return true; } /** * 顯示提示信息,與verifyShouldShowRequestPermissions(String[] permissions)搭配使用,提醒用戶需要授權,並打開應用詳細設置頁面 * * @since 2.5.0 * */ protected void showMissingPermissionDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("提示"); builder.setMessage("當前應用缺少必要權限。\\n\\n請點擊\\\"設置\\\"-\\\"權限\\\"-打開所需權限。"); // // 拒絕, 退出應用 // builder.setNegativeButton(R.string.cancel, // listener // ); builder.setPositiveButton(R.string.setting, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startAppSettings(); } }); builder.setCancelable(false); builder.show(); } /** * 啟動應用的設置 * * @since 2.5.0 * */ private void startAppSettings() { Intent intent = new Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); boolean grandResult=verifyPermissions(grantResults); for (int code:requestCodes){ if (code==requestCode){ if (grandResult){ permissionCallbackHashMap.get(code).onRequestCallback(); }else { if (verifyShouldShowRequestPermissions(permissions)){ final int code1=code; final String[] permission=permissions; AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setMessage("定位功能需要定位權限,請授權。") .setPositiveButton("確定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { ActivityCompat.requestPermissions(BaseActivity.this,permission, code1); } }); }else { showMissingPermissionDialog(); } } } } } }
這里需要指出的是,shouldShowRequestPermissionRationale()被放到onRequestPermissionsResult中進行判斷,這樣就能在系統直接返回PERMISSION_DENIED這個回調結果時判斷是否應該彈出解釋窗口或者在用戶選擇了不再提示之后彈出應用設置頁面。
這里有一段關於官方文檔的原話:
在某些情況下,您可能需要幫助用戶了解您的應用為什么需要某項權限。例如,如果用戶啟動一個攝影應用,用戶對應用要求使用相機的權限可能不會感到吃驚,但用戶可能無法理解為什么此應用想要訪問用戶的位置或聯系人。在請求權限之前,不妨為用戶提供一個解釋。請記住,您不需要通過解釋來說服用戶;如果您提供太多解釋,用戶可能發現應用令人失望並將其移除。
所以這個方法要慎用。
代碼地址:https://github.com/liberty2015/PermissionStudy
參考:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0830/3387.html
http://blog.csdn.net/lmj623565791/article/details/50709663
http://blog.csdn.net/qfanmingyiq/article/details/52561658
https://developer.android.com/guide/topics/security/permissions.html
https://developer.android.com/training/permissions/requesting.html