service做为四大组件之一,一般用来做长时间的耗时操作,像文件下载,音乐播放等。一般应用都会涉及到文件的下载,那怎么做到下载文件的同时,又不影响app使用,这时候就需要用到service组件了。而service比较消耗性能,启动后如果不手动停止,service会一直在后台运行,即使app退出,android为我们提供了一个IntentService服务类,很好的帮我们解决了这个问题,当任务完成后,IntentService会自动销毁,不需要我们手动stopService。
业务逻辑大概是这样:启动IntentService,在onStartCommand回调方法中初始化一个Notification,然后再onHandleIntent(子线程)中开始下载文件,并不断更新Notification的进度,当下载完成时,调用系统安装程序安装。代码如下:
service类:
public class DownloadService extends IntentService {
private NotificationManager notificationManger;
private Notification notification;
private NotificationCompat.Builder mBuilder;
private String title;
private boolean isRunning = false;
private static final int PUSH_NOTIFICATION_ID = (0x001);
private static final String PUSH_CHANNEL_ID = "PUSH_NOTIFY_ID";
private static final String PUSH_CHANNEL_NAME = "PUSH_NOTIFY_NAME";
public AppDownloadService() {
super(AppDownloadService.class.getSimpleName());
}
public AppDownloadService(String name) {
super(name);
}
@Override
protected void onHandleIntent(Intent intent) {
downloadTask(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if ( isRunning) {
return super.onStartCommand(intent, flags, startId);
}
notificationManger = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(PUSH_CHANNEL_ID, PUSH_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
if (notificationManger != null) {
notificationManger.createNotificationChannel(channel);
}
}
return super.onStartCommand(intent, flags, startId);
}
/**
* 方法: downloadTask <p>
* 描述: 开启一个下载线程 <p>
*/
protected void downloadTask(Intent intent) {
if (intent == null && isRunning) {
return;
}
isRunning = true;
title = intent.getStringExtra(KeyContacts.KEY_TITLE);// 下载链接
displayNotificationMessage(title);
url = intent.getStringExtra(KeyContacts.KEY_URL);// 下载链接
LogUtil.e("downloadTask download url:" + url);
if (TextUtils.isEmpty(url)) {
downloadCallBack.onError(DownloadUtil.ERROR);
return;
}
try {
String filePath = DownloadUtil.getTargetFile(AppDownloadService.this, url);
DownloadUtil.download(AppDownloadService.this, downloadCallBack, url, filePath);
} catch (Exception e) {
downloadCallBack.onError(DownloadUtil.ERROR);
}
}
@Override
public void success() {
updateNotification(DownloadUtil.FINISH, 0);
DownloadUtil.installApk(AppDownloadService.this, DownloadUtil.getTargetFile(AppDownloadService.this, url));
}
public void error(int errorType) {
if (errorType == DownloadUtil.ERROR) {
updateNotification(DownloadUtil.ERROR, 0);
} else if (errorType == DownloadUtil.SDCARDNOUSE) {
notificationManger.cancel(DownloadUtil.NOTIFY_ID_DOWNLOAD);
notificationManger.cancel(DownloadUtil.NOTIFY_ID_FINISHED);
ToastUtil.showCustomToast("SD卡无法使用");
}
}
public void downloading(int process) {
updateNotification(DownloadUtil.DOWNLOADING, process);
}
/**
* 方法: displayNotificationMessage <p>
* 描述: 下载时候显示一个通知栏 <p>
*/
private void displayNotificationMessage(String title) {
Intent notificationIntent = new Intent();
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
notificationIntent, 0);
mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle(title)
.setWhen(System.currentTimeMillis())
.setContentText(url)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(contentIntent)
.setChannelId(PUSH_CHANNEL_ID)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setTicker(title);
notification = mBuilder.build();
notification.flags |= Notification.FLAG_NO_CLEAR;
notificationManger.notify(DownloadUtil.NOTIFY_ID_DOWNLOAD, notification);
}
/**
* 方法: displayCancelNotification <p>
* 描述: 显示一个提示完成 或者出错的通知栏 <p>
*/
private void displayCancelNotification(String title, String content, int id) {
Intent notificationIntent = new Intent();
if (id == DownloadUtil.NOTIFY_ID_FINISHED) {
notificationIntent = DownloadUtil.getInstallIntent(this, DownloadUtil.getTargetFile(AppDownloadService.this, url));
}
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
notificationIntent, 0);
mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle(title)
.setWhen(System.currentTimeMillis())
.setContentText(content)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(contentIntent)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setTicker(content);
notification = mBuilder.build();
notification.defaults |= Notification.DEFAULT_SOUND;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManger.notify(id, notification);
}
/**
* 方法: updateNotification <p>
* 描述: 更新通知栏 <p>
*/
protected void updateNotification(int type, int progress) {
LogUtil.d("type=" + type + "---progress=" + progress);
switch (type) {
case DownloadUtil.DOWNLOADING:
notification.flags = Notification.FLAG_NO_CLEAR;
mBuilder.setProgress(100, progress, false)
.setContentInfo(progress + "%");
notificationManger.notify(DownloadUtil.NOTIFY_ID_DOWNLOAD, mBuilder.build());
break;
case DownloadUtil.FINISH:
notificationManger.cancel(DownloadUtil.NOTIFY_ID_DOWNLOAD);
displayCancelNotification(title, "下载完成,点击安装", DownloadUtil.NOTIFY_ID_FINISHED);
//notificationManger.cancel(NOTIFY_ID_FINISHED);
return;
case DownloadUtil.ERROR:
displayCancelNotification(title, "下载失败", DownloadUtil.NOTIFY_ID_ERROR);
notificationManger.cancel(DownloadUtil.NOTIFY_ID_DOWNLOAD);
notificationManger.cancel(DownloadUtil.NOTIFY_ID_FINISHED);
break;
default:
break;
}
}
}
service中用到的一些方法:
//获取下载路径:
public static String getTargetFile(Context mContext, String url) {
String fileName = getFileName(url);
if (TextUtils.isEmpty(fileName)) {
fileName = UUID.randomUUID().toString();
}
String filePath = getDownLoadFilePath(mContext);
if (TextUtils.isEmpty(filePath)) {
return "";
} else {
return filePath + File.separator + fileName;
}
}
public static String getDownLoadFilePath(Context context) {
if (FileUtil.isExternalStorageCanUse()) {
File tempPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator +AppConfig.SDCARD_DIR_PATH);
if (!tempPath.exists()) {
tempPath.mkdirs();
}
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + AppConfig.SDCARD_DIR_PATH;
} else if (FileUtil.isRootStorageCanUse()) {
return context.getCacheDir().getAbsolutePath();
} else {
//磁盘空间不足
return "";
}
}
//通过url生产文件名
public static String getFileName(String url) {
String filename = "";
// 从路径中获取
if (TextUtils.isEmpty(filename)) {
filename = url.substring(url.lastIndexOf("/") + 1);
}
return filename;
}
下载工具类:
public class DownloadUtil {
public static final int NOTIFY_ID_DOWNLOAD = 10001;
public static final int NOTIFY_ID_FINISHED = 10002;
public static final int NOTIFY_ID_ERROR = 10003;
public static final int FINISH = 1;
public static final int ERROR = 2;
public static final int DOWNLOADING = 3;
//SD卡无法使用
public static final int SDCARDNOUSE = 4;
/**
* 方法: getTargetFile <p>
* 描述: 得到下载的目标文件 如果为空则为UUID<p>
*/
public static String getTargetFile(Context mContext, String url) {
String fileName = getFileName(url);
fileName = MD5Util.md5(fileName);
if(!fileName.endsWith(".apk")){
fileName = fileName + ".apk";
}
String filePath = getDownLoadFilePath(mContext);
if (TextUtils.isEmpty(filePath)) {
return "";
} else {
return filePath + File.separator + fileName;
}
}
/**
* 方法: installApk <p>
* 描述: 发送安装APK指令 <p>
*/
public static void installApk(Context mContext, String fileName) {
mContext.startActivity(getInstallIntent(mContext, fileName));
}
/**
* 方法: getInstallIntent <p>
* 描述: 得到安装apk的intent <p>
*/
public static Intent getInstallIntent(Context mContext, String fileName) {
File apkFile = new File(fileName);
if (apkFile.exists()) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileProvider", apkFile);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
return intent;
}
return new Intent();
}
public static void download(Context mContext, DownloadCallBack downloadCallBack, String url, String filePath) {
int errorNumber = 1;
//下载失败尝试重新下载(只尝试2次)
while (!downloadInternal(mContext, downloadCallBack, url, filePath) && errorNumber < 2) {
errorNumber++;
}
}
/**
* 方法: download <p>
* 描述: 下载方法 <p>
*/
protected static boolean downloadInternal(Context mContext, DownloadCallBack downloadCallBack, String url, String filePath) {
int downLoadFileSize;
int fileSize = 0;
FileOutputStream fos = null;
InputStream is = null;
boolean success = true;
try {
if(url.startsWith("https://")){兼容https下载链接
SSLContext sslContext = SSLContext.getInstance("SSL");//第一个参数为 返回实现指定安全套接字协议的SSLContext对象。第二个为提供者
TrustManager[] tm = {new MyX509TrustManager()};
sslContext.init(null, tm, new SecureRandom());
HttpsURLConnection localURLConnection = (HttpsURLConnection) new URL(url).openConnection();
localURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
localURLConnection.setReadTimeout(100000);
localURLConnection.setConnectTimeout(100000);
localURLConnection.setRequestMethod("GET");
localURLConnection.setRequestProperty("Accept-Language", "zh-CN");
localURLConnection.setRequestProperty("Charset", "UTF-8");
localURLConnection.setRequestProperty("Connection", "Keep-Alive");
localURLConnection.setRequestProperty("Accept-Encoding", "identity");
localURLConnection.connect();
is = localURLConnection.getInputStream();
fileSize = localURLConnection.getContentLength();
}else {
HttpURLConnection localURLConnection = (HttpURLConnection) new URL(url).openConnection();
localURLConnection.setReadTimeout(100000);
localURLConnection.setConnectTimeout(100000);
localURLConnection.setRequestMethod("GET");
localURLConnection.setRequestProperty("Accept-Language", "zh-CN");
localURLConnection.setRequestProperty("Charset", "UTF-8");
localURLConnection.setRequestProperty("Connection", "Keep-Alive");
localURLConnection.setRequestProperty("Accept-Encoding", "identity");
localURLConnection.connect();
is = localURLConnection.getInputStream();
fileSize = localURLConnection.getContentLength();
}
if (is == null) {
downloadCallBack.onError(ERROR);
return false;
}
//filePath = DownloadUtil.getTargetFile(mContext,url);
if (TextUtils.isEmpty(filePath)) {
LogUtil.e("downloadTask STORE ERROR URL:" + url);
downloadCallBack.onError(SDCARDNOUSE);
return false;
}
File apkFile = new File(filePath);
File parentFile=apkFile.getParentFile();
if (!apkFile.exists()) {
if (!parentFile.exists()){
parentFile.mkdirs();
}
apkFile.createNewFile();
}
fos = new FileOutputStream(filePath, false);
// 把数据存入路径+文件名
byte buf[] = new byte[1024 * 4];
downLoadFileSize = 0;
downloadCallBack.onDownloading(0);
int readCount = 0;
int count =0;
long start = System.currentTimeMillis();
do {
// 循环读取
int numread = 0;
numread = is.read(buf);
if (numread == -1) {
break;
}
fos.write(buf, 0, numread);
downLoadFileSize += numread;
if(fileSize < 0){//做假效果
long now = System.currentTimeMillis();
if((now - start) % 1000 == 0){
count += 5 + (now - start) / 1000;
if(count > 98)
count = 98;
downloadCallBack.onDownloading(count);
}
}else{
if (readCount % 10 == 0 ) {
downloadCallBack.onDownloading(downLoadFileSize * 100 / fileSize);
}
}
readCount++;
} while (true);
downloadCallBack.onSuccess();
} catch (Exception e) {
downloadCallBack.onError(ERROR);
success = false;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return success;
}
}
}
回调接口:
public interface DownloadCallBack {
public void onError(int errorType);
public void onDownloading(int process);
public void onSuccess();
}
好了一个完整的前台下载service就完成了,记得需要在清单中注册service,并申请文件的读写权限,检查未知应用安装权限。检查未知应用安装权限比较特殊,不能像动态权限一样去申请,需要跳转到设置页面手动设置,所以我没想好在哪里检查合适,有好的解决方案的小伙伴可以私信告诉我。