Android優雅地申請動態權限

版權聲明:本文已授權微信公眾號:Android必修課,轉載請申明出處

Android6.0以上的系統中,引入了運行時權限檢查,運行時權限分為正常權限和危險權限,當我們的App調用了需要危險權限的api時,需要向系統申請權限,系統會彈出一個對話框讓用戶感知,只有當用戶授權以后,App才能正常調用api。

關于危險權限的說明,請參閱官方文檔:https://developer.android.google.cn/guide/topics/security/permissions#normal-dangerous

官方權限申請示例:

這里采用googleSamples中的權限申請框架EasyPermissions作為例子:

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks,EasyPermissions.RationaleCallbacks{
    private static final int RC_CAMERA_PERM = 123;
    private static final int RC_LOCATION_CONTACTS_PERM = 124;

    @AfterPermissionGranted(RC_CAMERA_PERM)
    public void cameraTask() {
        EasyPermissions.requestPermissions(
            this,
            getString(R.string.rationale_camera),
            RC_CAMERA_PERM,
            Manifest.permission.CAMERA);
    }

    @AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
    public void locationAndContactsTask() {
        EasyPermissions.requestPermissions(
            this,
            getString(R.string.rationale_location_contacts),
            RC_LOCATION_CONTACTS_PERM,
            LOCATION_AND_CONTACTS);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    @Override
    public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
    }

    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }
}

官方權限申請的例子,代碼量相當多,每個涉及危險權限的地方都得寫這么一堆代碼。


改造

既然官方例子無法滿足我們,那只能自己改造了,首先看看我們最后要實現的效果:

GPermisson.with(this)
    .permisson(new String[] {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA})
    .callback(new PermissionCallback() {
        @Override
        public void onPermissionGranted() {}

        @Override
        public void shouldShowRational(String permisson) {}

        @Override
        public void onPermissonReject(String permisson) {}
    }).request();
  • onPermissionGranted是權限申請通過回調。

  • shouldShowRational是權限被拒絕,但是沒有勾選“不再提醒"。

  • onPermissonReject是權限被拒絕,并且勾選了"不再提醒",即徹底被拒絕

可以看到,相對于官方例子,我們的api簡潔了很多,并且流式調用可以讓邏輯更容易接受。

怎么實現呢?慢慢看

1.編寫權限申請Activity

首先,我們封裝一個透明的Activity,在該Activity中進行權限申請

/*
 * 權限申請回調
 */
public interface PermissionCallback {
    void onPermissionGranted();

    void shouldShowRational(String permisson);

    void onPermissonReject(String permisson);
}


public class PermissionActivity extends Activity {
    public static final String KEY_PERMISSIONS = "permissions";
    private static final int RC_REQUEST_PERMISSION = 100;
    private static PermissionCallback CALLBACK;

    /*
     * 添加一個靜態方法方便使用
     */
    public static void request(Context context, String[] permissions, PermissionCallback callback) {
        CALLBACK = callback;
        Intent intent = new Intent(context, PermissionActivity.class);
        intent.putExtra(KEY_PERMISSIONS, permissions);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        if (!intent.hasExtra(KEY_PERMISSIONS)) {
            return;
        }
        // 當api大于23時,才進行權限申請
        String[] permissions = getIntent().getStringArrayExtra(KEY_PERMISSIONS);
        if (Build.VERSION.SDK_INT >= 23) {
            requestPermissions(permissions, RC_REQUEST_PERMISSION);
        }
    }

    @TargetApi(23)
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode != RC_REQUEST_PERMISSION) {
            return;
        }
        // 處理申請結果
        boolean[] shouldShowRequestPermissionRationale = new boolean[permissions.length];
        for (int i = 0; i < permissions.length; ++i) {
            shouldShowRequestPermissionRationale[i] = shouldShowRequestPermissionRationale(permissions[i]);
        }
        this.onRequestPermissionsResult(permissions, grantResults, shouldShowRequestPermissionRationale);
    }


    @TargetApi(23)
    void onRequestPermissionsResult(String[] permissions, int[] grantResults, boolean[] shouldShowRequestPermissionRationale) {
        int length = permissions.length;
        int granted = 0;
        for (int i = 0; i < length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                if (shouldShowRequestPermissionRationale[i] == true){
                    CALLBACK.shouldShowRational(permissions[i]);
                } else {
                    CALLBACK.onPermissonReject(permissions[i]);
                }
            } else {
                granted++;
            }
        }
        if (granted == length) {
            CALLBACK.onPermissionGranted();
        }
        finish();
    }
}

添加一個透明的主題:

<style name="Translucent">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:backgroundDimEnabled">false</item>
    <item name="android:windowActionBar">false</item>
    <item name="android:windowNoTitle">true</item>
    <item name="windowNoTitle">true</item>
    <item name="windowActionBar">false</item>
</style>

2.封裝一個門面類,提供api調用

public class GPermisson {
    // 權限申請回調
    private PermissionCallback callback;
    // 需要申請的權限
    private String[] permissions;
    private Context context;

    public GPermisson(Context context) {
        this.context = context;
    }

    public static GPermisson with(Context context) {
        GPermisson permisson = new GPermisson(context);
        return permisson;
    }

    public GPermisson permisson(String[] permissons) {
        this.permissions = permissons;
        return this;
    }

    public GPermisson callback(PermissionCallback callback) {
        this.callback = callback;
        return this;
    }

    public void request() {
        if (permissions == null || permissions.length <= 0) {
            return;
        }
        PermissionActivity.request(context, permissions, callback);
    }
}

至此,我們就簡單封裝好了一個權限請求庫,達到上述效果。

等等,這種方式足夠優雅了嗎?

想想,每個涉及權限的地方,我們還是需要寫一段權限請求代碼,還能簡化嗎?

上一篇我們通過AOP封裝了按鈕點擊的優雅實現,這里一樣可以用AOP來簡化我們的權限請求。

我們希望一個注解完成權限申請,例如:

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS})
private void initView() {}

這樣比上面的方法又簡化了很多,但是,有個問題:

大家知道,權限申請是會被拒絕的,甚至是會被勾選上“不再提示”,然后再拒絕。這樣被拒絕后再次申請權限是不會彈框提醒的。因此,我們需要處理:

  • 用戶點擊拒絕,但不勾選“不再提示”,下次請求權限時,系統彈窗依然會出現,而且shouldShowRequestPermissionRationale(permission)為true,意思是,用戶拒絕了你,你應該顯示一段文字或者其他信息,來說服用戶允許你的權限申請。

  • 用戶點擊拒絕,并勾選“不再提示”,下次請求權限時,系統彈窗不會再出現,而且shouldShowRequestPermissionRationale(permission)為false,此時你的權限申請被用戶徹底拒絕,需要跳轉到系統設置頁手動允許權限。

ok,我們知道了@Permission注解里,只有一個權限數組是不夠的,我們還需要有一個rationale信息和被徹底拒絕后讓用戶跳轉到設置頁的信息。

升級

1.定義注解

/** 注意,@Retention需要為RUNTIME,否則運行時時沒有這個注解的 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permission {
    /* Permissions */
    String[] permissions();
    /* Rationales */
    int[] rationales() default {};
    /* Rejects */
    int[] rejects() default {};
}

使用int[]而不使用String[],是因為String[]傳入的字串無法適配多語言。

2.改寫GPermission

public class GPermisson {
    private static PermissionGlobalConfigCallback globalConfigCallback;
    private PermissionCallback callback;
    private String[] permissions;
    private Context context;

    public GPermisson(Context context) {
        this.context = context;
    }

    public static void init(PermissionGlobalConfigCallback callback) {
        globalConfigCallback = callback;
    }

    static PermissionGlobalConfigCallback getGlobalConfigCallback() {
        return globalConfigCallback;
    }

    public static GPermisson with(Context context) {
        GPermisson permisson = new GPermisson(context);
        return permisson;
    }

    public GPermisson permisson(String[] permissons) {
        this.permissions = permissons;
        return this;
    }

    public GPermisson callback(PermissionCallback callback) {
        this.callback = callback;
        return this;
    }

    public void request() {
        if (permissions == null || permissions.length <= 0) {
            return;
        }
        PermissionActivity.request(context, permissions, callback);
    }

    /**
     * 寫一個接口,將申請被拒絕的上述兩種情況交給調用者自行處理,框架內不處理
     */
    public abstract class PermissionGlobalConfigCallback {
        abstract public void shouldShowRational(String permission, int ration);
        abstract public void onPermissonReject(String permission, int reject);
    }
}

3.Aspect切面處理類

@Aspect
public class PermissionAspect {
    @Around("execution(@me.baron.gpermission.Permission * *(..))")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) {
        try {
            // 獲取方法注解
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            Permission annotation = method.getAnnotation(Permission.class);
            // 獲取注解參數,這里我們有3個參數需要獲取
            final String[] permissions = annotation.permissions();
            final int[] rationales = annotation.rationales();
            final int[] rejects = annotation.rejects();
            final List<String> permissionList = Arrays.asList(permissions);

            // 獲取上下文
            Object object = joinPoint.getThis();
            Context context = null;
            if (object instanceof FragmentActivity) {
                context = (FragmentActivity) object;
            } else if (object instanceof Fragment) {
                context = ((Fragment) object).getContext();
            } else if (object instanceof Service) {
                context = (Service) object;
            }

            // 申請權限
            GPermisson.with(context)
                    .permisson(permissions)
                    .callback(new PermissionCallback() {
                        @Override
                        public void onPermissionGranted() {
                            try {
                                // 權限申請通過,執行原方法
                                joinPoint.proceed();
                            } catch (Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        }

                        @Override
                        public void shouldShowRational(String permisson) {
                            // 申請被拒絕,但沒有勾選“不再提醒”,這里我們讓外部自行處理
                            int index = permissionList.indexOf(permisson);
                            int rationale = -1;
                            if (rationales.length > index) {
                                rationale = rationales[index];
                            }
                            GPermisson.getGlobalConfigCallback().shouldShowRational(permisson, rationale);
                        }

                        @Override
                        public void onPermissonReject(String permisson) {
                            // 申請被拒絕,且勾選“不再提醒”,這里我們讓外部自行處理
                            int index = permissionList.indexOf(permisson);
                            int reject = -1;
                            if (rejects.length > index) {
                                reject = rejects[index];
                            }
                            GPermisson.getGlobalConfigCallback().onPermissonReject(permisson, reject);
                        }
                    }).request();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用

1.引入Aspectj依賴,依賴方式見上一篇:

Android優雅地處理按鈕重復點擊

2.設置全局權限請求結果監聽

GPermisson.init(new PermissionGlobalConfigCallback() {
    @Override
    public void shouldShowRational(String permission, int ration) {
        showRationaleDialog(ration);
    }

    @Override
    public void onPermissonReject(String permission, int reject) {
        showRejectDialog(reject);
    }
});

private void showRationaleDialog(int ration) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("權限申請")
            .setMessage(getString(ration))
            .show();
}

private void showRejectDialog(int reject) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("權限申請")
            .setMessage(getString(reject))
            .setPositiveButton("跳轉到設置頁", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // 本人魅族手機,其他品牌的設置頁跳轉邏輯不同,請百度解決
                    Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
                    intent.addCategory(Intent.CATEGORY_DEFAULT);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    startActivity(intent);
                    dialog.dismiss();
                }
            })
            .setNegativeButton("取消", null)
            .show();
}

3.在需要權限的地方添加注解:

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {R.string.location_rationale, R.string.contact_rationale},
        rejects = {R.string.location_reject, R.string.contact_reject})
private void initView() {}

一旦權限申請被拒絕,將會回調到全局監聽中,這里我們只彈窗提醒,若需要其他形式的提醒,自行實現ui即可。運行效果:


注意

如果你們有過組件化開發,就應該馬上了解到,我們在上面使用@Permission注解傳入的rationale和reject的字符串id,在Module中是會報錯的,原因是Module中的R.string.xxx不是final常量,而注解值需要final常量值。

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {R.string.location_rationale, R.string.contact_rationale},
        rejects = {R.string.location_reject, R.string.contact_reject})
private void initView() {}

那么,如何處理在Module中的情況呢,這里我想到了一個思路:

既然R.string.xxx不是常量,我們就給注解值傳入我們自定義的常量:

public class Permissions {
    public static final int LOCATION_RATIONALE = 100;
    public static final int LOCATION_REJECT= 101;
    public static final int CONTACT_RATIONALE= 102;
    public static final int CONTACT_REJECT= 103;
}
@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {Permissions.LOCATION_RATIONALE, Permissions.CONTACT_RATIONALE},
        rejects = {Permissions.LOCATION_REJECT, Permissions.CONTACT_REJECT})
private void initView() {}

然后在全局的監聽中修改:

GPermisson.init(new PermissionGlobalConfigCallback() {
    @Override
    public void shouldShowRational(String permission, int ration) {
        if (ration == Permissions.LOCATION_RATIONALE) {
            showRationaleDialog(R.string.location_rationale);
        } else if (ration == Permissions.CONTACT_RATIONALE) {
            showRationaleDialog(R.string.contact_rationale);
        } else {
            showRationaleDialog(ration);
        }
    }

    @Override
    public void onPermissonReject(String permission, int reject) {
        if (reject == Permissions.LOCATION_RATIONALE) {
            showRejectDialog(R.string.location_reject);
        } else if (reject == Permissions.CONTACT_RATIONALE) {
            showRejectDialog(R.string.contact_reject);
        } else {
            showRejectDialog(reject);
        }
    }
});

可能不是那么優雅,如果有好的方式,請留言告知,讓大家學習學習……感謝。
源碼地址:(https://github.com/DevBraon/GPermission)

想解鎖更多姿勢,請關注微信公眾號:Android必修課

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容