相信大家对Android6.0以上的动态权限已经有所了解,很多童鞋也已经跃跃欲试地将自己项目的targetSDK升级到了23及其以上,很不幸的是我也成为了其中一员,然而我还是图样图森破了,升级之后的问题并没有想象的那么简单。在简单的改掉项目的targetSDK之后,由于华为手机的权限是可以动态修改的,即便是低于Android6.0以下的机器上,而这种情况在禁止权限之后,使用代码类似PermissionChecker.checkSelfPermission(),ContextCompat.checkSelfPermission(),甚至是从网上找的checkOp()等方法都是获取到的同意权限,这样在实际运行时就会有空值返回或者抛出IllegalstateException等异常,很是让人头痛。拿使用MediaPlayer的prepare()和start()方法时需要申请android.Manifest.permission.RECORD_AUDIO权限来讲,主要从下面三个维度考察,项目中的targetSDK是否低于23,测试机型版本号是否低于Android6.0,测试机型属于原生Android系统还是类似华为小米的定制系统。
在版本号Android6.0以下的正常测试机(以OPPO R7_Android4.4.4为例)中,我们暂且称之为老旧版,在项目还未升级(即项目的targetSDK<23)之前,在代码的清单文件中声明需要的权限配置正常,只会在APP安装时,提示这些安全权限且用户无法拒绝使用某一权限,用户只能选择同意安装或者拒绝安装,而在APP运行时是没有任何提示框提醒的,这也是旧版本的Android对权限管理处理方法。而在升级项目的targetSDK时,对这种老旧版是几乎没有影响的。
在版本号Android6.0以下的定制测试机(以HUAWEI MT7_Android4.4.2为例)中,我们称之为老版升级版,在项目未升级之前,各种效果和老旧版相同,不同的是升级之后,由于定制系统中用户可以选择对APP的某一权限进行授权、禁止、提示等功能,这样虽然代码中调用的是API低于23的方法,但应该走API23以后的执行流程(即对权限同意或拒绝的处理),也就是说在调用权限检查的类似代码时,无论用户选择同意或禁止,返回的都是禁止。升级后的代码处理,主要就是针对这种披着羊皮的狼的,这种狼是防不胜防。
针对Android6.0及其以上的机型(以HUAWEI MT9_Android7.0为例),被称作新版,在项目未升级之前,新版的效果和老版升级版中安装的升级之后的项目效果是一致的,也就是在APP运行过程中对使用到的权限会有单独的提示框提醒,同样在项目升级之后,由于在代码中调用的是API高于23的方法,所以对权限的检测和申请都是正常的,可以说这是实实在在的狼,因此只要做好正常的防狼手段就可以防住了。
下面以项目中使用android.Manifest.permission.RECORD_AUDIO权限对MediaRecorder进行操作时在上述三种版本中运行出现的问题处理为例进行剖析。
看一下在未升级之前的原始代码,在点击聊天的发送语音消息按钮时,直接切换语音消息和文字消息模式,没有对任何权限的检查:
1 public void onClick(View v) { 2 switch (v.getId()) { 3 case R.id.iv_chatting_sound: // 点击发送语音模式的按钮 4 clickSoundImage(); 5 break; 6 default: 7 break; 8 } 9 }
按照正常的升级之后的处理逻辑,在点击发送语音模式按钮的时候,要先检查当前Activity是否已经有android.Manifest.permission.RECORD_AUDIO的授权,如果有授权通过,再调用clickSoundImage()方法切换语音消息模式,否则在未授权通过时,仍然停留在文字消息模式界面,并给用户提示。升级后的修改代码如下:
1 public void onClick(View v) { 2 switch (v.getId()) { 3 case R.id.iv_chatting_sound: // 发送语音模式的按钮 4 PermissionUtil.needPermission(this, PermissionUtil.PER_RECORD_AUDIO, android.Manifest.permission.RECORD_AUDIO); 5 break; 6 default: 7 break; 8 } 9 }
PermissionUtil类是封装的权限检测类,相关的使用方法就是在需要检测权限的地方调用needPermission(Context context, int requestCode, String permission),使用注解的方式分别在权限同意和权限拒绝后执行两个不同注解的方法,类似下面这种情况:
1 @PermissionFail(requestCode=PermissionUtil.PER_RECORD_AUDIO) 2 private void clickSoundImageFail() { 3 //提示用户权限被拒 4 }
1 @PermissionSuccess(requestCode = PermissionUtil.PER_RECORD_AUDIO) 2 private void clickSoundImage() { 3 //权限授权成功,执行发送文字消息模式和语音消息模式的切换代码 4 }
代码修改截止到目前为止,在老旧版和新版的测试机上升级成功,都是没有问题的,可是运行到老版升级版上之后,即便用户拒绝了该权限,仍然是走权限授权成功之后的方法,接下来就是从羊群中找狼的节奏了。
遇到上面描述的问题,第一反应就是PermissionUtil类中检查权限的那段代码出现了问题,没有正确返回获取权限的处理结果,从网上另外找了几个方法,具体使用PermissionChecker.checkSelfPermission(),ContextCompat.checkSelfPermission(),checkOp()等都是一致返回授权成功的结果,这让我感到很惊讶啊。由于测试机是Android4.4版本的,所以调用权限检查返回的授权结果肯定是根据Android源码中的处理结果走的,API19的权限检查代码,是默认返回授权成功的,按照之前的版本逻辑,如果用户拒绝了某一权限,当前APP就已经安装失败了,更如何运行呢?所以我想到,在老版升级版中想让检查权限的返回值改变是不太现实的了,那就只能让代码过去这里,在系统自带的授权对话框中对处理结果进行监听了。那么就需要修改后续代码,也就是在实际调用MediaRecorder的地方。
1 /** 2 * 开始录制音频 3 */ 4 public void startRecording(OnSoundCallBack onSoundCallBack) throws IOException, IllegalStateException { 5 //OnSoundCallBack对象,音频录制过程中的回调监听 6 this.mOnSoundCallBack = onSoundCallBack; 7 //先停止上一次的录音 8 stopRecording(); 9 //初始化MediaRecorder准备新一轮录制 10 initMediaRecorder(); 11 //重新创建录音文件 12 mp3File=initFile().createNewFile(); 13 mMediaRecorder.setOutputFile(mp3File.getAbsolutePath()); 14 startRecorderTime = System.currentTimeMillis(); 15 setPrepare(true); 16 //准备录制,用户拒绝权限时会抛出IllegalStateException异常 17 mMediaRecorder.prepare(); 18 //根据是否授权prepare,开启或关闭回调 19 getRecordCall(getPrepare()); 20 if(getPrepare()){ 21 //在准备充分的时候启动录制 22 mMediaRecorder.start(); 23 } 24 } 25 26 /** 27 * 停止录制音频 28 */ 29 public String stopRecording() throws IOException, IllegalStateException { 30 //关闭回调 31 getRecordCall(false); 32 if(isRecordering()||!getPrepare()){ 33 //在录制过程中或者没有准备充分(即用户拒绝权限)时,释放MediaRecorder对象 34 releaseMediaRecorder(); 35 } 36 return mp3File.getAbsolutePath(); 37 } 38 39 private void initMediaRecorder(){ 40 if (mMediaRecorder == null) { 41 // 实例化MediaRecorder类对象: 42 mMediaRecorder = new MediaRecorder(); 43 // 设置录音来源 : 44 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 45 // 设置输出格式: 46 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR); 47 // 设置编码方式 48 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 49 mMediaRecorder.setAudioSamplingRate(SAMPLE_RATE_IN_HZ); 50 } 51 } 52 53 private void releaseMediaRecorder(){ 54 if (mMediaRecorder != null) { 55 try { 56 mMediaRecorder.stop(); 57 mMediaRecorder.release(); 58 }catch (Exception e){ 59 // 在释放时,即便有异常也释放 60 } 61 mMediaRecorder = null; 62 } 63 } 64 65 /** 66 * 音频录制过程中的回调 67 */ 68 private void getRecordCall(boolean isTrue) { 69 this.isRecording = isTrue; 70 if (isRecording) { 71 newGetSoundMaxThread(); 72 }else { 73 mHandler.sendEmptyMessage(HANDLER_WHAT_RECORD_STOP); 74 } 75 } 76 77 /** 78 * 开启子线程发送Handler调用OnSoundCallBack回调 79 */ 80 private void newGetSoundMaxThread() { 81 new Thread(new Runnable() { 82 @Override 83 public void run() { 84 while (isRecording) { 85 mHandler.sendEmptyMessage(HANDLER_WHAT_RECORD_ING); 86 if (startRecorderTime+ RECORD_TIME_MAX<=System.currentTimeMillis() ) { 87 mHandler.sendEmptyMessage(HANDLER_WHAT_RECORD_OUTTIME); 88 } 89 SystemClock.sleep(100); 90 } 91 } 92 }).start(); 93 }
在使用老版升级版跑上面那段修改后的代码时,是没有问题的,修改之前的代码主要少了两部分内容。
第一是抛出IllegalStateException异常。这是针对老版升级版在拒绝相关权限之后的处理方式。一般代码在startRecording()和stopRecording()两个方法只抛出了编译时的IOException,没有抛出IllegalStateException这个运行时异常,修改后的代码不仅多抛出了这个异常,同时需要增加对这个异常的处理。那么在什么状态下会抛出这个异常呢?如果当前APP被用户拒绝使用android.Manifest.permission.RECORD_AUDIO权限,在调用prepare()和start()两个底层方法时,会抛出IllegalStateException异常。所以如果捕获到这个异常,就说明在代码运行到这里时,并没有获取到相对应的权限,这时只要停止接下来的操作,并提示用户去打开相关权限就可以了。
第二是增加setPrepare()和getPrepare()两个方法做准备处理。这是针对老版升级版在弹出提示框时的处理方式。修改之前是没有调用上述代码15行的setPrepare(true),以及后边没有对getPrepare()的调用,如果只是从这些代码流程中看,似乎加上那几句话没有任何作用。那么问题来了,为什么要多加这几行代码呢?然而这正是MediaRecorder类提供prepare()这个看似没有作用的方法的一个重要原因。如果用户安装APP时选择权限提示,在运行到prepare()这句代码时,就会在当前代码所在线程创建并弹出权限提示框,此时如果用户进行权限处理操作,可以在长按按钮的地方调用setPrepare(false),使音频录制操作停止,待用户处理完权限操作之后,再重新开始录制。
增加上边两个处理,就可以解决当前遇到的各种问题,以此记录备案!