Android7.0下载Apk自动安装

作者: 陈丰尧 | 来源:发表于2017-04-20 14:19 被阅读1403次

    Android7.0下载Apk自动安装

    1. 整体需求

    1. 下载APK文件
      • 使用DownloadManager来下载
      • 在应用界面中展示下载进度
    2. 安装下载后的APK文件
      • root模式: 可以自动安装,不需要用户主动点击
      • 正常模式: 弹出安装应用页面,需要兼容7.0以上版本

    2. DownloadManager

    DownloadManager是Android提供的用于下载的类,使用起来比较简单,它包含两个静态内部类DownloadManager.Query和DownloadManager.Request;
    DownloadManager.Request用来请求一个下载,DownloadManager.Query用来查询下载信息

    2.1. 下载

    1. 获取DownloadManager对象

    DownloadManager对象属于系统服务,通过getSystemService来进行安装

    DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
    

    一般获取完成后会变成全局变量,方便之后使用

    2. 开始下载

    在使用DownloadManager进行下载的时候,就会用到DownloadManager.Request

    //使用DownLoadManager来下载
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
    //将文件下载到自己的Download文件夹下,必须是External的
    //这是DownloadManager的限制
    File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
    request.setDestinationUri(Uri.fromFile(file));
    //添加请求 开始下载
    long downloadId = mDownloadManager.enqueue(request);
    

    首先会创建出一个DownloadManager.Request对象,在构造方法中接收Uri,其实就是下载地址,
    然后是文件的存放路径,这里需要说明,DownloadManager下载的位置是不能放到内置存贮位置的,必须放到Enviroment中,这里建议放到自己应用的文件夹,不要直接放到SD卡中,也就是通过getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)获取到的路径,该位置的文件是属于应用自己的,在应用卸载时也会随着应用一起被删除掉,并且在使用该文件夹的时候,是不需要SD卡读写权限的
    然后通过request.setDestinationUri来设置存储位置,最后将请求加入到downloadManager中,会获得一个downloadID,这个downloadID比较重要,之后下载状态,进度的查询都靠这个downloadID

    2.2. 进度查询

    在查询下载进度的时候,会通过downloadId来指定查询某一任务的具体进度

    /**
     * 获取进度信息
     * @param downloadId 要获取下载的id
     * @return 进度信息 max-100
     */
    public int getProgress(long downloadId) {
        //查询进度
        DownloadManager.Query query = new DownloadManager.Query()
                .setFilterById(downloadId);
        Cursor cursor = null;
        int progress = 0;
        try {
            cursor = mDownloadManager.query(query);//获得游标
            if (cursor != null && cursor.moveToFirst()) {
                //当前的下载量
                int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                //文件总大小
                int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return progress;
    }
    

    在查询进度的时候会使用到DownloadManager.Query这个类,在查询的时候,也是使用的Cursor,跟查询数据库是一样的,进度信息会需要拿到文件的总大小,和当前大小,自己算一下,最后Cursor对象在使用过后不要忘记关闭了

    2.3 下载完成

    下载完成后,DownloadManager会发送一个广播,并且会包含downloadId的信息

    //下载完成的广播
    private class DownloadFinishReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            //下载完成的广播接收者
            long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        }
    }
    

    注册这个广播接收者

    //注册下载完成的广播
    mReceiver = new DownloadFinishReceiver();
    registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    

    其他

    这里需要注意一点,在下载完成后需要提升一下文件的读写权限,否则在安装的时候会出现apk解析失败的页面,就是别人访问不了我们的apk文件

    /**
     * 提升读写权限
     * @param filePath 文件路径
     * @return
     * @throws IOException
     */
    public static void setPermission(String filePath)  {
        String command = "chmod " + "777" + " " + filePath;
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec(command);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    chmod 是Linux下设置文件权限的命令,后面的三个数字每一个代表不同的用户组
    权限分为三种:读(r=4),写(w=2),执行(x=1)
    那么这三种权限就可以组成7种不同的权限,分别用1-7这几个数字代表,例如7 = 4 + 2 + 1,那么就代表该组用户拥有可读,可写,可执行的权限;5 = 4 + 1,就代表可读可执行权限
    而三位数字就带包,该登陆用户,它所在的组,以及其他人

    安装

    1. 普通模式

    1. 7.0之前

    在7.0之前安装的时候,只需要通过隐式Intent来跳转,并且指定安装的文件Uri即可

    Intent intent = new Intent(Intent.ACTION_VIEW);
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setDataAndType(Uri.fromFile(new File(apkPath)),
                        "application/vnd.android.package-archive");context.startActivity(intent);
    

    2. 7.0之后

    在Android7.0之后的版本运行上述代码会出现 android.os.FileUriExposedException
    "私有目录被限制访问"是指在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。
    而7.0的" StrictMode API 政策" 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。
    之前代码用到的Uri.fromFile就是商城一个file://的Uri
    在7.0之后,我们需要使用FileProvider来解决

    FileProvider

    第一步:
    在AndroidManifest.xml清单文件中注册provider

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.example.chenfengyao.installapkdemo"
        android:grantUriPermissions="true"
        android:exported="false">
        <!--元数据-->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_path" />
    </provider>
    

    需要注意一下几点:

    1. exported:必须为false
    2. grantUriPermissions:true,表示授予 URI 临时访问权限。
    3. authorities 组件标识,都以包名开头,避免和其它应用发生冲突。

    第二步:
    指定共享文件的目录,需要在res文件夹中新建xml目录,并且创建file_paths

    <resources xmlns:android="http://schemas.android.com/apk/res/android">
        <paths>
            <external-path path="" name="download"/>
        </paths>
    </resources>
    

    path="",是有特殊意义的,它代表根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。

    第三部:
    使用FileProvider

    Intent intent = new Intent(Intent.ACTION_VIEW);
    File file = (new File(apkPath));
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
    Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    context.startActivity(intent);
    

    相较于之前的代码,会把Uri改成使用FiliProvider创建的Uri,并且添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)来对目标应用临时授权该Uri所代表的文件,而且getUriForFile中的authority参数需要填写清单文件中的authorities的值

    3. 混合

    兼容7.0的安装代码是不能在7.0之前的版本运行的,这个时候就需要进行版本的判断了

    //普通安装
    private static void installNormal(Context context,String apkPath) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        //版本在7.0以上是不能直接通过uri访问的
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
            File file = (new File(apkPath));
            // 由于没有在Activity环境下启动Activity,设置下面的标签
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
            Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(new File(apkPath)),
                    "application/vnd.android.package-archive");
        }
        context.startActivity(intent);
    }
    

    2.root模式

    如果应用已经获取了root权限了,那么我们可以实现自动安装,即不会出现应用安装的页面,会在后台自己慢慢的安装,这个时候使用的就是用代码去写命令行了

    /**
     * 应用程序运行命令获取 Root权限,设备必须已破解(获得ROOT权限)
     *
     * @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath(); RootCommand(apkRoot);
     * @return  0 命令执行成功
     */
    public static int RootCommand(String command) {
        Process process = null;
        DataOutputStream os = null;
        try {
            process = Runtime.getRuntime().exec("su");
            os = new DataOutputStream(process.getOutputStream());
            os.writeBytes(command + "\n");
            os.writeBytes("exit\n");
            os.flush();
            int i = process.waitFor();
            Log.d("SystemManager", "i:" + i);
            return i;
        } catch (Exception e) {
            Log.d("SystemManager", e.getMessage());
            return -1;
        } finally {
            try {
                if (os != null) {
                    os.close();
                }
                process.destroy();
            } catch (Exception e) {
            }
        }
    }
    

    这个方法就是将命令写入到手机的shell中,su就代表root权限了,而命令执行成功的话,会返回0的,接下来是安装命令

    String command = "pm install -r " + mApkPath;
    

    -r 代表强制安装,否则如果手机中已有该应用的话就会安装失败了,值得注意的是,要想等待命令执行的结果这个过程是很漫长的,所以在使用命令的时候是需要放到主线程中的

    3. 整体项目

    在写完整代码的时候需要把下载的代码写到Service中,否则你的downloadid就得通过别的方式去存储了,而查询下载进度,也是需要一直去查了,那么就需要写一个循环,并且放到子线程中,我们用RxJava做会比较舒服


    1. 一些工具代码

    1. IOUtils

    package com.example.chenfengyao.installapkdemo.utils;
    
    import android.content.Context;
    import android.os.Environment;
    
    import java.io.Closeable;
    import java.io.File;
    import java.io.IOException;
    
    /**
     * Created by 陈丰尧 on 2017/4/16.
     */
    
    public class IOUtils {
        public static void closeIO(Closeable... closeables) {
            if (closeables != null) {
                for (Closeable closeable : closeables) {
                    if (closeable != null) {
                        try {
                            closeable.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        /**
         * 删除之前的apk
         *
         * @param apkName apk名字
         * @return
         */
        public static File clearApk(Context context, String apkName) {
            File apkFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), apkName);
            if (apkFile.exists()) {
                apkFile.delete();
            }
            return apkFile;
        }
    }
    

    这里面主要用到了删除之前apk的代码,下载前如果有历史版本,就把它删掉,下载新的

    2. InstallUtil

    package com.example.chenfengyao.installapkdemo.utils;
    
    import android.content.Context;
    import android.content.Intent;
    import android.net.Uri;
    import android.os.Build;
    import android.support.v4.content.FileProvider;
    import android.widget.Toast;
    
    import java.io.File;
    
    import io.reactivex.Observable;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.schedulers.Schedulers;
    
    /**
     * If there is no bug, then it is created by ChenFengYao on 2017/4/19,
     * otherwise, I do not know who create it either.
     */
    public class InstallUtil {
        /**
         *
         * @param context
         * @param apkPath 要安装的APK
         * @param rootMode 是否是Root模式
         */
        public static void install(Context context, String apkPath,boolean rootMode){
            if (rootMode){
                installRoot(context,apkPath);
            }else {
                installNormal(context,apkPath);
            }
        }
    
        /**
         * 通过非Root模式安装
         * @param context
         * @param apkPath
         */
        public static void install(Context context,String apkPath){
            install(context,apkPath,false);
        }
    
        //普通安装
        private static void installNormal(Context context,String apkPath) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            //版本在7.0以上是不能直接通过uri访问的
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
                File file = (new File(apkPath));
                // 由于没有在Activity环境下启动Activity,设置下面的标签
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
                Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
                //添加这一句表示对目标应用临时授权该Uri所代表的文件
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
            } else {
                intent.setDataAndType(Uri.fromFile(new File(apkPath)),
                        "application/vnd.android.package-archive");
            }
            context.startActivity(intent);
        }
    
        //通过Root方式安装
        private static void installRoot(Context context, String apkPath) {
            Observable.just(apkPath)
                    .map(mApkPath -> "pm install -r " + mApkPath)
                    .map(SystemManager::RootCommand)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(integer -> {
                        if (integer == 0) {
                            Toast.makeText(context, "安装成功", Toast.LENGTH_SHORT).show();
                        } else {
                            Toast.makeText(context, "root权限获取失败,尝试普通安装", Toast.LENGTH_SHORT).show();
                            install(context,apkPath);
                        }
                    });
        }
    }
    

    该类只负责安装APK,如果是Root模式的话,会首先进行尝试,如果失败了,还会调用一次普通模式,进行安装的,注意root模式安装的代码,不要忘记放到子线程中去执行了

    3. SystemManager

    package com.example.chenfengyao.installapkdemo.utils;
    
    import android.util.Log;
    
    import java.io.DataOutputStream;
    import java.io.IOException;
    
    /**
     * Created by 陈丰尧 on 2017/4/16.
     */
    
    public class SystemManager {
    
        /**
         * 应用程序运行命令获取 Root权限,设备必须已破解(获得ROOT权限)
         *
         * @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath();
         * @return  0 命令执行成功
         */
        public static int RootCommand(String command) {
            Process process = null;
            DataOutputStream os = null;
            try {
                process = Runtime.getRuntime().exec("su");
                os = new DataOutputStream(process.getOutputStream());
                os.writeBytes(command + "\n");
                os.writeBytes("exit\n");
                os.flush();
                int i = process.waitFor();
    
                Log.d("SystemManager", "i:" + i);
                return i;
            } catch (Exception e) {
                Log.d("SystemManager", e.getMessage());
                return -1;
            } finally {
                try {
                    if (os != null) {
                        os.close();
                    }
                    process.destroy();
                } catch (Exception e) {
                }
            }
        }
    
        /**
         * 提升读写权限
         * @param filePath 文件路径
         * @return
         * @throws IOException
         */
        public static void setPermission(String filePath)  {
            String command = "chmod " + "777" + " " + filePath;
            Runtime runtime = Runtime.getRuntime();
            try {
                runtime.exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    该类主要就是放一些需要写入到shell中的代码

    2. DownLoadService

    package com.example.chenfengyao.installapkdemo;
    
    import android.app.DownloadManager;
    import android.app.Service;
    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.Binder;
    import android.os.Environment;
    import android.os.IBinder;
    import android.support.annotation.Nullable;
    import android.util.Log;
    import android.util.LongSparseArray;
    
    import com.example.chenfengyao.installapkdemo.utils.IOUtils;
    import com.example.chenfengyao.installapkdemo.utils.InstallUtil;
    import com.example.chenfengyao.installapkdemo.utils.SystemManager;
    
    import java.io.File;
    
    /**
     * If there is no bug, then it is created by ChenFengYao on 2017/4/20,
     * otherwise, I do not know who create it either.
     */
    public class DownloadService extends Service {
        private DownloadManager mDownloadManager;
        private DownloadBinder mBinder = new DownloadBinder();
        private LongSparseArray<String> mApkPaths;
        private boolean mIsRoot = false;
        private DownloadFinishReceiver mReceiver;
    
        @Override
        public void onCreate() {
            super.onCreate();
            mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
            mApkPaths = new LongSparseArray<>();
            //注册下载完成的广播
            mReceiver = new DownloadFinishReceiver();
            registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        }
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        @Override
        public void onDestroy() {
            unregisterReceiver(mReceiver);//取消注册广播接收者
            super.onDestroy();
        }
    
        public class DownloadBinder extends Binder{
            /**
             * 下载
             * @param apkUrl 下载的url
             */
            public long startDownload(String apkUrl){
                //点击下载
                //删除原有的APK
                IOUtils.clearApk(DownloadService.this,"test.apk");
                //使用DownLoadManager来下载
                DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
                //将文件下载到自己的Download文件夹下,必须是External的
                //这是DownloadManager的限制
                File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
                request.setDestinationUri(Uri.fromFile(file));
    
                //添加请求 开始下载
                long downloadId = mDownloadManager.enqueue(request);
                Log.d("DownloadBinder", file.getAbsolutePath());
                mApkPaths.put(downloadId,file.getAbsolutePath());
                return downloadId;
            }
    
            public void setInstallMode(boolean isRoot){
                mIsRoot = isRoot;
            }
    
            /**
             * 获取进度信息
             * @param downloadId 要获取下载的id
             * @return 进度信息 max-100
             */
            public int getProgress(long downloadId) {
                //查询进度
                DownloadManager.Query query = new DownloadManager.Query()
                        .setFilterById(downloadId);
                Cursor cursor = null;
                int progress = 0;
                try {
                    cursor = mDownloadManager.query(query);//获得游标
                    if (cursor != null && cursor.moveToFirst()) {
                        //当前的下载量
                        int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                        //文件总大小
                        int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
    
                        progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
                    }
                } finally {
                    if (cursor != null) {
    
                        cursor.close();
                    }
                }
    
                return progress;
            }
    
        }
    
        //下载完成的广播
        private class DownloadFinishReceiver extends BroadcastReceiver{
    
            @Override
            public void onReceive(Context context, Intent intent) {
                //下载完成的广播接收者
                long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                String apkPath = mApkPaths.get(completeDownloadId);
                Log.d("DownloadFinishReceiver", apkPath);
                if (!apkPath.isEmpty()){
                    SystemManager.setPermission(apkPath);//提升读写权限,否则可能出现解析异常
                    InstallUtil.install(context,apkPath,mIsRoot);
                }else {
                    Log.e("DownloadFinishReceiver", "apkPath is null");
                }
            }
        }
    }
    
    • Service和Client通信是使用Binder来做的,提供开始下载,设置安装模式和获取进度的方法
    • DownloadFinishReceiver是用来监听下载完成的广播接收者,当下载完成后就直接调用InstallUtil来去自动安装,广播再使用过后不要忘记取消监听了
    • LongSparseArray 可以理解为key值是long类型的HashMap,但是效率要稍高一点,在Android中都推荐使用各种的SparseArray

    3. Activity

    1. xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >
    
        <ProgressBar
            android:id="@+id/down_progress"
            android:max="100"
            style="@style/Widget.AppCompat.ProgressBar.Horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/down_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="开始下载"/>
    
        <Switch
            android:id="@+id/install_mode_switch"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="普通模式"
            />
    
    </LinearLayout>
    

    布局文件就比较简单了,progressBar来显示进度,switch来切换模式,然后就是一个下载的按钮

    2. Activity

    package com.example.chenfengyao.installapkdemo;
    
    import android.content.ComponentName;
    import android.content.Intent;
    import android.content.ServiceConnection;
    import android.os.Bundle;
    import android.os.IBinder;
    import android.support.v7.app.AppCompatActivity;
    import android.widget.Button;
    import android.widget.ProgressBar;
    import android.widget.Switch;
    import android.widget.Toast;
    
    import java.util.concurrent.TimeUnit;
    
    import io.reactivex.Observable;
    import io.reactivex.Observer;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.disposables.Disposable;
    import io.reactivex.schedulers.Schedulers;
    
    public class MainActivity extends AppCompatActivity {
    
        private static final String APK_URL = "http://101.28.249.94/apk.r1.market.hiapk.com/data/upload/apkres/2017/4_11/15/com.baidu.searchbox_034250.apk";
        private Switch installModeSwitch;
        private ProgressBar mProgressBar;
        private Button mDownBtn;
        private DownloadService.DownloadBinder mDownloadBinder;
        private Disposable mDisposable;//可以取消观察者
    
        private ServiceConnection mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mDownloadBinder = (DownloadService.DownloadBinder) service;
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                mDownloadBinder = null;
            }
        };
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            installModeSwitch = (Switch) findViewById(R.id.install_mode_switch);
            mProgressBar = (ProgressBar) findViewById(R.id.down_progress);
            mDownBtn = (Button) findViewById(R.id.down_btn);
    
            Intent intent = new Intent(this, DownloadService.class);
            startService(intent);
            bindService(intent, mConnection, BIND_AUTO_CREATE);//绑定服务
    
    
            installModeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
                if (isChecked) {
                    buttonView.setText("root模式");
                } else {
                    buttonView.setText("普通模式");
                }
                if (mDownloadBinder != null) {
                    mDownloadBinder.setInstallMode(isChecked);
                }
            });
    
            mDownBtn.setOnClickListener(v -> {
                if (mDownloadBinder != null) {
                    long downloadId = mDownloadBinder.startDownload(APK_URL);
                    startCheckProgress(downloadId);
                }
    
            });
    
        }
    
        @Override
        protected void onDestroy() {
            if (mDisposable != null) {
                //取消监听
                mDisposable.dispose();
            }
            super.onDestroy();
        }
    
        //开始监听进度
        private void startCheckProgress(long downloadId) {
            Observable
                    .interval(100, 200, TimeUnit.MILLISECONDS, Schedulers.io())//无限轮询,准备查询进度,在io线程执行
                    .filter(times -> mDownloadBinder != null)
                    .map(i -> mDownloadBinder.getProgress(downloadId))//获得下载进度
                    .takeUntil(progress -> progress >= 100)//返回true就停止了,当进度>=100就是下载完成了
                    .distinct()//去重复
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new ProgressObserver());
        }
    
    
        //观察者
        private class ProgressObserver implements Observer<Integer> {
    
            @Override
            public void onSubscribe(Disposable d) {
                mDisposable = d;
            }
    
            @Override
            public void onNext(Integer progress) {
                mProgressBar.setProgress(progress);//设置进度
            }
    
            @Override
            public void onError(Throwable throwable) {
                throwable.printStackTrace();
                Toast.makeText(MainActivity.this, "出错", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onComplete() {
                mProgressBar.setProgress(100);
                Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
    • 在Activity中需要startService和bindService都使用,因为我们需要和Service建立联系,又需要让Service脱离Activity的运行
    • 主要说一下checkProgress的代码,在该方法中会使用RxJava来达到轮询的功能
      • interval: 该操作符会一直无限的发射事件,从1,2,3,一直这样下去,100代表第一个事件延迟100ms,200代表每个事件之间有200ms的间隔
      • filter: 会过滤掉不符合条件的事件,例如如果binder为空的话,事件就不往下传递了
      • map: 当事件到这里的时候,就通过binder来查询一下进度
      • takeUntil: 事件持续到什么时候为止,因为interval是无限发射的,总需要一个结束的情况,就使用这个takeUntil,一直到进度达到100的时候就不再查询了,相当于跳出循环的条件,会触发观察者的onComplete方法
      • distinct: 去重,因为下载进度会查到很多的重复数据,这些数据没必要都设置到progressBar中,可以利用该操作符去去重
      • 线程切换: 子线程发布事件,主线程观察,要刷新UI嘛
      • 最后订阅一个观察者,这个观察者也是自己的类实现了Observer的接口
    • ProgressObserver:
      • 在RxJava2中,Observer会在订阅的时候传入一个Disposable,该对象可以允许观察者主动的去取消事件,在Activity的onDestroy中会去取消事件
      • onNext中是设置给ProgressBar进度信息
      • onComplete是下载完成

    相关文章

      网友评论

      • 媛兮姑娘521:/**
        * 获取进度信息
        * @param downloadId 要获取下载的id
        * @Return 进度信息 max-100
        */
        public int getProgress(long downloadId) {
        //查询进度
        DownloadManager.Query query = new DownloadManager.Query()
        .setFilterById(downloadId);
        Cursor cursor = null;
        int progress = 0;
        try {
        cursor = mDownloadManager.query(query);//获得游标
        if (cursor != null && cursor.moveToFirst()) {
        //当前的下载量
        int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
        //文件总大小
        int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));

        progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
        }
        } finally {
        if (cursor != null) {

        cursor.close();
        }
        }

        return progress;
        }

        }

        你好:我这里得到的progress一直是0,无限循环,麻烦指点下问题出在哪里了呢?谢谢
      • 媛兮姑娘521:你好:我想请问下,我运行这个Demo,不稳定呢,有时候点击“开始下载”可以出现进度条下载,有时候直接没反应呢。也没有报错,麻烦楼主给个回复,万分感谢

      本文标题:Android7.0下载Apk自动安装

      本文链接:https://www.haomeiwen.com/subject/cfllzttx.html