對於權限,每一個android開發者應該很熟悉了,對於targetSDK大於23的時候須要對某些敏感權限進行動態申請,好比獲取通信錄權限、相機權限、定位權限等。
在android 6.0中也同時添加了權限組的概念,若用戶贊成組內的某一個權限,那麼系統默認app可使用組內的全部權限,無需再次申請。
這裏貼一張權限組的圖片:
java
先介紹一下android 6.0以上動態申請權限的流程,申請權限,用戶能夠點擊拒絕,再次申請的時候能夠選擇再也不提醒。
下面說介紹一下運行時申請權限須要用到的API,代碼示例使用kotlin實現
android
<uses-permission android:name="android.permission.XXX"/>
複製代碼
// (API) int checkSelfPermission (Context context, String permission)
ContextCompat.checkSelfPermission(context, Manifest.permission.XXX) != PackageManager.PERMISSION_GRANTED
複製代碼
// (API) void requestPermissions (Activity activity, String[] permissions, int requestCode)
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_CODE_CALL_PHONE)
複製代碼
// (API) void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
}
複製代碼
// (API) boolean shouldShowRequestPermissionRationale (Activity activity, String permission)
ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)
複製代碼
狀況 | 返回值 |
---|---|
第一次打開App時 | false |
上次彈出權限點擊了禁止(但沒有勾選「下次不在詢問」) | true |
上次選擇禁止並勾選「下次不在詢問 」 | false |
注:若是用戶在過去拒絕了權限請求,並在權限請求系統對話框中選擇了 Don't ask again 選項,此方法將返回 false。若是設備規範禁止應用具備該權限,此方法也會返回 false。
git
咱們作移動端須要直接與用戶交互,須要多考慮如何根用戶交互才能達到最好的體驗。下面我結合google samples中動態申請權限示例android-RuntimePermissions
github.com/googlesampl…
以及動態申請權限框架easypermissions
github.com/googlesampl…
來對交互上作一個總結。
github
首先說明,Android不建議App直接進行撥打電話這種敏感操做,建議跳轉至撥號界面,並將電話號碼傳入撥號界面中,這裏僅做參考案例,下面每中狀況都是用戶從用戶第一次申請權限開始(權限詢問狀態)
api
直接容許權限。
數組
拒絕以後再次申請容許
微信
再也不提醒以後引導至設置界面面
app
話很少說,上代碼。
框架
/** * 建立伴生對象,提供靜態變量 */
companion object {
const val TAG = "MainActivity"
const val REQUEST_CODE_CALL_PHONE = 1
}
...
// 這裏進行調用requestPermmission()進行撥號前的權限請求
...
private fun callPhone() {
val intent = Intent(Intent.ACTION_CALL)
val data = Uri.parse("tel:9898123456789")
intent.data = data
startActivity(intent)
}
/** * 提示用戶申請權限說明 */
@TargetApi(Build.VERSION_CODES.M)
fun showPermissionRationale(rationale: String) {
Snackbar.make(view, rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction("肯定") {
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
}.setDuration(3000)
.show()
}
/** * 用戶點擊撥打電話按鈕,先進行申請權限 */
private fun requestPermmission(context: Context) {
// 判斷是否須要運行時申請權限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 判斷是否須要對用戶進行提醒,用戶點擊過拒絕&&沒有勾選再也不提醒時進行提示
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
// 給用於予以權限解釋, 對於已經拒絕過的狀況,先提示申請理由,再進行申請
showPermissionRationale("須要打開電話權限直接進行撥打電話,方便您的操做")
} else {
// 無需說明理由的狀況下,直接進行申請。如第一次使用該功能(第一次申請權限),用戶拒絕權限並勾選了再也不提醒
// 將引導跳轉設置操做放在請求結果回調中處理
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
}
} else {
// 擁有權限直接進行功能調用
callPhone()
}
}
/** * 權限申請回調 */
@TargetApi(Build.VERSION_CODES.M)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
// 根據requestCode判斷是那個權限請求的回調
if (requestCode == REQUEST_PERMISSION_CODE_CALL_PHONE) {
// 判斷用戶是否贊成了請求
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
callPhone()
} else {
// 未贊成的狀況
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
// 給用於予以權限解釋, 對於已經拒絕過的狀況,先提示申請理由,再進行申請
showPermissionRationale("須要打開電話權限直接進行撥打電話,方便您的操做")
} else {
// 用戶勾選了再也不提醒,引導用戶進入設置界面進行開啓權限
Snackbar.make(view, "須要打開權限才能使用該功能,您也能夠前往設置->應用。。。開啓權限",
Snackbar.LENGTH_INDEFINITE)
.setAction("肯定") {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent,REQUEST_SETTINGS_CODE)
}
.show()
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_SETTINGS_CODE) {
Toast.makeText(this, "再次判斷是否贊成了權限,再進行自定義處理",
Toast.LENGTH_LONG).show()
}
}
}
複製代碼
上面介紹了單一權限的申請,簡單的一個申請代碼量其實已經不小了,對於某一個功能須要多個權限更是須要複雜的邏輯判斷。google給咱們推出了一個權限申請的開源框架,下面圍繞着EasyPermission進行說明。
使用方法不介紹了,看一下demo就能夠了,網上也有不少的文章這裏引用前人的總結。ide
我在使用的時候發現了有這樣一個問題,使用版本是pub.devrel:easypermissions:2.0.0
,在demo中使用多個權限申請的時候贊成一個,拒絕一個,沒有勾選不在提醒。這個時候,第二次申請權限,在提示用戶使用權限時候點擊取消,會彈出跳轉到設置手動開啓的彈框。這個作法是不合適的,用戶並無點擊不在提醒,能夠在app內部引導用戶受權,確定是哪裏的邏輯有問題。先貼圖
@Override
public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
// (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
// This will display a dialog directing them to enable the permission in app settings.
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
new AppSettingsDialog.Builder(this).build().show();
}
}
複製代碼
在判斷EasyPermissions.somePermissionPermanentlyDenied()
的時候判斷出了問題,彈出了dialog(這裏的對話框使用Activity實現的)
這裏我會跟着demo使用的思路,對源碼進行閱讀。建議下載源碼,上面有連接
在點擊兩個權限的按鈕以後調用以下方法
@AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
public void locationAndContactsTask() {
if (hasLocationAndContactsPermissions()) {
// 若是有權限,toast
Toast.makeText(this, "TODO: Location and Contacts things", Toast.LENGTH_LONG).show();
} else {
// 沒有權限,進行申請權限,交由EasyPermission類管理
EasyPermissions.requestPermissions(
this,
getString(R.string.rationale_location_contacts),
RC_LOCATION_CONTACTS_PERM,
LOCATION_AND_CONTACTS);
}
}
複製代碼
按照使用的思路梳理,先無論註解部分。跟進EasyPermissions.requestPermissions
/** * 請求多個權限,若是系統須要就彈出權限說明 * * @param host context * @param rationale 想用戶說明爲何須要這些權限 * @param requestCode 請求碼用於onRequestPermissionsResult回調中肯定是哪一次申請 * @param perms 具體須要的權限 */
public static void requestPermissions( @NonNull Activity host, @NonNull String rationale, int requestCode, @Size(min = 1) @NonNull String... perms) {
requestPermissions(
new PermissionRequest.Builder(host, requestCode, perms)
.setRationale(rationale)
.build());
}
複製代碼
很明顯,調用了內部的requestPermissions()
方法,繼續跟
public static void requestPermissions( @NonNull Fragment host, @NonNull String rationale, int requestCode, @Size(min = 1) @NonNull String... perms) {
requestPermissions(
new PermissionRequest.Builder(host, requestCode, perms)
.setRationale(rationale)
.build());
}
複製代碼
構建者Builder模式建立了一個PermissionRequest.Builder對象,傳入真正的requestPermissions()
方法,跟吧
public static void requestPermissions(PermissionRequest request) {
// 在請求權限以前檢查是否已經包含了這些權限
if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
// 已經存在了權限,給權限狀態數組賦值PERMISSION_GRANTED,並進入請求完成部分。不進行這條處理分支的分析,本身看一下吧
notifyAlreadyHasPermissions(
request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
return;
}
// 經過helper類來輔助調用系統api申請權限
request.getHelper().requestPermissions(
request.getRationale(),
request.getPositiveButtonText(),
request.getNegativeButtonText(),
request.getTheme(),
request.getRequestCode(),
request.getPerms());
}
複製代碼
跟requestPermissions()
方法
public void requestPermissions(@NonNull String rationale, @NonNull String positiveButton, @NonNull String negativeButton, @StyleRes int theme, int requestCode, @NonNull String... perms) {
// 這裏遍歷調用系統api ,shouldShowRequestPermissionRationale,是否須要提示用戶申請說明
if (shouldShowRationale(perms)) {
showRequestPermissionRationale(
rationale, positiveButton, negativeButton, theme, requestCode, perms);
} else {
// 抽象方法,其實就是在不一樣的子類裏調用系統api
// ActivityCompat.requestPermissions(getHost(), perms, requestCode);方法
directRequestPermissions(requestCode, perms);
}
}
複製代碼
到這裏,第一次的請求流程已經結束,與用戶交互,按咱們上面gif的演示,對一個權限容許,一個權限拒絕。
這時候回到Activity中的回調onRequestPermissionsResult
方法中
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// 交給EasyPermissions類進行處理事件
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
複製代碼
跟進去!
public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, @NonNull Object... receivers) {
// 建立兩個list用於收集請求權限的結果
List<String> granted = new ArrayList<>();
List<String> denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
String perm = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(perm);
} else {
denied.add(perm);
}
}
// 遍歷
for (Object object : receivers) {
// 若是有某個權限被贊成了,回調到Activity中的onPermissionsGranted方法
if (!granted.isEmpty()) {
if (object instanceof PermissionCallbacks) {
((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted);
}
}
// 若是有某個權限被拒絕了,回調到Activity中的onPermissionsDenied方法
if (!denied.isEmpty()) {
if (object instanceof PermissionCallbacks) {
((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied);
}
}
// 若是請求的權限都被贊成了,進入咱們被@AfterPermissionGranted註解的方法,這裏對註解的使用不進行詳細分析了。
if (!granted.isEmpty() && denied.isEmpty()) {
runAnnotatedMethods(object, requestCode);
}
}
}
複製代碼
咱們對權限一個容許一個拒絕,因此會回調onPermissionsGranted
和onPermissionsDenied
。在demo中的onPermissionsDenied
方法進行了處理
@Override
public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
// (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
// This will display a dialog directing them to enable the permission in app settings.
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
new AppSettingsDialog.Builder(this).build().show();
}
}
複製代碼
作了一個判斷,`EasyPermissions.somePermissionPermanentlyDenied,這裏回調傳入的是一個list,咱們來繼續分析。跟進去,一直跟!
public static boolean somePermissionPermanentlyDenied(@NonNull Activity host, @NonNull List<String> deniedPermissions) {
return PermissionHelper.newInstance(host)
.somePermissionPermanentlyDenied(deniedPermissions);
}
複製代碼
又進入了helper輔助類
public boolean somePermissionPermanentlyDenied(@NonNull List<String> perms) {
for (String deniedPermission : perms) {
if (permissionPermanentlyDenied(deniedPermission)) {
return true;
}
}
return false;
}
複製代碼
循環遍歷了每一權限。有一個是true就返回true。繼續跟!
public boolean permissionPermanentlyDenied(@NonNull String perms) {
// 返回了shouldShowRequestPermissionRationale的非值,就是系統API shouldShowRequestPermissionRationale的非值
return !shouldShowRequestPermissionRationale(perms);
}
複製代碼
這裏並無過濾掉用戶已經贊成的權限,正常的交互不會進入new AppSettingsDialog.Builder(this).build().show();
,可是在Rationale彈框點擊取消的時候會出問題,咱們看一下關於權限說明的rationale彈框的具體實現。
從demo申請權限requestPermissions
方法中,調用的showRequestPermissionRationale
方法。在ActivityPermissionHelper
類中找到具體的實現
@Override
public void showRequestPermissionRationale(@NonNull String rationale, @NonNull String positiveButton, @NonNull String negativeButton, @StyleRes int theme, int requestCode, @NonNull String... perms) {
FragmentManager fm = getHost().getFragmentManager();
// Check if fragment is already showing
Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
if (fragment instanceof RationaleDialogFragment) {
Log.d(TAG, "Found existing fragment, not showing rationale.");
return;
}
// 建立了一個DialogFragment並顯示出來
RationaleDialogFragment
.newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
.showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
}
複製代碼
查看RationaleDialogFragment
類,裏面代碼很少,找到取消按鈕的實現。
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Rationale dialog should not be cancelable
setCancelable(false);
// 建立listener
RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
RationaleDialogClickListener clickListener =
new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
// 將listener傳入dialog中
return config.createFrameworkDialog(getActivity(), clickListener);
}
複製代碼
查看RationaleDialogClickListener
代碼
@Override
public void onClick(DialogInterface dialog, int which) {
int requestCode = mConfig.requestCode;
if (which == Dialog.BUTTON_POSITIVE) { // 點擊肯定
String[] permissions = mConfig.permissions;
if (mRationaleCallbacks != null) {
mRationaleCallbacks.onRationaleAccepted(requestCode);
}
if (mHost instanceof Fragment) {
PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
} else if (mHost instanceof Activity) {
PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
} else {
throw new RuntimeException("Host must be an Activity or Fragment!");
}
} else { // 點擊取消
if (mRationaleCallbacks != null) {
mRationaleCallbacks.onRationaleDenied(requestCode);
}
// 調用下面方法
notifyPermissionDenied();
}
}
private void notifyPermissionDenied() {
if (mCallbacks != null) {
// 這裏回調了Activity的onPermissionsDenied()方法,傳入兩個權限
// 不一樣與用戶點擊拒絕,用戶點擊拒絕的時候,此處僅傳遞了一個拒絕的權限,而這裏將用於已經容許的權限和拒絕的權限都傳入到裏面去。
mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions));
}
}
複製代碼
接下來在執行somePermissionPermanentlyDenied()
判斷的時候,已經被容許的權限在內部調用系統APIshouldShowRequestPermissionRationale
是否須要說明的時候返回的是false,在easyPermission中被認爲是用戶勾選了再也不提醒,因此致使出了問題。
至此,問題找到了,咱們該如何處理呢?咱們能夠在onPermissionsDenied
方法先對已經擁有的權限作一個篩選,將沒有經過用戶贊成的權限塞入somePermissionPermanentlyDenied
中,便可解決問題。固然,也能夠改內部代碼,從新編譯打包放到工程內。
既然代碼都分析到這裏了,就繼續說說EasyPermissions中設計比較巧妙的點吧。若是細心看代碼,會發如今工程裏rationale的彈框是用DialogFragment
實現的,而AppsettingDialog是在AppSettingsDialogHolderActivity
(一個空的Activity)上經過AppSettingsDialog
類中內部完成的AlertDialog的建立和顯示(AppSettingsDialog並非一個dialog,只是一個輔助類)。
public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {
...
}
複製代碼
public class AppSettingsDialog implements Parcelable {
...
}
複製代碼
public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
...
}
複製代碼
真正的去往設置的dialog是在AppSettingsDialog中建立的
AlertDialog showDialog(DialogInterface.OnClickListener positiveListener, DialogInterface.OnClickListener negativeListener) {
AlertDialog.Builder builder;
if (mThemeResId > 0) {
builder = new AlertDialog.Builder(mContext, mThemeResId);
} else {
builder = new AlertDialog.Builder(mContext);
}
return builder
.setCancelable(false)
.setTitle(mTitle)
.setMessage(mRationale)
.setPositiveButton(mPositiveButtonText, positiveListener)
.setNegativeButton(mNegativeButtonText, negativeListener)
.show();
}
複製代碼
爲何要建立一個單獨的Activity來承載dialog呢?個人理解是這樣來處理,能夠統一了咱們本身工程中onActivityResult
方法,在跳轉設置的dialog上不管點擊肯定和取消,都會涉及到Activity的跳轉,都會回調到onActivityResult ()
方法,執行統一的用戶給予權限或拒絕權限的處理。
參考google samples,我的認爲最友好的申請權限流程應該是