写在前面 看了下上一篇文章的写作日期,转眼之间已经过去了大半个月了……一个国庆小长假下来也是放松了不少,不过学习也不能过于疏忽了。以前偷懒没有看适配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 制表还是挺麻烦的……偷个懒
动态权限申请实操 前面简介写完了,下面开始实操,申请权限主要分为以下几个步骤:
检查是否拥有权限
如果以前用户拒绝过,提示
申请权限
在回调中查看是否申请成功
首先是检查和申请权限,虽然步骤是以上所述,但是代码比较简单,就不一一拆开了,代码中的注释都比较详细。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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: 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几乎都是标配了,所以说我觉得问题不大,依赖如下:
1 2 3 4 5 6 compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar' compile 'io.reactivex.rxjava2:rxjava:2.1.2' compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
申请权限代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 () { } });
当然了,如果你希望在权限被拒绝时进行进一步的操作,可以使用如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 rxPermissions .requestEach(Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE) .subscribe(permission -> { if (permission.granted) { } else if (permission.shouldShowRequestPermissionRationale) } else { } });
恩,是的,下面一段代码是从仓库上复制来的。。。好了,不要在意这些细节,接着看一下关于File uri 在 Android N 上引发异常的适配。
7.0 适配 File Uri 这里拿拍照作为例子,平时我们调用系统相机实现拍照功能代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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.fromFile(imageFile); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); 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 行为变更,中这段话描述了原因:
行,你说用什么就用什么。接下来先详细的记录一下操作过程,之后再解释一下细节。首先在清单文件中声明:
1 2 3 4 5 6 7 8 9 10 11 <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文件:
1 2 3 4 5 6 7 8 9 10 <?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 >
拍照代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 = FileProvider.getUriForFile(MainActivity.this , "com.xiasuhuei321.studyforpermission.takePhotoN" , imageFile); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); if (intent.resolveActivity(getPackageManager()) != null ) { startActivityForResult(intent, REQUEST_TAKE_PHOTO); }
恩,留个小坑,操作流程写了,余下的具体解析待填。 好了,决定不填了,就是这么任性,哈哈哈哈哈哈哈。