重新梳理Android权限管理

文章一般首发在个人公众号:追风记忆,公众号微信号:zhuifengThread

Android Developer指南中,对Android安全体系结构的核心有这么一个说法:默认情况下,任何应用程序都无权执行任何会对其他应用程序、操作系统或者用户产生负面影响的操作。这句话其实就很好的诠释了权限管理的意义,即用户才是手中设备的主人,没有用户的允许,设备不可以私自记录用户的通讯录,不可以上传用户的姓名和身份证号,更不可以偷偷地窃取属于用户的高级隐私。但在如今的手机程序中,特别是一些流氓应用,私自获取用户高级权限的现象也不少见。随着Android版本的更新,对于权限这一块也比以往做得更好了。这一次重新梳理权限管理环节,并通过实例展示在Android 6.0版本后的权限处理过程。

什么是Android权限?

权限(Permission),顾名思义是一种对信息访问的申请。Android的权限有上百种,例如应用程序尝试调用拨号权限、调用摄像头权限、调用读取短信权限、调用读取通讯录权限等等。对于这些权限,Android将其按照危险等级进行了划分分组,分成如下的三种类别:

  • 正常权限(PROTECTION_NORMAL):指的是应用程序需要访问的一些数据资源,但并不涉及到用户的隐私或者对其他应用程序无害。例如设置闹钟就是属于正常权限。Android在处理正常权限时并不会提示用户,而用户也没有办法取消这些正常权限

  • 签名权限(PROTECTION_SIGNATURE):指的是Android在安装时授予应用程序的权限,利用签名权限,两个签名相同的应用程序就可以进行安全的数据共享。

  • 危险权限(PROTECTION_DANGEROUS ):指的是直接触碰到用户隐私或者影响其他程序操作的权限,对于这一类的权限,Android会以弹窗的方式向用户进行问询,应用程序必须要经过用户的授权后才可以进行相应的行为

以危险权限为例,Android规定了如下的权限必须请求用户的许可。

危险权限组 危险权限组含义 危险权限举例 危险权限含义
CALENDAR 日历 READ_CALENDAR 读取日历
CALL_LOG 通话记录 READ_CALL_LOG 读取通话记录
CAMERA 摄像头 CAMERA 打开摄像头
CONTACTS 联系人 READ_CONTACTS 读取联系人
LOCATION 定位 ACCESS_FINE_LOCATION 获取定位
MICROPHONE 麦克风 RECORD_AUDIO 录音
PHONE 电话 CALL_PHONE 打电话
SENSORS 传感器 BODY_SENSORS 自身状态
SMS 短信 SEND_SMS 发送短信
STORAGE 存储 READ_EXTERNAL_STORAGE 读取外部存储

Android权限获取的方式

对于程序中申请的权限,都应该在AndroidManifest.XML文件中进行注册,否则申请的权限将无法发挥作用。下图中的AndroidManifest文件中添加了打电话摄像头的权限。

AndroidManifest注册权限.JPG

Android权限获取可以分成两个阶段,在Android 6.0之前,所申请的权限只要在AndroidManifest文件中列举就可以了,并会在程序安装时全部显示在安装页面上,这个过程并不区分权限是否为常规权限还是正常权限。这种方式是造成早期Android系统在隐私性做的不好的直接原因,因为用户在安装应用程序时,很多时候并不会去仔细查看程序弹出的方框到底包含了哪些危险的权限,为了尽快的进入程序首页,一般都会同意全部弹出的权限,这就给了很多流氓程序肆意发挥的入口。下图展示了Android 5.0安装界面的部分危险权限截图。

Android 5.0权限获取.JPG

Google显然也注意到了这一点,于是在Android 6.0中推出了一种运行时权限管理机制,这种机制对原有的权限处理方式进行了很大程度的改善:应用程序安装后,点开程序时,不再是列出程序申请的所有权限,而是将部分危险权限与应用本身的功能相关联。例如相机应用,只有当用户点击拍照按钮时,系统就会弹出申请摄像头的权限,这种方式将用户的注意力集中到了当下的操作上,使得用户有足够的时间和意愿去判定是否同意程序的权限申请,并且用户随时可以在设置中关掉授予程序的危险权限,从而极大程度上避免了对危险权限的放行,保护了用户的隐私。

Android 6.0之后的运行时权限处理机制很好的解决了危险权限的获取问题,它具有如下的两个行为:

  • 如果应用程序在当前的权限组(一组权限的集合)中没有任何权限,那么在请求权限时,系统会显示该权限组的请求对话框,例如程序请求CALL_PHONE权限,那么Android将弹出CALL权限对话框显示应用希望拨打电话功能。

  • 如果一个权限组中的任意一个权限被授权,那么该权限组中的其他权限都会被Android默认授权。例如上面的CALL_PHONE权限被允许,那么PHONE权限组中的其它权限,例如READ_PHONE_NUMBERS读取电话号码的权限就会默认被授权,并且不会向用户弹框显示权限申请过程。

运行时权限处理机制中的第二点的特性并不被Google推崇,Google认为后续的Android版本中这个特征可能会发生变化,并建议开发者应明确指出所需要的每一个权限。

Android实现权限管理

关于Android权限更详细的介绍可以在官方的Android Developer指南中查阅。重点是如何在实践中学会使用Android权限,后半部分将会以代码和流程图的方式展示Android权限管理。

Android权限处理可以分解为三个部分:

  1. 检查权限:权限是否为危险权限,正常权限会被系统默认允许,危险权限需要用户手动允许,所以我们的权限讨论范围是危险权限的获取,在Android中检查权限是否获取的方法是ContextCompat.checkSelfPermission(),这个方法返回一个int类型的PERMISSION_GRANTED或者PERMISSION_DENIED,一般来说,程序刚申请权限的时候都是处于PERMISSION_DENIED状态,因此需要后续的申请过程。

  2. 请求权限:当权限并没有被允许的情况下,就需要向用户请求处理权限申请,在应用层上则表现为Android系统会弹出一个对话框,提示用户进行操作。

运行时权限处理机制

从代码层面考虑,Android提供了一个requestPermissions()的调用方法来请求相应权限,这个方法接受目标Activity、 需要请求授权的权限组和识别权限请求的请求代码作为参数传递,并且它是一个异步的方法,并返回产生的结果。

  1. 处理权限响应:当用户对弹出的权限申请框进行响应后,Android会调用onRequestPermissionsResult()方法,将用户的响应作为参数传递。开发者必须使用@Override声明覆盖这个方法,来确认这个权限是否真的被用户所允许,并进行后续的业务逻辑编写。

权限获取的一般过程就是遵循上面的三个步骤进行的,但是千万不要忘记了所申请的权限一定要在AndroidManifest.xml中注册,不然就准备尝尝异常抛出铁拳的力量吧。

当然,更清晰明了的是用流程图来展示权限申请和授权的过程。

处理流程

单个权限的获取过程

下面以获取打电话的权限为例,通过代码实现的方式来解释这个流程的具体做法。以下面一个Demo的页面为测试对象,只要点击获取电话权限按钮,就会弹出权限提示窗,然后允许该请求,就可以实现跳转到拨号页面进行通话的功能。

页面示例

第一部分是检测权限部分。点击获取电话权限按钮,就会调用程序中的callPermission()这个方法,在callPermission中调用checkSelfPermission的方法进行权限检测,实参是当前的Activity对象和对应的权限,这个方法返回一个int类型的值,其中若权限允许则返回值为0的PERMISSION_GRANTED,否则返回值为-1的PERMISSION_DENIED,当权限已经被允许的情况下,直接调用else语句中的callPhone()方法,意味着直接可以拨打电话了。

当权限检测为未允许的情况下,进入请求权限状态,即if语句中的requestPermissions这个方法,这个方法会创建一个字符串数组,将请求的权限同一放入这个数组中,最后一个参数是一个int类型的requestCode,该值在后续的处理权限中发挥作用,并且这个值不一定取1,只要这个值大于等于0即可。为了方便起见,这里取1作为请求码。

@Override    
    public void onClick(View view) {        
        switch (view.getId()){            
            case R.id.getCallPermission:                
                Toast.makeText(MainActivity.this, "获取打电话权限", Toast.LENGTH_SHORT).show();                         callPermission();                
                break;            
            case R.id.getCameraPermission:                
                Toast.makeText(MainActivity.this, "转到第二个页面", Toast.LENGTH_SHORT).show();                         Intent intent = new Intent(this, SecondActivity.class);                                               startActivity(intent);                
            default:                    
                break;        
        }    
    }    
    /**     
    * 查询app是否有相关权限    
    * 如果有就直接调用写的方法     
    * 没有的话就需要申请权限     
    */    
    private void callPermission(){        
        if(ActivityCompat.checkSelfPermission(MainActivity.this,                                                Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){                                   ActivityCompat.requestPermissions(MainActivity.this,                        
              new String[]{Manifest.permission.CALL_PHONE}, 1);        
        }else {            
            callPhone();        
        }    
    }    

当用户点击了权限的弹窗后,Android会调用下面的onRequestPermissionsResult的方法,这个方法接受从requestPermissions()方法传递的requestCode、权限字符串数组和用户响应数组这三种作为参数,用户响应数组中的元素个数应与申请的权限字符串数组中元素个数保持一致。requestCode的作用是作为请求权限时权限处理成功的一种标识,只有这个标识匹配正确了,才能进一步的核对用户响应数组中的元素是否与PERMISSION_GRANTED相等,从而验证权限是否真正的被用户所允许。所以上一步的requestCode在这里发挥了作用。**应当注意的是,由于这个实例只用了一个权限,所以应该通过索引的方式来获取用户响应数组中的第一个元素grantResult[0]。

/** 
* 权限申请的回调结果 
* @param requestCode 请求码 
* @param permissions 请求权限 
* @param grantResults 授权结果,是一个int型数组,若有多个授权,则依次读取 */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,                                       @NonNull int[] grantResults) {                                                      super.onRequestPermissionsResult(requestCode, permissions, grantResults);    
      if(requestCode == 1){        
          if(grantResults[0] == PackageManager.PERMISSION_GRANTED){            
              callPhone();        
            }else {            
              Toast.makeText(this, "权限未授权!", Toast.LENGTH_SHORT).show();        
        }    
    }
}
/** 
* 打电话,注意异常处理,不然会报错 
*/
private void callPhone(){    
    try{        
        Intent intent = new Intent(Intent.ACTION_CALL);        
        Uri uri = Uri.parse("tel:" + 10086);        
        intent.setData(uri);        
        startActivity(intent);    
    }catch (SecurityException e){        
        e.printStackTrace();    
    }
}

对于单个的权限而言,上述的流程就可以完成权限获取的全部操作,在手机端运行程序,点击获取电话权限后就会弹出权限窗口,点击允许后转到电话拨打的界面。

那么如果想一次性申请多个权限,该如何处理这种需求?

多个权限的获取过程

假设需要一个按钮来获取两个权限:打电话权限和摄像头权限。处理的方式和上面的大同小异,如果你注意到上述请求权限和处理权限响应的方法中,它们都是接收一个权限字符串数组和用户响应字符串数组,那么问题就很好解决了。思路如下:

  • 构建一个申请权限的ArrayList

  • 检测权限,并将没有被授予允许的权限通通addArrayList

  • 转换ArrayList变为requestPermissions的参数

  • 依次读取用户响应数组中的grantCode,判断是否授权

  • 授权过程结束

下面的代码展示了如何一键处理两个权限的过程。

private void callAllPermissions(){    
    List<String> permissionsList = new ArrayList<>(); 
    // 如果没有被授权,那么就add到permissionsList中
    if(ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)            
       != PackageManager.PERMISSION_GRANTED){                                                              permissionsList.add(Manifest.permission.CALL_PHONE);    
    }    
    if(ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)            
       != PackageManager.PERMISSION_GRANTED){        
        permissionsList.add(Manifest.permission.CAMERA);    
    }    
    //不为空,说明有需要授权的部分,则进行请求权限步骤    
    if(!permissionsList.isEmpty()){        
        ActivityCompat.requestPermissions(this,                
              permissionsList.toArray(new String[permissionsList.size()]), 1);    
    }
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {    
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);    
    switch (requestCode){        
        case 1:            
            int resultLength = grantResults.length;            
            //说明回调成功了,权限授权被允许            
            if(resultLength > 0){                
                for(int grantCode : grantResults){                    
                    if(grantCode == PackageManager.PERMISSION_GRANTED){                                                      Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();                   
                 }else{           
                        Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();                                    }                
                }            
            }           
            break;            
        default:                
            break;    
    }
}

上述的过程完成后,程序所需要的权限得到了满足,便可以继续的进行后续的业务逻辑。但是仍然要提醒一点,Android 6.0以后,权限是可以由用户手动关闭的,并不是永久授权,这意味着今天的授权成功并不代表着明天就不需要授权了,因此权限的检查是必须要有的一个步骤。

总结

在以前学习Android的时候接触过权限处理,所以这次结合业务上遇到权限处理的问题,借助Android Developer的指南,对Android 6.0后的权限问题进行了一次重新的梳理。通过实例和流程图来展示Android对于危险权限的获取过程和一些应该注意的地方。同时也应该时刻的关注官网的指南,因为权限问题可能随着版本的更迭而发生一些调整或改变,不然很容易出现代码一样但出现异常的情况。


相关参考:
Android Developer

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

推荐阅读更多精彩内容