手机系统应用的权限有什么管理办法吗

mirrorside · 4 天前 via Android · 948 次点击

手持三星 s20+,权限一直在用 appops 管理,一般下载的 app 除了必要权限基本都是关闭,今天突然想起我权限设置里一直都是选的不显示系统应用,打开一看,一个权限至少有几十个上百个系统应用正开启着呢

问题来了,除了一些名字上一看就知道用不上某些权限的系统应用,*大部分系统应用我完全不熟悉,没法下手管理。。。这咋整

11 条回复    2021-10-15 11:54:43 +08:00

SenLief
    1

SenLief   4 天前 via Android

换手机
mirrorside
    2

mirrorside   4 天前 via Android

@SenLief 这 个人觉得是个手机应该都会有这问题
dingwen07
    3

dingwen07   4 天前

操作系统要获取权限不要太简单。
不过你可以阅读你使用的操作系统的隐私政策。
mirrorside
    4

mirrorside   4 天前 via Android

@dingwen07 算了。。几百个系统应用基本不可能管理的了了。。主要是这次不小心打开了系统应用的权限显示一下子搞得我强迫症犯了。。

想了想还是把这个显示关掉当作不存在吧。。。

SenLief
    5

SenLief   4 天前 via Android

@mirrorside 权限问题确实 ios 好些
Karonheaven
    6

Karonheaven   4 天前

如果不想解锁 BL 刷第三方系统的话,基本上内置程序都有点隐私毛病
或许可以试试在设置里面禁用一些用不上的内置应用,或者用 IceBox 冻起来( MIUI 可以禁用,不知道三星是否可以)
mirrorside
    7

mirrorside   4 天前 via Android

@Karonheaven 系统应用太多了,大几十个,许多根本不知道是什么东西,搜都要都半天才知道,而且也不知道禁用权限会不会出问题,算了,把显示系统应用权限关掉,眼不见心不烦吧。。。
cheng6563
    8

cheng6563   3 天前

得了吧,系统应用可以随时提升到 root,AppOps 里面那几个遮羞布有什么用
psklf
    9

psklf   3 天前

系统应用且你看不懂,一般不管。
mirrorside
    10

mirrorside   3 天前 via Android

@cheng6563 是的。。我想想后也懒得管了。。
alittlecat
    11

alittlecat   2 天前

虽然跑题但是..我就是因为和这些应用斗智斗勇到心力憔悴 一票否决了安卓机的

iOS开发之系统权限

背景
配合监管要求,针对APP用户获取管理权限做如下整改:

APP在首次启动页面对权限进行解释说明(包括解释权限的涵义以及说明使用权限的用途)
个人中心-设置,增加权限设置菜单,展示涉及获取的所有权限,以及展示当前开启/关闭状态
一切为了监管……
软件环境
Xcode11.2
Swift4.2
iOS13.2.2
开门见山,上代码
工具类:SystemPermissionMonitor.swift

检测相机权限
static func checkCamera() -> (Bool, AVAuthorizationStatus) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
return (status == .authorized, status)
}

检测相册权限
// (iOS 8, *)
static func checkPhotoLibrary() -> (Bool, PHAuthorizationStatus) {
let status = PHPhotoLibrary.authorizationStatus()
return (status == .authorized, status)
}
// (iOS, introduced: 6.0, deprecated: 9.0, message: “Use PHAuthorizationStatus in the Photos framework instead”)
static func checkPhotoLibrary4oldVersion() -> (Bool, ALAuthorizationStatus) {
let status = ALAssetsLibrary.authorizationStatus()
let authorized = status == .authorized
return (authorized, status)
}

获取相册权限
static func openPhotoLibraryAlert() -> (Bool, PHAuthorizationStatus) {
let sema = DispatchSemaphore(value: 0)
var status: PHAuthorizationStatus = .notDetermined
PHPhotoLibrary.requestAuthorization { (authStatus) in
status = authStatus
sema.signal()
}
let _ = sema.wait(timeout: DispatchTime.distantFuture)
return (status == .authorized, status)
}

检测定位权限
static func checkLocation() -> (Bool, CLAuthorizationStatus) {
let status = CLLocationManager.authorizationStatus()
return (status == .authorizedAlways || status == .authorizedWhenInUse, status)
}

检测通知权限
static func checkNotification() -> (Bool, Any?) {
if #available(iOS 10.0, *) {
let sema = DispatchSemaphore(value: 0)
var status: UNAuthorizationStatus = .notDetermined
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
status = settings.authorizationStatus
sema.signal()
}
let _ = sema.wait(timeout: DispatchTime.distantFuture)
return (status == .authorized, status)
} else {
let settings = UIApplication.shared.currentUserNotificationSettings
let types = settings?.types
let isEnabled = types == [.alert, .badge, .sound]
return (isEnabled, types)
}
}

ViewModel:SystemPermissionViewModel.swift

App需要用到的权限枚举
enum PermissionType: Int {
case PhotoLibrary
case Camera
case Location
case Notification
case Audio
case Others
}

 

实体
struct PermissionModel {
var name: String?
var status: Any?
var authorized: Bool = false
var permissionType: PermissionType = .Others
init(_ name: String?, permissionType: PermissionType) {
self.name = name
self.permissionType = permissionType
}
}

 

数据初始化
var permissionArray: [PermissionModel] = []
override func setup() {
super.setup()
let permissionNameArray = [“相册”,”相机”,”地理位置”,”通知”, “麦克风”, “其他”]
let permissionTypeArray: [PermissionType] = [.PhotoLibrary, .Camera, .Location, .Notification, .Audio, .Others]
permissionArray = []
for (i, name) in permissionNameArray.enumerated() {
let model = PermissionModel(name, permissionType: permissionTypeArray[i])
permissionArray.append(model)
}
// Check permission
checkPermission()
// Reload tableView
updateDataArray(permissionArray)
}

 

检测是否开启各权限
private func checkPermission() {
let (authorized4photoLibrary, status4photoLibrary) = SystemPermissionMonitor.checkPhotoLibrary()
filterPermissionArray(authorized4photoLibrary, status: status4photoLibrary, permissionType: .PhotoLibrary)

let (authorized4camera, status4camera) = SystemPermissionMonitor.checkCamera()
filterPermissionArray(authorized4camera, status: status4camera, permissionType: .Camera)

let (authorized4location, status4location) = SystemPermissionMonitor.checkLocation()
filterPermissionArray(authorized4location, status: status4location, permissionType: .Location)

let (authorized4notification, status4notification) = SystemPermissionMonitor.checkNotification()
filterPermissionArray(authorized4notification, status: status4notification, permissionType: .Notification)

let (authorized4audio, status4audio) = SystemPermissionMonitor.checkAudio()
filterPermissionArray(authorized4audio, status: status4audio, permissionType: .Audio)
}

 

过滤不需要显示的权限&更新权限实体状态
private func filterPermissionArray(_ authorized: Bool, status: Any?, permissionType: PermissionType) {
for (i, model) in permissionArray.enumerated() {
if model.permissionType != permissionType { continue }

if isNotDetermined(status, permissionType: permissionType) {
// Remove permissions that user has not yet made a choice with regards to this application
permissionArray.remove(at: i)
} else {
// Update the value of the property
permissionArray[i].authorized = authorized
permissionArray[i].status = status
}
break
}
}

 

判断用户是否尚未对此应用程序的权限作出选择(.notDetermined)
/// Whether user has not yet made a choice with regards to this application
private func isNotDetermined(_ status: Any?, permissionType: PermissionType) -> Bool {
switch permissionType {
case .PhotoLibrary:
let status: PHAuthorizationStatus? = status as? PHAuthorizationStatus
return status == .notDetermined
case .Camera, .Audio:
let status: AVAuthorizationStatus? = status as? AVAuthorizationStatus
return status == .notDetermined
case .Location:
let status: CLAuthorizationStatus? = status as? CLAuthorizationStatus
if #available(iOS 13, *) {
// “Allow once”, your app will be notified that the CLAuthorizationStatus changed to authorizedWhenInUse. Just like you’re used to in older iOS version when you get a permanent permission. It is now allowed for your app to start requesting locations, no code changes necessary.
// Users can jump out and back into your app, and you will still have location permission. It’s only after a (longer) period of inactivity that iOS will revoke the permission and turn the CLAuthorizationStatus back to notDetermined
// Return false, adapting to the new ‘allow just once’ location permission in iOS 13
return false
}
return status == .notDetermined
case .Notification:
if #available(iOS 10.0, *) {
let status: UNAuthorizationStatus? = status as? UNAuthorizationStatus
return status == .notDetermined
}
break
default:
break
}
return false
}

效果图

%title插图%num

深入理解Android 7.0系统权限更改相关文档

摘要:

Android 6.0之后的版本增加了运行时权限,应用程序在执行每个需要系统权限的功能时,需要添加权限请求代码(默认权限禁止),否则应用程序无法响应;Android 7.0在Android 6.0的基础上,对系统权限进一步更改,这次的权限更改包括三个方面:

  1. APP应用程序的私有文件不再向使用者放宽
  2. Intent组件传递file://URI的方式可能给接收器留下无法访问的路径,触发FileUriExposedException异常,推荐使用FileProvider
  3. DownloadManager不再按文件名分享私人存储的文件。旧版应用在访问COLUMN_LOCAL_FILENAME时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException

简单的三句话,无法让TeachCourse真正理解Android 7.0系统权限更改的含义,如果不按照文档的方式去做,API 24开发的应用程序是否就用不了?

Android 7.0系统权限变更

一、深入理解FileProvider

FileProvider属于Android 7.0新增的一个类,该类位于v4包下,详情可见android.support.v4.content.FileProvider,使用方法类似与ContentProvider,简单概括为三个步骤,这里先以调用系统相机拍照并保存sdcard公共目录为例,演示使用过程:

  • 在资源文件夹res/xml下新建file_provider.xml文件,文件声明权限请求的路径,代码如下:
  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  3. <!–3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()–>
  4. <external-path name=“ext_root” path=“/” />
  5. </paths>
  • AndroidManifest.xml添加组件provider相关信息,类似组件activity,指定resource属性引用上一步创建的xml文件(后面会详细介绍各个属性的用法),代码如下:
  1. <!– 定义FileProvider –>
  2. <provider
  3. android:name=“android.support.v4.content.FileProvider”
  4. android:authorities=“@string/install_apk_path”
  5. android:exported=“false”
  6. android:grantUriPermissions=“true”>
  7. <meta-data
  8. android:name=“android.support.FILE_PROVIDER_PATHS”
  9. android:resource=“@xml/file_provider” />
  10. </provider>
  • *后一步,Java代码申请权限,使用新增的方法getUriForFile()grantUriPermission(),代码如下(后面会详细介绍方法对应参数的使用):
  1. if (Build.VERSION.SDK_INT > 23) {
  2. /**Android 7.0以上的方式**/
  3. Uri contentUri = getUriForFile(this, getString(R.string.install_apk_path), file);
  4. grantUriPermission(getPackageName(), contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  5. intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
  6. }
  • 修改build.gradle文件compileSdkVersion大于或等于24,targetSdkVersion等于24,使用Android 7.0模拟器运行Demo,效果图:

Android 7.0系统权限更改

那么,我们已经了解Android 7.0系统权限申请的步骤,接下来说明每一个步骤需要注意的事项、相关方法参数的说明、属性的含义以及可以的申请权限目录(*后下载相关Demo)。

1.1 定义一个FileProvider

直接使用FileProvider本身或者它的子类,需要在AndroidManifest.xml文件中声明组件的相关属性,包括:

  • android:name,对应属性值:android.support.v4.content.FileProvider或者子类完整路径
  • android:authorities,对应属性值是一个常量,通常定义的方式packagename.fileprovider,例如:cn.teachcourse.fileprovider
  • android:exported,对应属性值是一个boolean变量,设置为false
  • android:grantUriPermissions,对应属性值也是一个boolean变量,设置为true,允许获得文件临时的访问权限
  1. <manifest>
  2. <application>
  3. <provider
  4. android:name=“android.support.v4.content.FileProvider”
  5. android:authorities=“com.mydomain.fileprovider”
  6. android:exported=“false”
  7. android:grantUriPermissions=“true”>
  8. </provider>
  9. </application>
  10. </manifest>

想要关联res/xml文件夹下创建的file_provider.xml文件,需要在<provider>标签内,添加<meta-data>子标签,设置<meta-data>标签的属性值,包括:

  • android:name,对应属性值是一个固定的系统常量android.support.FILE_PROVIDER_PATHS
  • android:resource,对应属性值指向我们的xml文件@xml/file_provider
  1. <provider
  2. android:name=“android.support.v4.content.FileProvider”
  3. android:authorities=“com.mydomain.fileprovider”
  4. android:exported=“false”
  5. android:grantUriPermissions=“true”>
  6. <meta-data
  7. android:name=“android.support.FILE_PROVIDER_PATHS”
  8. android:resource=“@xml/file_provider” />
  9. </provider>

1.2 指定授予临时访问权限的文件目录

上一步说明了怎么定义一个FileProvider,这一步主要说明怎么定义一个@xml/file_provider文件。Android Studio或Eclipse开发工具创建Android项目的时候默认不会创建res/xml文件夹,需要开发者手动创建,点击res文件夹新建目录,命名xml,如下图:

Android Studio新建xml目录

然后,在xml文件夹下新建一个xml文件,文件命名file_provider.xml,指定根标签为paths,如下图:

xml新建file_provider.xml

在xml文件中指定文件存储的区块和区块的相对路径,在<paths>根标签中添加<files-path>子标签(稍后详细列出所有子标签),设置子标签的属性值,包括:

  • name,是一个虚设的文件名(可以自由命名),对外可见路径的一部分,隐藏真实文件目录
  • path,是一个相对目录,相对于当前的子标签<files-path>根目录
  • <files-path>,表示内部内存卡根目录,对应根目录等价于Context.getFilesDir(),查看完整路径:
    /data/user/0/cn.teachcourse.demos/files
  • 代码如下:
  1. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  2. <files-path name=“my_images” path=“images/”/>
  3. </paths>

<paths>根标签下可以添加的子标签也是有限的,参考官网的开发文档,除了上述的提到的<files-path>这个子标签外,还包括下面几个:

  1. <cache-path>,表示应用默认缓存根目录,对应根目录等价于getCacheDir(),查看完整路径:/data/user/0/cn.teachcourse.demos/cache
  2. <external-path>,表示外部内存卡根目录,对应根目录等价于
    Environment.getExternalStorageDirectory()
    查看完整路径:/storage/emulated/0
  3. <external-files-path>,表示外部内存卡根目录下的APP公共目录,对应根目录等价于
    Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
    查看完整路径:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/files/Download
  4. <external-cache-path>,表示外部内存卡根目录下的APP缓存目录,对应根目录等价于Context.getExternalCacheDir(),查看完整路径:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/cache

*终,在file_provider.xml文件中,添加上述5种类型的临时访问权限的文件目录,代码如下:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  3. <!–
  4. 1、name对应的属性值,开发者可以自由定义;
  5. 2、path对应的属性值,当前external-path标签下的相对路径
  6. 比如:/storage/emulated/0/92Recycle-release.apk
  7. sdcard路径:/storage/emulated/0(WriteToReadActivity.java:176)
  8. at cn.teachcourse.nougat.WriteToReadActivity.onClick(WriteToReadActivity.java:97)
  9. at android.view.View.performClick(View.java:5610)
  10. at android.view.View$PerformClick.run(View.java:22265)
  11. 相对路径:/
  12. –>
  13. <!–1、对应内部内存卡根目录:Context.getFileDir()–>
  14. <files-path
  15. name=“int_root”
  16. path=“/” />
  17. <!–2、对应应用默认缓存根目录:Context.getCacheDir()–>
  18. <cache-path
  19. name=“app_cache”
  20. path=“/” />
  21. <!–3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()–>
  22. <external-path
  23. name=“ext_root”
  24. path=“pictures/” />
  25. <!–4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)–>
  26. <external-files-path
  27. name=“ext_pub”
  28. path=“/” />
  29. <!–5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()–>
  30. <external-cache-path
  31. name=“ext_cache”
  32. path=“/” />
  33. </paths>

1.3 生成指定文件的Content URI

Content URI方便与另一个APP应用程序共享同一个文件,共享的方式通过ContentResolver.openFileDescriptor获得一个ParcelFileDescriptor对象,读取文件内容。那么,如何生成一条完整的Content URI呢?TeachCourse总结后,概括为三个步骤,*步:明确上述5种类型中的哪一种,第二步:明确指定文件的完整路径(包括目录、文件名),第三步:调用getUriForFile()方法生成URI

  1. File imagePath = new File(Environment.getExternalStorageDirectory(), "download");
  2. File newFile = new File(imagePath, "default_image.jpg");
  3. Uri contentUri = getUriForFile(getContext(), "cn.teachcourse.fileprovider", newFile);

1.4 授予Content URI临时访问权限

上一步获得的Content URI,并没有获得指定文件的读写权限,想要获得文件的读写权限需要调用Context.grantUriPermission(package, Uri, mode_flags)方法,该方法向指定包名的应用程序申请获得读取或者写入文件的权限,参数说明如下:

  • package,指定应用程序的包名,Android Studio真正的包名指build.gradle声明的applicationId属性值;getPackageName()AndroidManifest.xml文件声明的package属性值,如果两者不一致,就不能提供getPackageName()获取包名,否则报错!
  • Uri,指定请求授予临时权限的URI,例如:contentUri
  • mode_flags,指定授予临时权限的类型,选择其中一个常量或两个:Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION

授予文件的临时读取或写入权限,如果不再需要了,TeachCourse该如何撤销授予呢?撤销权限有两种方式:*种:通过调用revokeUriPermission()撤销,第二种:重启系统后自动撤销

1.5 对外提供可访问的Content URI

有多种方式可以向客户端APP提供可访问文件的Content URI,其中一种常用的方式是通过发送Intent给需要启动的Activity,在重写的startActivityResult()方法中获取授予临时权限的Content URI或向用户提供可访问的接口来获取文件,后面的这种方式获取文件后转换成Content URI,以文章开头拍照的功能为例,TeachCourse想要在sdcard的公共目录pictures/查看已保存的照片,实现过程:

  • 请求授予访问公共目录的权限,代码如下:
  1. if (Build.VERSION.SDK_INT > 23) {
  2. /**Android 7.0以上的方式**/
  3. mStorageManager = this.getSystemService(StorageManager.class);
  4. StorageVolume storageVolume = mStorageManager.getPrimaryStorageVolume();
  5. Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES);
  6. startActivityForResult(intent, REQUEST_CODE_GRAINT_URI);
  7. }
  • 在重写的startActivityResult()方法中获取授予临时权限的Content URI,代码如下:
  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. super.onActivityResult(requestCode, resultCode, data);
  4. switch (requestCode) {
  5. case REQUEST_CODE_GRAINT_URI:
  6. updateDirectoryEntries(data.getData());
  7. Log.d(TAG, "onActivityResult:Uri= "+data.getData());
  8. break;
  9. }
  10. }
  • 查询Environment.DIRECTORY_PICTURES目录,返回的Content URI包含的文件和文件类型相关信息,代码如下:
  1. private static final String[] DIRECTORY_SELECTION = new String[]{
  2. DocumentsContract.Document.COLUMN_DISPLAY_NAME,
  3. DocumentsContract.Document.COLUMN_MIME_TYPE,
  4. DocumentsContract.Document.COLUMN_DOCUMENT_ID,
  5. };
  6. @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  7. private void updateDirectoryEntries(Uri uri) {
  8. ContentResolver contentResolver = this.getContentResolver();
  9. Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
  10. DocumentsContract.getTreeDocumentId(uri));
  11. Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
  12. DocumentsContract.getTreeDocumentId(uri));
  13. try (Cursor docCursor = contentResolver
  14. .query(docUri, DIRECTORY_SELECTION, null, null, null)) {
  15. while (docCursor != null && docCursor.moveToNext()) {
  16. mPath_tv.setText(docCursor.getString(docCursor.getColumnIndex(
  17. DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
  18. }
  19. }
  20. try (Cursor childCursor = contentResolver
  21. .query(childrenUri, DIRECTORY_SELECTION, null, null, null)) {
  22. while (childCursor != null && childCursor.moveToNext()) {
  23. String fileName = childCursor.getString(childCursor.getColumnIndex(
  24. DocumentsContract.Document.COLUMN_DISPLAY_NAME));
  25. String mimeType = childCursor.getString(childCursor.getColumnIndex(
  26. DocumentsContract.Document.COLUMN_MIME_TYPE));
  27. Log.e(TAG, "updateDirectoryEntries: "+fileName+"\n"+mimeType);
  28. }
  29. }
  30. }

运行Demo,控制台打印效果图:

Android 7.0访问sdcard

更多说明,可以参考Google提供的例子

二、深入理解DownloadManager

同样,为了方便理解DownloadManager的用法,首先以一个简单例子开始:从指定的url下载资源,然后显示下载资源的相关信息,运行Demo的效果图:

DownloadManager详解

Android 7.0系统权限更改的第三点,简单的说:通过访问COLUMN_LOCAL_FILENAME,在Android 7.0系统上可能无法获取Demo效果图fileName对应的文件路径,这时候可能触发异常SecurityException,打印的log信息,如下:

  1. Caused by: java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
  2. at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1499)
  3. at cn.teachcourse.download.DownloadManagerActivity.query(DownloadManagerActivity.java:244)
  4. at cn.teachcourse.download.DownloadManagerActivity.access$100(DownloadManagerActivity.java:34)
  5. at cn.teachcourse.download.DownloadManagerActivity$1.onReceive(DownloadManagerActivity.java:186)
  6. at android.app.LoadedApk$ReceiverDispatcher$Args.run(LoadedApk.java:1122)
  7. at android.os.Handler.handleCallback(Handler.java:751)
  8. at android.os.Handler.dispatchMessage(Handler.java:95)
  9. at android.os.Looper.loop(Looper.java:154)
  10. at android.app.ActivityThread.main(ActivityThread.java:6077)
  11. at java.lang.reflect.Method.invoke(Native Method)
  12. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
  13. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)

2.1 关于DownloadManager

DownloadManager是一个用于处理长时间HTTP请求的系统服务,客户端请求的URI可能是将要下载的指定的文件,处于后台的下载管理器将控制着下载的任务,并监测下载的状态,在下载失败或连接改变以及系统重启后尝试重新下载。

  • 如何初始化DownloadManager实例?首先调用getSystemService(String)方法,传入DOWNLOAD_SERVICE常量,来初始化DownloadManager实例,代码如下:
mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
  • 如何配置请求参数?首先需要使用到内部类DownloadManager.Request,查看源码学习该类的各个方法的使用,TeachCourse简单总结:该类主要用于配置一条新下载任务相关内容,这些内容包括下载任务的保存路径,下载任务所处的网络状态(WiFi或流量状态)和下载任务通知栏显示样式等等,代码如下:
  1. /**
  2. * 设置请求下载的数据
  3. */
  4. private void initData() {
  5. //Request内部类配置新下载任务相关内容,比如:保存路径,WiFi或流量状态,下载通知栏样式
  6. request = new DownloadManager.Request(Uri.parse(mUrl + mFileName));
  7. request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, mFileName);
  8. request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE);
  9. request.setTitle("正在下载应用程序");
  10. request.setDescription("92回收,就爱回收APP");
  11. request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
  12. }
  • 如何开启下载任务?下载任务参数配置完成后,就可以开启后台服务下载,同一个DownloadManager实例,可以开启多个下载任务,需要上一步中配置多条URI,每个下载任务分配唯一的id,代码如下:
  1. /**
  2. * 下载任务的唯一标识ID,用于查询下载文件的相关信息
  3. */
  4. private void start() {
  5. mDownloadUniqueId = mDownloadManager.enqueue(request);
  6. mDownloadManager_btn.setText("正在下载。。。");
  7. mDownloadManager_btn.setClickable(false);
  8. }
  • DownloadManager通过两种状态的广播,*种:任务下载完成后发送,广播拦截器过滤action是DownloadManager.ACTION_DOWNLOAD_COMPLETE(关于广播的知识,不懂的可以参考TeachCourse博客另外的几篇文章);第二种:点击通知栏进度条后发送,广播拦截器过滤action是DownloadManager.ACTION_NOTIFICATION_CLICKED,代码如下:
  1. /**
  2. * 注册下载完成广播接收器,还可以注册其它监听器,比如:DownloadManager.ACTION_NOTIFICATION_CLICKED
  3. */
  4. private void registerReceiverCompleted() {
  5. IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
  6. registerReceiver(mBroadcastReceiver, intentFilter);
  7. }
  8. /**
  9. * 接收下载完成广播
  10. */
  11. private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
  12. @Override
  13. public void onReceive(Context context, Intent intent) {
  14. long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
  15. if (mDownloadUniqueId == reference) {
  16. query(reference);
  17. mShowInformation_tv.setText(information);
  18. mDownloadManager_btn.setText("点击下载");
  19. mDownloadManager_btn.setClickable(true);
  20. }
  21. }
  22. };
  • 如何查询下载任务的相关信息?首先需要使用到内部类DownloadManager.Query,查看源码学习该类各个方法的使用,TeachCourse简单总结:该类正如文章开头样式的例子,通过分配的id查询下载任务相关的信息,这些信息包括文件类型、文件的Uri和文件的长度等,代码如下:
  1. /**
  2. * 查询下载任务相关的信息,比如:文件名、文件大小、文件类型等
  3. *
  4. * @param reference
  5. */
  6. private void query(long reference) {
  7. DownloadManager.Query query = new DownloadManager.Query();
  8. /**指定查询条件**/
  9. query.setFilterById(reference);
  10. /**查询正在等待、运行、暂停、成功、失败状态的下载任务**/
  11. query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
  12. Cursor cursor = mDownloadManager.query(query);
  13. if (cursor.moveToFirst()) {
  14. int fileId = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
  15. int fileTitleId = cursor.getColumnIndex(DownloadManager.COLUMN_TITLE);
  16. int fileDescriptionId = cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
  17. int fileTypeId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE);
  18. int fileLengthId = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
  19. int fileUriId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
  20. /**过时的方式:DownloadManager.COLUMN_LOCAL_FILENAME**/
  21. int fileNameId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
  22. int statusCodeId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
  23. int statusReasonId = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
  24. int downloadSizeId = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
  25. int lastModifiedTimeId = cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
  26. int mediaUriId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI);
  27. String id = cursor.getString(fileId);
  28. String fileTitle = cursor.getString(fileTitleId);
  29. String description = cursor.getString(fileDescriptionId);
  30. String type = cursor.getString(fileTypeId);
  31. String length = cursor.getString(fileLengthId);
  32. String statusCode = cursor.getString(statusCodeId);
  33. String statusReason = cursor.getString(statusReasonId);
  34. String downloadSize = cursor.getString(downloadSizeId);
  35. String modifiedTime = cursor.getString(lastModifiedTimeId);
  36. String mediaUri = cursor.getString(mediaUriId);
  37. String fileUri = cursor.getString(fileUriId);
  38. String fileName = null;
  39. if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
  40. openFile(type, Uri.parse(fileUri));
  41. fileName = Uri.parse(fileUri).getPath();
  42. } else {
  43. /**Android 7.0以上的方式:请求获取写入权限,这一步报错**/
  44. fileName = cursor.getString(fileNameId);
  45. openFile(type, Uri.parse(fileUri));
  46. }
  47. /**清空StringBuffer存储的数据**/
  48. mStringBuffer.delete(0, mStringBuffer.length());
  49. mStringBuffer.append("id:" + id + "\n");
  50. mStringBuffer.append("fileTitle:" + fileTitle + "\n");
  51. mStringBuffer.append("description:" + description + "\n");
  52. mStringBuffer.append("type:" + type + "\n");
  53. mStringBuffer.append("length:" + length + "\n");
  54. mStringBuffer.append("fileName:" + fileName + "\n");
  55. mStringBuffer.append("fileUri:" + fileUri + "\n");
  56. mStringBuffer.append("statusCode:" + statusCode + "\n");
  57. mStringBuffer.append("statusReason:" + statusReason + "\n");
  58. mStringBuffer.append("downloadSize:" + downloadSize + "\n");
  59. mStringBuffer.append("modifiedTime:" + modifiedTime + "\n");
  60. mStringBuffer.append("mediaUri:" + mediaUri + "\n");
  61. information = mStringBuffer.toString();
  62. }
  63. cursor.close();
  64. }
  • 代码加入判断语句,如果非Android 7.0系统继续访问COLUMN_LOCAL_FILENAME获得文件存储的*对路径(上面中间部分代码),openFile()方法代码如下:
  1. /**
  2. * 根据文件的类型,指定可以打开的应用程序
  3. *
  4. * @param type
  5. * @param uri
  6. */
  7. private void openFile(String type, Uri uri) {
  8. if (type.contains("image/")) {
  9. try {
  10. ParcelFileDescriptor descriptor = getContentResolver().openFileDescriptor(uri, "r");
  11. FileDescriptor fileDescriptor = descriptor.getFileDescriptor();
  12. Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
  13. mShowPic_iv.setVisibility(View.VISIBLE);
  14. mShowPic_iv.setImageBitmap(bitmap);
  15. } catch (FileNotFoundException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

现在,我们已经掌握了DownloadManager怎么实例化、怎么配置下载任务、怎么开启后台服务以及如何查询任务相关信息,想要实现一个应用程序版本更新就变得很简单,实现多任务下载也不是难事,完整源码参考文章后台通过的Demo。

三、关于ParcelFileDescriptor和FileDescriptor总结

官网的文档推荐我们使用ContentResolver.openFileDescriptor()方法,获得一个ParcelFileDescriptor对象,再通过getFileDescriptor()方法返回一个FileDescriptor,它们之间的关系参考上面的代码。

FileDescriptor通常被称为文件描述符,可以理解成本地的一个文件,通过流的方式读取文件内容以及通过流的方式写入数据到文件,这里是读取或写入数据到FileDescriptor中,假如我们的Uri表示的是一个txt文件,获取FileDescriptor对象后,通过下面的代码读取txt文件的内容:

FileInputStream fis = new FileInputStream(fd);

同理,写入数据到txt文件,代码如下:

  1. FileOutputStream out = new FileOutputStream(fd);
  2. out.write('写入数据到txt文件中');
  3. out.close();

获取到输入流或输出流后,剩下的就是关于流的操作了,划分为:文件字节流、文件字符流、缓冲流、数组流等

3.1 改写上面的例子

openFile()方法使用封装好的decodeFileDescriptor(),查看BitmapFactory.decodeFileDescriptor()相关源码,学习如何读取文件描述符中的内容,这里TeachCourse根据读取流的方式,改写如下:

  1. ...
  2. Bitmap bitmap = BitmapFactory.decodeStream(getStreamByFileDescriptor(fileDescriptor));
  3. ...
  4. /**
  5. * 通过流的方式读取内容
  6. *
  7. * @param fileDescriptor
  8. * @return
  9. */
  10. private InputStream getStreamByFileDescriptor(FileDescriptor fileDescriptor) {
  11. return new FileInputStream(fileDescriptor);
  12. }

于是,可以对FileDescriptor进行简单的封装成writeData()readData(),代码如下:

  1. /**往FileDescriptor中写入数据
  2. * @param fileDescriptor
  3. * @param content
  4. */
  5. private void writeData(FileDescriptor fileDescriptor, String content) {
  6. FileOutputStream fos = new FileOutputStream(fileDescriptor);
  7. try {
  8. fos.write(content.getBytes());
  9. } catch (IOException e) {
  10. e.printStackTrace();
  11. } finally {
  12. try {
  13. fos.close();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. /**从FileDescriptor中读取数据
  20. * @param fileDescriptor
  21. * @return
  22. */
  23. private String readData(FileDescriptor fileDescriptor) {
  24. FileInputStream fis = new FileInputStream(fileDescriptor);
  25. byte[] b = new byte[1024];
  26. int read;
  27. String content=null;
  28. try {
  29. while ((read = fis.read(b)) != -1) {
  30. content = new String(b, 0, read);
  31. }
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. } finally {
  35. try {
  36. fis.close();
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. return content;
  42. }

总结:

Android 7.0系统的权限更改,包括三个方面,文章从第二方面开始讲解,着重介绍了FileProviderDownloadManager两个类的使用,花了好长时间整理、测试和编辑,如果对你有帮忙,别忘了收藏和分享咯!

  1. FileProvider源码路径:nougat/WriteToReadActivity.java
  2. DownloadManager源码路径:download/DownloadActivity.java
  3. Demo源码
  4. 参考资料:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html

一行代码搞定Android 6.0动态权限申请

1、前言
从Android 6.0(API 23)开始,对系统权限做了很大的改变。在之前用户安装APP前,只是把APP需要使用的权限列出来给用户告知一下,APP安装后都可以访问这些权限。从6.0开始,一些敏感权限,需要在使用时动态申请,并且用户可以选择拒*授权访问这些权限,已授予过的权限,用户也可以去APP设置页面去关闭授权。这对用户来说提高了安全性,可以防止一些应用恶意访问用户数据,但是对于开发来说,也增加了不少工作量,这块不做适配处理的话,APP在访问权限的时候会容易crash。

2、权限等级和权限组
权限主要分为normal、dangerous、signature和signatureOrSystem四个等级,常规情况下我们只需要了解前两种,即正常权限和危险权限。

2.1、正常权限
正常权限涵盖应用需要访问其沙盒外部数据或资源,但对用户隐私或其他应用操作风险很小的区域。应用声明其需要正常权限,系统会自动授予该权限。例如设置时区,只要应用声明过权限,系统就直接授予应用此权限。下面是截止到API 23的普通权限(需*访问)

%title插图%num

2.2、危险权限
危险权限涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如读取用户联系人,在6.0以上系统中,需要在运行时明确向用户申请权限。

2.3、权限组
系统根据权限用途又定义了权限组,每个权限都可属于一个权限组,每个权限组可以包含多个权限。例如联系人权限组,包含读取联系人、修改联系人和获取账户三个权限。
* 如果应用申请访问一个危险权限,而此应用目前没有对应的权限组内的任何权限,系统会弹窗提示用户要访问的权限组(注意不是权限)。例如无论你申请READ_CONTACTS还是WRITE_CONTACTS,都是提示应用需要访问联系人信息。
* 如果用户申请访问一个危险权限,而应用已经授权同权限组的其他权限,则系统会直接授权,不会再与用户有交互。例如应用已经请求并授予了READ_CONTACTS权限,那么当应用申请WRITE_CONTACTS时,系统会立即授予该权限。下面为危险权限和权限组:
%title插图%num

3、运行时请求权限
关于运行时请求权限,官网介绍的很清楚,也有很多其他文章介绍,这里只是简单罗列一下。

3.1、检查权限
应用每次需要危险权限时,都要判断应用目前是否有该权限。兼容库中已经做了封装,只需要通过下面代码即可:

int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.WRITE_CALENDAR);

如果有权限则返回PackageManager.PERMISSION_GRANTED,否则返回PackageManager。PERMISSION_DENIED。

3.2、请求权限
当应用需要某个权限时,可以申请获取权限,这时会有弹出一个系统标准Dialog提示申请权限,此Diolog不能定制,用户同意或者拒*后会通过方法onRequestPermissionsResult()返回结果。当用户拒*过此权限申请时,再次申请Dialog上可以勾选不再提示,这种情况下,以后再申请权限不会弹Dialog直接返回拒*。所以一些依赖某些敏感权限的应用,需要自己去处理,向用户解释 为什么需要此权限,说服用户授予权限。请求权限代码如下:

ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE);

3.3、处理权限请求响应
当用户处理权限请求后,系统会回调申请权限的Activity的onRequestPermissionsResult()方法,只需要覆盖此方法,就能获得返回结果

@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

// permission was granted, yay! Do the
// contacts-related task you need to do.

} else {

// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
}
}

3.4、考虑使用intent
有很多权限操作可以考虑调用其他应用,这样的话当前应用就不需要申请权限。例如想要获取相机照相,可以通过ACTION_IMAGE_CAPTURE唤起相机应用去完成,相机应用会把照片返回。同样拨打电话、访问联系人,都可以考虑使用类似方法。相比较其他应用,这类专门的应用做一些操作更容易让用户接受。

4、 关于国产机6.0以下系统
部分国产厂商定制了系统(例如小米、华为),在6.0以下系统就可以单独控制权限。但是通常它们处理的不够彻底,代码中判断是否授权的时候,返回的是已经授权。而真正去做操作的时候,却会因为没有权限导致应用crash。例如我曾遇到过的场景:
* 访问联系人,可以拿到Cursor对象,但是cursor.moveToFirst()会返回false。
* 访问摄像头时,获取到的Camera对象为空

所以在低版本手机上,不要以为拥有系统授权就万事大吉了,一定要多加条件判读和测试。

5、PermissionGrantor,一行代码搞定动态权限申请。
上面运行时请求权限中,我们看到了权限申请依赖于Activity和Fragment,和startActivityForResult()方法的使用类似,必须依赖于Activity和Fragment的回调方法。正常情况下还能满足需求,可是当申请的权限的代码在一个独立的模块中时,例如我封装了一个UI控件,控件中某个操作需要申请权限,或者项目采用了MVVM框架,需要在一些view类或者model类中申请权限,这是处理起来就会很麻烦。
我在自己代码中就遇到类似情况,后面我采用了使用一个单独的Activity来申请权限,通过回调的方式通知业务层授权结果。感觉使用起来挺方便,就把它放到maven仓库中了,项目中通过加入如下依赖即可。

compile ‘com.github.dfqin:grantor:2.1.0’

5.1 PermissionGrantor使用
申请权限很简单,只需要下面一句话即可。

PermissionsUtil.requestPermission(Activity activity, PermissionListener listener,
String[] permissions);

下面是一个申请摄像头的例子:

private void requestCemera() {
if (PermissionsUtil.hasPermission(this, Manifest.permission.CAMERA)) {
//有访问摄像头的权限
} else {
PermissionsUtil.requestPermission(this, new PermissionListener() {
@Override
public void permissionGranted(@NonNull String[] permissions) {
//用户授予了访问摄像头的权限
}

@Override
public void permissionDenied(@NonNull String[] permissions) {
//用户拒*了访问摄像头的申请
}
}, new String[]{Manifest.permission.CAMERA});
}
}

PermissionsUtil.requestPermission还有两个重载的实现。用户拒*授权时,会有一个默认Dialog提示用户开通权限。这个Dialog的内容可以定制,也可以不显示此Dialog