Android 6.0 动态权限申请与 7.0 适配 File Uri 小记

写在前面

看了下上一篇文章的写作日期,转眼之间已经过去了大半个月了……一个国庆小长假下来也是放松了不少,不过学习也不能过于疏忽了。以前偷懒没有看适配6.0 和 7.0的东西,最近在下也是正式的抛弃了大三时买的ip6,入了一台小米mix2。mix2是基于Android 7.1的系统的,自己平时也喜欢用自己的手机调试应用,各种没有适配导致的崩溃自然也是免不了的。以前没看权限适配一是因为6.0以上的系统覆盖率不是很高,二也是因为那时候要看的东西很多,这东西不是很急迫,现在各个厂商的手机出厂系统都是6.0以上的了,也是时候看一下了。本文包括以下内容:

  • Android 6.0 动态权限申请
  • RxPermission
  • Android 7.0 File Uri 导致的崩溃以及如何适配

Android 6.0 动态权限申请

这里先推荐一波官方的文档:https://developer.android.com/training/permissions/requesting.html#perm-request,文档讲的还是比较详尽的,爱自己折腾的同学(比如我)看到这应该say goodbye了

再见

扯犊子时间到此为止,继续正文。系统权限分为几个保护级别,这里需要了解的是两个保护级别是正常权限和危险权限。正常权限只要应用声明了,系统就会自动给应用该权限。而危险权限在Android 6.0 及以上时,则需要通过动态申请来获取权限。当然,考虑到一些兼容性问题,项目的 targetSdkVersion <= 22 时,并不需要动态申请权限。但适配是早晚要去适配的。。。躲也躲不掉,还是先了解下为妙。任何权限都可以属于一个权限组,危险权限也有自己的组别,当你请求了某组权限中的某个权限成功,那么该组的其他权限系统也将授予。比如申请了 STORAGE 权限组的 READ_EXTERNAL_STORAGE 权限,那么该组的 WRITE_EXTERNAL_STORAGE 权限在使用时就无需申请了。接下来放一张危险权限组及危险权限的 截图 ,markdown 制表还是挺麻烦的……偷个懒

危险权限组及危险权限

动态权限申请实操

前面简介写完了,下面开始实操,申请权限主要分为以下几个步骤:

  • 检查是否拥有权限
  • 如果以前用户拒绝过,提示
  • 申请权限
  • 在回调中查看是否申请成功

首先是检查和申请权限,虽然步骤是以上所述,但是代码比较简单,就不一一拆开了,代码中的注释都比较详细。

    int permission = ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.CAMERA);
                if (permission == PackageManager.PERMISSION_GRANTED) {
        // 有此权限
        Toast.makeText(MainActivity.this, "已经具有该权限,无需再申请", Toast.LENGTH_SHORT).show();
    } else {
        // 无此权限,申请权限
        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.CAMERA)) {
            // 如果之前请求过权限但用户拒绝了请求
            Toast.makeText(MainActivity.this, "请求相机权限,将用于拍照", Toast.LENGTH_SHORT).show();
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
                }
            }, 500);
        } else {
            // 请求权限
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }


    // 回调
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CAMERA:
                // 如果请求被取消了,result数组将是空的
                if (grantResults.length > 0 &&
                        grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // 成功申请该权限
                    Toast.makeText(this, "成功申请相机权限", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(this, "您已拒绝该权限,无法使用相机功能 GG,请手动打开该权限", Toast.LENGTH_SHORT).show();
                }
                break;

            default:
                break;
        }
    }

上面的 shouldShowRequestPermissionRationale 方法会返回一个布尔值,这个方法返回值的规则如下:

  • 如果之前请求过此权限但用户拒绝了请求,将返回 true
  • 如果用户在过去拒绝了请求,并在权限请求系统对话框中选择了Don't ask again,此方法将返回false。
  • 如果设备规范禁止应用具有该权限,返回false

我这个代码的基本流程和文档中是一致的,后来在测试的时候发现第一次请求也会走false那个选项,如果我拒绝了这个权限,后续就不会再有权限申请弹窗了,在申请权限的时候系统会自动拒绝。所以我觉得正确的做法应该是在回调中判断权限被拒绝时使用shouldShowRequestPermissionRationale方法,如果返回false则打开设置界面让用户去打开权限。当然,由于国产手机的各种系统定制。。。打开权限设置界面可能并不是一个非常轻松的过程……需要适配……这里就不做这件事了。

RxPermission

上面的权限申请相对来说还是比较繁琐的,接下来介绍一下三方RxPermission,从Rx这俩字你应该可以看出来,这玩意是需要依赖RxJava的。不过对于现在的应用开发来说,RxJava,Okhttp几乎都是标配了,所以说我觉得问题不大,依赖如下:

    // rxpermission
    compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
    // rxjava
    compile 'io.reactivex.rxjava2:rxjava:2.1.2'
    // rxandroid
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

申请权限代码如下:

    RxPermissions rxPermissions = new RxPermissions(MainActivity.this);
                rxPermissions.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            .subscribe(new Observer<Boolean>() {
        @Override
        public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {

        }

        @Override
        public void onNext(@io.reactivex.annotations.NonNull Boolean isGranted) {
            if (isGranted) {
                Toast.makeText(MainActivity.this, "权限申请成功", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(MainActivity.this, "权限申请失败,用户拒绝了此权限", Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onError(@io.reactivex.annotations.NonNull Throwable e) {
            e.printStackTrace();
            Toast.makeText(MainActivity.this, "权限申请失败,代码异常", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onComplete() {

        }
    });

当然了,如果你希望在权限被拒绝时进行进一步的操作,可以使用如下代码:

rxPermissions
    .requestEach(Manifest.permission.CAMERA,
             Manifest.permission.READ_PHONE_STATE)
    .subscribe(permission -> { // will emit 2 Permission objects
        if (permission.granted) {
           // `permission.name` is granted !
        } else if (permission.shouldShowRequestPermissionRationale)
           // Denied permission without ask never again
        } else {
           // Denied permission with ask never again
           // Need to go to the settings
        }
    });

恩,是的,下面一段代码是从仓库上复制来的。。。好了,不要在意这些细节,接着看一下关于File uri 在 Android N 上引发异常的适配。

7.0 适配 File Uri

这里拿拍照作为例子,平时我们调用系统相机实现拍照功能代码如下:

        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Toast.makeText(MainActivity.this, "SD卡状态异常", Toast.LENGTH_SHORT).show();
            return;
        }
        long dateTaken = System.currentTimeMillis();
        // 图像名称
        CharSequence fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken);
        // 图像路径
        String path = Environment.getExternalStorageDirectory().toString() +
                File.separator + "forpermission" + File.separator + fileName + ".jpg";
        File imageFile = new File(path);
        if (!imageFile.getParentFile().exists()) {
            imageFile.getParentFile().mkdirs();
        }
        if (!imageFile.exists()) try {
            imageFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 根据文件解析出文件对应的Uri
        Uri uri = Uri.fromFile(imageFile);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        // 判断是否有 Activity 能处理 intent
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_TAKE_PHOTO);
        }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if(resultCode == RESULT_OK){
            if(requestCode == REQUEST_TAKE_PHOTO){
                Toast.makeText(this, "拍照成功", Toast.LENGTH_SHORT).show();
            }
        }
    }

在 7.0 以前运行这段代码是没问题的,那么在 7.0 之后呢?

异常

抛出了 FileUriExposedException 异常,恩,直接crash,心里相想必有一万头草泥马奔腾而过吧。解释官网上也有,Android 7.0 行为变更,中这段话描述了原因:

原因

行,你说用什么就用什么。接下来先详细的记录一下操作过程,之后再解释一下细节。首先在清单文件中声明:

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.xiasuhuei321.studyforpermission.takePhotoN"
        android:exported="false"
        android:grantUriPermissions="true">

        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>

    </provider>

接下来新建xml包并新建file_paths xml文件:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path
        name="root"
        path="."/>

    <external-files-path
        name="camera_photo"
        path="/storage/emulated/0/forpermission/" />
</paths>

拍照代码:

        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Toast.makeText(MainActivity.this, "SD卡状态异常", Toast.LENGTH_SHORT).show();
            return;
        }
        long dateTaken = System.currentTimeMillis();
        // 图像名称
        CharSequence fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken);
        // 图像路径
        String path = Environment.getExternalStorageDirectory().toString() +
                File.separator + "forpermission" + File.separator + fileName + ".jpg";
        File imageFile = new File(path);
        if (!imageFile.getParentFile().exists()) {
            imageFile.getParentFile().mkdirs();
        }
        if (!imageFile.exists()) try {
            imageFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 根据文件解析出文件对应的Uri
        Uri uri = FileProvider.getUriForFile(MainActivity.this,
                "com.xiasuhuei321.studyforpermission.takePhotoN", imageFile);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        // 判断是否有 Activity 能处理 intent
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_TAKE_PHOTO);
        }

恩,留个小坑,操作流程写了,余下的具体解析待填。
好了,决定不填了,就是这么任性,哈哈哈哈哈哈哈。

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

推荐阅读更多精彩内容