這也許是Android一句話許可權適配的更優解決方案

GeorgeQin發表於2019-04-25

背景

關於執行時的許可權不用多說,這個概念已經很久,近期工信部在強推TargetSDK26,我這邊做了一些適配工作,其中有一項就是執行時許可權,今天將對執行時許可權提供一個更優雅的解決方案,如果你還不瞭解執行時許可權,請移步:Android執行時許可權淺談

現狀:

以直接呼叫打電話功能為例

首先我們專案中可能會有這麼一個方法:

    /**
     * 撥打指定電話
     */
    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);
    }
複製程式碼

那麼在適配動態許可權以前,在我們任意用到打電話的業務頁面我們可能就是這麼用:

 public void makeCall() {
     Utils.makeCall(BeforeActivity.this, "10086");
    }
複製程式碼

於是乎,某一天,我們應用要適配targetSdk 26,首先我們要適配的就是動態許可權,所以下面的程式碼就會變成這樣:

  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等。雖然也能實現我們的功能,但無論自己適配還是現有開源庫方案大體上都會或多或少有以下幾個問題:

現有許可權庫存在的問題:

  • 每個頁面都要重寫onPermissionResult方法、維護requestCode、或者第三方庫封裝的onPermissionResult方法,如果專案龐大,適配到每個業務點會非常繁瑣。
  • 許可權申請還區分Activity和Fragment,又要分別處理
  • 每個許可權都要寫大量的if else程式碼去做版本判斷,判斷新老機型分別處理

基於第一個業務繁瑣的問題,很多應用選擇適配許可權的時候,把所用到的敏感許可權放在一個特定的頁面去申請,比如歡迎頁(某知名音樂播放器等),如果授權不成功,則會直接無法進入應用,這樣雖然省事,但是使用者體驗不好,我在應用一開啟,提示需要電話許可權,使用者會很疑惑。這樣其實就違背了“執行時授權”的初衷,谷歌希望我們在真正呼叫的該功能的時候去請求,這樣許可權請求和使用者的目的是一致的,也更容易授予許可權成功。

那麼能不能做到如下幾個點呢?

對許可權適配的期望:

  • 基於使用者體驗考慮,我不希望在應用一開啟就向使用者索取一堆授權,異或是跳一個頁面專門去授權、困擾我們寶貴的使用者
  • 不需要去重寫onPermissionResult、甚至不需要Activity和Fragment。
  • 去除版本判斷。無論什麼系統版本的新老手機,都是走一個方法
  • 一行程式碼完成從許可權檢查、請求、到最終完我要做的事情
  • 我不需要在原有專案中改太多程式碼

帶著上述幾個問題,我們今天的主角:SoulPermission應運而生。

當使用了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();
                    }
                });
    }
複製程式碼

SoulPermission:

優勢:

  • 解耦Activity和Fragment、不再需要Context、不再需要onPermissionResult
  • 內部涵蓋版本判斷,一行程式碼解決許可權相關操作,無需在呼叫業務方寫許可權適配程式碼,繼而實現真正呼叫時請求的“真執行時許可權”
  • 接入成本低,零入侵,僅需要在gradle配置一行程式碼

工作流程:

如果我以在Android手機上要做一件事(doSomeThing),那麼我最終可以有兩個結果:

  • A:可以做
  • B:不能做

基於上述兩種結果,那麼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,我們再來看看效果:

在這裡插入圖片描述

主要功能的原始碼分析:

優雅的避掉onPermissionResult:

適配許可權最大的痛點在於:專案業務頁面繁多,如果你想實現“真執行時許可權”的話就需要在業務的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怎麼來呢?我們繼續想辦法。

再捨去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初始化即可。

能否更簡便一點?

最後避掉Application(免初始化):

我們可以自定義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都沒有關係了。

去除if&else、涵蓋版本判斷:

雖然我們完成了對執行時許可權的申請流程,但是畢竟只針對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很好的適配了真執行時許可權、除了上述三個個主要功能以外還提供以下功能:

  • 支援多項許可權同時請求
  • 支援檢查通知許可權
  • 支援系統許可權頁面跳轉
  • 支援debug模式

GitHub以及Sample地址

相關文章