安卓 实现下载功能(30)

看了第一行代码,里面使用AsyncTask异步请求、OkHttp网络连接以及Service服务实现了一个从指定网站下载文件。

整个过程大致说一下:

1.首先创建一个布局以及Activity来启动服务进行开始下载,暂停下载,取消下载的服务。

2.在服务中连接一个异步请求,在异步请求中下载文件,而且在服务中创建一个Notification通知,提示使用者目前的下载状态。

3.在异步请求中进行下载文件的操作,通过OkHttp得到文件总长度,然后再从OkHttp中得到response,在response中得到输入流,进而通过存储到本地的文件长度和文件总长度的比例得到下载进度。

首先实现一个借口封装一个下载接收器:

public interface DownloadListener {
    //通知当前的下载进度
    void onProgress(int progress);
    //通知下载成功事件
    void onSuccess();
    //通知下载失败事件
    void onFailed();
    //通知下载暂停事件
    void onPaused();
    //通知下载取消事件
    void onCanceled();
}

之后实现一个下载的异步请求:

public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    private static final int TYPE_SUCCESS = 0;
    private static final int TYPE_FAILED = 1;
    private static final int TYPE_PAUSED = 2;
    private static final int TYPE_CANCELED = 3;

    private DownloadListener listener;

    private boolean isCanceled = false;
    private boolean isPaused = false;

    private int lastProgress;

    public DownloadTask(DownloadListener listener){
        this.listener = listener;
    }

    //params:在执行AsyncTask时需要传入的参数,在整个后台任务中使用。
    //progres:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
    //result:任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为进度单位。

    //这个方法会在后台任务开始执行前调用,用于进行一些界面上的初始化操作。
//    @Override
//    protected void onPreExecute() {
//        super.onPreExecute();
//    }

    //在该方法中的所有代码都会在子线程中运行,也就是说所有耗时任务都会在这里实现。
    //params:在执行AsyncTask时需要传入的参数,在整个后台任务中使用。(可变参数)
    @Override
    protected Integer doInBackground(String... params) {
        // 输入流
        InputStream is = null;
        // RandomAccessFile类的主要功能是完成随机读取功能,可以读取指定位置的内容。
        // 之前的File类只是针对文件本身进行操作的,而如果要想对文件内容进行操作,则可以使用RandomAccessFile类
        // 此类属于随机读取类,可以随机读取一个文件中指定位置的数据
        RandomAccessFile savedFile = null;
        // 文件
        File file = null;
        try{
            long downloadedLength = 0;
            //传入可变参数的第一个参数为Url
            String downloadUrl = params[0];
            //public String substring(int beginIndex)
            //第一个int为开始的索引,对应String数字中的开始位置,到String最后
            //lastIndexOf 是定位到最后一个"/"的位置
            //这里得到的就是文件名字
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            //File sdCard = Environment.getExternalStorageDirectory();
            //这个sdCard的路径为mnt/sdcard/ 即为SD卡根路径,我们可以指定访问的文件夹名
            //File directory_pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
            //第二种方法是一个更加方便的访问Android给我们提供好的一些公共目录的方法,第一种方式更加灵活,可以自己指定目录。
            //这里是获取公共目录Downloads的路径
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            //通过得到的directory以及filename创建文件
            file = new File(directory + fileName);
            //得到文件的长度
            if(file.exists()){
                downloadedLength = file.length();
            }
            //从网上(使用OkHttp)得到文件的长度(通过Response中的长度)这个是完整的文件长度,downloadedLength为下载的长度
            long contentLength = getContentLength(downloadUrl);
            if(contentLength == 0){
                return TYPE_FAILED;
            }else if(contentLength == downloadedLength){
                return TYPE_SUCCESS;
            }
            OkHttpClient client = new OkHttpClient();
            //添加头部
            Request request = new Request.Builder()
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if(response != null){
                //输入流得到response中的byteStream
                is = response.body().byteStream();
                //使用RandomAccessFile来读写文件
                savedFile = new RandomAccessFile(file, "rw");
                //从已经下载到的那个字节开始读取文件
                savedFile.seek(downloadedLength);

                byte[] b = new byte[1024];
                int total = 0;
                int len;
                //读InputStream中的byte[]
                while((len = is.read(b)) != -1){
                    //这里在循环中,每次都询问一次,这样如果实时过来暂停和取消信息,可以直接暂停和取消。
                    if(isCanceled){
                        return TYPE_CANCELED;
                    }else if(isPaused){
                        return TYPE_PAUSED;
                    }else{
                        total += len;
                        //继续存入文件
                        savedFile.write(b, 0, len);
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                        //转入onProgressUpdate
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return  TYPE_SUCCESS;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //把输入流、文件等东西全部关闭
            try{
                if(is != null){
                    is.close();
                }
                if(savedFile != null){
                    savedFile.close();
                }
                if(isCanceled && file != null){
                    file.delete();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    //当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate(Progress)方法就会很快被调用
    //该方法中携带的参数就是后台任务中传递过来的,在这个方法中可以利用参数中的数值对界面元素进行UI更新操作。
    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if(progress < lastProgress){
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    //当后台任务执行完毕并通过return语句发挥的时候,这个方法会被调用。
    //返回的数据作为参数传递到此方法中,可以使用返回数据进行UI操作。
    @Override
    protected void onPostExecute(Integer staus) {
        switch (staus){
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_CANCELED:
                listener.onCanceled();
            default:
                break;
        }
    }

    public void pauseDownload(){
        isPaused = true;
    }

    public void cancelDownload(){
        isCanceled = true;
    }

    //通过下载的Url来进行OkHttp连接,得到Response之后,分析Response的长度,返回文件的长度。
    private long getContentLength(String downloadUrl) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if(response != null && response.isSuccessful()){
            long contentLength = response.body().contentLength();
            //关闭response
            response.body().close();
            return contentLength;
        }
        return 0;
    }
}

接着是布局文件以及AndroidManifest中的配置:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/start_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="start download"/>

    <Button
        android:id="@+id/pause_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="pause download"/>

    <Button
        android:id="@+id/cancel_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="cancel download"/>
</LinearLayout>
<service android:name=".tools.DownloadService"
            android:enabled="true"
            android:exported="true"/>

实现一个服务:

public class DownloadService extends Service {

    private DownloadTask downloadTask;

    private String downloadUrl;

    private DownloadListener listener = new DownloadListener() {

        //Foreground Service:前台服务,因为Service实际上始终是工作在后台的。由于Service工作在后台的原因,
        //使用者并不知道它在运行, 有时候开发者需要使用者知道某个Service在运行时,就需要Foreground Service。
        //简单来说就是使用Notification通知来告知用户Service正在运行。
        @Override
        public void onProgress(int progress) {
            getNotificationManager().notify(1, getNotification("Downloading...", progress));
        }

        //下载成功过后关闭前台服务,显示提示。
        @Override
        public void onSuccess() {
            downloadTask = null;
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Downloading Success", -1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
        }

        //下载失败过后关闭前台服务,显示提示。
        @Override
        public void onFailed() {
            downloadTask = null;
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Downloading Failed", -1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
        }

        //下载暂停过后显示提示。
        @Override
        public void onPaused() {
            downloadTask = null;
            Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
        }

        //下载取消过后关闭前台服务,显示提示。
        @Override
        public void onCanceled() {
            downloadTask = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
        }
    };

    //这里设定了一种Binder来使Activity与Service进行绑定
    private DownloadBinder mBinder = new DownloadBinder();

    //在Bind的过程中bind的是DownloadBinder这一种Binder
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    public class DownloadBinder extends Binder{

        //这里写了一个开始进行下载的方法,其中得到Url以及配置异步请求
        public void startDownload(String url){
            if(downloadTask == null){
                downloadUrl = url;
                downloadTask = new DownloadTask(listener);
                //开始执行异步请求
                downloadTask.execute(downloadUrl);
                //开启前台服务
                startForeground(1, getNotification("Downloading...", 0));
                Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
            }
        }

        public void pauseDownload(){
            //如果下载正在处理,那么将其暂停
            if(downloadTask != null){
                downloadTask.pauseDownload();
            }
        }

        public void cancelDownload(){
            //如果正在进行下载请求,则取消
            if(downloadTask != null){
                downloadTask.cancelDownload();
            }else {
                //如果没有正在进行下载请求,则删除文件,停止前台服务
                if(downloadUrl != null){
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + fileName);
                    if(file.exists()){
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    //获取系统服务通知服务
    private NotificationManager getNotificationManager(){
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    //这里是创建一个Notification通知来进行提示。
    private Notification getNotification(String title, int progress){
        //指出点击这个通知进入哪个界面
        Intent intent = new Intent(this, DownloadActivity.class);
        //供当前App或之外的其他App调用,而常见的是供外部App使用,外部App执行这个 PendingIntent时,间接地调用里面的Intent
        //即外部App延时执行PendingIntent中描述的Intent及其最终行为
        //即使当前App已经不存在了,也能通过存在于PendingIntent里的 Context来执行Intent。
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);
        if(progress > 0){
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);
        }
        return builder.build();
    }
}

实现一个Activity进行开启服务,实现下载的入口:

public class DownloadActivity extends Activity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //把设置好的service与DownloadBinder绑定。
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

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

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

        Button startDownload = findViewById(R.id.start_download);
        Button pauseDownload = findViewById(R.id.pause_download);
        Button cancelDownload = findViewById(R.id.cancel_download);

        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);

        //开启服务
        Intent intent = new Intent(this, DownloadService.class);
        startService(intent);
        bindService(intent, connection, BIND_AUTO_CREATE);
        if(ContextCompat.checkSelfPermission(DownloadActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(DownloadActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
        }//添加权限(显示添加权限的界面)
    }

    @Override
    public void onClick(View v) {
        if(downloadBinder == null){
            return;
        }
        switch (v.getId()){
            case R.id.start_download:
                String url = "https://mirrors.tuna.tsinghua.edu.cn/apache/accumulo/1.7.4/accumulo-1.7.4-bin.tar.gz";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;
            default:
                break;
        }
    }

    //实现权限管理
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if(grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
                    Toast.makeText(DownloadActivity.this, "拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    //退出时unBind
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);
    }
}

一个开始下载的流程(个人理解),整个过程就是一个 Activity <--> Service <--> AsyncTask:

1.首先在Activity中启动服务,判断好是否有读写权限,之后点击开始下载的按钮,将url传入。

2.接着转入Service当中,在Service中配置好,之后开始执行异步请求,然后向用户发送一个请求正在进行的信息。

3.接着转入异步请求当中,在异步请求中,我们首先初始化好输入流,然后定义好文件存储的路径。这时候有两个操作:第一个操作是,从OkHttp的Response中获取文件的大小(长度),这就是我们需要下载的文件大小(长度)。第二个操作是,判断本地是否存在一个相同名字的文件,如果不存在则创建一个文件,开始传输数据,如果存在,那么就找到他的最后一个字节的位置,然后定位,从这个位置下载文件(也就是断点续传?)。

4.在异步请求中,正在下载文件的过程中,每获得一份数据(暂且这么称呼,不断循环的获取数据最后得到所有数据),系统都会判断一次是否得到一个暂停或者取消的指令(这个指令是通过Activity给Service传入,然后Service传入AsyncTask),如果有这些指令,直接暂停或取消,跳出doInBackground()方法。

5.在每获得一次数据中,如果成功的获取到了这一份数据,那么在这次循环的最后会调用publishProgress(progress);方法,这个方法可以转入onProgressUpdate()方法中,用来得到目前的进度,可以通过进度实时的更改UI,反馈给用户正在下载的进度。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值