美文网首页
android——文件断点续传下载(二)

android——文件断点续传下载(二)

作者: 刘孙猫咪 | 来源:发表于2019-01-20 19:15 被阅读0次
    GIF.gif
    上篇博客写的是单个文件单个线程的断点续传下载(文件断点续传下载(一)),这里是在之前的基础上进行了扩展,变成了多文件,多线程同时下载/暂停,同时添加了通知栏下载状态显示,虽然代码是基于之前的,还是做了不少改动。
    1、activity中显示的是一个列表,点击事件的触发是在列表的条目中,就需要在点击开始时,将点击事件通过接口回调到activity中,进行权限的申请
    2、当多任务、多线程同时操作时,利用BroadcastReceiver进行通信会导致界面不有点卡,就改用了Messenger+Handler进行通信
    3、需要对Notification做8.0的适配
    4、利用TimerTask、Timer进行消息的轮询
    5、改用线程池对线程的管理
    先看下列表适配器中的逻辑,很简单,
    public class FileListAdapter extends BaseAdapter {
        private Context mContext;
        private List<FileInfo> list;
    
        public FileListAdapter(Context context, List<FileInfo> list) {
            this.mContext = context;
            this.list = list;
        }
        @Override
        public int getCount() {
            return list == null ? 0 : list.size();
        }
    
        @Override
        public Object getItem(int position) {
            return list.get(position);
        }
    
        @Override
        public long getItemId(int position) {
            return position;
        }
    
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            final FileInfo fileInfo = list.get(position);
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = LayoutInflater.from(mContext).inflate(R.layout.file_list_adapter_item, parent, false);
                viewHolder.tvFileName = convertView.findViewById(R.id.tv_file_name);
                viewHolder.progressBar = convertView.findViewById(R.id.progress_bar);
                viewHolder.btnStop = convertView.findViewById(R.id.btn_stop);
                viewHolder.btnStart = convertView.findViewById(R.id.btn_start);
    
                viewHolder.progressBar.setMax(100);
                viewHolder.tvFileName.setText(fileInfo.fileName);
                viewHolder.btnStart.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (listener != null) {
                            listener.doStart(position, fileInfo);
                        }
                    }
                });
                viewHolder.btnStop.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (listener != null) {
                            listener.doStop(position, fileInfo);
                        }
                    }
                });
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
    
            viewHolder.progressBar.setProgress(fileInfo.finished);
    
            return convertView;
        }
    
        /**
         * 更新列表项中条目下载进度
         */
        public void updateProgress(List<FileInfo> fileInfos) {
            this.list = fileInfos;
            notifyDataSetChanged();
        }
    
        class ViewHolder {
            TextView tvFileName;
            ProgressBarView progressBar;
            Button btnStop, btnStart;
        }
    
        private onItemClick listener;
    
        public void setOnItemClick(onItemClick listener) {
            this.listener = listener;
        }
    
        public interface onItemClick {
            /**
             * 开始下载
             *
             * @param position
             */
            void doStart(int position, FileInfo fileInfo);
    
            /**
             * 暂停下载
             *
             * @param position
             */
            void doStop(int position, FileInfo fileInfo);
        }
    }
    

    不过有点细节需要注意的,赋值一次后不需要变动的数据,就不需要数据复用了,这样会提高一些性能,这里文件名称的赋值、ProgressBar的初始化、开始/暂停的点击事件就没有进行复用,然后在回调事件中去做权限的申请、开启下载/暂停下载的逻辑处理。
    之前是通过startService的方式开启一个Service的,这里没有采用该方式,用的是绑定服务的方式,在onCreate()方法中绑定一个Service;

    //绑定service
    Intent intent = new Intent(this, DownLoadService.class);
    bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE);
    

    Service绑定后,就会回调ServiceConnection中的onServiceConnected()方法,在该方法中利用Messenger同时在Service的onBind()方法中也利用Messenger进行activity和Service数据通信双向绑定;

    ServiceConnection serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                //绑定回调
                //获得service中的Messenger
                mServiceMessenger = new Messenger(service);
                //创建activity中的Messenger
                Messenger messenger = new Messenger(mHandler);
                //创建一个消息
                Message message = new Message();
                message.what = DownLoadService.MSG_BIND;
                message.replyTo = messenger;
                //使用service的messenger发送activity中的Messenger
                try {
                    mServiceMessenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                //解绑回调
            }
        };
    
    @Override
        public IBinder onBind(Intent intent) {
            //创建一个Messenger对象
            Messenger messenger = new Messenger(mHandler);
            //返回Messenger的binder
            return messenger.getBinder();
        }
    
    Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what) {
                    case MSG_INIT:
                        mSGINIT(msg);
                        break;
                    case MSG_BIND:
                        //处理绑定的Messenger
                        mActivityMessenger = msg.replyTo;
                        break;
                    case MSG_START:
                        mSGSTART(msg);
                        break;
                    case MSG_STOP:
                        mSGSTOP(msg);
                        break;
                }
            }
        };
    

    实现了activity和service的通信绑定,在点击开始或者暂停时就可以Messenger给service发送消息了;

    private void doStart(FileInfo fileInfo) {
            //将开始下载的文件id存储在集合中
            if (fileIds.contains(fileInfo.id)) {
                //正在下载提示用户 并返回
                Toast.makeText(this, "该文件正在下载中...", Toast.LENGTH_LONG).show();
                return;
            }
            //将文件id添加到集合中
            fileIds.add(fileInfo.id);
            Message message = new Message();
            message.what = DownLoadService.MSG_START;
            message.obj = fileInfo;
            try {
                mServiceMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            Log.e("doStart-->", "doStart");
        }
    
    private void doStop(FileInfo fileInfo) {
            //暂停下载时将存储的下载文件id移除
            for (int i = 0; i < fileIds.size(); i++) {
                Integer integer = fileIds.get(i);
                if (integer == fileInfo.id) {
                    fileIds.remove(integer);
                    break;
                }
            }
            Log.e("doStop-->", "doStop");
            Message message = new Message();
            message.what = DownLoadService.MSG_STOP;
            message.obj = fileInfo;
            try {
                mServiceMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    

    在开始下载时需要将下载文件的id缓存的集合中,用于管理用户多次点击同一个正在下载的文件,在暂停下载或下载完成后在从集合中移除对应的文件id,消息发送后,在Service中Handler的handerMessage()方法中根据msg.whate进行处理。
    开始下载时还是和之前一样,在子线程中获取文件的大小,在本地创建文件夹,然后通过handler通知进行下载,只是改成了利用线程池进行线程的管理;

    /**
         * 开始下载
         *
         * @param msg
         */
        private void mSGSTART(Message msg) {
            FileInfo fileInfo = (FileInfo) msg.obj;
            Log.e(TAG, ACTION_START);
            //开启线程
            InitThread thread = new InitThread(fileInfo);
            DownLoadTask.sExecutorService.execute(thread);
        }
    

    在下载的时候需要对多线程、多任务进行管理,同时开始时通过消息告诉activity去开启消息通知栏,显示下载进度;

    /**
         * 初始化处理
         *
         * @param msg
         */
        private void mSGINIT(Message msg) {
            FileInfo fileInfo = (FileInfo) msg.obj;
            Log.e(TAG, MSG_INIT + "");
            //启动下载任务
            DownLoadTask task = new DownLoadTask(DownLoadService.this, fileInfo, 1, mActivityMessenger);
            task.downLoad();
            //把下载任务添加到集合中
            mTasks.put(fileInfo.id, task);
            //给Activity发送消息
            Message message = new Message();
            message.what = MSG_START;
            message.obj = fileInfo;
            try {
                mActivityMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    

    在activity的handler中就可以通知开启通知状态栏,

    /**
         * 开始下载通知通知栏
         *
         * @param msg
         */
        private void mSGSTART(Message msg) {
            //显示一个通知
            FileInfo info = (FileInfo) msg.obj;
            if (info != null) {
                mNotificationUtil.showNotification(info);
            }
        }
    
    /**
     * 通知的工具类
     */
    public class NotificationUtil {
        //实例化Notification通知管理类
        private NotificationManager mNotificationManager;
        //管理Notification的集合
        private Map<Integer, Notification.Builder> mNotification;
        private Context mContext;
    
        public NotificationUtil(Context context) {
            mContext = context;
            mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            mNotification = new HashMap<>();
        }
    
        /**
         * 显示notification
         */
        public void showNotification(FileInfo fileInfo) {
            //是否存在该文件下载通知
            if (!mNotification.containsKey(fileInfo.id)) {
                //android8.0 notification适配
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    NotificationChannel channel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT);
                    channel.canBypassDnd();//可否绕过请勿打扰模式
                    channel.enableLights(true);//有新消息时手机有闪光
                    channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);//锁屏显示通知
                    channel.setLightColor(Color.argb(100, 100, 100, 100));//指定闪光时的灯光颜色
                    channel.canShowBadge();//有新消息时桌面角标显示
                    channel.enableVibration(true);//振动
                    AudioAttributes audioAttributes = channel.getAudioAttributes();//获取系统响铃
                    channel.getGroup();//获取通知渠道组
                    channel.setBypassDnd(true);//设置可以绕过请勿打扰模式
                    channel.setVibrationPattern(new long[]{100, 100, 200});//振动模式
                    channel.shouldShowLights();//是否会闪灯
    
                    mNotificationManager.createNotificationChannel(channel);
    
                }
                //不存在就新增
                //创建通知的对象
                Notification.Builder notification;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                    //android8.0 notification适配
                    notification = new Notification.Builder(mContext)
                            .setChannelId("channel_id");
                } else {
                    notification = new Notification.Builder(mContext);
                }
                //设置滚动文字
                notification.setTicker(fileInfo.fileName + "开始下载")
                        .setWhen(System.currentTimeMillis())//设置通知显示的时间
                        .setSmallIcon(R.mipmap.ic_launcher_round)//设置图标
                        .setContentTitle(fileInfo.fileName)
                        .setContentText(fileInfo.fileName + "下载中...")
                        .setAutoCancel(true);//设置通知的特性  FLAG_AUTO_CANCEL点击通知栏自动消失
    
                //设置点击通知栏的操作
                Intent intent = new Intent(mContext, MainActivity.class);
                PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
                notification.setContentIntent(pendingIntent);
                //发出通知广播
                mNotificationManager.notify(fileInfo.id, notification.build());
                //把通知加到集合中
                mNotification.put(fileInfo.id, notification);
            }
        }
    
        /**
         * 取消通知
         *
         * @param id
         */
        public void cancleNotification(int id) {
            //取消通知
            mNotificationManager.cancel(id);
            //同时删除集合中的通知
            mNotification.remove(id);
        }
    
        /**
         * 更新通知进度
         *
         * @param id
         * @param progress
         */
        public void updateNotification(int id, int progress) {
            Notification.Builder notification = mNotification.get(id);
            if (notification != null) {
                //更新进度条
                notification.setProgress(100, progress, false);
                //重新发送通知
                mNotificationManager.notify(id, notification.build());
            }
        }
    }
    

    消息通知栏是通过NotificationUtil工具类来管理的,调用showNotification()方法创建并显示一个通知状态栏,在进度更新时调用updateNotification()方法,下载完成或者取消通知状态栏调用cancleNotification()就可以了,NotificationUtil类中已经对Notification做了8.0的适配处理,关于Notification的8.0适配网上很多资料,不过需要注意在调用setChannelId()时设置的id要和NotificationChannel创建时传入的id一致;

    NotificationChannel channel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT);
    

    在DownLoadTask中的downLoad()中还是要根据数据库查询的结果进行下载,数据库中如果有下载记录,直接将查询到的下载记录传入到下载线程中,如果没有就需要新增一个下载任务实例,在新增的时候需要注意,要根据传入的单个任务的下载线程个数来弄,要处理好每个线程的开始下载位置和结束下载位置;

    public void downLoad() {
            //读取数据库的线程信息
            List<ThreadInfo> threadInfos = dao.queryThread(mFileInfo.url);
            if (threadInfos.size() == 0) {
                //获得每个线程下载的长度 分线程下载,多个线程下载一个文件
                int length = mFileInfo.length / mThreadCount;
                for (int i = 0; i < mThreadCount; i++) {
                    ThreadInfo threadInfo = new ThreadInfo();
                    threadInfo.id = i;
                    threadInfo.url = mFileInfo.url;
                    threadInfo.thread_start = length * i;
                    threadInfo.thread_end = (i + 1) * length - 1;
                    threadInfo.finished = 0;
                    if (i == mThreadCount - 1) {
                        //最后一个线程下载的结束位置
                        threadInfo.thread_end = mFileInfo.length;
                    }
                    //添加到线程集合信息
                    threadInfos.add(threadInfo);
                    //向数据库中插入一条信息
                    if (!dao.isExists(threadInfo.url, threadInfo.id)) {
                        dao.insertThread(threadInfo);
                    }
                }
            }
            threadList = new ArrayList<>();
            //启动多个线程进行下载
            for (ThreadInfo threadInfo : threadInfos) {
                DownLoadThread downLoadThread = new DownLoadThread(threadInfo);
                DownLoadTask.sExecutorService.execute(downLoadThread);
                threadList.add(downLoadThread);
            }
            //启动定时任务
            mTimer.schedule(timerTask, 1000, 1000);
        }
    

    在开始的同时开启一个Timer进行轮询;

    TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                if (isPause) {
                    //暂停时要将定时器清除掉
                    cancelTimer();
                }
                //发送消息修改activity进度
                Message message = new Message();
                message.what = DownLoadService.MSG_UPDATE;
                message.arg1 = mFinished * 100 / mFileInfo.length;
                message.arg2 = mFileInfo.id;
                Log.e("DownLoadService", mFinished + "");
                try {
                    messenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        };
    
        /**
         * 清除定时器
         */
        private void cancelTimer(){
            //取消定时器
            timerTask.cancel();
            timerTask = null;
            mTimer.cancel();
            mTimer = null;
        }
    

    在下载任务进行的同时会每隔一段时间回调TimerTask中的run方法,通过Messenger向activity传递下载的进度,在暂停下载是需要将Timer移除并置为null。
    在DownLoadThread线程中下载完毕后做法和之前一样,多了要将Timer移除并置为null;

    /**
         * 下载线程
         */
        class DownLoadThread extends Thread {
            private ThreadInfo mThreadInfo;
            //线程是否执行完毕
            public boolean isFinished = false;
    
            public DownLoadThread(ThreadInfo threadInfo) {
                mThreadInfo = threadInfo;
            }
    
            @Override
            public void run() {
                super.run();
                HttpURLConnection conn = null;
                RandomAccessFile raf = null;
                InputStream input = null;
                try {
                    URL url = new URL(mThreadInfo.url);
                    conn = (HttpURLConnection) url.openConnection();
                    conn.setReadTimeout(6000);
                    conn.setRequestMethod("GET");
                    //设置下载位置
                    int start = mThreadInfo.thread_start + mThreadInfo.finished;
                    conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.thread_end);
                    //设置一个文件写入位置
                    File file = new File(DownLoadService.DOWNLOAD_PATH, mFileInfo.fileName);
                    raf = new RandomAccessFile(file, "rwd");
                    //设置文件写入位置
                    raf.seek(start);
                    mFinished += mThreadInfo.finished;
                    //开始下载了
                    if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                        //读取数据
                        input = conn.getInputStream();
                        byte[] buffer = new byte[1024 * 4];
                        int len = -1;
                        while ((len = input.read(buffer)) != -1) {
                            //写入文件
                            raf.write(buffer, 0, len);
                            //下载进度发送广播给activity
                            //累加整个文件的进度
                            mFinished += len;
                            //累加每个线程下载的进度
                            int currentFinished = mThreadInfo.finished;
                            mThreadInfo.finished = currentFinished + len;
    
                            //下载暂停是要把进度保存在数据库中
                            if (isPause) {
                                dao.updateThread(mThreadInfo.url, mThreadInfo.id, mThreadInfo.finished);
                                return;
                            }
                        }
                        //标识线程执行完毕
                        isFinished = true;
                        //检查下载任务是否执行完毕
                        checkAllThreadsFinished();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (conn != null) {
                            conn.disconnect();
                        }
                        if (raf != null) {
                            raf.close();
                        }
                        if (input != null) {
                            input.close();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    /**
         * 判断是否所有线程执行完毕
         */
        private synchronized void checkAllThreadsFinished() {
            boolean allFinished = true;
            //遍历线程集合判断线程是否执行完毕
            for (DownLoadThread downLoadThread : threadList) {
                if (!downLoadThread.isFinished) {
                    allFinished = false;
                    break;
                }
            }
            if (allFinished) {
                cancelTimer();
                //发送消息告诉activity下载完毕
                Message message = new Message();
                message.what = DownLoadService.MSG_FINISHED;
                message.obj = mFileInfo;
                try {
                    messenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                //删除线程信息
                dao.deleteThread(mFileInfo.url);
    
            }
        }
    

    在activity更新下载进度时要根据对应的文件id去更新进度;

    /**
         * 下载进度更新
         *
         * @param msg
         */
        private void mSGUPDATE(Message msg) {
            int finished = msg.arg1;
            int id = msg.arg2;
            updataItem(id, finished);
            //更新通知进度
            mNotificationUtil.updateNotification(id, finished);
        }
    
        /**
         * 下载完成后更新
         *
         * @param msg
         */
        private void mSGFINISHED(Message msg) {
            //更新进度为0
            FileInfo fileInfo = (FileInfo) msg.obj;
            //下载完成后移除存储的文件id
            for (int i = 0; i < fileIds.size(); i++) {
                Integer integer = fileIds.get(i);
                if (integer == fileInfo.id) {
                    fileIds.remove(integer);
                    break;
                }
            }
            if (fileInfo != null) {
                updataItem(fileInfo.id, fileInfo.length);
            }
            Log.e("fileInfo.length-->", fileInfo.length + "");
            Toast.makeText(MainActivity.this, fileInfo.fileName + "下载完毕", Toast.LENGTH_LONG).show();
            //下载完毕后取消通知
            mNotificationUtil.cancleNotification(fileInfo.id);
        }
    
        private void updataItem(int id, int finished) {
            for (int i = 0; i < lists.size(); i++) {
                FileInfo fileInfo = lists.get(i);
                if (fileInfo.id == id) {
                    lists.remove(i);
                    fileInfo.finished = finished;
                    lists.add(i, fileInfo);
                    break;
                }
            }
            adapter.updateProgress(lists);
        }
    

    源码地址

    相关文章

      网友评论

          本文标题:android——文件断点续传下载(二)

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