去年底,上级主管部门为加强国内Android应用隐私管理,出台了一系列规定,我们的App也做了相应的修改。主要一条修改为,隐私提示与权限获取顺序。修改测试过程中,发觉部分同学对Android权限相关知识和历史并不了解,就此疫情期间忙里偷闲,整理些东西供参阅。
首先,从一张图开始此文。
时间回到2013年,苹果公司发布IOS7系统。其中一项令开发者头疼的修改点:隐私中增加相册、录音等权限,App如需使用相应权限,需要申请并由用户同意(IOS7以前,可以直接访问相册)。
针对此点,很多App在首次启动时一通弹窗,申请各式各样的权限。后来苹果为改善用户体验,在App Store审核时要求App必须在使用前一刻才能申请权限,有效改善了此类问题。比如一款直播App,当你启动App时并不需要相机、录音权限,等到你开播时才需要申请这两个权限。这一场景,其实就类似今天要提到的Android动态授权。
谷歌于2015年推出Android 6.0 Marshmallow,其中一个主要特点便是加入了危险权限管理。这里的“危险权限管理”就带来了“运行时权限”这个新特性。
“危险权限管理”即在进行一些涉及到用户隐私的操作时,需要获取用户的授权才能使用。如通讯录、短信、相机、定位等隐私权限。获取用户权限,谷歌提倡在应用运行时向其授权,简称,运行时权限(也被叫做“动态权限/动态授权”,后文称“动态权限”)。
那,在这之前,Android权限管理是怎样的呢?自己杜撰了下国内Android权限管理经历的大概四个阶段。
早期Android系统(Android 6.0以前),在安装App前,会罗列出App申请的所有权限。如果继续安装,视为用户同意赋予App所需权限。
例如:sony L36h Android 4.2.2系统。在尝试安装App时,弹窗罗列了App申请的全部权限。只能对所需权限进行查看,无法拒绝授权,可选择取消安装或继续安装。
Sony L36h安装提示
这种方式,对于开发者极为友好,仅需在Manifest中配置App所需权限即可,代码就可以直接调用了。但是对于用户来说,这种方法存在极大的安全隐患。
例:获取手机IMEI,需要PHONE_STATE权限;访问网络,需要INIERNET权限。只许在Manifest文件中添加权限即可。
<!-- PHONE_STATE权限-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 网络权限-->
<uses-permission android:name="android.permission.INTERNET" />
基于以上背景,为解决部分敏感权限被不合理使用,国内部分公司的安全类App,开始监控应用获取手机敏感权限并做出提示。如360手机卫士、腾讯手机管家等产品,当监测到有App尝试使用短信权限、定位等敏感权限,会告知用户,并可以拒绝赋予权限。刚开始,还比较顺利。但随着手机厂商逐渐开始修改ROM,第三方安全App的兼容、性能问题逐步爆发。
例:HTC T328 Android 4.0.2系统。浏览器扫码功能触发相机调用时,360手机卫士会弹出权限提示窗,用户可以允许或拒绝授权。注意,此窗由第三方安全软件弹出,非系统级弹窗,跟后面要说的两种弹窗有所区别。
360手机卫士 弹窗
随着时间的推移,手机厂商开始发力,纷纷将第三方软件的权限提示功能直接做入ROM。
例:小米4,基于Android 4.4.4的MIUI7;oppo R9,基于Android 5.1的ColorOS 3.0,浏览器扫码功能触发相机调用时,会弹出权限提示窗。此窗,由ROM也就是系统自己弹出,为系统级权限弹窗。
小米4授权
OPPO R9授权
以上3个时期,App在申请权限时都不需做改变,只需配置Manifest。2015年推出的Android 6.0,加入了危险权限管理。因手机厂商对ROM的修改,部分6.0以上机器并不支持此项特性。
到了第四阶段,App需要在对权限代码进行修改后,才能正常使用对应权限。简单理解为3步:1、判断是否授权;2、如果未授权需申请权限,根据授权结果继续执行;3、已授权可以继续操作。
例:Pixel2,原生Android 10;华为mate8,基于Android 8.0的EMUI8。浏览器扫码功能触发相机调用时,会弹出权限提示窗。此窗,由App通知系统弹出,为系统级权限弹窗。
pixel2授权弹窗
华为mate8授权弹窗
第三阶段与第四阶段,同为系统弹出授权弹窗。二者有什么区别吗?
首先,从UI上很难判断所弹授权窗为第三阶段或第四阶段。第三阶段弹的系统授权窗大都带有一个倒计时自动拒绝逻辑;第四阶段弹的系统授权窗基本不带自动拒绝逻辑。此点可以粗略判断系统使用的哪种机制。
其次,从原理上。第三阶段的弹窗,为系统监测到App在使用危险权限行为自动弹出弹窗。第四阶段的弹窗,为App发觉自己没有权限,让系统弹出的弹窗。粗俗的理解,第三阶段,你去朋友家串门,到门口看到大门敞开就直接往里走,触发了红外线报警器,报警器通知了你朋友;第四阶段,你去朋友家串门,到门口发觉门关着,就按下门铃呼叫朋友给你开门。
目前,国内主要处于第三阶段(涵盖Android4.0~7.1)和第四阶段(涵盖Android6.0~10),此点将在后文用到。
因为动态权限特性,仅从Android 6.0开始拥有,所以,可以简单粗暴的通过不提升targetSDK(targetSDK<23)的方式,便可不触发此特性。
targetSDK18正常获取IMEI
仅提升targetSDK到26直接运行崩溃
如果不改变任何代码,直接将targetSDK提升到26,然后运行App,做同样操作时会发生异常甚至崩溃,崩溃举例如下:
无PHONE_STATE获取IMEI崩溃
产生这个崩溃的原因,是在Android 6.0及以上,未获取权限的情况下直接执行了需要权限的操作。那么如何解决呢,就涉及到了真正的修改方案。
首先,我们需要判断自己是否拥有权限。判断时间点为执行需要权限的对应操作前。如我们在获取IMEI前,需要判断是否拥有PHONE_STATE权限。
我们可以调用ContextCompat.checkSelfPermission()方法检测授权状态,返回的结果为PackageManager中的两个常量:PERMISSION_GRANTED(已授权)和PERMISSION_DENIED(未授权)。
当已授权时,就可以执行你原有的操作了。代码如下:
// 检测PHONE_STATE 如果已授权
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
//做你想做的
}
那么如果未授权怎么办?
如果App未获得授权,我们就需要向用户申请授权。可以调用requestPermissions()方法来请求授权。代码如下:
// 检测PHONE_STATE 如果未授权
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
//申请权限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), PERMISSIONS_REQUEST_PHONE_STATE)
}
requestPermissions()中的第三个参数是一个int型请求码,方便回调处理。
调用申请授权方法后,ROM会调起一个系统级弹窗(如下图),这个dialog你无法定制。当用户点击同意后,系统会记录,下次再判断权限时就会返回已授权状态;当App卸载时,记录会被清除。
Android 10授权弹窗
以上,就完成了最朴素版的授权逻辑。整体代码如下:
// 检测PHONE_STATE 如果未授权
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
//申请权限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), PERMISSIONS_REQUEST_PHONE_STATE)
}else {
//如果已授权做你想做的
}
那么弹出申请弹窗之后呢?上面说道,弹出的dialog为系统的,我们无法在dialog中加代码,但当弹窗被用户点击后,会触发回调,我们在指定函数中处理回调即可。
直接在Activity或Fragment中重写onRequestPermissionsResult()函数,来处理权限申请结果。requestPermissions()的第三个参数,将在这里被用到。代码如下:
// 处理授权弹窗回调
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when(requestCode){
// 识别刚刚用到的请求码,根据请求码识别不同弹窗回调并处理
PERMISSIONS_REQUEST_PHONE_STATE ->{
// 如果用户点击“允许”
if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "用户允许权限!",Toast.LENGTH_SHORT).show()
// 可以继续执行你原来想做的事情了
}else{
Toast.makeText(this, "用户拒绝权限!",Toast.LENGTH_SHORT).show()
// 用户拒绝了,你想咋办?
}
return;
}
// 可以识别其他请求码并处理
}
}
这样,就完成了授权流程。然后,为提升授权概率,对流程进行优化。
首先,系统授权窗我们无法定制,但是我们可以在这之前做个引导。在触发系统弹窗之前,弹出一个引导UI,来告知用户将要申请权限,并说明所需权限可带来哪些更好体验。尤其当你申请的权限看似与主要功能并无关系时,比如一个相机App如果需要申请定位权限的时候。
其次,谷歌官方还提供了个函数shouldShowRequestPermissionRationale(),这个函数可以用来判断,用户上次是否拒绝了且未选则不再询问。可以在授权前,通过此判断,来决定给用户展示首次授权引导或非首次授权引导。
最后,当用户还是选择了拒绝授权时,如果是必要权限(比如导航软件申请定位权限),我们可以通过处理授权回调,在用户点击拒绝时弹出引导,告知用户功能不可用,并引导用户重新授权或到设置中手动开启权限。
以上3部分大体流程如下:
引导授权流程
综上,动态权限主要实现步骤
a) 判断如果没有被拒过,弹出首次授权引导。
b) 判断如果被据过,弹出非首次授权引导。
系统一共提供如下4个函数完成动态权限相关操作。
/**
* 检查指定的权限是否授权(Context对象调用)
*/
public static int checkSelfPermission (Context context,
String permission)
/**
* 在没有授权的情况下,有些时候可能需要提示给用户为什么需要改权限,就通过该函数来实现。
* 关于shouldShowRequestPermissionRationale的返回值问题,我们分三种情况
* 1. 第一次打开App时 -> false
* 2. 上次弹出权限点击了禁止(但没有勾选“下次不在询问”) -> true
* 3. 上次选择禁止并勾选:下次不在询问 -> false
*/
public static boolean shouldShowRequestPermissionRationale (Activity activity,
String permission)
/**
* 申请指定的权限(Activity或者Fragment对象调用)
* @param permissions 权限列表,可以同时申请多个权限
* @param requestCode 该次权限申请对应的requestCode。和 onRequestPermissionsResult()回调函数里面的requestCode对应
*/
public static void requestPermissions (Activity activity,
String[] permissions,
int requestCode)
/**
* 处理请求权限的响应,当用户对请求权限的dialog做出响应之后,系统会回调该函数(Activity或者Fragment中重写)
* @param requestCode 申请权限对应的requestCode
* @param permissions 权限列表
* @param grantResults 权限列表对应的返回值,判断permissions里面的每个权限是否申请成功
*/
public abstract void onRequestPermissionsResult (int requestCode,
String[] permissions,
int[] grantResults)
写到这里,动态授权实现demo部分均已完成,实际业务场景肯定比以上流程复杂的多。
动态权限为Android 6.0新特性,那低于6.0的系统,该如何写适配代码呢?
首先想到的,是判断系统版本,针对6.0以上使用动态权限代码,针对低版本,使用老代码。
fun test(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
// 走动态授权
return
else
// 走非动态授权
return
}
其实,可以不必如此麻烦。对于低版本,可以不必单独写代码适配。在不支持动态授权的系统上,Manifest中申请过的权限,checkSelfPermission()方法,会直接返回PERMISSION_GRANTED。
另外,根据系统版本区分是否支持动态权限,实际是不靠谱的。前文有提到,部分手机厂商在ROM提升到Android 6.0以后,阉割了动态权限特性。目前没有找到准确的API判断当前系统是否支持动态权限。这会带来什么问题呢?
举一个前不久遇到的实例。App的某一功能,是对别人显示我所在城市(地理位置属于敏感数据),用户反馈关闭系统定位权限后,仍会显示他所在城市。我们需要考虑如何解决用户的问题,所以增加个需求,如果用户关闭了定位权限,则不获取城市。那么问题来了,怎么判断用户是否关闭了定位权限呢?为了避开不支持动态权限的ROM,需求只能退一步,6.0及以上系统做以上逻辑,6.0以下直接不获取地理位置。但是根据测试经验6.0以上系统仍不一定支持动态权限, 7.0及以上系统,绝大部分ROM支持动态权限。所以妥协决定7.0以下全部不获取,7.0以上调checkSelfPermission()判断是否授权,少数不支持动态权限的设备会误认为已授权,需要增加设置项关闭功能。(提升到Android8.0应该是绝对安全的,不过覆盖量太少)
以下为目前主流国内厂商对动态权限支持情况。(测试方法:在全新安装未进行过授权操作的情况下,使用checkSelfPermission()检查PHONE_STATE、定位、相机权限,返回如果是PERMISSION_GRANTED,则认为不支持动态权限)
基于Android6.0的ROM | 基于Android7.0的ROM | |
---|---|---|
小米 | 支持 | |
华为 | 支持 | 支持 |
OPPO | 不支持 | 支持 |
VIVO | 不支持 | 7.1.1不支持
7.1.2支持部分权限 |
魅族 | 支持 | |
锤子 | 不支持 | |
360 | 不支持 | 不支持 |
中兴 | 支持 |
Android 8.0授权弹窗:
关于权限弹窗,针对同一个App的同一个权限,有时弹窗不带“拒绝&不再询问”选项,有时带此选项。如下图是谷歌原生系统、小米MIUI系统的两种弹窗对比。这是什么原因呢?Android原生实现:App全新安装后首次申请权限,弹窗不带此选项,即图左效果。当用户拒绝授权后,App下次再申请该权限时,则带此选项,即图右效果。但是,国内部分手机厂商并未遵循此标准,比如华为的Android 10之前的系统、OPPO/VIVO的部分权限,授权弹窗不管是否首次,都带此选项。此为系统行为,App无法决定。
pixel2不再询问
MIUI不再询问
系统的授权弹窗,实际具有3项(允许、拒绝、 拒绝不再询问)。但设置中的App权限选项,有的系统有2项(允许、拒绝),有的有3项(允许、询问、拒绝)。授权弹窗选项与设置中的选项对应关系如下。
以原生Android 10系统为例:
pixel2弹窗对应设置
以基于Android 9.0的MIUI10.4.8为例:
MIUI弹窗对应设置
弹窗弹出,用户操作指定选项后,下次再调用四个函数会有如下现象:
UI选项与函数调用结果
Android 6.0系统开始,权限被分为Normal permissions、Signature permissions、Dangerous permissions,其中Signature permissions比较超纲,仅介绍普通权限和危险权限。
其中普通权限使用方法跟低版本一样,只用在Manifest里申请就可使用。大部分低风险权限,不需要通过确认框这种形式让用户显示的同意。比如访问网络、检查WiFi状态等权限。
另一种危险权限,也就是本文介绍的对象,它的产生主要为了保护用户隐私,换言之,涉及到用户隐私的一些权限,属于危险权限。例如:相机权限、定位权限、PHONE_STATE(可读取手机IMEI等识别码)权限等。
危险权限和权限组。(不同系统危险权限可能不同)
关于权限,还有一个权限组的概念。例如,读取外置存储权限(READ_EXTERNAL_STORAGE)和写入外置存储权限(WRITE_EXTERNAL_STORAGE),同属存储权限组(STORAGE)。
权限组有什么作用呢?在Android O之前,同一权限组的权限,只要用户授权一个,则整个权限组都被授权。
例如:
结果:可以直接使用,同组权限不需再申请。
而Android O对此进行了修改。同一权限组不同权限,必须都要动态申请权限。但是如果第一个被用户同意了,后面的同组权限再申请时,就不会再弹窗而是被直接同意了。
例如:
步骤一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE
步骤二:在程序中只申请了READ_EXTERNAL_STORAGE权限,用户同意后
步骤三:在程序中未申请WRITE_EXTERNAL_STORAGE权限,并尝试直接使用
结果:崩溃。
修改步骤三:在程序中申请WRITE_EXTERNAL_STORAGE权限
结果:不会弹出授权弹窗,同一权限组直接被自动授权
But,部分ROM修改了此逻辑。比如,华为9.0以下系统,遵循的是原生系统Android 8.0之前的逻辑。但是,华为9.0以后系统和小米6.0以后系统,都用的比原生系统Android 8.0更严格的逻辑。每个权限都需要单独申请权限,而且会单独弹窗要求用户确认。
例如:
步骤一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE
步骤二:在程序中只申请了READ_EXTERNAL_STORAGE权限,用户同意后
步骤三:在程序中申请WRITE_EXTERNAL_STORAGE权限
结果:会弹出授权弹窗,需要用户再次授权
带来问题:相同权限组不同权限的授权弹窗是一毛一样的。这就导致用户很懵逼,明明刚刚授权过了,为什么又要问我一次。
不同ROM权限组内影响
所以,部分手机上,你会发觉有些App,先后弹出两个访问文件存储的权限弹窗。那是因为写App的时候,先后申请了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限导致。如何解决?
查看requestPermissions()方法的第二个参数,为一个数组。也就是说,可以传入一个权限列表。
/**
* 申请指定的权限(Activity或者Fragment对象调用)
* @param permissions 权限列表,可以同时申请多个权限
* @param requestCode 该次权限申请对应的requestCode。和 onRequestPermissionsResult()回调函数里面的requestCode对应
*/
public static void requestPermissions (Activity activity,
String[] permissions,
int requestCode)
经测试,如果直接调该方法同时传入READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE只会弹出一个授权窗,而且用户同意后可以同时获得两个权限。如果传入不同组权限,则按先后每组弹出一个弹窗。而且,这种单次传入多组权限的情况,弹窗中大都会出现一个m/n的编号,以标识弹到第几个,还剩几个。如下图分别是MIUI10(基于android9)和EMUI10(基于android10)的弹窗样式:
红米Note8Pro连续授权窗
华为Mate20连续授权窗
后期的一些权限策略变化,仅列部分户感知较大的。
两大平台,都在多个版本中对用户隐私进行了优化,仅定位权限的优化就多次提及。
可见,在手机逐渐转化为人体器官之一的今天,IOS和Android两大移动平台对于权限、隐私的管理越发严苛,而且趋同的速度约来越快。估计以后Android App想访问网络也需申请授权。但手机厂商自行定制修改ROM,仍是开发者最头疼的问题。
参考文献:谷歌官方文档:https://developer.android.com/training/permissions/requesting.html
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。