Android6.0权限适配

Code4Android .jpg

源码传送门

前言

现在谈论Android权限适配可能有点没必要,因为网上关于权限适配的文章很多,搜一下Android6.0权限适配关键词能搜到一堆文章,而且很多写的还很不错。不过自己想了想还是总结一下,因为那些文章都是别人的,不是自己的,之前一直想总结一下,但是一直没做,今天就简单记录一下,方便以后查阅,也对Android6.0的权限机制再次进行一次全面的认识。
从Android M开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。这样更友好的让用户选择,当真正需要权限的时候再去申请权限,而不是Android M之前在安装时一下子去申请。

正常权限

正常权限不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。而不需要我们去请求权限。

ACCESS_LOCATION_EXTRA_COMMANDS  
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

危险权限

危险权限涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。

在危险权限中,我们需要了解一个权限组的概念,所有危险的 Android 系统权限都属于权限组,如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有拥有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。

例如我们需要读取获取手机卡imsi,此时需要请求权限READ_PHONE_STATE,发现此时提示框也展示了请求打电话权限。(系统只告诉用户应用需要的权限组,而不告知具体权限)其实READ_PHONE_STATE和打电话权限CALL_PHONE都属于一个权限组PHONE,如果我们此时允许了权限,那么下次再其他地方使用了打电话权限时系统将立即授予该权限。

这里写图片描述

注:任何权限都可属于一个权限组,包括正常权限和应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。

权限组 权限
CALENDAR READ_CALENDAR
WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS

权限请求API

    /**
     * 确定权限是否已经被授予
     * @param permission 被检测权限的名字.
     * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} 如果权限被授予,
     * {@link android.content.pm.PackageManager#PERMISSION_DENIED} 如果权限被拒绝返回值.
     */
    public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) 



    /**
     * 是否显示自定义UI提示用户
     * 华为手机测试 第一次使用时返回false
     * 如果拒绝返回true
     * 如果拒绝并点击不在提醒返回false
     * 已经同意过权限,但在设置拒绝此时返回true
     * 没有同意过权限,在设置中开启并拒绝权限返回false
     * @param activity   请求权限Activity.
     * @param permission 需要请求的权限.
     * @return 是否显示自定义对话框提示用户.
     */
    public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
                                                               @NonNull String permission)




    /**
     * 给应用申请权限,申请的权限必须在manifest文件注册,正常权限在安装时自动被授权,不需要使用此方法请求权限
     * 请求之后会弹出系统提示框,供我们选择是拒绝还是允许,点击后
     * android.support.v4.app.ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(
     * int, String[], int[])} 方法将会被回调,
     * @param activity 请求权限的Activity.
     * @param permissions 需要请求的权限.
     * @param requestCode 指定一个请求码,用于区别返回结果
     *
     */
    public static void requestPermissions(final @NonNull Activity activity,
                                          final @NonNull String[] permissions, final int requestCode)



    /**
     * 调用requestPermissions方法请求权限的回调
     *需要注意的是可能请求的权限与用户互动中断;正在这种情况下回调将接收一个空的permissions和grantResults数组
     * @param permissions 请求的权限. 不为null,长度可能为0.
     * @param grantResults 请求权限的结果PERMISSION_GRANTED表示权限被允许,PERMISSION_DENIED表示权限被拒绝
     */
    void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                    @NonNull int[] grantResults)

需要注意的是,对于如果在Activity中请求权限则可使用上面API ActivityCompat类,如果在Frament请求权限则,需要使用Fragment类中的对应方法,否则回调会有问题。

简单封装

 /**
     * 判断是否具备所有权限
     *
     * @param permissions 所有权限
     * @return true 具有所有权限  false没有具有所有权限,此时包含未授予的权限
     */
    public static boolean isHasPermissions(String... permissions) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return true;
        for (String permission : permissions) {
            if (!isHasPermission(permission))
                return false;
        }
        return true;
    }

    /**
     * 判断该权限是否已经被授予
     *
     * @param permission
     * @return true 已经授予该权限 ,false未授予该权限
     */
    private static boolean isHasPermission(String permission) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return true;
        return ContextCompat.checkSelfPermission(MyApplication.getAppContext(), permission) == PackageManager.PERMISSION_GRANTED;
    }
    
    /**
     * 请求权限,经测试发现TabActivity管理Activity时,在Activity中请求权限时需要传入父Activity对象,即TabActivity对象
     * 并在TabActivity管理Activity中重写onRequestPermissionsResult并分发到子Activity,否则回调不执行  。TabActivity回调中  调用getLocalActivityManager().getCurrentActivity().onRequestPermissionsResult(requestCode, permissions, grantResults);分发到子Activity

     * 
     *
     * @param object      Activity or Fragment
     * @param requestCode 请求码
     * @param permissions 请求权限
     */
    public static void requestPermissions(Object object, int requestCode, String... permissions) {
        ArrayList<String> arrayList = new ArrayList<>();
        for (String permission : permissions) {
            if (!isHasPermissions(permission)) {
                arrayList.add(permission);
            }
        }
        if (arrayList.size() > 0) {
            if (object instanceof Activity) {
                Activity activity = (Activity) object;
                Activity activity1 = activity.getParent() != null && activity.getParent() instanceof TabActivity ? activity.getParent() : activity;
                ActivityCompat.requestPermissions(activity1, arrayList.toArray(new String[]{}), requestCode);
            } else if (object instanceof Fragment) {
                Fragment fragment = (Fragment) object;
                //当Fragment嵌套Fragment时使用getParentFragment(),然后在父Fragment进行分发,否则回调不执行
                Fragment fragment1 = fragment.getParentFragment() != null ? fragment.getParentFragment() : fragment;
                fragment1.requestPermissions(arrayList.toArray(new String[]{}), requestCode);
            } else {
                throw new RuntimeException("the object must be Activity or Fragment");
            }
        }
    }

如果想展示自定义UI友好的提示用户申请该权限的原因,则需要使用shouldShowRequestPermissionRationale方法,简要封装如下

 public static boolean shouldShowRequestPermissionRationale(@NonNull Object object, String... permissions) {
        for (String permission : permissions) {
            if (object instanceof Activity) {
                if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) object, permission)) {
                    return true;
                }
            } else if (object instanceof Fragment) {
                if(((Fragment) object).shouldShowRequestPermissionRationale(permission)){
                    return true;
                }
            } else {
                throw new RuntimeException("the object must be Activity or Fragment");
            }


        }
        return false;
    }

    /**
     * 二次申请权限时,弹出自定义提示对话框
     *
     * @param activity
     * @param message
     * @param iPermissionRequest
     * @see com.example.xh.ui.BaiduLocationFragment 可以查看该类onRequestPermissionsResult方法当选择永不提醒时的处理办法。
     */
    public static void showDialog(Activity activity, String message, final IPermissionRequest iPermissionRequest) {
        new AlertDialog.Builder(activity)
                .setPositiveButton("允许", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        iPermissionRequest.agree();
                        dialog.dismiss();
                    }
                })
                .setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        iPermissionRequest.refuse();
                        dialog.dismiss();
                    }
                })
                .setCancelable(false)
                .setMessage(message)
                .show();
    }

弹出对话框后,点击了拒绝或者允许后,给一个回调,方便进行不同的处理,当然如果统一处理的话,就不需要写接口,直接在上述点击允许的时候请求权限,点击不允许的时候,显示一个Toast再次做下权限拒绝提示。当然也可在onRequestPermissionsResult中进行判断,当选中永不提醒后给用户一个友好跳转到权限设置界面。
接口方法

public interface IPermissionRequest {
    void agree();
    void refuse();
}

特殊权限

有许多权限其行为方式与正常权限及危险权限都不同。SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS 特别敏感,因此大多数应用不应该使用它们。如果某应用需要其中一种权限,必须在清单中声明该权限,并且发送请求用户授权的 intent(注意特殊权限和危险权限请求方式不一样)。系统将向用户显示详细管理屏幕,以响应该 intent。

请求WRITE_SETTINGS权限

  /**
     * 测试请求WRITE_SETTINGS权限
     */
    @OnClick(R.id.request_write_setting)
    @TargetApi(android.os.Build.VERSION_CODES.M)
    public void requestWriteSetting() {
        if (!Settings.System.canWrite(this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, requestCodeWriteSetting);
        } else {
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 已经被授权", Toast.LENGTH_SHORT).show();
        }
    }
    
    
    @TargetApi(Build.VERSION_CODES.M)
    private void showToast() {
        if (Settings.System.canWrite(this)) {
            //检查返回结果
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 被授权", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 没有被授权", Toast.LENGTH_SHORT).show();
        }
    }

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == requestCodeWriteSetting) {
            showToast();
        }else if(requestCode==requestCodeAlertWindow){
            showToastAlerterWindow();
        }
    }

请求SYSTEM_ALERT_WINDOW权限

 /**
     * 测试请求SYSTEM_ALERT_WINDOW权限
     */
    @OnClick(R.id.request_alert_window)
    @TargetApi(android.os.Build.VERSION_CODES.M)
    public void requestAlertWindow() {
        if (!Settings.canDrawOverlays(this)) {
            Intent intent = new Intent(Settings. ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, requestCodeAlertWindow);
        } else {
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 已经被授权", Toast.LENGTH_SHORT).show();
        }
    }
 @TargetApi(Build.VERSION_CODES.M)
    private void showToastAlerterWindow() {
        if (Settings.System.canWrite(this)) {
            //检查返回结果
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 被授权", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 没有被授权", Toast.LENGTH_SHORT).show();
        }
    }
WRITE_SETTINGS权限设置界面
SYSTEM_ALERT_WINDOW权限设置界面

注意:权限必须在清单文件中声明,否则进入上面界面时开关是不可点击的灰色。

打开权限设置界面

在上面危险权限申请中,如果用户拒绝了权限,并且选中永不提醒,那么下次请求权限时直接执行onRequestPermissionsResult回调,并且返回状态是权限被拒绝状态,那么若想授予权限,必须去手机的权限管理中设置,如果用户去手机里找是不是很麻烦,况且一步人不知道设置权限的地方在哪,那么为了程序的体验更好,我们可以在我们的应用中引导用户跳转到设置权限的界面。实现代码如下

    /**
     * 打开应用权限设置界面
     */
    @OnClick(R.id.open_permission_setting)
    public void requestOpenPermissionSetting() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        // Uri uri = Uri.fromParts("package", getPackageName(), null);
        Uri uri1=Uri.parse("package:" + getPackageName());
        intent.setData(uri1);
        startActivity(intent);
    }

介绍到此就结束了,水平有限若有问题请指出,Hava a wonderful day.

最后放几篇感觉不错的文章:

Android6.0权限适配之WRITE_EXTERNAL_STORAGE(SD卡写入)

谷歌文档 在运行时请求权限

谷歌文档 系统权限

Android权限机制与适配经验

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 从6.0 MarshMallow开始,Android支持动态权限管理,即有些权限需要在使用到的时候动态申请,根据用...
    看书的小蜗牛阅读 6,918评论 0 26
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 最近关于6.0权限适配的问题,还是有很多,我之前在别的文章中提到过,应该怎样去做适配,但是并没有给出完整代码,只给...
    mymdeep阅读 217评论 0 1
  • Android 5.0与6.0权限的不同 5.0以前,只需要manifest.xml中注册声明即可 5.0以后,用...
    飞奔的小马阅读 393评论 0 0
  • 失眠的夜 星星也入眠 焦虑恣意蔓延 忆及逝去时光 如雨打明窗般失真 满目苍凉 物是人非 爱与不爱已不再重要 从举棋...
    如雾起时阅读 345评论 0 0