Android OkDownload的使用

一、前言:

OkDownload是一个android下载框架,是FileDownloader的升级版本,也称FileDownloader2;是一个支持多线程,多任务,断点续传,可靠,灵活,高性能以及强大的下载引擎。

  • 结合OkGo的request进行网络请求,支持与OkGo保持相同的配置方法和传参方式
  • 支持断点下载,支持突然断网,强杀进程后,继续断点下载
  • 每个下载任务具有无状态、下载、暂停、等待、出错、完成共六种状态
  • 所有下载任务按照tag区分,切记不同的任务必须使用不一样的tag,否者断点会发生错乱
  • 相同的下载url地址,如果使用不一样的tag,也会认为是两个下载任务
  • 不同的下载url地址,如果使用相同的tag,会认为是同一个任务,导致断点错乱
  • 默认同时下载数量为3个,默认下载路径/storage/emulated/0/download,下载路径和下载数量都可以在代码中配置
  • 下载文件名可以自己定义,也可以不传,让框架自动获取文件名

gitHub地址:https://github.com/lingochamp/okdownload

二、使用:

1、依赖

仓库依赖:

repositories {
    maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}

jar包依赖:

// okdownload核心库
implementation 'com.liulishuo.okdownload:okdownload:1.0.5'
//存储断点信息的数据库
implementation 'com.liulishuo.okdownload:sqlite:1.0.5' 
//提供okhttp连接,如果使用的话,需要引入okhttp网络请求库
implementation 'com.liulishuo.okdownload:okhttp:1.0.5'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'

2、全局控制

OkDownload.with().setMonitor(monitor);
DownloadDispatcher.setMaxParallelRunningCount(3); //最大并行下载数
RemitStoreOnSQLite.setRemitToDBDelayMillis(3000);
OkDownload.with().downloadDispatcher().cancelAll();
OkDownload.with().breakpointStore().remove(taskId);

3、组件注入

OkDownload.Builder builder = new OkDownload.Builder(context)
        .downloadStore(downloadStore)
        .callbackDispatcher(callbackDispatcher)
        .downloadDispatcher(downloadDispatcher)
        .connectionFactory(connectionFactory)
        .outputStreamFactory(outputStreamFactory)
        .downloadStrategy(downloadStrategy)
        .processFileStrategy(processFileStrategy)
        .monitor(monitor);

OkDownload.setSingletonInstance(builder.build());

4、动态串行队列

DownloadSerialQueue serialQueue = new DownloadSerialQueue(commonListener);

serialQueue.enqueue(task1);
serialQueue.enqueue(task2);

serialQueue.pause();

serialQueue.resume();

int workingTaskId = serialQueue.getWorkingTaskId();
int waitingTaskCount = serialQueue.getWaitingTaskCount();

DownloadTask[] discardTasks = serialQueue.shutdown();

4、合并几个下载的侦听器

DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();

DownloadListener combinedListener = new DownloadListenerBunch.Builder()
                   .append(listener1)
                   .append(listener2)
                   .build();

DownloadTask task = new DownloadTask.build(url, file).build();
task.enqueue(combinedListener);

5、任务的动态更改侦听器

// all attach or detach is based on the id of Task in fact. 
UnifiedListenerManager manager = new UnifiedListenerManager();

DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();
DownloadListener listener3 = new DownloadListener3();
DownloadListener listener4 = new DownloadListener4();

DownloadTask task = new DownloadTask.build(url, file).build();

manager.attachListener(task, listener1);
manager.attachListener(task, listener2);

manager.detachListener(task, listener2);

// all listeners added for this task will be removed when task is end.
manager.addAutoRemoveListenersWhenTaskEnd(task.getId());

// enqueue task to start.
manager.enqueueTaskWithUnifiedListener(task, listener3);

manager.attachListener(task, listener4);

5、为任务配置特殊配置文件

DownloadTask.Builder builder = new DownloadTask.Builder(url, file);

// Set the minimum internal milliseconds of progress callbacks to 100ms.(default is 3000)
builder.setMinIntervalMillisCallbackProcess(100);

// set the priority of the task to 10, higher means less time to wait to download.(default is 0)
builder.setPriority(10);

// set the read buffer to 8192 bytes for the response input-stream.(default is 4096)
builder.setReadBufferSize(8192);

// set the flush buffer to 32768 bytes for the buffered output-stream.(default is 16384)
builder.setFlushBufferSize(32768);

// set this task allow using 5 connections to download data.
builder.setConnectionCount(5);

// build the task.
DownloadTask task = builder.build();

三、具体属性介绍:

1、 使用

DownloadTask task = new DownloadTask.Builder(url, parentFile)
         .setFilename(filename) 
         .setMinIntervalMillisCallbackProcess(30) // 下载进度回调的间隔时间(毫秒)
         .setPassIfAlreadyCompleted(false)// 任务过去已完成是否要重新下载
         .setPriority(10)
         .build();
task.enqueue(listener);//异步执行任务
task.cancel();// 取消任务
task.execute(listener);// 同步执行任务
DownloadTask.enqueue(tasks, listener); //同时异步执行多个任务

2、 配置 DownloadTask

  • setPreAllocateLength(boolean preAllocateLength) //在获取资源长度后,设置是否需要为文件预分配长度
  • setConnectionCount(@IntRange(from = 1) int connectionCount) //需要用几个线程来下载文件
  • setFilenameFromResponse(@Nullable Boolean filenameFromResponse)//如果没有提供文件名,是否使用服务器地址作为的文件名
  • setAutoCallbackToUIThread(boolean autoCallbackToUIThread) //是否在主线程通知调用者
  • setMinIntervalMillisCallbackProcess(int minIntervalMillisCallbackProcess) //通知调用者的频率,避免anr
  • setHeaderMapFields(Map headerMapFields)//设置请求头
  • addHeader(String key, String value)//追加请求头
  • setPriority(int priority)//设置优先级,默认值是0,值越大下载优先级越高
  • setReadBufferSize(int readBufferSize)//设置读取缓存区大小,默认4096
  • setFlushBufferSize(int flushBufferSize)//设置写入缓存区大小,默认16384
  • setSyncBufferSize(int syncBufferSize)//写入到文件的缓冲区大小,默认65536
  • setSyncBufferIntervalMillis(int syncBufferIntervalMillis)//写入文件的最小时间间隔
  • setFilename(String filename)//设置下载文件名
  • setPassIfAlreadyCompleted(boolean passIfAlreadyCompleted)//如果文件已经下载完成,再次发起下载请求时,是否忽略下载,还是从头开始下载
  • setWifiRequired(boolean wifiRequired)//只允许wifi下载

案例

private DownloadTask createDownloadTask(ItemInfo itemInfo) {
    return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH)) //设置下载地址和下载目录,这两个是必须的参数
        .setFilename(itemInfo.pkgName)//设置下载文件名,没提供的话先看 response header ,再看 url path(即启用下面那项配置)
        .setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
        .setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
        .setConnectionCount(1)  //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
        .setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
        .setMinIntervalMillisCallbackProcess(100) //通知调用者的频率,避免anr,默认3000
        .setWifiRequired(false)//是否只允许wifi下载,默认为false
        .setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
        //.setHeaderMapFields(new HashMap>())//设置请求头
        //.addHeader(String key, String value)//追加请求头
        .setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
        .setReadBufferSize(4096)//设置读取缓存区大小,默认4096
        .setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
        .setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
        .setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
        .build();
}

3、任务队列的构建、开始和停止

DownloadContext.Builder builder = new DownloadContext.QueueSet()
        .setParentPathFile(parentFile)
        .setMinIntervalMillisCallbackProcess(150)
        .commit();
builder.bind(url1);
builder.bind(url2).addTag(key, value);
builder.bind(url3).setTag(tag);
builder.setListener(contextListener);

DownloadTask task = new DownloadTask.Builder(url4, parentFile).build();
builder.bindSetTask(task);

DownloadContext context = builder.build();
context.startOnParallel(listener);
context.stop();

4、获取任务状态

Status status = StatusUtil.getStatus(task);
Status status = StatusUtil.getStatus(url, parentPath, null);
Status status = StatusUtil.getStatus(url, parentPath, filename);

boolean isCompleted = StatusUtil.isCompleted(task);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, null);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, filename);

Status completedOrUnknown = StatusUtil.isCompletedOrUnknown(task);

四、使用:

1、必要的配置:

1、申请两个权限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES
2、配置 FileProvider
3、添加依赖

2、Activity

public class DownloadActivity extends ListActivity {
    static final String URL1 = "https://oatest.dgcb.com.cn:62443/mstep/installpkg/yidongyingxiao/90.0/DGMmarket_rtx.apk";
    static final String URL2 = "https://cdn.llscdn.com/yy/files/xs8qmxn8-lls-LLS-5.8-800-20171207-111607.apk";
    static final String URL3 = "https://downapp.baidu.com/appsearch/AndroidPhone/1.0.78.155/1/1012271b/20190404124002/appsearch_AndroidPhone_1-0-78-155_1012271b.apk";

    ProgressBar progressBar;
    List list;
    HashMap map = new HashMap<>();

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String[] array = {"使用DownloadListener4WithSpeed",
            "使用DownloadListener3",
            "使用DownloadListener2",
            "使用DownloadListener3",
            "使用DownloadListener",
            "=====删除下载的文件,并重新启动Activity=====",
            "查看任务1的状态",
            "查看任务2的状态",
            "查看任务3的状态",
            "查看任务4的状态",
            "查看任务5的状态",};
        setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, array));
        list = Arrays.asList(new ItemInfo(URL1, "com.yitong.mmarket.dg"),
            new ItemInfo(URL1, "哎"),
            new ItemInfo(URL2, "英语流利说"),
            new ItemInfo(URL2, "百度手机助手"),
            new ItemInfo(URL3, "哎哎哎"));
        progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
        progressBar.setIndeterminate(false);
        getListView().addFooterView(progressBar);

        new File(Utils.PARENT_PATH).mkdirs();
        //OkDownload.setSingletonInstance(Utils.buildOkDownload(getApplicationContext()));//注意只能执行一次,否则报错
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //OkDownload.with().downloadDispatcher().cancelAll();
        for (String key : map.keySet()) {
            DownloadTask task = map.get(key);
            if (task != null) {
                task.cancel();
            }
        }
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        switch (position) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
                download(position);
                break;
            case 5:
                Utils.deleateFiles(new File(Utils.PARENT_PATH), null, false);
                recreate();
                break;
            default:
                ItemInfo itemInfo = list.get(position - 6);
                DownloadTask task = map.get(itemInfo.pkgName);
                if (task != null) {
                    Toast.makeText(this, "状态为:" + StatusUtil.getStatus(task).name(), Toast.LENGTH_SHORT).show();
                }

                BreakpointInfo info = StatusUtil.getCurrentInfo(itemInfo.url, Utils.PARENT_PATH, itemInfo.pkgName);
                //BreakpointInfo info = StatusUtil.getCurrentInfo(task);
                if (info != null) {
                    float percent = (float) info.getTotalOffset() / info.getTotalLength() * 100;
                    Log.i("bqt", "【当前进度】" + percent + "%");
                    progressBar.setMax((int) info.getTotalLength());
                    progressBar.setProgress((int) info.getTotalOffset());
                } else {
                    Log.i("bqt", "【任务不存在】");
                }
                break;
        }
    }

    private void download(int position) {
        ItemInfo itemInfo = list.get(position);
        DownloadTask task = map.get(itemInfo.pkgName);
        // 0:没有下载  1:下载中  2:暂停  3:完成
        if (itemInfo.status == 0) {
            if (task == null) {
                task = createDownloadTask(itemInfo);
                map.put(itemInfo.pkgName, task);
            }
            task.enqueue(createDownloadListener(position));
            itemInfo.status = 1; //更改状态
            Toast.makeText(this, "开始下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 1) {//下载中
            if (task != null) {
                task.cancel();
            }
            itemInfo.status = 2;
            Toast.makeText(this, "暂停下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 2) {
            if (task != null) {
                task.enqueue(createDownloadListener(position));
            }
            itemInfo.status = 1;
            Toast.makeText(this, "继续下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 3) {//下载完成的,直接跳转安装APP
            Utils.launchOrInstallApp(this, itemInfo.pkgName);
            Toast.makeText(this, "下载完成", Toast.LENGTH_SHORT).show();
        }
    }

    private DownloadTask createDownloadTask(ItemInfo itemInfo) {
        return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH)) //设置下载地址和下载目录,这两个是必须的参数
            .setFilename(itemInfo.pkgName)//设置下载文件名,没提供的话先看 response header ,再看 url path(即启用下面那项配置)
            .setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
            .setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
            .setConnectionCount(1)  //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
            .setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
            .setMinIntervalMillisCallbackProcess(100) //通知调用者的频率,避免anr,默认3000
            .setWifiRequired(false)//是否只允许wifi下载,默认为false
            .setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
            //.setHeaderMapFields(new HashMap>())//设置请求头
            //.addHeader(String key, String value)//追加请求头
            .setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
            .setReadBufferSize(4096)//设置读取缓存区大小,默认4096
            .setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
            .setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
            .setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
            .build();
    }

    private DownloadListener createDownloadListener(int position) {
        switch (position) {
            case 0:
                return new MyDownloadListener4WithSpeed(list.get(position), progressBar);
            case 1:
                return new MyDownloadListener3(list.get(position), progressBar);
            case 2:
                return new MyDownloadListener2(list.get(position), progressBar);
            case 3:
                return new MyDownloadListener1(list.get(position), progressBar);
            default:
                return new MyDownloadListener(list.get(position), progressBar);
        }
    }
}

3、辅助工具类

public class Utils {
    public static final String PARENT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/aatest";

    public static void launchOrInstallApp(Context context, String pkgName) {
        if (!TextUtils.isEmpty(pkgName)) {
            Intent intent = context.getPackageManager().getLaunchIntentForPackage(pkgName);
            if (intent == null) {//如果未安装,则先安装
                installApk(context, new File(PARENT_PATH, pkgName));
            } else {//如果已安装,跳转到应用
                context.startActivity(intent);
            }
        } else {
            Toast.makeText(context, "包名为空!", Toast.LENGTH_SHORT).show();
            installApk(context, new File(PARENT_PATH, pkgName));
        }
    }

    //1、申请两个权限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES ;2、配置FileProvider
    public static void installApk(Context context, File file) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        Uri uri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
            //【content://{$authority}/external/temp.apk】或【content://{$authority}/files/bqt/temp2.apk】
        } else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//【file:///storage/emulated/0/temp.apk】
            uri = Uri.fromFile(file);
        }
        Log.i("bqt", "【Uri】" + uri);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

    public static OkDownload buildOkDownload(Context context) {
        return new OkDownload.Builder(context.getApplicationContext())
            .downloadStore(Util.createDefaultDatabase(context)) //断点信息存储的位置,默认是SQLite数据库
            .callbackDispatcher(new CallbackDispatcher()) //监听回调分发器,默认在主线程回调
            .downloadDispatcher(new DownloadDispatcher()) //下载管理机制,最大下载任务数、同步异步执行下载任务的处理
            .connectionFactory(Util.createDefaultConnectionFactory()) //选择网络请求框架,默认是OkHttp
            .outputStreamFactory(new DownloadUriOutputStream.Factory()) //构建文件输出流DownloadOutputStream,是否支持随机位置写入
            .processFileStrategy(new ProcessFileStrategy()) //多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入
            //.monitor(monitor); //下载状态监听
            .downloadStrategy(new DownloadStrategy())//下载策略,文件分为几个线程下载
            .build();
    }

    /**
     * 删除一个文件,或删除一个目录下的所有文件
     *
     * @param dirFile      要删除的目录,可以是一个文件
     * @param filter       对要删除的文件的匹配规则(不作用于目录),如果要删除所有文件请设为 null
     * @param isDeleateDir 是否删除目录,false时只删除目录下的文件而不删除目录
     */
    public static void deleateFiles(File dirFile, FilenameFilter filter, boolean isDeleateDir) {
        if (dirFile.isDirectory()) {//是目录
            for (File file : dirFile.listFiles()) {
                deleateFiles(file, filter, isDeleateDir);//递归
            }
            if (isDeleateDir) {
                System.out.println("目录【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));//必须在删除文件后才能删除目录
            }
        } else if (dirFile.isFile()) {//是文件。注意 isDirectory 为 false 并非就等价于 isFile 为 true
            String symbol = isDeleateDir ? "\t" : "";
            if (filter == null || filter.accept(dirFile.getParentFile(), dirFile.getName())) {//是否满足匹配规则
                System.out.println(symbol + "- 文件【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));
            } else {
                System.out.println(symbol + "+ 文件【" + dirFile.getAbsolutePath() + "】不满足匹配规则,不删除");
            }
        } else {
            System.out.println("文件不存在");
        }
    }

    public static void dealEnd(Context context, ItemInfo itemInfo, @NonNull EndCause cause) {
        if (cause == EndCause.COMPLETED) {
            Toast.makeText(context, "任务完成", Toast.LENGTH_SHORT).show();
            itemInfo.status = 3; //修改状态
            Utils.launchOrInstallApp(context, itemInfo.pkgName);
        } else {
            itemInfo.status = 2; //修改状态
            if (cause == EndCause.CANCELED) {
                Toast.makeText(context, "任务取消", Toast.LENGTH_SHORT).show();
            } else if (cause == EndCause.ERROR) {
                Log.i("bqt", "【任务出错】");
            } else if (cause == EndCause.FILE_BUSY || cause == EndCause.SAME_TASK_BUSY || cause == EndCause.PRE_ALLOCATE_FAILED) {
                Log.i("bqt", "【taskEnd】" + cause.name());
            }
        }
    }
}

4、 辅助Bean

public class ItemInfo {
    String url;
    String pkgName; //包名
    int status;  // 0:没有下载 1:下载中 2:暂停 3:完成

    public ItemInfo(String url, String pkgName) {
        this.url = url;
        this.pkgName = pkgName;
    }
}

5、DownloadListener4WithSpeed

public class MyDownloadListener4WithSpeed extends DownloadListener4WithSpeed {
    private ItemInfo itemInfo;
    private long totalLength;
    private String readableTotalLength;
    private ProgressBar progressBar;//谨防内存泄漏
    private Context context;//谨防内存泄漏

    public MyDownloadListener4WithSpeed(ItemInfo itemInfo, ProgressBar progressBar) {
        this.itemInfo = itemInfo;
        this.progressBar = progressBar;
        context = progressBar.getContext();
    }

    @Override
    public void taskStart(@NonNull DownloadTask task) {
        Log.i("bqt", "【1、taskStart】");
    }

    @Override
    public void infoReady(@NonNull DownloadTask task, @NonNull BreakpointInfo info, boolean fromBreakpoint, @NonNull Listener4SpeedAssistExtend.Listener4SpeedModel model) {
        totalLength = info.getTotalLength();
        readableTotalLength = Util.humanReadableBytes(totalLength, true);
        Log.i("bqt", "【2、infoReady】当前进度" + (float) info.getTotalOffset() / totalLength * 100 + "%" + "," + info.toString());
        progressBar.setMax((int) totalLength);
    }

    @Override
    public void connectStart(@NonNull DownloadTask task, int blockIndex, @NonNull Map> requestHeaders) {
        Log.i("bqt", "【3、connectStart】" + blockIndex);
    }

    @Override
    public void connectEnd(@NonNull DownloadTask task, int blockIndex, int responseCode, @NonNull Map> responseHeaders) {
        Log.i("bqt", "【4、connectEnd】" + blockIndex + "," + responseCode);
    }

    @Override
    public void progressBlock(@NonNull DownloadTask task, int blockIndex, long currentBlockOffset, @NonNull SpeedCalculator blockSpeed) {
        //Log.i("bqt", "【5、progressBlock】" + blockIndex + "," + currentBlockOffset);
    }

    @Override
    public void progress(@NonNull DownloadTask task, long currentOffset, @NonNull SpeedCalculator taskSpeed) {
        String readableOffset = Util.humanReadableBytes(currentOffset, true);
        String progressStatus = readableOffset + "/" + readableTotalLength;
        String speed = taskSpeed.speed();
        float percent = (float) currentOffset / totalLength * 100;
        Log.i("bqt", "【6、progress】" + currentOffset + "[" + progressStatus + "],速度:" + speed + ",进度:" + percent + "%");
        progressBar.setProgress((int) currentOffset);
    }

    @Override
    public void blockEnd(@NonNull DownloadTask task, int blockIndex, BlockInfo info, @NonNull SpeedCalculator blockSpeed) {
        Log.i("bqt", "【7、blockEnd】" + blockIndex);
    }

    @Override
    public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull SpeedCalculator taskSpeed) {
        Log.i("bqt", "【8、taskEnd】" + cause.name() + ":" + (realCause != null ? realCause.getMessage() : "无异常"));
        Utils.dealEnd(context, itemInfo, cause);
    }
}

参考:https://www.cnblogs.com/baiqiantao/p/10679677.html

你可能感兴趣的:(Android OkDownload的使用)