Android开发学习 -- Day20 下载功能Demo

在前面的学习中,我们已经掌握了Service的用法,今天尝试实现一个在服务中经常会用到的功能 -- 下载。

在这个demo中,将涉及到许多前面的知识,大伙儿准备好了么?首先我们需要添加待会儿需要用到的依赖库

dependencies {
    ...
    implementation 'com.squareup.okhttp3:okhttp:3.10.0'
}

这里只添加了一个OKHttp的依赖,用于网络功能的部分,我们将使用OKHttp实现。

接下来定一个回调接口,用于对下载过程中的各种状态进行监听和回调。新建DownloadListener接口,代码如下:

public interface DownloadListener {
    void onProgress(int progress);

    void onSuccess();

    void onFailed();

    void onPaused();

    void onCancled();
}

这里我们简单的定义了5个回调方法,onProgress()用于通知下载进度,onSuccess()用于通知下载成功事件,onFailed()用于通知下载失败事件,onPaused()用于通知暂停下载事件,onCancled()用于通知取消下载事件。

回调接口定义好后,就可以开始编写下载功能了,这里我们用刚刚学过的AsyncTask来实现,新建DownloadTask并继承AsyncTask:

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_CANCLED = 3;

    private DownloadListener listener;

    private boolean isCancled = false;
    private boolean isPaused = false;
    private int lastProgress;

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

    @Override
    protected Integer doInBackground(String... params) {
        InputStream in = null;
        RandomAccessFile savedFile = null;
        File file = null;
        try {
            long downloadedLength = 0; // 记录已下载的文件长度
            String downloadUrl = params[0];
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();

            file = new File(directory + fileName);
            if (file.exists()) {
                downloadedLength = file.length();
            }
            long contentLength = 0;
            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()
                    .url(downloadUrl)
                    .build();

            Response response = client.newCall(request).execute();
            if (response != null) {
                in = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");
                savedFile.seek(downloadedLength);
                byte[] b = new byte[1024];
                int total = 0;
                int len;

                while ((len = in.read(b)) != -1) {
                    if (isCancled) {
                        return TYPE_CANCLED;
                    } else if (isPaused) {
                        return TYPE_PAUSED;
                    } else {
                        total += len;
                        savedFile.write(b, 0, len);
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (savedFile != null) {
                    savedFile.close();
                }
                if (isCancled && file != null) {
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    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.close();
            return contentLength;
        }
        return 0;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if (progress > lastProgress) {
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    @Override
    protected void onPostExecute(Integer status) {
        switch (status) {
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_CANCLED:
                listener.onCancled();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            default:
                break;
        }
    }

    public void pauseDownload() {
        isPaused = true;
    }

    public void cancleDownload() {
        isCancled = true;
    }
}

代码比之前的练习稍微长了少许,一点点来分析。先来看AsyncTask的3个泛型参数,这里我们把第一个泛型参数指定为String类型,表示在AsyncTask时需要传一个String类型的参数给后台任务;第二个泛型指定为Integer,表是用整型数据作为进度的显示单位;第三个泛型指定为Integer,表示将使用整型数据来反馈执行结果。

接下来我们定义了4个整型常量来表示下载的状态,TYPE_SUCCESS表示下载成功、TYPE_FAILED表示下载失败、TYPE_PAUSED表示下载暂停,TYPE_CANCLED表示下载取消。然后在DownloadTask的构造函数中要求传入我们刚才创建的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。

接下来就是重写doInBackground()、onProgressUpdate、onPostExecute方法了。

先来看doInBackground()方法,我们从参数中获得到了下载的URL,并根据URL解析出下载的文件名,然后指定下载目录Environment.DIRECTORY_DOWNLOADS,也就是/sdcard/Downlaod/文件夹。然后我们判断一下本地是否已经存在下载文件,如果存在就对比一下本地文件和远端文件的大小,如果大小一致,则不用从新下载,直接返回TYPE_SUCCESS状态。如果获取不到远端文件的大小,则认为下载失败,直接返回TYPE_FAILED状态。

如果本地文件不存在,且能够获取到远端的文件大小,则开始使用OkHttp来发送网络请求,然后开始写入数据。在这个过程中,我们还得判断用户是否有暂停和取消下载的动作,如果有则返回对应的状态,如TYPE_CANCLED或者TYPE_PAUSED。暂停和取消的状态,我们都是使用布尔型来进行控制的。最后直到完成数据的写入,返回TYPE_SUCCESS状态。这里由于涉及到读写,因此需要捕获异常,并在finally中关闭资源。

onProgressUpdate()方法就比较简单了,首先从参数中获取到传来的下载进度,然后和上一次的值做对比,如果有变化,就调用DownloadListener的onProgress()方法来通知下载进度跟新。

onPostExecute()同样很简单,根据后台任务传入的下载状态来进行回调,分别调用DownloadListener的各个对应方法。

这样下载功能就算基本完成了,我们接下来还需要一个下载的Service来保证DownloadTask一直在后台中执行,新建一个Service -- DownloadService:

public class DownloadService extends Service {

    private DownloadTask downloadTask;
    private String downloadUrl;

    private DownloadListener listener = new DownloadListener() {
        @Override
        public void onProgress(int progress) {
            getNotificationManager().notify(1, getNotification("Download...", progress));
        }

        @Override
        public void onSuccess() {
            downloadTask = null;
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Download Success", -1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onFailed() {
            downloadTask = null;
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Download Failed", -1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPaused() {
            downloadTask = null;
            Toast.makeText(DownloadService.this, "Download Paused", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onCancled() {
            downloadTask = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Download Cancled", Toast.LENGTH_SHORT).show();
        }
    };

    class DownloadBinder extends Binder {

        public void startDownload(String url) {
            if (downloadTask == null) {
                downloadUrl = url;
                downloadTask = new DownloadTask(listener);
//                downloadTask.execute(downloadUrl);
                downloadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, downloadUrl);
                startForeground(1, getNotification("Downloading...", 0));
                Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
            }
        }

        public void pausedDownload() {
            if (downloadTask != null) {
                downloadTask.pauseDownload();
            }
        }

        public void cancelDownload() {
            if (downloadTask != null) {
                downloadTask.cancleDownload();
            } else {
                if (downloadUrl != null) {
                    String fielname = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + fielname);
                    if (file.exists()) {
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "Cancled", Toast.LENGTH_SHORT).show();
                }
            }
        }

    }

    private IBinder mBinder = new DownloadBinder();

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private NotificationManager getNotificationManager() {
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    private Notification getNotification(String title, int progress) {
        Intent intent = new Intent(this, DownloadPracticeActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentIntent(pendingIntent);
        builder.setContentTitle(title);

        if (progress > 0) {
            builder.setContentText(progress + "%s");
            builder.setProgress(100, progress, false);
        }

        return builder.build();
    }
}

我们首先创建了DownloadListener的匿名类实例,并实现了onProgress()、onSuccess()、onFailed()、onPaused()和onCancled()5个方法。在onProgress()方法中,我们通过调用getNotificationManager()方法构建了一个用于显示下载进度的通知,然后调用NotificationManager中的notify方法去触发这个通知。这样在通知栏下拉就能看到当前的下载进度了。其他几个方法类似,都是构造一个通知来告诉用户下载的状态。

接下来为了让DownloadService可以和Activity进行通信,我们创建了一个DownloadBinder。DownloadBinder中提供了startDownload()、pausedDownload()、cancelDownload()3个方法。startDownload()方法中,我们创建了DownloadTask的实例,然后把刚才的DownloadListener最为参数传进去,然后调用executeOnExecutor开启下载。同时为让这个下载成为一个前台服务,我们还调用了startForeground()方法,这样就在系统的状态栏创建了一个持续存在通知。pausedDownload()很简单,只是调用了DownloadTask的pauseDownload()方法。cancelDownload()也类似,只是多了一步删除文件的操作。

Service完成后,我们就可以开始编写主Activity了,在布局中添加3个按钮,分别为开始下载,暂停下载和取消下载。

public class DownloadPracticeActivity extends BaseActivity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_download_practice);
        setTitle("Download Practice");
        Button startDownload = findViewById(R.id.btn_strat_download);
        Button pauseDownlaod = findViewById(R.id.btn_pause_download);
        Button cancleDownlaod = findViewById(R.id.btn_cancle_download);
        startDownload.setOnClickListener(this);
        pauseDownlaod.setOnClickListener(this);
        cancleDownlaod.setOnClickListener(this);

        Intent intent = new Intent(this, DownloadService.class);
        startService(intent);
        bindService(intent, connection, BIND_AUTO_CREATE);

        if (ContextCompat.checkSelfPermission(DownloadPracticeActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(DownloadPracticeActivity.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.btn_strat_download:
                String url = "http://img01.oneniceapp.com/app/nice-main-4.8.6-release.apk";
                downloadBinder.startDownload(url);
                break;
            case R.id.btn_pause_download:
                downloadBinder.pausedDownload();
                break;
            case R.id.btn_cancle_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(this, "You have denied the permission, app will finished", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

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

可以看到我们先创建了一个ServiceConnection的匿名类,然后在onServiceConnected()方法中获得了DownloadBinder实例,有了这个实例,我们就可以调用DownloadService中的各种方法了。

在onCreate()方法中,我们通过startService和bindService()方法启动和绑定服务。启动服务可以保证DownloadService一直在后台运行,绑定服务可以让DownloadPracticeActivity和DownloadService进行通信。除此之外,还要申请下写sd卡的权限WRITE_EXTERNAL_STORAGE。在点击事件中,如果点击下载按钮,就调用DownloadBinder中的startDownload()方法,并把url传入,其他的类似。最后需要注意的是要解绑服务,不然会造成内存泄露。

重新运行app,查看效果:


综合这个练习,总结一下,最近的学习中掌握了很多和Service相关的重要知识点,包括Android多线程编程,服务的基本用法,服务的生命周期,前台服务等。同时至此,我们已经将Android的四大组件都学习完了。

关注获取更多内容
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容