downloadManagerSupport7.0

DownloadManager

  1. downloadmanager概述
    DownloadManager是Android SDK中封装的下载文件类,可以很方便开发者使用下载文件。其具体看官方APIhttps://developer.android.com/reference/android/app/DownloadManager.html

  2. 使用DownloadManager下载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    public class DownloadUtils {

    private DownloadManager mDownloadManager;
    private Context mContext;
    private long downloadId;
    private String apkName;

    public DownloadUtils(Context context) {
    mContext = context;
    }

    public void download(String url, String name) {
    final String packageName = "com.android.providers.downloads";
    int state = mContext.getPackageManager().getApplicationEnabledSetting(packageName);
    //检测下载管理器是否被禁用
    if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
    AlertDialog.Builder builder = new AlertDialog.Builder(mContext).setTitle("温馨提示").setMessage
    ("系统下载管理器被禁止,需手动打开").setPositiveButton("确定", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    dialog.dismiss();
    try {
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + packageName));
    mContext.startActivity(intent);
    } catch (ActivityNotFoundException e) {
    Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
    mContext.startActivity(intent);
    }
    }
    }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    dialog.dismiss();
    }
    });
    builder.create().show();
    } else {
    //正常下载流程
    apkName = name;
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
    request.setAllowedOverRoaming(false);
    //通知栏显示 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    request.setTitle(AppUtils.getAppName(mContext));
    request.setDescription("正在下载中...");
    request.setVisibleInDownloadsUi(true);
    //设置下载存放的文件夹和文件名字 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkName);

    //获取DownloadManager
    mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
    downloadId = mDownloadManager.enqueue(request);
    mContext.registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
    checkStatus();
    }
    };

    /**
    * 检查下载状态
    */
    private void checkStatus() {
    DownloadManager.Query query = new DownloadManager.Query();
    query.setFilterById(downloadId);
    Cursor cursor = mDownloadManager.query(query);
    if (cursor.moveToFirst()) {
    int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
    switch (status) {
    //下载暂停
    case DownloadManager.STATUS_PAUSED:
    break;
    //下载延迟
    case DownloadManager.STATUS_PENDING:
    break;
    //正在下载
    case DownloadManager.STATUS_RUNNING:
    break;
    //下载完成
    case DownloadManager.STATUS_SUCCESSFUL:
    installAPK();
    break;
    //下载失败
    case DownloadManager.STATUS_FAILED:
    Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();
    break;
    }
    }
    cursor.close();
    }

    /**
    * 7.0兼容
    */
    private void installAPK() {
    File apkFile =
    new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), apkName);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Uri apkUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".provider", apkFile);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    } else {
    intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
    }
    mContext.startActivity(intent);
    }
    }

android 7.0 DownloadManager不在按照文件名分享私人存储的文件

  1. 按照开发者官网上的描述,DownloadManager不在支持使用COLUMN_LOCAL_FILENAME访问路径,其会触发SecurityException。官方建议有DownloadManager公开的文件,首选
    的访问方式是使用ContentResolver.openFileDescriptor()。

  2. 如果使用downloadManager做下载功能的且没有做7.0适配的话,在7.0读取数据库内容时会遇到如下错误:

    1
    java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead

在Android 7.0中通过DownloadManager根据downId获取的uri安装apk是会得到如下错误:

1
2
Caused by: android.os.FileUriExposedException:
file:///storage/emulated/0/Download/myApp.apk exposed beyond app through Intent.getData()

同时在7.0上通过DownloadManager根据downId获取的uri变为:

1
content://downloads/all_downloads/430

这些都是由于Android7.0执行了“StrictMode API 政策禁”的原因,可以用FileProvider来解决这一问题。

  1. 原来的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    DownloadManager manager = (DownloadManager) mContext
    .getSystemService(Context.DOWNLOAD_SERVICE);
    Cursor downloadCursor = dm.query(myDownloadQuery);
    if (myDownload != null && downloadCursor.moveToFirst()) {
    int fileNameIdx = downloadCursor
    .getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
    String filePath = downloadCursor.getString(fileNameIdx);
    }

7.0获取文件路径

  1. DownloadManager.COLUMN_LOCAL_URI:DownloadManager的数据库表中应该是存放了对应的URI,目测应该是file: 协议开头。那看来可以通过查询这个URI,随后将URI转换成文件path路径。
  2. 代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    DownloadManager manager = (DownloadManager) mContext
    .getSystemService(Context.DOWNLOAD_SERVICE);
    Cursor downloadCursor= dm.query(myDownloadQuery);
    if (downloadCursor!= null && downloadCursor.moveToFirst()) {
    String filePath = null;
    String downloadFileLocalUri = downloadCursor
    .getString(downloadCursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
    if (downloadFileLocalUri != null) {
    filePath = new File(Uri.parse(downloadFileLocalUri).getPath())
    .getAbsolutePath();
    }
    }

FileProvider

Why FileProvider?

  1. 老生常谈的内容,为什么Android推荐使用FileProvider呢?其实FileProvider也不是一个新概念了,Google很早就提出这个概念了。这是因为Android上的权限管理过于松散,但是随着系统版本的不断提升,权限的不断收紧,对于进程之间的数据共享就产生了问题。于是FileProvider就出现了。
  2. 它通过在AndroidManifest中定义Provider,为指定的文件提供一个ContentURI,通过这个URI临时赋予第三方应用授权处理指定的某些文件。简而言之,也可以说成是使用ContentURI代替了FileURI。

fileprovider 使用场景

  1. 正如上面我们说到的DownloadManager的变更中,我们通过DownloadManager下载了一个APK。此时我们需要发送一个Intent,带着我们下载下来的APK的URI,通知系统的APK安装管理服务,安装这个应用。
  2. 此时,这个Intent就要带着这个URI离开我们的App,去到另一个应用。此时,我们就需要对这个URI进行处理。如果发送的Intent带的是一个FileURI,就会导致:
    FileUriExposedException。

使用方法:

<1> AndroidManifest.xml:在manifest中加入如下代码:

1
2
3
4
5
6
7
8
9
10
<provider
android:exported="false"
android:grantUriPermissions="true"
android:authorities="com.×××.fileprovider"
android:name="android.support.v4.content.FileProvider">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
/>
</provider>

  • exported要设置为false,否则会报安全异常。
  • grantUriPermissions设置为true,表示的是设置URI临时访问权限。
  • authorities可以随便写,不过一般最好是包名.fileprovider。当然也可以是其他。

<2> file_paths.xml:

  • 在manifest中有下面这一句:

    1
    android:resource="@xml/file_paths"
  • 我们需要在res/xml目录下新建一个file_paths.xml的文件,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <paths>
    <root-path
    name="/"
    path="/" />
    </paths>
    </resources>
  • 作用是指定临时授权的路径,其选项有:

选项 含义
root-paty 根目录(/)
external-path Context.getExternalStorageDirectory()
file-path Context.getFilesDir()
cache-path Context.getCacheDir()

其中root-path指的是Android系统根目录,众所周知Android是基于Linux内核,所以Android的根目录就是“/”

<3> 使用FileProvider:

  • 当我们拿到一个Apk文件时,需要启动安装界面进行安装。则可以使用以下方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (Build.VERSION.SDK_INT >= 24) {
    File apkFile=new File(apkFile Path);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri contentUri = FileProvider.getUriForFile(mContext,
    "com.×××.fileprovider", apkFile);
    intent.setDataAndType(contentUri,
    "application/vnd.android.package-archive");
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    mContext.startActivity(intent);
    }
  • 使用FileProvider.getUriForFile()将一个FileURI转换成ContentURI

  • 并且不要忘记添加Intent.FLAG_GRANT_READ_URI_PERMISSION临时的读或者写权限。这样就完成一个FileProvider共享的使用了。

后记

  1. 已兼容7.0私有文件权限问题
  2. 对于部分机型默认或者一些原因,下载管理器是被禁用掉的,必须手动开启或者写代码去跳转到设置界面开启,代码中已兼容。

来源:
http://www.ryanhuen.tech/2017/02/28/Android-N-Diff/
http://www.jianshu.com/p/3eb4106133f4
http://blog.csdn.net/yulianlin/article/details/52775160