Android6.0动态权限控制
在Android6.0(M)之前,在用户安装应用的时候会产生一个权限列表,只有用户允许这些权限后,应用才可以正常的安装,这就会产生一个问题,这些权限对用户是不具有感知性的,也就是说用户都不知道你要这些权限干什么,我明明装的是一个阅读类型的应用,你却要我拨打电话的权限,你想干嘛呢?当然绝大部分的开发者是善意的,但也避免不了一些特殊人群利用这些“漏洞”做一些不好的事情。
而在Android6.0(M)之后,用户是可以不管权限直接安装应用的,当应用需要调用某些权限的时候,会给予用户一个通知与说明,我要这些权限干什么,这样下来可以让用户有更加清醒的权限分配意识,也在一定程度上更加人性化的保护了用户的隐私,避免了“权限一刀切”。
权限分组
在Android6.0(M)之后,对权限进行了分类,大致有这三种:
普通权限 也就是正常权限,是对手机的一些正常操作,对用户的隐私没有太大影响的权限,比如手机的震动,网络访问,蓝牙等权限,这些权限会在应用被安装的时候默认授予,用户不能拒绝,也不能取消。
详细请参考:https://developer.android.com/guide/topics/permissions/normal-permissions.html
ACCESS_LOCATION_EXTRA_COMMANDS ACCESS_NETWORK_STATE ACCESS_NOTIFICATION_POLICY ACCESS_WIFI_STATE BLUETOOTH BLUETOOTH_ADMIN BROADCAST_STICKY CHANGE_NETWORK_STATE CHANGE_WIFI_MULTICAST_STATE CHANGE_WIFI_STATE DISABLE_KEYGUARD EXPAND_STATUS_BAR GET_PACKAGE_SIZE INSTALL_SHORTCUT INTERNET KILL_BACKGROUND_PROCESSES MODIFY_AUDIO_SETTINGS NFC READ_SYNC_SETTINGS READ_SYNC_STATS RECEIVE_BOOT_COMPLETED REORDER_TASKS REQUEST_IGNORE_BATTERY_OPTIMIZATIONS REQUEST_INSTALL_PACKAGES SET_ALARM SET_TIME_ZONE SET_WALLPAPER SET_WALLPAPER_HINTS TRANSMIT_IR UNINSTALL_SHORTCUT USE_FINGERPRINT VIBRATE WAKE_LOCK WRITE_SYNC_SETTINGS
对于上面这些权限,和Android6.0(M)之前的系统一样,在AndroidManifest.xml声明即可。
|
|
危险权限
详细请参考文档:https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous
其实就是运行中需要处理的权限,也是我们最需要注意的权限,这些权限会关系到用户的隐私或影响到其他应用的运行,这些危险权限,谷歌还做了一个权限组,以分组的形式来呈现:
由于运行权限机制的出现,变得我们需要对新开发的应用去做适配,如果是6.0之前开发的谷歌做了良好的适配:当你的应用targetSdkVersion小于23的时候,就算你运行在Android6.0系统上,它也会默认采用以前的权限管理机制,也就是一刀切。当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它才会采用新的这套权限管理机制。
所以如果你想逃开这个“麻烦”,只要把targetSdkVersion的版本设置为低于23就可以了,不过不建议采用这种方案,该来的总是要来的,随着国产手机ROM的更新,比如小米,华为等也开始有部分机型进行了系统升级,所以这是种趋势。 说了这么多,那么来看下怎么进行Android6.0(M)的权限管理适配吧,其实很简单,只需要记住下面几个API方法就可以:(API23之后提供)
|
|
运行时权限请求
如果您的应用需要危险权限,则每次执行需要这一权限的操作时您都必须检查自己是否具有该权限。用户始终可以自由调用此权限,因此,即使应用昨天使用了相机权限,它不能确定今天仍具有该权限。
检查权限
要检查您是否具有某项权限,请调用 ContextCompat.checkSelfPermission() 方法。例如,以下代码段显示了如何检查 Activity 是否具有在日历中进行写入的权限。
|
|
如果应用具有此权限,方法将返回 PackageManager.PERMISSION_GRANTED,并且应用可以继续操作。如果应用不具有此权限,方法将返回 PERMISSION_DENIED,且应用必须明确向用户要求权限。
请求权限
如果您的应用需要应用清单中列出的危险权限,那么,它必须要求用户授予该权限。Android 为您提供了多种权限请求方式。调用这些方法将显示一个标准的 Android 对话框,不过,您不能对它们进行自定义。
如果应用尚无所需的权限,则应用必须调用一个 requestPermissions() 方法,以请求适当的权限。应用将传递其所需的权限,以及您指定用于识别此权限请求的整型请求代码。此方法异步运行:它会立即返回,并且在用户响应对话框之后,系统会使用结果调用应用的回调方法,将应用传递的相同请求代码传递到 requestPermissions()。
|
|
处理权限请求的响应
当应用请求权限时,系统将向用户显示一个对话框。当用户响应时,系统将调用应用的 onRequestPermissionsResult() 方法,向其传递用户响应。您的应用必须替换该方法,以了解是否已获得相应权限。回调会将您传递的相同请求代码传递给 requestPermissions()。
|
|
不再提醒勾选
上面的那张图,有的朋友应该已经留意到了有个不再提醒的勾选框,如果用户勾选了不再提醒,然后把你拒绝了,那你的应用就GG了,其实这里还有一个API方法:
|
|
当用户勾选了不再提醒的框并把你拒之门外了,这个方法的返回值是true,它可以帮助你再一次的提醒用户,你需要这个权限,也是你最后翻身的方法了。但是在实际开发中,不得不说的一个坑,由于国内第三方ROM对系统改造的太严重,比如小米,亲测有些机型的这个方法是不起作用的,永远的是返回false,这个时候该怎么办,就要另外想解决方案了。
权限分组的意义
需要注意的一点是上面的权限分组,比如读写文件权限它们是属于同一个权限组的,你如果拿到了他们其中的一个权限,那么也同时会有另一个权限,同理,如果你拿到读取通讯录的权限,那么你同事也会拥有写入通讯录的权限,这样就避免了我们在申请相关权限的时候需要些老长老长的权限代码了。
WRITE_EXTERNAL_STORAGE READ_EXTERNAL_STORAGE
特殊权限
特殊权限,比如: 系统级别对话框:SYSTEM_ALERT_WINDOW 修改系统设置:WRITE_SETTINGS 这2个特殊权限,我们需要在startActivityForResult里调用即可,这2个权限一般是不会用到,会用到的地方要么是黑科技或者是反用户体验的场景,这里就不再做过多描述,有兴趣的朋友自己探索吧。
这里需要另外提到的一个权限:READ_PHONE_STATE 我们可以通过这个权限来获取机器的唯一标识码,很多第三方统计是基于这个标识码来完成统计的,但是在我们应用一开始运行的时候,这个运行权限我们是没有的,在Application里我们也不能对权限进行获取,所以这点也需要我们去注意。
6.0权限的最佳使用方法
应用如果一味要求用户提供授权,可能会让用户无所适从。如果用户发现应用难以使用,或者担心应用会滥用其信息,他们可能不愿意使用该应用,甚至会将其完全卸载。以下最佳做法有助于避免此类糟糕的用户体验。
考虑使用intent
许多情况下,您可以使用以下两种方式之一来让您的应用执行某项任务。您可以将应用设置为要求提供权限才能执行操作。或者,您可以将应用设置为使用 intent,让其他应用来执行任务。
例如,假设应用需要使用设备相机才能够拍摄照片。应用可以请求 CAMERA 权限,以便允许其直接访问相机。然后,应用将使用 Camera API 控制相机并拍摄照片。利用此方法,您的应用能够完全控制摄影过程,并支持您将相机 UI 整合至应用中。
不过,如果您无需此类完全控制,则可以使用 ACTION_IMAGE_CAPTURE intent 来请求图像。发送该 intent 时,系统会提示用户选择相机应用(如果没有默认相机应用)。用户使用选定的相机应用拍摄照片,该相机应用会将照片返回给应用的 onActivityResult() 方法。
同样,如果您需要拨打电话、访问用户的联系人或要执行其他操作,可以通过创建适当的 intent 来完成,或者您也可以请求相应的权限并直接访问相应的对象。每种方法各有优缺点。
仅要求您需要的权限,在需要的时候才请求
每次您要求权限时,实际上是在强迫用户作出决定。您应尽量减少提出这些请求的次数。如果用户运行的是 Android 6.0(API 级别 23)或更高版本,则每次用户尝试要求提供权限的新应用功能时,应用都必须中断用户的操作并发起权限请求。如果用户运行的是较早版本的 Android,则在安装应用时需要为应用的每一权限请求给予授权;如果列表过长或看起来不合适,用户可能会决定不安装该应用。为此,您应尽量减少应用需要的权限数。
某些情况下,一项或多项权限可能是应用所必需的。在这种情况下,合理的做法是,在应用启动之后立即要求提供这些权限。例如,如果您运行摄影应用,应用需要访问设备的相机。在用户首次启动应用时,他们不会对提供相机使用权限的要求感到惊讶。但是,如果同一应用还具备与用户联系人共享照片的功能,您不应在应用首次启动时要求用户提供 READ_CONTACTS 权限,而应等到用户尝试使用“共享”功能之后,再要求提供该权限。
解释需要权限的原因
系统在您调用 requestPermissions() 时显示的权限对话框将说明应用需要的权限,但不会解释为何需要这些权限。某些情况下,用户可能会感到困惑。因此,最好在调用 requestPermissions() 之前向用户解释应用需要相应权限的原因。
例如,摄影应用可能需要使用位置服务,以便能够为照片添加地理标签。通常,用户可能不了解照片能够包含位置信息,并且对摄影应用想要了解具体位置感到不解。因此在这种情况下,应用最好在调用 requestPermissions() 之前告知用户此功能的相关信息。
自定义权限
要自定义权限必须使用一个或多个
|
|
注:系统不允许多个软件包使用同一名称声明权限,除非所有软件包都使用同一证书签署。如果软件包声明权限,则系统不允许用户安装具有相同权限名称的其他软件包,除非这些软件包使用与第一个软件包相同的证书签署。为避免命名冲突,建议对自定义权限使用相反域名样式命名,例如 com.example.myapp.ENGAGE_HYPERSPACE。
protectionLevel 属性是必要属性,用于指示系统如何向用户告知需要权限的应用,或者谁可以拥有该权限,具体如链接的文档中所述。
android:permissionGroup 属性是可选属性,只是用于帮助系统向用户显示权限。大多数情况下,您要将此设为标准系统组(列在 android.Manifest.permission_group 中),但您也可以自己定义一个组。建议使用现有的组,因为这样可简化向用户显示的权限 UI。
Android7.0系统权限更改
随着Android版本越来越高,Android对隐私的保护力度也越来越大。从Android6.0引入的动态权限控制(Runtime Permissions)到Android7.0的“私有目录被限制访问”,“StrictMode API 政策”。这些更改在为用户带来更加安全的操作系统的同时也为开发者带来了一些新的任务。如何让你的APP能够适应这些改变而不是crash,是摆在每一位Android开发者身上的责任。
目录被限制访问
一直以来,在目录及文件的访问保护方面iOS做的是很到位的,如:iOS的沙箱机制。但,Android在这方面的保护就有些偏弱了,在Android中应用可以读写手机存储中任何一个目录及文件,这也带来了很多的安全问题。现在Android也在着力解决这一问题。
在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。对于这个权限的更改开发者需要留意一下改变:
私有文件的文件权限不在放权给所有的应用,使用 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 进行的操作将触发 SecurityException。
应对策略:这项权限的变更将意味着你无法通过File API访问手机存储上的数据了,基于File API的一些文件浏览器等也将受到很大的影响,看到这大家是不是惊呆了呢,不过迄今为止,这种限制尚不能完全执行。 应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。 但是,Android官方强烈反对放宽私有目录的权限。可以看出收起对私有文件的访问权限是Android将来发展的趋势。
给其他应用传递 file:// URI 类型的Uri,可能会导致接受者无法访问该路径。 因此,在Android7.0中尝试传递 file:// URI 会触发 FileUriExposedException。
应对策略:大家可以通过使用FileProvider来解决这一问题。
DownloadManager 不再按文件名分享私人存储的文件。COLUMN_LOCAL_FILENAME在Android7.0中被标记为deprecated , 旧版应用在访问 COLUMN_LOCAL_FILENAME时可能出现无法访问的路径。 面向 Android N 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException。
应对策略:大家可以通过ContentResolver.openFileDescriptor()来访问由 DownloadManager 公开的文件。
应用间共享文件
在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片。
应对策略:若要在应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider类。 如需有关权限和共享文件的更多信息,请参阅共享文件。
在Android7.0上调用系统相机拍照,裁切照片
调用系统相机拍照
在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:
|
|
在Android7.0上使用上述方式调用系统相拍照会抛出如下异常:
|
|
这是由于Android7.0执行了“StrictMode API 政策禁”的原因,不过小伙伴们不用担心,上文讲到了可以用FileProvider来解决这一问题, 现在我们就来一步一步的解决这个问题。
使用FileProvider
使用FileProvider的大致步骤如下: 第一步:在manifest清单文件中注册provider
|
|
心得:exported:要求必须为false,为true则会报安全异常。grantUriPermissions:true,表示授予 URI 临时访问权限。
第二步:指定共享的目录
为了指定共享的目录我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:
|
|
- <\files-path/>代表的根目录: Context.getFilesDir()
- <\external-path/>代表的根目录: Environment.getExternalStorageDirectory()
- <\cache-path/>代表的根目录: getCacheDir()
心得:上述代码中path="",是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path=“pictures”, 那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
第三步:使用FileProvider
上述准备工作做完之后,现在我们就可以使用FileProvider了。 还是以调用系统相机拍照为例,我们需要将上述拍照代码修改为如下:
|
|
上述代码中主要有两处改变:
将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri。
添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。
心得:上述代码通过FileProvider的Uri getUriForFile (Context context, String authority, File file) 静态方法来获取Uri,该方法中authority参数就是清单文件中注册provider的android:authorities=“com.jph.takephoto.fileprovider”。 对Web服务器如tomcat,IIS比较熟悉的小伙伴,都只知道为了网站内容的安全和高效,Web服务器都支持为网站内容设置一个虚拟目录,其实FileProvider也有异曲同工之处。
将getUriForFile方法获取的Uri打印出来如下:
|
|
其中camera_photos就是file_paths.xml中paths的name。
因为上述指定的path为path="",所以content://com.jph.takephoto.fileprovider/camera_photos/代表的真实路径就是根目录,即:/storage/emulated/0/。 content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg代表的真实路径是:/storage/emulated/0/temp/1474960080319.jpg。
裁切照片
在Android7.0之前,你可以通过如下方法来裁切照片:
|
|
和拍照一样,上述代码在Android7.0上同样会引起android.os.FileUriExposedException异常,解决办法就是上文说说的使用FileProvider。
然后,将上述代码改为如下即可:
|
|
参考文章:
https://developer.android.com/about/versions/marshmallow/index.html https://developer.android.com/about/versions/marshmallow/index.html http://www.jianshu.com/p/a37f4827079a http://www.jianshu.com/p/56b9fb319310