可以知道当前下载进度,并且可以下载一部分的时候进行停止下载,下载一部分的时候进行继续下载
我看的时候是基于Android7.0的,我又加了新特性NotificationChannel通知渠道,还有下载完成扫描文件
用到了回调接口,绑定服务,前台服务,子线程异步消息处理机制,IO操作,网络请求,通知
先从最简单的开始
这里就一个网络权限,和一个写入权限(写入权限需要动态获取)
依赖就一个Okhttp
implementation 'com.squareup.okhttp3:okhttp:3.4.1'
这是下载用的一个回调接口,定义了5个方法,在Service中实例化并实现这5个回调方法,作用是更新进度,和下载结果的显示
public interface DownloadListener {
//下载进度显示
void onProgress(int progress);
//下载状态显示,成功,失败,暂停,取消
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
}
这个是活动,启动服务的方式为绑定服务,所以初始化了ServiceConnection类和服务中自定义的DownloadBinder类,在连接成功时获取DownloadBinder对象
设置了三个按钮分别是开始下载,暂停下载,取消下载,具体实现是DownloadBinder中对应的这三个方法
在Activity创建的时候onCreate()中绑定服务,并且动态申请写入权限,在Activity销毁的Destroy()中解绑服务
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
//初始化对象
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_main);
Button startDownload= (Button) findViewById(R.id.start_download);
Button pauseDownload= (Button) findViewById(R.id.pause_download);
Button cancelDownload= (Button) findViewById(R.id.cancel_download);
//下载按钮
startDownload.setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = 26)
@Override
public void onClick(View v) {
String url="http://f.hiphotos.baidu.com/image/pic/item/b7fd5266d016092446517fdadd0735fae7cd34ff.jpg";
downloadBinder.startDownload(url);
}
});
//暂停按钮
pauseDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downloadBinder.pauseDownload();
}
});
//取消按钮
cancelDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downloadBinder.cancelDownload();
}
});
//绑定启动服务
Intent intent=new Intent(this,DownloadService.class);
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},1);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,String [] permissions,int[] grantResults){
switch (requestCode){
case 1:
if (grantResults.length>0&&grantResults[0]!=PackageManager.PERMISSION_GRANTED){
Toast.makeText(this,"拒绝权限将无法正常使用",Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
@Override
protected void onDestroy(){
super.onDestroy();
//断开服务的绑定
unbindService(connection);
}
}
这个是开启子线程实现具体网络请求和下载操作的类
这里直接继承了AsyncTask<>异步消息处理类(本质也是Android异步消息处理机制),对于异步消息处理机制不懂的请看
https://blog.csdn.net/yh18668197127/article/details/86224318
构造方法获取Context对象和DownloadListener对象
在doInBackground进行具体下载操作
先判断本地文件大小和远程文件的大小是否一致判断是否下载成功
下载的时候网络请求当前已下载的byte到文件末尾,写入文件到本地的时候也是从当前已下载的末尾开始写入
在写入文件的时候调用publishProgress(progress);传递下载进度
在onProgressUpdate中接收下载进度参数与当前下载进度对比,进行下载进度的更新
在onPostExecute中接收返回值,根据返回值来更新下载状态
public class DownloadTask extends AsyncTask{
private static final String TAG = "DownloadTask";
public static final int TYPE_SUCCESS=0;
public static final int TYPE_FAILED=1;
public static final int TYPE_PAUSED=2;
public static final int TYPE_CANCELED=3;
//初始化对象
private DownloadListener listener;
private Context context;
private boolean isCanceled=false;
private boolean isPaused=false;
private int lastProgress=0;
public DownloadTask(DownloadListener listener,Context context){
this.listener=listener;
this.context=context;
}
@Override
protected Integer doInBackground(String ... params) {
InputStream inputStream = null;
RandomAccessFile savedFile = null;
File file = null;
try {
long downloadedLength = 0;//已下载长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory= Environment.getExternalStorageDirectory().getPath();
Log.i(TAG, "doInBackground: "+directory+" "+fileName);
file = new File(directory + fileName);
if (file.exists()) {
downloadedLength = file.length();
}
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) {
inputStream = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadedLength);
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = inputStream.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);
publishProgress(progress);
}
}
//下载完成调用系统文件扫描机制,否则电脑连接显示不了文件
MediaScannerConnection.scanFile(context, new String[] { file.getAbsolutePath() }, null, null);
return TYPE_SUCCESS;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCanceled && file != null) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
@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_PAUSED:
listener.onPaused();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
default:
break;
}
}
public void pauseDownload(){
isPaused=true;
}
public void cancelDownload(){
isCanceled=true;
}
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.body().close();
return contentLength;
}
return 0;
}
}
自定义了DownloadBinder继承Binder
创建了DownloadListener对象并实现了它的五个回调方法
通过通知的方式来显示下载进度,和下载结果
//这是一个服务
public class DownloadService extends Service {
final String CHANNEL_ID = "channel_id_1";
final String CHANNEL_NAME = "channel_name_1";
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener = new DownloadListener() {
@RequiresApi(api = 26)
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1, getNotification("Downloading...", progress));
}
@RequiresApi(api = 26)
@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();
}
@RequiresApi(api = 26)
@Override
public void onFailed() {
downloadTask = null;
stopForeground(true);
getNotificationManager().notify(1, getNotification("Download Failed", -1));
Toast.makeText(DownloadService.this, "Download Success", 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();
}
};
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
@RequiresApi(api = 26)
public void startDownload(String url) {
if (downloadTask == null) {
downloadUrl = url;
downloadTask = new DownloadTask(listener,getApplication());
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();
}
if (downloadUrl != null) {
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
// String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
String directory = Environment.getExternalStorageDirectory().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();
}
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private NotificationManager getNotificationManager() {
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
@RequiresApi(api = 26)
private Notification getNotification(String s, int progress) {
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivities(this, 0, new Intent[]{intent}, 0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//只在Android O之上需要渠道
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
//如果这里用IMPORTANCE_NOENE就需要在系统的设置里面开启渠道,
//通知才能正常弹出
getNotificationManager().createNotificationChannel(notificationChannel);
}
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(s);
if (progress > 0) {
builder.setContentText(progress + "%");
builder.setProgress(100, progress, false);
}
return builder.build();
}
}