8.0APK下载并跳转安装--DownloadManager、FileProvider、BroadcastReceiver的结合使用

暴骏奇
2023-12-01

我们希望应用的下载更新可以不受UI周期的约束,这里下载就涉及到Google提供的大文件下载管理类DownloadManager,下载完成后通过BroadcastReceive接收下载完成的消息开启应用安装。下面正式开启步骤解析

本博客Demo地址:https://download.csdn.net/download/g_ying_jie/10697856

第一步,传入apk的下载地址,利用DownloadManager下载安装包

protected static void downloadApk(Context context, String apkUrl, String apkName) {
        //获取DownloadManager对象
        DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
        //指定APK缓存路径和应用名称,可在SD卡/Android/data/包名/file/Download文件夹中查看
        request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, apkName.concat(".apk"));
        //设置网络下载环境为wifi
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
        //设置显示通知栏,下载完成后通知栏自动消失
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
        //设置通知栏标题
        request.setTitle("we_chat");
        request.setDescription("APK下载中...");
        request.setAllowedOverRoaming(false);
        //获得唯一下载id
        DOWNLOAD_ID = downloadManager.enqueue(request);
    }
DOWNLOAD_ID是下载的唯一标识,后期可以用来查询下载的文件信息。

第二步,新建BroadcastReceiver接收下载完成消息,并回调状态

package com.example.gu.download;

import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.RequiresApi;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;

import java.io.File;
import java.util.Objects;


public class UpdateBroadcastReceiver extends BroadcastReceiver {

    private DownloadCompleteCallBack callBack;

    public UpdateBroadcastReceiver(DownloadCompleteCallBack callBack) {
        this.callBack = callBack;
    }

    public void onReceive(Context context, Intent intent) {
        //下载完成
        long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (id == UpdateUtil.DOWNLOAD_ID) {
            callBack.canInstall(id);
        }
    }


    public interface DownloadCompleteCallBack {
        void canInstall(long id);
    }

}

第三步,在MainActivity注册广播,注入DownloadCompleteCallBack的回调

receiver = new UpdateBroadcastReceiver(this);
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(receiver, filter);

    不要忘记注销

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (receiver != null)
            unregisterReceiver(receiver);
    }

第四步,在回调的canInstall中执行安装流程

    protected static void installApp(Context mContext, long id) {
        File apkFile = queryApkPath(mContext, id);
        if (apkFile != null) {
            try {
                //8.0跳转设置允许安装未知应用
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    boolean hasInstallPermission = mContext.getPackageManager().canRequestPackageInstalls();
                    if (!hasInstallPermission) {
                        startInstallPermissionSettingActivity(mContext);
                        return;
                    }
                }

                Intent intent = new Intent(Intent.ACTION_VIEW);
                //兼容7.0之后禁用在应用外部公开file://URI,以FileProvider替换
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    //给目标应用一个临时授权
                    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    //注意此处的authority必须与manifest的provider保持一致
                    intent.setDataAndType(FileProvider.getUriForFile(mContext, mContext.getPackageName().concat(".fileprovider"), apkFile), "application/vnd.android.package-archive");
                } else {
                    intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
                }

                //验证是否有APP可以接受此Intent,防止FC
                if (mContext.getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
                    mContext.startActivity(intent);
                }

            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

  其中queryApkPath方法调用下载返回的ID查询apk文件信息,代码如下

    private static File queryApkPath(Context context, long id) {
        File apkFile = null;
        DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(id);
        Cursor cur = manager.query(query);
        if (cur != null) {
            if (cur.moveToFirst()) {
                // 获取文件下载路径
                String filePath = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                if (!TextUtils.isEmpty(filePath)) {
                    apkFile = new File(Objects.requireNonNull(Uri.parse(filePath).getPath()));
                }
            }
            cur.close();
        }
        return apkFile;
    }

  startInstallPermissionSettingActivity方法用于8.0之后跳转允许安装未知应用的设置页面,代码如下

    /**
     * 跳转到设置-允许安装未知来源-页面
     */
    @RequiresApi(api = Build.VERSION_CODES.O)
    private static void startInstallPermissionSettingActivity(Context mContext) {
        Uri packageURI = Uri.parse("package:".concat(mContext.getPackageName()));
        //注意这个是8.0新API
        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
        ((Activity) mContext).startActivityForResult(intent, INSTALL_REQUESTCODE);
    }

第五步,在onActivityResult方法中监听8.0未知应用授权情况,授权成功再次发起安装流程

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == UpdateUtil.INSTALL_REQUESTCODE && resultCode == RESULT_OK) {
            UpdateUtil.installApp(MainActivity.this, DOWNLOAD_ID);
        } else {
            Toast.makeText(this, "授权失败,应用未能安装", Toast.LENGTH_LONG).show();
        }
    }

敲黑板划重点,整体的流程都已经走完了,细心的读者会发现在7.0之后引入了FileProvider来防止在应用外部公开file://URI,

那么这个该怎么配置各种属性呢?接着往下看

FileProvider使用第一步,在res下新建xml文件夹,在其下新建一个file_paths.xml文件,如下

<?xml version="1.0" encoding="utf-8"?>
<external-files-path
    name="download"
    path="Download" />

path就是FileProvider共享的文件夹目录,即/storage/emulated/0/Android/data/包名/files/Download文件夹被共享

其他属性汇总:更多有关FileProvider属性可前往FileProvider详解

files-path  ==>  /data/data/包名/files
cache-path  ==>  /data/data/com.jph.simple/cache
external-path  ==>  /storage/emulated/0
external-cache-path  ==>  /storage/emulated/0/Android/data/包名/cache

等同于如下路径:

<root-path/> 代表设备的根目录new File("/");
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()

 

FileProvider使用第二步,在manifest中申明provider

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.gu.download.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
注解:
name的值一般都固定为android.support.v4.content.FileProvider。如果开发者继承了FileProvider,则可以写上其绝对路径。
authorities字段的值用来表明使用的使用者,在FileProvider的函数getUriForFile需要传入该参数,通用写法是包名+fileprovider。
exported 的值为false,表示该FileProvider只能本应用使用,不是public的。
grantUriPermissions 的值为true,表示允许赋予临时权限。

 

 

 

 

 

 

 类似资料: