關於運行時的權限不用多說,這個概念已經好久,近期工信部在強推TargetSDK26,我這邊作了一些適配工做,其中有一項就是運行時權限,今天將對運行時權限提供一個更優雅的解決方案,若是你還不瞭解運行時權限,請移步:Android運行時權限淺談html
首先咱們項目中可能會有這麼一個方法:java
/** * 撥打指定電話 */
public static void makeCall(Context context, String phoneNumber) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
複製代碼
那麼在適配動態權限之前,在咱們任意用到打電話的業務頁面咱們可能就是這麼用:android
public void makeCall() {
Utils.makeCall(BeforeActivity.this, "10086");
}
複製代碼
因而乎,某一天,咱們應用要適配targetSdk 26,首先咱們要適配的就是動態權限,因此下面的代碼就會變成這樣:git
public void makeCall() {
//6.0如下 直接便可撥打
if (android.os.Build.VERSION.SDK_INT < M) {
Utils.makeCall(BeforeActivity.this, "10086");
} else {
//6.0以上
if (ContextCompat.checkSelfPermission(BeforeActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(BeforeActivity.this, new String[]{Manifest.permission.CALL_PHONE},
REQUEST_CODE_CALL);
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_CALL) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(BeforeActivity.this, "本次撥打電話受權失敗,請手動去設置頁打開權限,或者重試受權權限", Toast.LENGTH_SHORT).show();
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
複製代碼
以上就是撥打電話功能新老權限版本的基本實現(還不包括shouldShowRequestPermissionRationale的部分)。 目前也有一些知名的開源庫,如PermissionsDispatcher,RXPermission等。雖然也能實現咱們的功能,但不管本身適配仍是現有開源庫方案大致上都會或多或少有如下幾個問題:github
基於第一個業務繁瑣的問題,不少應用選擇適配權限的時候,把所用到的敏感權限放在一個特定的頁面去申請,好比歡迎頁(某知名音樂播放器等),若是受權不成功,則會直接沒法進入應用,這樣雖然省事,可是用戶體驗很差,我在應用一打開,提示須要電話權限,用戶會很疑惑。這樣其實就違背了「運行時受權」的初衷,谷歌但願咱們在真正調用的該功能的時候去請求,這樣權限請求和用戶的目的是一致的,也更容易授予權限成功。數組
那麼能不能作到以下幾個點呢?app
帶着上述幾個問題,咱們今天的主角:SoulPermission應運而生。ide
當使用了SoulPermission之後,最直觀上看,咱們上面的代碼就變成了這樣:源碼分析
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
Toast.makeText(AfterActivity.this, "本次撥打電話受權失敗,請手動去設置頁打開權限,或者重試受權權限", Toast.LENGTH_SHORT).show();
}
});
}
複製代碼
若是我以在Android手機上要作一件事(doSomeThing),那麼我最終能夠有兩個結果:gradle
基於上述兩種結果,那麼SoulPermission的大體工做流程以下:
從開始到結束展現了咱們上述打電話的流程,A即直接撥打,B即toast提示用戶,沒法繼續後續操做,綠色部分流程便可選部分,即對shouldShowRequestPermissionRationale的處理,那麼完整權限流程下來,咱們撥打電話的代碼就是這麼寫:
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
//綠色框中的流程
//用戶第一次拒絕了權限且沒有勾選"再也不提示"的狀況下這個值爲true,此時告訴用戶爲何須要這個權限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(AfterActivity.this)
.setTitle("提示")
.setMessage("若是你拒絕了權限,你將沒法撥打電話,請點擊授予權限")
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用戶肯定之後,從新執行請求原始流程
makeCall();
}
}).create().show();
} else {
Toast.makeText(AfterActivity.this, "本次撥打電話受權失敗,請手動去設置頁打開權限,或者重試受權權限", Toast.LENGTH_SHORT).show();
}
}
});
}
複製代碼
上述即是其在知足運行時權限下的完整工做流程。那麼關於版本兼容呢? 針對部分手機6.0如下手機,SoulPermission也作了兼容,能夠經過AppOps 檢查權限,內部將權限名稱作了相應的映射,它的大致流程就是下圖: (這個檢查結果不必定準確,可是即便不許確,也默認成功(A),保證咱們回調能往下走,不會阻塞流程,有些在6.0如下本身實現了權限系統的手機(如vivo,魅族)等也是走此A的回調,最終會走到它們本身的權限申請流程)
基於對於代碼中對新老系統版本作了控制,而在權限拒絕裏面不少處理也是又能夠提取的部分,咱們能夠把回調再次封裝一下,進一步減小重複代碼:
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener {
private String rationaleMessage;
private Runnable retryRunnable;
/** * @param rationaleMessage 當用戶首次拒絕彈框時候,根據權限不一樣給用戶不一樣的文案解釋 * @param retryRunnable 用戶點從新受權的runnable 即從新執行原方法 */
public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) {
this.rationaleMessage = rationaleMessage;
this.retryRunnable = retryRunnable;
}
@Override
public void onPermissionDenied(Permission permission) {
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
//綠色框中的流程
//用戶第一次拒絕了權限、而且沒有勾選"再也不提示"這個值爲true,此時告訴用戶爲何須要這個權限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(rationaleMessage)
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用戶肯定之後,從新執行請求原始流程
retryRunnable.run();
}
}).create().show();
} else {
//此時請求權限會直接報未授予,須要用戶手動去權限設置頁,因此彈框引導用戶跳轉去設置頁
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "異常,請前往設置->權限管理,打開" + permissionDesc + "。")
.setPositiveButton("去設置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去設置頁
SoulPermission.getInstance().goPermissionSettings();
}
}).create().show();
}
}
}
複製代碼
而後咱們在App全部打電話的入口處作一次調用:
/** * 撥打指定電話 */
public static void makeCall(final Context context, final String phoneNumber) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.CALL_PHONE,
new CheckPermissionWithRationaleAdapter("若是你拒絕了權限,你將沒法撥打電話,請點擊授予權限",
new Runnable() {
@Override
public void run() {
//retry
makeCall(context, phoneNumber);
}
}) {
@Override
public void onPermissionOk(Permission permission) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
});
}
複製代碼
那麼這樣下來,在Activity和任何業務頁面的調用就只有一行代碼了:
findViewById(R.id.bt_call).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UtilsWithPermission.makeCall(getActivity(), "10086");
}
});
複製代碼
其中徹底拒絕之後,SoulPermission 提供了跳轉到系統權限設置頁的方法,咱們再來看看效果:
不少時候,其實綠色部分(shouldShowRequestPermissionRationale)其實並不必定必要,反覆的彈框用戶可能會厭煩,大多數狀況,咱們這麼封裝就好:
public abstract class CheckPermissionAdapter implements CheckRequestPermissionListener {
@Override
public void onPermissionDenied(Permission permission) {
//SoulPermission提供棧頂Activity
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "異常,請前往設置->權限管理,打開" + permissionDesc + "。")
.setPositiveButton("去設置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去設置頁
SoulPermission.getInstance().goPermissionSettings();
}
}).create().show();
}
}
複製代碼
咱們再寫一個選擇聯繫人的方法:
/** * 選擇聯繫人 */
public static void chooseContact(final Activity activity, final int requestCode) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.READ_CONTACTS,
new CheckPermissionAdapter() {
@Override
public void onPermissionOk(Permission permission) {
activity.startActivityForResult(new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), requestCode);
}
});
}
複製代碼
在Activity中也是一行解決問題:
findViewById(R.id.bt_choose_contact).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UtilsWithPermission.chooseContact(AfterActivity.this, REQUEST_CODE_CONTACT);
}
});
複製代碼
代碼細節請參考demo,咱們再來看看效果:
適配權限最大的痛點在於:項目業務頁面繁多,若是你想實現「真運行時權限」的話就須要在業務的Activity或者Fragment中去重寫權限請求回調方法,斟酌一番而且在參考了下RxPermission中對權限請求的處理,我決定用一樣的方式—用一個沒有界面的Fragment去完成咱們權限請求的操做,下面貼上部分代碼:
首先定義一個接口,用於封裝權限請求的結果
public interface RequestPermissionListener {
/** * 獲得權限檢查結果 * * @param permissions 封裝權限的數組 */
void onPermissionResult(Permission[] permissions);
}
複製代碼
而後是咱們的Fragment:
public class PermissionSupportFragment extends Fragment implements IPermissionActions {
/** * 內部維護requestCode */
private static final int REQUEST_CODE = 11;
/** * 傳入的回調 */
private RequestPermissionListener listener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//當狀態發生改變,好比設備旋轉時候,Fragment不會被銷燬
setRetainInstance(true);
}
/** * 外部請求的最終調用方法 * @param permissions 權限 * @param listener 回調 */
@TargetApi(M)
@Override
public void requestPermissions(String[] permissions, RequestPermissionListener listener) {
requestPermissions(permissions, REQUEST_CODE);
this.listener = listener;
}
@TargetApi(M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permission[] permissionResults = new Permission[permissions.length];
//拿到受權結果之後對結果作一些封裝
if (requestCode == REQUEST_CODE) {
for (int i = 0; i < permissions.length; ++i) {
Permission permission = new Permission(permissions[i], grantResults[i], this.shouldShowRequestPermissionRationale(permissions[i]));
permissionResults[i] = permission;
}
}
if (listener != null && getActivity() != null && !getActivity().isDestroyed()) {
listener.onPermissionResult(permissionResults);
}
}
}
複製代碼
其中Permission是咱們的權限名稱、授予結果、是否須要給用於一個解釋的包裝類:
public class Permission {
private static final String TAG = Permission.class.getSimpleName();
/** * 權限名稱 */
public String permissionName;
/** * 授予結果 */
public int grantResult;
/** * 是否須要給用戶一個解釋 */
public boolean shouldRationale;
/** * 權限是否已經被授予 */
public boolean isGranted() {
return grantResult == PackageManager.PERMISSION_GRANTED;
}
//。。。
}
複製代碼
至此,咱們已經利用本身實現的一個沒有界面的Fragment封裝了運行時權限相關的請求、RequestCode的維護、以及onPermissionResult的回調、在咱們真正調用的時候代碼是這樣的:
/** * * @param activity 棧頂 Activity * @param permissionsToRequest 待請求的權限 * @param listener 回調 */
private void requestRuntimePermission(final Activity activity, final Permission[] permissionsToRequest, final CheckRequestPermissionsListener listener) {
new PermissionRequester(activity)
.withPermission(permissionsToRequest)
.request(new RequestPermissionListener() {
@Override
public void onPermissionResult(Permission[] permissions) {
List<Permission> refusedListAfterRequest = new LinkedList<>();
for (Permission requestResult : permissions) {
if (!requestResult.isGranted()) {
refusedListAfterRequest.add(requestResult);
}
}
if (refusedListAfterRequest.size() == 0) {
listener.onAllPermissionOk(permissionsToRequest);
} else {
listener.onPermissionDenied(PermissionTools.convert(refusedListAfterRequest));
}
}
});
}
複製代碼
其中PermissionRequester也就是一個簡單的構建者模式,其中包含了對Activity的類型判斷,根據Activity類型去肯定Fragment的實現:若是是FragmentActivity的實例,則使用Support包中的Fragment,不然用默認的Fragment,這樣就兼容了有些應用的項目的基類不是AppComponentActivity(FragmentActivity)的情形,固然,原則上最低支持4.0,即默認Fragment的支持版本。
class PermissionFragmentFactory {
private static final String FRAGMENT_TAG = "permission_fragment_tag";
static IPermissionActions create(Activity activity) {
IPermissionActions action;
if (activity instanceof FragmentActivity) {
FragmentManager supportFragmentManager = ((FragmentActivity) activity).getSupportFragmentManager();
PermissionSupportFragment permissionSupportFragment = (PermissionSupportFragment) supportFragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (null == permissionSupportFragment) {
permissionSupportFragment = new PermissionSupportFragment();
supportFragmentManager.beginTransaction()
.add(permissionSupportFragment, FRAGMENT_TAG)
.commitNowAllowingStateLoss();
}
action = permissionSupportFragment;
} else {
android.app.FragmentManager fragmentManager = activity.getFragmentManager();
PermissionFragment permissionFragment = (PermissionFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (null == permissionFragment) {
permissionFragment = new PermissionFragment();
activity.getFragmentManager().beginTransaction()
.add(permissionFragment, FRAGMENT_TAG)
.commitAllowingStateLoss();
}
action = permissionFragment;
}
return action;
}
}
複製代碼
至此,整個請求鏈已經很像最外層暴露的CheckAndRequestPermission方法了,就差一個Activity了,那麼參數Activity怎麼來呢?咱們繼續想辦法。
固然是使用Application中的ActivityLifecycleCallbacks,使用它的 registerActivityLifecycleCallbacks,感知Activity聲明週期變化,獲取到當前應用棧頂的Activity,這樣咱們就不須要本身手動傳入了。
public class PermissionActivityLifecycle implements Application.ActivityLifecycleCallbacks {
WeakReference<Activity> topActWeakReference;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//原則上只須要onResume,兼容若是在onCreate的時候作權限申請保證此時有Activity對象
topActWeakReference = new WeakReference<>(activity);
}
//.....
@Override
public void onActivityResumed(Activity activity) {
topActWeakReference = new WeakReference<>(activity);
}
//.....
}
複製代碼
註冊它僅僅須要一個Application:
/** * @param context Application */
private void registerLifecycle(Application context) {
if (null != lifecycle) {
context.unregisterActivityLifecycleCallbacks(lifecycle);
}
lifecycle = new PermissionActivityLifecycle();
context.registerActivityLifecycleCallbacks(lifecycle);
}
複製代碼
這樣一來,只要調用了初始化方法registerLifecycle,咱們就能提供提供棧頂Activity了
/** * 獲取棧頂Activity * * @return 當前應用棧頂Activity * @throws InitException 初始化失敗 * @throws ContainerStatusException Activity狀態異常 */
private Activity getContainer() {
// may auto init failed
if (null == lifecycle || null == lifecycle.topActWeakReference) {
throw new InitException();
}
// activity status error
if (null == lifecycle.topActWeakReference.get() || lifecycle.topActWeakReference.get().isFinishing()) {
throw new ContainerStatusException();
}
return lifecycle.topActWeakReference.get();
}
複製代碼
結合起來回到咱們以前申請權限的方法(省略了日誌打印和線程的判斷,若是須要再細看源碼):
private void requestPermissions(final Permissions permissions, final CheckRequestPermissionsListener listener) {
//check container status
final Activity activity;
try {
activity = getContainer();
} catch (Exception e) {
//activity status error do not request
return;
}
//......
//finally request
requestRuntimePermission(activity, permissions.getPermissions(), listener);
}
複製代碼
至此,咱們已經能脫離Activity和Fragment,也無需重寫onPermissionResult了,只須要一個ApplicationContext初始化便可。
可否更簡便一點?
咱們能夠自定義ContentProvider來完成庫的初始化,咱們能夠參考Lifecycle組件的初始化:
//lifeCycle定義的初始化Provider
public class LifecycleRuntimeTrojanProvider extends ContentProvider {
@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}
}
複製代碼
和它的Manifest文件:
<application>
<provider android:name="android.arch.lifecycle.LifecycleRuntimeTrojanProvider" android:authorities="${applicationId}.lifecycle-trojan" android:exported="false" android:multiprocess="true" />
</application>
複製代碼
參照它的實現給咱們提供了一個很好的思路,咱們能夠自定義Provider去初始化一些庫或者其餘的內容,如今咱們寫一個本身的initContentProvider:
public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
//初始化咱們的庫
SoulPermission.getInstance().autoInit((Application) getContext());
return true;
}
//......
}
複製代碼
在庫的AndroidManifest文件中聲明:
<application>
<provider android:authorities="${applicationId}.permission.provider" android:name=".permission.InitProvider" android:multiprocess="true" android:exported="false"/>
</application>
複製代碼
至於爲何這個Context就是Application,咱們能夠參考ActivityThread中的對ContentProvider的初始化:
public void handleInstallProvider(ProviderInfo info) {
//即咱們的應用的Application
installContentProviders(mInitialApplication, Arrays.asList(info));
}
複製代碼
至此,咱們權限申請流程就跟Activity、Fragment、乃至Context都沒有關係了。
雖然咱們完成了對運行時權限的申請流程,可是畢竟只針對6.0以上機型,若是上面流程還想一句話完成的話,那咱們還得兼容老的機型,so,咱們須要作在方法內作一個版本判斷:
首先判斷系統版本
public static boolean isOldPermissionSystem(Context context) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
return android.os.Build.VERSION.SDK_INT < M || targetSdkVersion < M;
}
複製代碼
而後是檢查權限:
6.0以上固然是走系統Api:
class RunTimePermissionChecker implements PermissionChecker {
private String permission;
private Context context;
RunTimePermissionChecker(Context context, String permission) {
this.permission = permission;
this.context = context;
}
@TargetApi(M)
@Override
public boolean check() {
int checkResult = ContextCompat.checkSelfPermission(context, permission);
return checkResult == PackageManager.PERMISSION_GRANTED;
}
}
複製代碼
6.0如下、4.4以上經過AppOps反射獲取(爲了保證一致性,把權限名稱參數在check方法中作了映射,把權限的String參數映射成checkOp的整形參數):
class AppOpsChecker implements PermissionChecker {
private Context context;
private String permission;
AppOpsChecker(Context context, String permission) {
this.context = context;
this.permission = permission;
}
/** * 老的通過反射方式檢查權限狀態 * 結果可能不許確,若是返回false必定未授予 * 按需在裏面添加 * 若是沒匹配上或者異常都默認權限授予 * * @return 檢查結果 */
@Override
public boolean check() {
if (null == permission) {
return true;
}
switch (permission) {
case Manifest.permission.READ_CONTACTS:
return checkOp(4);
case Manifest.permission.WRITE_CONTACTS:
return checkOp(5);
case Manifest.permission.CALL_PHONE:
return checkOp(13);
case Manifest.permission.READ_PHONE_STATE:
return checkOp(51);
case Manifest.permission.CAMERA:
return checkOp(26);
case Manifest.permission.READ_EXTERNAL_STORAGE:
return checkOp(59);
case Manifest.permission.WRITE_EXTERNAL_STORAGE:
return checkOp(60);
case Manifest.permission.ACCESS_FINE_LOCATION:
case Manifest.permission.ACCESS_COARSE_LOCATION:
return checkOp(2);
default:
break;
}
return true;
}
boolean checkOp(int op) {
if (Build.VERSION.SDK_INT < KITKAT) {
return true;
}
try {
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
Method method = AppOpsManager.class.getDeclaredMethod("checkOp", int.class, int.class, String.class);
return 0 == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
複製代碼
和版本判斷起來就是這樣:
public static PermissionChecker create(Context context, String permission) {
if (PermissionTools.isOldPermissionSystem(context)) {
return new AppOpsChecker(context, permission);
} else {
return new RunTimePermissionChecker(context, permission);
}
}
複製代碼
再到咱們最終調用的權限檢測方法:
private boolean checkPermission(Context context, String permission) {
return CheckerFactory.create(context, permission).check();
}
複製代碼
最終咱們權限庫一行代碼從權限檢測、權限請求聯合起來的操做就是這樣:
/** * 多個權限的檢查與申請 * 在敏感操做前,先檢查權限和請求權限,當完成操做後可作後續的事情 * * @param permissions 多個權限的申請 Permissions.build(Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA) * @param listener 請求以後的回調 */
public void checkAndRequestPermissions(@NonNull Permissions permissions, @NonNull final CheckRequestPermissionsListener listener) {
//首先檢查權限
Permission[] checkResult = checkPermissions(permissions.getPermissionsString());
//獲得有多少權限被拒絕了
final Permission[] refusedPermissionList = filterRefusedPermissions(checkResult);
if (refusedPermissionList.length > 0) {
//是否能夠請求運行時權限,即6.0以上
if (canRequestRunTimePermission()) {
//請求權限,並把listener傳下去,也就是咱們一開始看請求流程分析中的那個方法
requestPermissions(Permissions.build(refusedPermissionList), listener);
} else {
//沒法請求權限,本次操做失敗
listener.onPermissionDenied(refusedPermissionList);
}
} else {
//沒有權限被拒絕,認爲全部權限都ok,回調成功
listener.onAllPermissionOk(checkResult);
}
}
複製代碼
至此,咱們的三個主要需求的源碼分析基本完成,若是有啥疑問和細節上的實現,能夠自行閱讀源碼便可。
SoulPermission很好的適配了真運行時權限、除了上述三個個主要功能之外還提供如下功能: