作者:郭霖老大
这是郭霖老大的服务知识实战代码
想要动手实战的话可以看看下面知识:
异步消息处理机制 AsyncTask用法 如何定义服务 活动与服务的通信
步骤:
1.定义一个Interface类DownloadListener可以在Download中根据下载任务的结果进行UI更新操作
2.定义一个下载任务类DownloadTask继承AsynTask类,然后重写doInBackground()方法子线程中执行
一些耗时的网络请求和文件进度和内容更新操作
3.创建一个DownloadService服务,在其中创建DonwloadListener匿名类实例,并在匿名类中实现一些方法。这些方法就是根据下载任务返回的结果在UI线程中更新UI。另外为了活动和服务可以通信我们创建了DownloadBinder类,可以开始下载,暂停下载与取消下载,这些方法可以在活动中调用
4.在MainActivity类中创建ServiceConnection的匿名类,在onServiceConnected()方法中获取到DownloadBinder的实例。
给三个按钮设置监听器,启动服务和绑定服务,然后请求访问SD卡权限,根据不同监听结果调用downloadBinder的方法控制下载进度
下面是一个回调接口类DownloadListener
package com.gougoucompany.clarence.servicebestpractice; /** * Created by Clarence on 2018/4/16. */ public interface DownloadListener { void onProgress(int progress); //通知当前进度 void onSuccess(); //通知下载成功 void onFailed(); //通知下载失败 void onPaused(); //通知下载暂停 void onCanceled(); //通知下载取消 }
定义一个服务DownloadService类
/* * @Author: Clarence * @Date: 2018-04-17 23:24:52 * @Last Modified by: Clarence * @Last Modified time: 2018-04-17 23:25:18 */ package com.gougoucompany.clarence.servicebestpractice; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Binder; import android.os.Environment; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import java.io.File; /** * 为了保证DownloadTask可以一直在后台运行,我们还需要创建一个下载的服务 */ 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("Downloading...", 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, "Paused", Toast.LENGTH_SHORT).show(); } @Override public void onCanceled() { downloadTask = null; stopForeground(true); Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show(); } }; //DownloadServer可以和活动进行通信 private DownloadBinder mBinder = new DownloadBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } //开始下载,停止下载,和取消下载方法 class DownloadBinder extends Binder { 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(); } if(downloadUrl != null) { //取消下载时需将文件删除,并将通知关闭 String fileName = downloadUrl.substring(downloadUrl. lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS ).getPath(); Log.d("DownloadService", "下载的文件路径是: " + directory + fileName); 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); } private Notification getNotification(String title, int progress) { Intent intent = new Intent(DownloadService.this, MainActivity.class); 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) { //当progress大于或等于0时才需要显示下载进度 builder.setContentText(progress + "%"); //setProgress()方法有三个参数,作用分别是传入通知的最大进入,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条 builder.setProgress(100, progress, false); } return builder.build(); } }
下面是一个异步消息处理的封装类,我们可以在子线程中完成耗时的操作,处理网络请求和写入文件
DownloadTask类
package com.gougoucompany.clarence.servicebestpractice; import android.os.AsyncTask; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * Created by Clarence on 2018/4/16. * 3个泛型参数分别是: * 1.第一个泛型参数指定为String,表示在执行任务的时候传入一个字符串参数给后台任务 * 2.第二个泛型参数指定为Integer,表示使用整形数据来作为进度显示单位 * 3.第三个泛型参数指定为Integer,表示使用整形数据来反馈执行结果 */ 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; //DownloadListener是一个回调接口,用于对下载过程中的各种状态进行监听和回调 private DownloadListener listener; private boolean isCanceled = false; private boolean isPaused = false; private int lastProgress; public DownloadTask(DownloadListener listener){ this.listener = listener; } /*这个方法中的所有代码都会在子线程中执行,我们应该在这个方法中处理所有的耗时任务。 * 任务一旦完成,可以通过return语句来将任务的执行结果返回*/ @Override protected Integer doInBackground(String... params) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try { long downloadedLength = 0; //记录已下载的文件长度 String downloadUrl = params[0]; //substring()方法从索引号为downloadUrl.lastIndexOf("/)返回的索引号开始截取 String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/")); //获取内置SD卡的Download路径 String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS). getPath(); Log.d("DownloadTask", "下载的文件路径是: " + filename+directory); file = new File(directory + filename); //判断文件是否存在,如果存在则获取已经下载了的文件长度 if(file.exists()){ downloadedLength = file.length(); } long contentLength = getContentLength(downloadUrl); //如果获得文件的长度是0则说明下载失败,如果等于已下载的文件长度,则说明下载成功结束 if(contentLength == 0) { return TYPE_FAILED; } else if(contentLength == downloadedLength) { //已下载字节和文件总字节相等,说明已经下载完成了 return TYPE_SUCCESS; } //创建一个OkHttpClient实例 OkHttpClient client = new OkHttpClient(); //我们要从下载了的文件之后继续下载,因此加入header开启断点续传的功能,指定从哪个字节开始下载 Request request = new Request.Builder() .addHeader("RANGE", "bytes=" + downloadedLength + "-") .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if(response != null){ is = response.body().byteStream(); savedFile = new RandomAccessFile(file, "rw"); /*RandomAccessFile对象的seek()设置此文件开头测量文件指针偏移量,在该位置发生下一个读取或写入操作 * 其实就是将文件指针移动到我们要开始写入的位置而已*/ savedFile.seek(downloadedLength); //跳过已下载的字节 byte b[] = new byte[1024]; int total = 0; int len; /** * 使用java的文件流方式,不断从网络上读取数据,不断写入到本地 * ,一直到文件全部下载完成为止。在这个过程中,我们还要判断用户有没有触发暂停 * 或者取消操作,如果有的话,则返回TYPE_PAUSED或TYPE_CANCELED来中断下载,如果没有 * 的话则实时计算当前的下载速度,然后调用publishProgress()方法进行通知 */ while ((len = is.read(b)) != -1) { if(isCanceled){ return TYPE_CANCELED; } else if(isPaused){ return TYPE_PAUSED; } else { total += len; //write(byte[] b, int offset, int len)方法从指定的字节数组开始到该文件偏移量off写入len字节 savedFile.write(b, 0, len); //计算已下载的百分比 int progress = (int) ((total + downloadedLength) * 100 / contentLength); 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(Exception e){ e.printStackTrace(); } } return TYPE_FAILED; } //调用publishProgress()方法在界面上更新当前的下载进度 @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_FAILED: listener.onFailed(); break; case TYPE_CANCELED: listener.onCanceled(); break; case TYPE_PAUSED: listener.onPaused(); 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; } }
下面是一个主活动MainActivity
/* * @Author: Clarence * @Date: 2018-04-17 23:26:00 * @Last Modified by: Clarence * @Last Modified time: 2018-04-17 23:49:36 */ package com.gougoucompany.clarence.servicebestpractice; import android.Manifest; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.os.IBinder; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private DownloadService.DownloadBinder downloadBinder; /** * 创建一个ServiceConnection的匿名类,然后在onServiceConnected方法中获取到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(this); pauseDownload.setOnClickListener(this); cancelDownload.setOnClickListener(this); Intent intent = new Intent(this, DownloadService.class); startService(intent); //启动服务 //参数分别是Intent对象、ServiceConnection实例,标志位表示在活动和服务进行绑定后自动创建服务 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); Log.d("MainActivity", "向用户请求权限"); } } @Override public void onClick(View v) { if(downloadBinder == null) { return; } switch(v.getId()){ case R.id.start_download: String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe"; 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, 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: break; } } @Override protected void onDestroy() { super.onDestroy(); //活动被销毁了,一定要记的对服务进行解绑 unbindService(connection); } }
下面是运行结果