Android使用DownloadManager下载安装包并跳转到安装界面

前言

App的版本升级迭代,是每一个开发者必须要考虑的事情。App的bug修复、新功能的开发都需要用户下载新的安装包,安装新版本来实现App的更新。

这篇文章主要就是介绍如何提示并引导用户在应用内下载最新的安装包并且安装新版本。

一、下载安装包

下载文件使用系统自带的DownloadManager实现。

1.什么是DownloadManager

下载管理器是一项系统服务,可处理长时间运行的HTTP下载。 客户端可以请求将URI下载到特定的目标文件。 下载管理器将在后台进行下载,处理HTTP交互,并在出现故障或在连接更改和系统重新启动后重试下载。

看一下DownloadManager中两个比较重要的类:

  • DownloadManager.Request 包含一个下载任务所需要的所有信息,例如文件下载地址、文件存储路径、通知栏内容、文件种类等等。
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl)); //下载地址
request.setTitle("软件更新")  //通知栏标题
        .setDescription("新版安装包正在下载中...")  //通知栏描述
        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) //通知栏展示方式
        .setAllowedOverMetered(true)  //是否可以通过计量网络进行连接
        .setAllowedOverRoaming(true)   //是否可以通过漫游网络进行连接
        .setMimeType("application/vnd.android.package-archive")  //设置文件类型
        .setVisibleInDownloadsUi(true);  //是否显示系统下载进度

指定目标存储路径

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    File file = new File(activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getFileName(activity));
    Uri uri = Uri.fromFile(file);
    activity.grantUriPermission(activity.getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    //如果使用content://开头 的 Uri 指定下载目标路径,会报错java.lang.IllegalArgumentException: Not a file URI
    //必须还是以file://开头的
    request.setDestinationUri(uri);
} else {
    PermissionUtil.requestPermissiom(activity, new OnPermissionListener() {
        /**
         * 同意授权
         */
        @Override
        protected void grant() {
            super.grant();
            request.setDestinationInExternalFilesDir(activity, Environment.DIRECTORY_DOWNLOADS, getFileName(activity)); 
        }
    }, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}

开始下载

    DownloadManager downloadManager = (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE);
    //将下载请求加入下载队列,加入下载队列后会给该任务返回一个long型的id,通过该id可以取消任务,重启任务、获取下载的文件等等
    if (downloadManager != null) {
        long downloadId = downloadManager.enqueue(request);
    }
关于版本适配需要注意的几点:

1、从Android M开始,存储文件到手机需要动态申请WRITE_EXTERNAL_STORAGE权限

2、设置文件存储路径在Android Q版本之前使用的是request.setDestinationInExternalFilesDir(Context context, String dirType, String subPath)方法,而在Android Q及以上版本提供了新方法setDestinationUri(Uri uri)

3、setDestinationUri方法的参数Uri如果使用content://开头 的 Uri 指定下载目标路径,会报错java.lang.IllegalArgumentException: Not a file URI,必须使用以file://开头的Uri

4、对于setVisibleInDownloadsUi方法,从Android Q开始,只有将文件保存到公共下载目录(Context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS))时才能在通知栏显示下载进度。

对于上述第4点我本人还存在一些疑惑,按照Api文档的说法,仅当文件下载地址为getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)时才能在通知栏显示下载进度。但是getExternalStoragePublicDirectory已经明确标示为Deprecated方法,并且推荐我们使用getExternalFilesDir方法访问存储空间,希望有知道的大神给我解答疑惑,非常感谢。

/**
 * 查询指定下载任务的状态
 *
 * @param downloadId 下载任务的id,DownloadManager.enqueue(request)时返回
 */
private static int getDownloadStatus(long downloadId, DownloadManager downloadManager) {
    DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
    Cursor cursor = downloadManager.query(query);
    if (cursor != null && cursor.moveToNext()) {
        return cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
    }
    return DownloadManager.ERROR_UNKNOWN;
}

大部分App都需要在安装包下载完成的时候,直接启动安装器帮助用户完成新版本的安装。我们可以使用轮询或广播检查下载任务的状态,当status等于DownloadManager.STATUS_SUCCESSFUL的时候就可以开始安装工作。

这些都是DownloadManager最基本的使用方法,想要了解更多DownloadManager Api可查阅官方文档https://developer.android.google.cn/reference/android/app/DownloadManager

二、安装apk

关于版本适配
  • Android N版本开始,使用file://开头的Uri可能会触发FileUriExposedException,所以需要通过FileProvider的方式创建Uri,具体方式可以参考https://blog.csdn.net/lmj623565791/article/details/72859156

  • Android O版本开始,安装apk需要申请REQUEST_INSTALL_PACKAGES【安装未知来源应用权限】。

//获取文件Uri
public static Uri getUri(Context context, File file) {
    Uri uri;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //第二个参数为 包名.fileprovider
        uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
    } else {
        uri = Uri.fromFile(file);
    }
    return uri;
}

//安装apk
private static void tryInstallApk(Activity activity) {
    File downloadFile = getDownloadFile(activity);
    if (!downloadFile.exists()) {
        return;
    }
    Intent intent = new Intent(Intent.ACTION_VIEW);
    //Android7.0之后需要使用FileProvider创建Uri
    Uri apkUri = FileUtil.getUri(activity, getDownloadFile(activity));
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            //Android8.0开始需要获取应用内安装权限
            boolean allowInstall = activity.getPackageManager().canRequestPackageInstalls();
            //如果还没有授权安装应用,去设置内开启应用内安装权限
            if (!allowInstall) {
                //注意这个是8.0新API
                Uri packageUri = Uri.parse("package:" + activity.getPackageName());
                Intent intentX = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri);
                activity.startActivityForResult(intentX, GET_UNKNOWN_APP_SOURCES);
                return;
            }
        }
        //Android N开始必须临时授权该Uri所代表的文件
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    activity.startActivity(intent);
}

//在设置界面授权完成后回到应用,检查是否授权成功。如果授权成功,执行安装;否则提示用户授权被拒绝
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK) {
        if (requestCode == GET_UNKNOWN_APP_SOURCES) {
            //8.0应用设置界面未知安装开源返回时候
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                boolean allowInstall = getPackageManager().canRequestPackageInstalls();
                if (allowInstall) {
                    // 执行安装app的逻辑
                   
                } else {
                    // 拒绝权限逻辑
                    ToastUtil.showShort(getApplicationContext(), "您拒绝了安装未知来源应用,应用暂时无法更新");
                }
            }
        }
    }
}

归纳总结

  • Android M版本开始,存储文件到手机需要动态申请WRITE_EXTERNAL_STORAGE权限
  • Android N版本开始,使用file://开头的Uri可能会触发FileUriExposedException,所以需要通过FileProvider的方式创建Uri,具体方式可以参考https://blog.csdn.net/lmj623565791/article/details/72859156
  • Android O版本开始,安装apk需要申请REQUEST_INSTALL_PACKAGES【安装未知来源应用权限】。
  • Android Q版本开始,改变了应用程序访问设备外部存储上文件的方式。具体变更可查看官方文档https://developer.android.google.cn/about/versions/10/privacy/changes。这些存储方式的变更,会影响我们对DownloadManager的使用,主要体现为:

1、设置文件存储路径在Android Q版本之前使用的是request.setDestinationInExternalFilesDir(Context context, String dirType, String subPath)方法,而在Android Q及以上版本提供了新方法setDestinationUri(Uri uri)。

2、setDestinationUri方法的参数Uri如果使用content://开头 的 Uri 指定下载目标路径,会抛异常java.lang.IllegalArgumentException: Not a file URI,必须使用以file://开头的Uri。

3、对于setVisibleInDownloadsUi方法,从Android Q开始,只有将文件保存到公共下载目录(Context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS))时才能在通知栏显示下载进度。

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