Android开发android基础知识Android开发

使用Android基础知识编写一个多任务多线程断点下载示例

2018-07-18  本文已影响29人  Luckychuan

前言

在学习完《第一行代码》的下载最佳实践后,打算利用Android的基础知识将此完善成一个多任务,断点,离线保存,支持后台的下载示例。适合刚把Android基础知识学完不知道怎么组合运用的初学者。用到的
Android知识有:
1.OkHttp断点下载;
2.Activity与Service通信;
3.权限获取;
4.RecyclerView和Adapter实现列表;
5.SQLite保存数据;
6.AsyncTask异步下载。

效果演示

下载、暂停、后台下载

功能点

1.能多条任务同时下载;
2.支持暂停,继续下载;
3.finish掉Activity后能在后台下载;
4.stopService退出程序时,使用数据库保存进度。

结构

1.结构图

结构图.PNG

2.结构分析

(1)UI部分

MainActivity中通过调用notifyDataSetChanged()等方法刷新RecyclerView的数据;当用户点击了RecyclerView中的StartButton和CancelButton,通过OnItemButtonClickListener回调MainActivity。

(2)Activity与Service通信

由于下载是个耗时操作,我们要使用Service来做数据和逻辑操作。使用Binder和OnTaskDataChangeListener实现Activity与Service通信。

(3)Service调用SQLite

在Service中调用DatabaseManager将当前的下载数据保存。

(4)DownloadManager

使用DownloadManager管理下载任务。Service调用DownloadManager做下载操作,DownloadManager通过DownloadView回调Service做数据更新。

(5)DownloadAsyncTask异步下载

DownloadManager用HashMap管理DownloadAsyncTask做多任务下载,DownloadAsyncTask用DownLoadListener将每一个任务的下载状态回调给DownloadManager。

具体实现

1.获取权限

实现下载功能我们需要“网络权限” 和 “存储读写权限”。

<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>

Android 6.0 以上还要在Activity请求权限。较为基础没什么多说的,直接上代码:

MainActivity.java

    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //获取权限
        int readStorageCheck = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
        if (readStorageCheck == PackageManager.PERMISSION_GRANTED) {
            initUI();
        } else {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
        }
    }
    
        @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        //当权限获得时
        if (grantResults.length > 0 &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            initUI();
        } else {
            if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                //弹出对话框提示用户接收权限
                AlertDialog.Builder dialog = new AlertDialog.Builder(this);
                dialog.setMessage("程序要获得权限后才能运行");
                dialog.setCancelable(false);
                dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        //请求读取手机存储的权限
                        ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
                    }
                });
                dialog.create().show();
            }
        }
    }

2.Activity与Service通信

(1)创建DownloadBinder

Activity需要Service做的事情有:
1.添加新任务;
2.点击了“开始”按钮;
3.点击了“暂停”按钮;
4.点击了“取消”按钮;
5.点击stopService退出程序时,使用数据库保存进度。
因此在DownloadBinder定义5个方法,使用url做参数,可以确定当前是哪一个下载任务被用户操作。

DownloadService.java

    class DownloadBinder extends Binder {

        //点击了开始按钮
        public void startDownload(String url) {        
        
        }

        //点击了暂停按钮
        public void pauseDownload(String url) {

        }

        //点击了取消按钮
        public void cancelDownload(String url) {

        }

        //新建任务
        public void newTask(String url) {

        }

        //stopService退出程序
        public void saveProgress() {

        }

    }

(2)创建OnTaskDataChangeListener

使用OnTaskDataChangeListener让Service回调Activity

DownloadService.java

    interface OnTaskDataChangeListener {
    
        //应用打开时,Activity初始化数据
        void onInitFinish(List<Task> list);

        //添加了新任务
        void onDataInsert(int position);

        //任务的状态发送了变化,例如:更新下载进度,开始和暂停转换
        void onDataChange(int position);

        //任务被取消
        void onDataRemove(int position);
    }

(3)绑定Service

当Activity启动时,绑定Service。同时调用mServiceBinder.setListener(MainActivity.this);将MainActivity实现的OnTaskDataChangeListener接口传到Service。这时Service就知道Activity启动了。这时候在setListener()方法中调用mListener.onInitFinish(mTasks)给Actvity初始化数据界面。

MainActivity.java

    //绑定Service,实现Activity和Service通信
    private DownloadService.DownloadBinder mServiceBinder;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mServiceBinder = (DownloadService.DownloadBinder) service;
            mServiceBinder.setListener(MainActivity.this);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mServiceBinder = null;
        }
    };
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //绑定Service
        Intent serviceIntent = new Intent(this, DownloadService.class);
        startService(serviceIntent);
        bindService(serviceIntent, mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }

DownloadService.java

    private ArrayList<Task> mTasks;
    private OnTaskDataChangeListener mListener;
    private DatabaseManager mDataBaseManager;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new DownloadBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        mListener = null;
        return super.onUnbind(intent);
    }   

     class DownloadBinder extends Binder {

        public void setListener(OnTaskDataChangeListener listener) {
            mListener = listener;

            if (mTasks == null) {
                mTasks = new ArrayList<>();
                //在数据库中取得数据
                mTasks.addAll(mDataBaseManager.query());
            }

            mListener.onInitFinish(mTasks);

        }
    }

3.Service中的逻辑操作

(1)创建数据java bean

Task.java

public class Task implements Serializable {

        private String url;
        private String name;
        private int progress;

        private boolean isDownloading;

        public Task(String url, String name) {
            this.url = url;
            this.name = name;
            isDownloading = true;
        }

        public Task(String url, String name, int progress) {
            this.url = url;
            this.name = name;
            this.progress = progress;
            this.isDownloading = false;
        }

    }

(2)定义Service中的成员变量

由于我们要实现Activity被销毁后还能继续在后台下载,因此将数据列表ArrayList<Task> mTasks的变化放在Service中,而不是在Activity中。使用DatabaseManager mDataBaseManager;做数据库操作。使用DownloadManager mDownloadManager;做下载操作。

DownloadService.java

    private  ArrayList<Task> mTasks;
    private OnTaskDataChangeListener mListener;
    private DatabaseManager mDataBaseManager;
    private DownloadManager mDownloadManager;
    
     @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: ");
        if (mDownloadManager == null) {
            Log.d(TAG, "onCreate: init mDownloadManager");
            mDownloadManager = new DownloadManager(this);
        }

        if (mDataBaseManager == null) {
            Log.d(TAG, "onCreate: init mDataBaseManager");
            mDataBaseManager = DatabaseManager.getInstance(getApplicationContext());
        }

        //后台下载
        Notification.Builder builder = new Notification.Builder(getApplicationContext());
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentText("下载");
        builder.setContentTitle("下载");
        Notification notification = builder.build();
//        notification.flags = Notification.FLAG_FOREGROUND_SERVICE;
        startForeground(0, notification);

        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        manager.notify(0, notification);

    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy: ");
        stopForeground(true);
        super.onDestroy();
    }

(3)实现DownloadBinder的方法

DownloadService.java

    class DownloadBinder extends Binder {

        public void startDownload(String url) {
            mDownloadManager.addDownloadTask(url);
        }

        public void pauseDownload(String url) {
            mDownloadManager.pauseDownload(url);
        }

        public void cancelDownload(String url) {
            mDownloadManager.cancelDownload(url);
        }

        public void newTask(String url) {
            //判断任务是否已经存在
            for (Task t : mTasks) {
                if (t.getUrl().equals(url)) {
                    Toast.makeText(getApplicationContext(), "任务已经存在", Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            String name = url.substring(url.lastIndexOf("/") + 1);
            Task task = new Task(url, name);
            mTasks.add(task);

            if (mListener != null) {
                mListener.onDataInsert(mTasks.size() - 1);
            }

            startDownload(url);
        }

        public void saveProgress() {
            for (Task task : mTasks) {
                Log.d(TAG, "saveProgress: "+task.toString());

                //停止下载
                if (task.getProgress() < 100) {
                    if(task.isDownloading()){
                        mDownloadManager.pauseDownload(task.getUrl());
                    }
                }

                //保存到数据库
                if (mDataBaseManager.query(task.getUrl()) == null) {
                    mDataBaseManager.insert(task);
                }else{
                    mDataBaseManager.update(task.getUrl(), task.getProgress());
                }
            }
        }

    }

(4)用DownloadView作DownloadManager下载状态的回调

DownloadView.java

    public interface DownloadView {

    void onDownloadPause(String url);

    void updateProgress(String url, int progress);

    void onFail(String url);

    void onCancel(String url);
    
    }

让Service实现DownloadView的方法,当DownloadManager的下载状态变化时,回调Service更新task数据。

DownloadService.java

    private  ArrayList<Task> mTasks;
    private OnTaskDataChangeListener mListener;
    private DatabaseManager mDataBaseManager;
    private DownloadManager mDownloadManager;
    
    @Override
    public void onDownloadPause(String url) {
        int position = getPosition(url);
        Task task = mTasks.get(position);
        task.setDownloading(false);

        if (mListener != null) {
            mListener.onDataChange(position);
        }
    }

    @Override
    public void updateProgress(String url, int progress) {
        int position = getPosition(url);
        Task task = mTasks.get(position);
        task.setProgress(progress);

        if (progress < 100) {
            task.setDownloading(true);
        } else {
            task.setDownloading(false);
        }

        if (mListener != null) {
            mListener.onDataChange(position);
        }

        Log.d(TAG, "updateProgress: " + progress);
    }

    @Override
    public void onFail(String url) {
        int position = getPosition(url);
        Task task = mTasks.get(position);
        task.setProgress(-1);
        task.setDownloading(false);

        if (mListener != null) {
            mListener.onDataChange(position);
        }
    }

    @Override
    public void onCancel(String url) {
        int position = getPosition(url);
        Task task = mTasks.get(position);
        mTasks.remove(position);

        if (mDataBaseManager.query(task.getUrl()) != null) {
            mDataBaseManager.delete(url);
        }

        if (mListener != null) {
            mListener.onDataRemove(position);
        }
    }

    /**
     * 通过url找到当前task在list的position
     *
     * @param url
     * @return
     */
    private int getPosition(String url) {
        for (int i = 0; i < mTasks.size(); i++) {
            Task task = mTasks.get(i);
            if (url.equals(task.getUrl())) {
                return i;
            }
        }
        return -1;
    }

4.DownloadManager管理下载任务

在DownloadManager使用HashMap管理DownloadAsyncTask。
1.在事件“新建任务”,“开始下载”中,我们都要新建DownloadAsyncTask;
2.当正在下载时,对于事件“暂停”和“取消”,我们只要根据url在map中找到当前asyncTask,将其暂停和取消。由于asyncTask的生命周期已经完成了,我们要将其在map中remove。
3.当用户在暂停状态下点击了取消按钮,由于当前任务已经在map中remove,要重新新建DownloadAsyncTask,才能将其取消。

根据以上分析,可以抽象出以下方法,并让DownloadManager继承并实现。

DownloadModel.java

    public abstract class DownloadModel {
        abstract void addDownloadTask(String url);
        abstract void pauseDownload(String url);
        abstract void cancelDownload(String url);
    }

DownloadManager.java

    public class DownloadManager extends DownloadModel {

        private HashMap<String, DownloadAsyncTask> mMap = new HashMap<>();
        private DownloadView mView;

        public DownloadManager(DownloadView view) {
            mView = view;
        }


        @Override
        public void addDownloadTask(final String url) {
            DownloadAsyncTask asyncTask = new DownloadAsyncTask(new DownloadAsyncTask.DownLoadListener() {

                @Override
                public void onDownloadPause() {
                    mMap.remove(url);
                    mView.onDownloadPause(url);
                }

                @Override
                public void updateProgress(int progress) {
                    mView.updateProgress(url, progress);
                }

                @Override
                public void onFail() {
                    mMap.remove(url);
                    mView.onFail(url);
                }

                @Override
                public void onCancel() {
                    mMap.remove(url);
                    mView.onCancel(url);

                }

                @Override
                public void onFinish() {
                    mMap.remove(url);
                }
            });
            mMap.put(url, asyncTask);
            //实现多任务下载,开始任务
            asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url);
        }

        @Override
        public void pauseDownload(String url) {
            DownloadAsyncTask task = mMap.get(url);
            task.setPause();
            mMap.remove(task);
        }

        @Override
        public void cancelDownload(String url) {
            DownloadAsyncTask task = mMap.get(url);
            //当未下载时点击取消,要新建AsyncTask
            if (task == null) {
                addDownloadTask(url);
            }
            task.setCancel();
            mMap.remove(task);
        }

    }

同时我们还需要在DownloadAsyncTask创建DownloadListener接口回调将当前AsyncTask的下载状态状态返回给DownloadManager,并由DownloadManager再通过url返回给Service。

DownloadAsyncTask.java

    public interface DownLoadListener {
        void onDownloadPause();
        void updateProgress(int progress);
        void onFail();
        void onCancel();
        void onFinish();
    }

5.DownloadAsyncTask实现下载

(1)创建类 DownloadAsyncTask

DownloadAsyncTask extends AsyncTask<String, Integer, Integer>,其中String为下载的url,第一个Integer为下载进度,第二个Integer为下载状态。

(2)下载的状态标识。

DownloadAsyncTask.java

    private static final int STATUS_SUCCEED = 1;
    private static final int STATUS_PAUSED = 2;
    private static final int STATUS_CANCELED = 3;
    private static final int STATUS_FAILED = 4;

当AsyncTask正在doInBackground()时,用户点击暂停或取消时,使用isPause或isCancel中断任务

DownloadAsyncTask.java

    private boolean isPause = false;
    private boolean isCancel = false;
    
     public void setPause() {
        isPause = true;
    }

    public void setCancel() {
        isCancel = true;
    }

(3)重写doInBackground方法

下载要用到OkHttp,引入闭包

    compile 'com.squareup.okhttp3:okhttp:3.4.1'

首先得到File文件。当新建AsyncTask时,有可能是“下载”,也有可能是在暂停状态下“取消”。若是取消,将file文件删除,并结束任务,返回状态STATUS_CANCELED。若是下载,通过file.exists()判断是重头下载还是继续下载,并得到已下载进度downloadedLength。然后通过OkHttp获取内容的大小contentLength。若contentLength为0,则无法下载;若contentLength == downloadedLength则表示下载完成。

DownloadAsyncTask.java

     @Override
    protected Integer doInBackground(String... params) {
        String url = params[0];
        String name = url.substring(url.lastIndexOf("/"));

        long downloadedLength = 0;
        String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
        File file = new File(directory + name);

        //判断任务是开始还是取消
        if(isCancel){
            if(file.exists()){
                file.delete();
            }
            return STATUS_CANCELED;
        }

        if(file.exists()){
            downloadedLength = file.length();
        }

        long contentLength = getContentLength(url);
        //无法下载
        if (contentLength == 0) {
            return STATUS_FAILED;
        } else if (contentLength == downloadedLength) {
            return STATUS_SUCCEED;
        }

        //开始下载
        isPause = false;
        isCancel = false;

        InputStream is = null;
        RandomAccessFile saveFile = null;
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().
                addHeader("RANGE", "bytes=" + downloadedLength + "-") //指定从哪一个字节下载
                .url(url).build();
        try {
            Response response = client.newCall(request).execute();
            //写入到本地
            if (response != null) {
                Log.d(TAG, "doInBackground: response not null");
                is = response.body().byteStream();
                saveFile = new RandomAccessFile(file, "rw");
                saveFile.seek(downloadedLength);
                int len;
                byte[] buffer = new byte[1024];
                while ((len = is.read(buffer)) != -1) {
                    if (isPause) {
                        return STATUS_PAUSED;
                    } else if (isCancel) {
                        if(file.exists()){
                            file.delete();
                        }
                        return STATUS_CANCELED;
                    }
                    //获取已下载的进度
                    saveFile.write(buffer, 0, len);
                    downloadedLength += len;
                    int progress = (int) (downloadedLength * 100 / contentLength);
                    publishProgress(progress);
                }

                response.body().close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (saveFile != null) {
                    saveFile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();

            }
        }

        if (contentLength == downloadedLength) {
            return STATUS_SUCCEED;
        }
        return STATUS_FAILED;
    }
    
    
    private long getContentLength(String url) {
        long contentLength = 0;
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(url).build();
        Response response = null;
        try {
            response = client.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (response != null && response.isSuccessful()) {
            contentLength = response.body().contentLength();
            response.close();
        } else {
            Log.d(TAG, "getContentLength: response null");
        }
        return contentLength;
    }

源代码

https://github.com/Luckychuan/MultiThreadDownloadDemo

·

上一篇下一篇

猜你喜欢

热点阅读