Android权限适配

一、动态权限的开始

Android系统目前已经更新到8.0(O),自从Android6.0(M)开始,权限适配问题成为我们开发适配工作之一。在6.0之前,应用申请权限只需要将权限在AndroidManifest中注册声明即可,用户安装应用时会有一个权限列表,只有同意这些权限后,我们才能安装成功,这就使用户不得不忍受一些无理的权限要求(访问通讯录、访问短信等);在6.0之后,Google出去安全隐私考虑,引入运行时权限,可见官网:运行时权限
将系统权限分为两类,一类是Normal permissions,这类权限在一般不涉及用户隐私,是不需要用户授权,用户安装app时默认授予,另一类是dangerous permissions,这类权限一般涉及到用户的隐私,需要用户先进行授权。
危险权限是分组的,在Android6.0的机器上,基于授权机制,如果用户申请了某个危险权限,假如app已经申请过同组的其他危险权限,那么系统会立即授权,而不需要用户点击,比如,你已经申请过 permission:android.permission.READ_EXTERNAL_STORAGE权限,那么当你再次申请permission:android.permission.WRITE_EXTERNAL_STORAGE权限时,系统会自动授权,而不再由用户确认。(对于危险权限,我们在授权时最好用到一个申请一个,不要依赖这个机制,因为8.0对此机制已经修改,后面在说)

  • Dangerous Permissions权限列表:

https://developer.android.com/guide/topics/permissions/requesting.html?hl=zh-cn

    group:android.permission-group.CONTACTS
      permission:android.permission.WRITE_CONTACTS
      permission:android.permission.GET_ACCOUNTS
      permission:android.permission.READ_CONTACTS
    
    group:android.permission-group.PHONE
      permission:android.permission.READ_CALL_LOG
      permission:android.permission.READ_PHONE_STATE
      permission:android.permission.CALL_PHONE
      permission:android.permission.WRITE_CALL_LOG
      permission:android.permission.USE_SIP
      permission:android.permission.PROCESS_OUTGOING_CALLS
      permission:com.android.voicemail.permission.ADD_VOICEMAIL
    
    group:android.permission-group.CALENDAR
      permission:android.permission.READ_CALENDAR
      permission:android.permission.WRITE_CALENDAR
    
    group:android.permission-group.CAMERA
      permission:android.permission.CAMERA
    
    group:android.permission-group.SENSORS
      permission:android.permission.BODY_SENSORS
    
    group:android.permission-group.LOCATION
      permission:android.permission.ACCESS_FINE_LOCATION
      permission:android.permission.ACCESS_COARSE_LOCATION
    
    group:android.permission-group.STORAGE
      permission:android.permission.READ_EXTERNAL_STORAGE
      permission:android.permission.WRITE_EXTERNAL_STORAGE
    
    group:android.permission-group.MICROPHONE
      permission:android.permission.RECORD_AUDIO
    
    group:android.permission-group.SMS
      permission:android.permission.READ_SMS
      permission:android.permission.RECEIVE_WAP_PUSH
      permission:android.permission.RECEIVE_MMS
      permission:android.permission.RECEIVE_SMS
      permission:android.permission.SEND_SMS
      permission:android.permission.READ_CELL_BROADCASTS

二、动态权限的检测和申请

由于运行时权限的出现,我们必须对新的app做适配,当然,我们不必担心之前的app,Google对此做了兼容。当我们的应用targetSdkVersion小于23时,即使是装在6.0的手机上,也会依然使用6.0之前的权限规则,当targetSdkVersion大于23时,我们才使用新的这套运行时权限规则。
权限检测:
(这里我就不说第三方权限检测库的使用了.)

  /*
     *  1.检查权限是否存在
     *  2.(1)若权限存在:直接打开app
     *    (2)若权限不存在,请求改权限
     *  3.重写onRequestPermissionsResult()方法,判断是权限返回结果,在做处理
     */
    private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            //有权限:打开相机
            AlbumBlock.doActionImageCapture(this);
        } else {
            //无权限:申请权限
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }


 /*打开相机*/
    public static void doActionImageCapture(Activity activity) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        File file = getOutputMediaFile(activity, false);

        if (file == null) {
            ToastUtil.makeToast(activity, "无法保存图片");
            return;
        }

        Uri imgUri = Uri.fromFile(file);
        if (imgUri != null) {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
            activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
        } else {
            ToastUtil.makeToast(activity, "SD卡不可用,相机照片无法存储!");
        }

    }


三、权限申请的回调

当我们申请权限后,需要对用户操作的结果进行处理。一般我们会重写onRequestPermissionsResult()方法。在这里需要注意的是,一旦当第二次拒绝权限时,会有一个是否询问的勾选按钮,一旦用户选择了不在询问,那么ActivityCompat.requestPermissions()方法将不在起作用,这个时候ActivityCompat.shouldShowRequestPermissionRationale()方法会返回false,我们可以通过这个判断进行一些自己的处理。

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

        if (grantResults.length == 0) {
            return;
        }
        switch (requestCode) {
            case REQUEST_CAMERA:
                for (int i = 0; i < permissions.length; i++) {
                    String perm = permissions[i];
                    //依次判断权限
                    if (Manifest.permission.CAMERA.equals(perm)) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            //打开相机
                            AlbumBlock.doActionImageCapture(this);
                        } else {
                            //拒绝
                            /*如果用户选择了拒绝,并且选择了不在提醒,那么这个方法会返回false,这样我们就可以做一些自己的
                            * 提醒,避免一些不好的体验。这个时候requestPermissions()是不起作用的,所以我们需要告诉用户怎么打开权限。
                            * */
                            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, perm)) {
                                showRequstPermissionDialog();
                            }
                        }
                    }
                }
                break;
        }

      private void showRequstPermissionDialog() {
            new AlertDialogUtil(this)
                    .setTitle("相机权限未开启")
                    .setMessage("请在设置中开启相机权限")
                    .setNegativeButton("暂不", null)
                    .setPositiveButton("去设置", new AlertDialogUtil.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog) {
                            try {
                            /*
                            * 当然在这里我们可以针对不同的手机品牌,跳转到不同设置界面。
                            * 后面会给出相应的工具类。
                            */
                                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                                Uri uri = Uri.fromParts("package", getPackageName(), null);
                                intent.setData(uri);
                                startActivityForResult(intent, REQUEST_CODE_SETTINGS);
                            } catch (Exception e) {
    
                            }
                        }
                    }).show();
    
        }

四、7.0权限的更改及FileProvider的使用

我们知道Android6.0引入了运行时权限"Runtime Permissions",那么Android7.0对于权限这块,则增加了"StrictMode API 政策"。详见官网:https://developer.android.com/about/versions/nougat/android-7.0-changes.html#perm
(在这里推荐一篇介绍适配7.0的博客:http://www.jianshu.com/p/56b9fb319310)7.0之后,如果一项包含文件 file:// URI类型 的 Intent 离开了我们应用,那么我们的APP会出故障,并出现 FileUriExposedException 异常,例如调用系统相机拍照或者裁剪照片。对于这个问题,可根据官网给出的方法去解决:

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最
简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

7.0之前打开相机的方式可参照上面的代码,这个方式在7.0系统的手机由于"StrictMode API 政策禁"会报错,所以我们需要通过 FileProvider来解决这个问题。下面是具体使用步骤(参考上面那篇博客所写):

第一步:在AndroidManifest 功能清单文件中注册Provider###

 <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.ff.app.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

第二步:指定共享目录

我们需要再res目录下创建一个xml目录,然后创建一个file_paths(这个名字只要和你注册时使用的一致就行)文件。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="files_path" path="images/" /> //相当 Context.getFilesDir() + path, name是分享url的一部分

    <cache-path name="cache_path" path="path/" /> //getCacheDir()

    <external-path name="external_path" path="ffapp/images/" /> //Environment.getExternalStorageDirectory()

    <external-files-path name="external_files" path="path/" />//getExternalFilesDir(String) Context.getExternalFilesDir(null)

    <external-cache-path name="external_cache" path="images/" /> //Context.getExternalCacheDir()

</paths>

上述代码中path="",是有特殊意义的,它代表根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="images/",
那么它代表着根目录下的images下的目录(eg:/storage/emulated/0/images/),如果你向其它应用分享images下目录范围之外的文件是不行的。

第三步:使用FileProvider

 /*打开相机*/
    public static void doActionImageCapture(Activity activity) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        File file = getOutputMediaFile(activity, false);

        if (file == null) {
            ToastUtil.makeToast(activity, "无法保存图片");
            return;
        }

        Uri imgUri;
        /*这里就是对FileProvider的使用*/
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            imgUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", file);
            //将存储图片的Uri读写权限授权给拍照工具应用
            List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
            for (ResolveInfo resolveInfo : resInfoList) {
                String packageName = resolveInfo.activityInfo.packageName;
                //添加这一句表示对目标应用临时授权该Uri所代表的文件
                activity.grantUriPermission(packageName, imgUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
        } else {
            imgUri = Uri.fromFile(file);
        }
        if (imgUri != null) {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
            activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
        } else {
            ToastUtil.makeToast(activity, "SD卡不可用,相机照片无法存储!");
        }

    }

在上面代码中我们判断了系统版本,如果是7.0之后的,我们则将Uri转换为一个content类型的Uri,并且对目标应用(相机)临时授权该Uri所代表的文件。getUriForFile()方法的中authority参数就是我们注册时 android:authorities="com.ff.app.fileprovider"指定的值。这里使用activity.getPackageName()是为了便于模块的抽离。

好了这就是7.0的FileProvider简单使用。

五、8.0权限的更改

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会"错误"地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。(在说6.0权限时我们说到了这个机制,并说过不要依赖,这里8.0已经修复了)

对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

这意味着,我们应用中用到的所有权限必须一个一个的声明,举个例子:在8.0之前,我们在申请 Manifest.permission.WRITE_EXTERNAL_STORAGE的时候,系统会自动对同一个权限组的Manifest.permission.READ_EXTERNAL_STORAGE进行授权,而我们不需要再去申请,但是放到8.0及之后的系统上,如果app中同时用到这两个权限而我们只申请了一个的话,系统会报错。

对于开发者来说,我们需要明确的知道我们的应用涉及到了哪些危险权限,并且在使用时对于其进行授权申请。
Android 8.0 行为变更

六、开发中遇到的问题

关于权限的适配暂时说到这里,下面说一个开发中遇到的奇怪问题,暂时没有解决。在开发内部员工OA的app时用到了扫一扫的功能,一般扫一扫肯定需要相机权限,所以按照权限的适配去判断后,发现下面这段代码总是返回有权限,无论权限是授权的还是禁止的。(原因暂时不确定)

 private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            //有权限:打开相机
            AlbumBlock.doActionImageCapture(this);
        } else {
            //无权限:申请权限
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }

那这样检测不就没有用吗,的确是的。后来看了很多资料,觉得可能是Android系统过于碎片化的原因,那么怎么去解决呢?首先我们需要换一种思路了,既然是调用相机,那么我们看看相机是否可用不就行了?所以在检测相机权限的时候我们在增加一个方法。

 private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            if(isCameraCanUse()){
                //有权限:打开相机
                AlbumBlock.doActionImageCapture(this);
            }else{
                //去设置界面:下面会提供一个去大部分主流手机系统的设置界面的工具类
                
            }
            
        } else {
            //无权限:申请权限
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }

 /**
     * 测试当前摄像头能否被使用
     *
     * @return
     */
    public static boolean isCameraCanUse() {
        boolean canUse = true;
        Camera mCamera = null;
        try {
            mCamera = Camera.open(0);
            mCamera.setDisplayOrientation(90);
        } catch (Exception e) {
            canUse = false;
        }
        if (canUse) {
            mCamera.release();
            mCamera = null;
        }
        //Timber.v("isCameraCanuse="+canUse);
        return canUse;
    }

问题解决了,这里提供一个博主提供的工具类,感觉是一个厉害的工具类。
这里贴出网址:https://github.com/SenhLinsh/Utils-Everywhere里面有一个OSUtils可以判断各大手机品牌的系统,还有一个AppUtils中有个方法,这列贴出来,配合使用。。

/**
     * 跳转: 「权限设置」界面
     * <p>
     * 根据各大厂商的不同定制而跳转至其权限设置
     * 目前已测试成功机型: 小米V7V8V9, 华为, 三星, 锤子, 魅族; 测试失败: OPPO
     *
     * @return 成功跳转权限设置, 返回 true; 没有适配该厂商或不能跳转, 则自动默认跳转设置界面, 并返回 false
     */
    public static Intent getPermissionSettingIntent(Activity activity) {
        Intent intent = new Intent();
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        OSUtils.ROM romType = OSUtils.getRomType();
        switch (romType) {
            case EMUI: // 华为
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"));
                break;
            case Flyme: // 魅族
                intent.setAction("com.meizu.safe.security.SHOW_APPSEC");
                intent.addCategory(Intent.CATEGORY_DEFAULT);
                intent.putExtra("packageName", activity.getPackageName());
                break;
            case MIUI: // 小米
                String rom = getMiuiVersion();
                if ("V6".equals(rom) || "V7".equals(rom)) {
                    intent.setAction("miui.intent.action.APP_PERM_EDITOR");
                    intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
                    intent.putExtra("extra_pkgname", activity.getPackageName());
                } else if ("V8".equals(rom) || "V9".equals(rom)) {
                    intent.setAction("miui.intent.action.APP_PERM_EDITOR");
                    intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
                    intent.putExtra("extra_pkgname", activity.getPackageName());
                } else {
                    intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                            .setData(Uri.parse("package:" + activity.getPackageName()));
                }
                break;
            case Sony: // 索尼
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity"));
                break;
            case ColorOS: // OPPO
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.PermissionManagerActivity"));
                break;
            case EUI: // 乐视
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps"));
                break;
            case LG: // LG
                intent.setAction("android.intent.action.MAIN");
                intent.putExtra("packageName", activity.getPackageName());
                ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
                intent.setComponent(comp);
                break;
            case SamSung: // 三星
            case SmartisanOS: // 锤子
                intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                        .setData(Uri.parse("package:" + activity.getPackageName()));
                break;
            default:
                intent.setAction(Settings.ACTION_SETTINGS);
                break;
        }
        return intent;
    }


    /**
     * 获取 MIUI 版本号
     */
    private static String getMiuiVersion() {
        String propName = "ro.miui.ui.version.name";
        String line;
        BufferedReader input = null;
        try {
            Process p = Runtime.getRuntime().exec("getprop " + propName);
            input = new BufferedReader(
                    new InputStreamReader(p.getInputStream()), 1024);
            line = input.readLine();
            input.close();
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return line;
    }

这个问题暂时这么解决吧,能力不够还得继续学习!!

关于代码:传送门

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

推荐阅读更多精彩内容