说说如何使用 Android(安卓)服务下载文件(支持断点续传)
16lz
2021-01-25
1 添加网络库
在 build.gradle 中添加 okhttp3 库:
compile 'com.squareup.okhttp3:okhttp:3.10.0'
2 定义监听器
定义下载监听器,监听下载过程中的各种情况:
public interface DownloadListener { /** * 当前下载进度 * * @param progress 下载进度 */ void onProgress(int progress); /** * 下载成功 */ void onSuccess(); /** * 下载失败 */ void onFailed(); /** * 暂停下载 */ void onPaused(); /** * 取消下载 */ void onCanceled();}
3 定义异步下载任务
public class DownloadTask extends AsyncTask<String, Integer, DownloadTask.DownloadResult> { private static final String TAG = "DownloadTask"; /** * 下载结果 */ public enum DownloadResult { //下载成功 SUCCESS, //下载失败 FAILED, //下载被暂停 PAUSED, //下载被取消 CANCELED } //是否取消下载 private boolean isCanceled = false; //是否暂停下载 private boolean isPaused = false; //上一次的下载进度 private int lastProgress; private DownloadListener listener; public DownloadTask(DownloadListener listener) { this.listener = listener; } /** * 下载 * * @param params * @return */ @Override protected DownloadResult doInBackground(String... params) { InputStream inputStream = null; RandomAccessFile downloadedFile = null; File file = null; try { long downloadedLength = 0;//文件已下载的大小 String url = params[0];//获取下载地址 String fileName = url.substring(url.lastIndexOf("/")); //指定 SD 卡的 Download 目录 String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); file = new File(directory + fileName); if (file.exists()) {//文件已存在,则读取已下载的文件字节数(用于断点续传) downloadedLength = file.length(); } long contentLength = getContentLength(url);//获取待下载文件的总长度 Log.d(TAG, "doInBackground: 待下载文件的总长度:" + contentLength); Log.d(TAG, "doInBackground: 已下载文件的总长度:" + downloadedLength); if (contentLength == 0) {//下载失败 return DownloadResult.FAILED; } else if (contentLength == downloadedLength) {//下载成功 return DownloadResult.SUCCESS; } OkHttpClient client = buildHttpClient(); Request request = new Request.Builder() //指定从那个字节开始下载,即【断点下载】 .addHeader("RANGE", "bytes=" + downloadedLength + "-") .url(url) .build(); Response response = client.newCall(request).execute(); if (response != null) { inputStream = response.body().byteStream(); downloadedFile = new RandomAccessFile(file, "rw"); downloadedFile.seek(downloadedLength);//跳转到未下载的字节节点 byte[] bytes = new byte[1024]; int total = 0;//下载的总字节数 int readLength;//读取的字节数 while ((readLength = inputStream.read(bytes)) != -1) { //在下载的过程中,判断下载是否被取消或者被暂停 if (isCanceled) {//取消 return DownloadResult.CANCELED; } else if (isPaused) {//暂停 return DownloadResult.PAUSED; } else { total += readLength; downloadedFile.write(bytes, 0, readLength); //计算已下载的进度(单位:百分比) int progress = (int) ((total + downloadedLength) * 100 / contentLength); //发布下载进度 publishProgress(progress); } } response.body().close(); return DownloadResult.SUCCESS; } } catch (IOException e) { e.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } if (downloadedFile != null) { downloadedFile.close(); } if (isCanceled && file != null) { file.delete(); } } catch (IOException e) { e.printStackTrace(); } } return DownloadResult.FAILED; } @NonNull private OkHttpClient buildHttpClient() { return new OkHttpClient.Builder().retryOnConnectionFailure(true).connectTimeout(30, TimeUnit.SECONDS).build(); } /** * 更新当前的下载进度 * * @param values */ @Override protected void onProgressUpdate(Integer... values) { int progress = values[0]; if (progress > lastProgress) { listener.onProgress(progress); lastProgress = progress; } } /** * 根据下载结果调用相应的回调方法 * * @param result */ @Override protected void onPostExecute(DownloadResult result) { switch (result) { case SUCCESS: listener.onSuccess(); break; case FAILED: listener.onFailed(); break; case PAUSED: listener.onPaused(); break; case CANCELED: listener.onCanceled(); break; default: break; } } /** * 暂停下载 */ public void pause() { isPaused = true; } /** * 取消下载 */ public void cancel() { isCanceled = true; } /** * 获取待下载文件的总长度 * * @param url 下载文件地址 * @return */ private long getContentLength(String url) throws IOException { Log.d(TAG, "getContentLength:url:" + url); OkHttpClient client = buildHttpClient(); Request request = new Request.Builder().url(url).build(); Response response = client.newCall(request).execute(); if (response != null && response.isSuccessful()) { long contentLength = response.body().contentLength(); response.close(); return contentLength; } else { return 0; } }}
DownloadTask 类做了这些工作:
1、继承 AsyncTask 类并传入三个参数:
extends AsyncTask<String, Integer, DownloadTask.DownloadResult>
参数 | 说明 |
---|---|
String | 表示传递到后台服务的参数类型,这里参数指的是文件下载 URL 地址。 |
Integer | 表示使用整型数据作为下载进度的显示单位。 |
DownloadResult | 使用 DownloadResult 枚举类型来定义下载结果。 |
2、定义了枚举类型的下载结果。
3、重写了这些方法:
* doInBackground - 执行下载操作。
* onProgressUpdate - 更新当前的下载进度。
* onPostExecute - 根据下载结果调用相应的回调方法。
在 doInBackground 中,根据已下载的文件字节总数与要下载的字节总数做比较,让程序直接跳过已下载的字节,所以支持断点续传哦O(∩_∩)O哈哈~
4 定义下载服务
public class DownloadService extends Service { private DownloadTask task; private String url; public static final int NOTIFICATION_ID = 1; private DownloadListener listener = new DownloadListener() { @Override public void onProgress(int progress) { getNotificationManager().notify(NOTIFICATION_ID, buildNotification("下载中……", progress)); } @Override public void onSuccess() { task = null; //关闭前台服务 stopForeground(true); //通知下载成功 getNotificationManager().notify(NOTIFICATION_ID, buildNotification("下载成功", -1)); Toast.makeText(DownloadService.this, "下载成功", Toast.LENGTH_SHORT).show(); } @Override public void onFailed() { task = null; //关闭前台服务 stopForeground(true); //通知下载失败 getNotificationManager().notify(NOTIFICATION_ID, buildNotification("下载失败", -1)); Toast.makeText(DownloadService.this, "下载失败", Toast.LENGTH_SHORT).show(); } @Override public void onPaused() { task = null; Toast.makeText(DownloadService.this, "暂停下载", Toast.LENGTH_SHORT).show(); } @Override public void onCanceled() { task = null; //关闭前台服务 stopForeground(true); //通知下载取消 Toast.makeText(DownloadService.this, "取消下载", Toast.LENGTH_SHORT).show(); } }; private NotificationManager getNotificationManager() { return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } /** * 构建【下载进度】通知 * * @param title 通知标题 * @param progress 下载进度 */ private Notification buildNotification(String title, int progress) { Intent intent = new Intent(this, MainActivity.class); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)); builder.setContentTitle(title); int requestCode = 0; int flags = PendingIntent.FLAG_UPDATE_CURRENT; builder.setContentIntent(PendingIntent.getActivity(this, requestCode, intent, flags)); if (progress > 0) { builder.setContentText(progress + "%"); builder.setProgress(100, progress, false); } return builder.build(); } public DownloadService() { } @Override public IBinder onBind(Intent intent) { return new DownloadBinder(); } /** * 通过 Binder 让服务与活动之间实现通信 */ protected class DownloadBinder extends Binder { /** * 开始下载 * * @param url */ public void start(String url) { if (task == null) { DownloadService.this.url = url; task = new DownloadTask(listener); task.execute(url); startForeground(NOTIFICATION_ID, buildNotification("开始下载……", 0)); Toast.makeText(DownloadService.this, "开始下载……", Toast.LENGTH_SHORT).show(); } } /** * 暂停下载 */ public void pause() { if (task != null) { task.pause(); } } /** * 取消下载 */ public void cancel() { if (task != null) { task.cancel(); return; } if (url == null) { return; } //删除已下载文件 String fileName = url.substring(url.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); File file = new File(directory + fileName); if (file.exists()) { file.delete(); } //关闭通知 getNotificationManager().cancel(NOTIFICATION_ID); stopForeground(true); Toast.makeText(DownloadService.this, "被取消", Toast.LENGTH_SHORT).show(); } }}
这个大类做了以下工作:
1. 通过匿名类创建了 DownloadListener 实例。
2. 构建【下载进度】通知。
3. 通过 Binder 让服务与活动之间实现通信。
这里的 setProgress 定义如下:
public Builder setProgress(int max, int progress, boolean indeterminate)
参数 | 说明 |
---|---|
max | 最大进度。 |
progress | 当前进度。 |
indeterminate | 是否使用模糊进度条。 |
5 定义控制按钮
布局文件:
<?xml version="1.0" encoding="utf-8"?><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" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="开始下载" /> <Button android:id="@+id/pause" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="暂停下载" /> <Button android:id="@+id/cancel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="取消下载" />LinearLayout>
这里,我们定义了【开始】、【暂停】与【取消】按钮。
6 定义活动类
public class MainActivity extends AppCompatActivity implements View.OnClickListener { public static final int REQUEST_CODE = 1; private DownloadService.DownloadBinder binder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { binder = (DownloadService.DownloadBinder) service; } @Override public void onServiceDisconnected(ComponentName name) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.start).setOnClickListener(this); findViewById(R.id.pause).setOnClickListener(this); findViewById(R.id.cancel).setOnClickListener(this); Intent intent = new Intent(this, DownloadService.class); //启动服务,让【下载】服务持续在后台运行 startService(intent); //绑定服务,让活动与服务之间实现通信 bindService(intent, connection, BIND_AUTO_CREATE); //申请权限 if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE); } } @Override public void onClick(View v) { if (binder == null) { return; } switch (v.getId()) { case R.id.start: String url = "http://mirrors.shu.edu.cn/apache/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.zip"; binder.start(url); break; case R.id.pause: binder.pause(); break; case R.id.cancel: binder.cancel(); break; default: break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_CODE: if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "权限被拒绝啦", Toast.LENGTH_SHORT).show(); finish(); } break; default: break; } } @Override protected void onDestroy() { super.onDestroy(); //解绑服务 unbindService(connection); }}
活动类中做了这些事:
1. 建立与服务的绑定关系。
2. 为按钮定义点击事件。
3. 申请权限。
注意: 在 onDestroy() 中要记得解绑服务哦。
在代码中,我们把 Maven 作为下载演示路径。O(∩_∩)O哈哈~
7 声明权限与服务
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> intent-filter> activity> <service android:name=".DownloadService" android:enabled="true" android:exported="true">service>application>
8 运行
第一次运行会弹出权限申请:
点击 ALLOW 后,就可以点击相应的按钮进行下载控制啦。点击【开始】按钮后,可以在通知中看到下载进度:
一个支持断点续传的 Android 下载服务就完成啦,是不是很有成就感呀O(∩_∩)O哈哈~
更多相关文章
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- 【Android】自定义progressBar样式
- 解决android工程引用多个jar包导致的文件重复的错误
- Android和java创建xml文件和解析xml文件剖析
- Android之创建文件、目录
- android 拷贝文件到其他目录下