前言
应用更新应该是现在每个应用必备的一个功能。正是通过不断的更新,不断的调优,才使我们的应用更完善。当然在各大应用市场中,它们已经帮我们实现了这项功能,但是有一个问题,当我们的应用是在某度市场下载的应用,如果那天我们不在使用某度市场,而是用别的市场,之前的发布的市场无法通知我们的应用,那么是不是我们就无法更新了。所以封装一个自己的应用自动更新还是比较有必要的。那么今天我们就来学习一下,如何封装自己的应用自动更新功能。
自动更新的意义
- 能及时告知所有用户有新的版本
- 对用户来说,更新更加简单,无须打开第三方应用(避免应用来回切换,同时减少打开其他应用后用户不再回到本应用)
- 可以强制用户更新(一切特定的场景下)
- 更多的自动控制权
分析原理
- apk安装包文件下载
- 利用Notification通知用户更新进度
- 文件下载后调用系统安装应用
其实说白了就是下载更新的apk然后安装。如果对断电续传和通知不了解的话先看先这个小项目后台异步断电续传文件下载这个小项目是我学习第一行代码时写的,在写这篇文章突然想起来,现在回头看看,即使是入门,代码写的也是真心好。条例清晰,接口回调,方法封装,虽然小但是逻辑很清晰。
实践
我们先开下大体的思路流程:
大致流程就是这样。其实说白了就是下载任务然后安装。这里核心是下载部分那么我就可以用后台异步断电续传文件下载这个例子下载(已经合并2个例子放到一个工程中了)。在这里我在提供例外一种方法。
- UpdateDownLoadListener这个类就是下载回调的监听
public interface UpdateDownLoadListener {
/**
* 下载请求开始回调
*/
public void onStarted();
/**
* 进度更新回调
*
* @param progress
* @param downloadUrl
*/
public void onProgressChanged(int progress, String downloadUrl);
/**
* 下载完成回调
*
* @param completeSize
* @param downloadUrl
*/
public void onFinished(int completeSize, String downloadUrl);
/**
* 下载失败回调
*/
public void onFailure();
}
- UpdateDownLoadRequest 真正的处理文件下载和线程间的通信
public class UpdateDownLoadRequest implements Runnable {
private String downloadUrl;
private String downloadPath;
private UpdateDownLoadListener mLoadListener;
private long contentLength;
private boolean isDownloading = false;
private UpdateDownRequestHandle mHandle;
public UpdateDownLoadRequest(String downloadUrl, String downloadPath, UpdateDownLoadListener loadListener) {
this.downloadPath = downloadPath;
this.downloadUrl = downloadUrl;
this.mLoadListener = loadListener;
this.isDownloading = true;
this.mHandle = new UpdateDownRequestHandle();
}
//真正的建立连接
private void makeRequest() throws IOException {
if (!Thread.currentThread().isInterrupted()) {
try {
URL url = new URL(downloadUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.connect();//阻塞我们当前的线程
contentLength = connection.getContentLength();
if (!Thread.currentThread().isInterrupted()) {
//完成文件的下载
mHandle.sendResponseMessage(connection.getInputStream());
}
} catch (IOException e) {
throw e;
}
}
}
@Override
public void run() {
try {
makeRequest();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 包含了下载过程中所有可能出现的异常情况
*/
public enum FailureCode {
UnknownHost, Socket, SocketTimeout, ConnectTimeout, IO, HttpResponse, Json, Interrupted
}
/**
* 文件下载 将消息传递个主线程
*/
public class UpdateDownRequestHandle {
private static final int SUCCESS_MESSAGE = 0;
private static final int FAILURE_MESSAGE = 1;
private static final int START_MESSAGE = 2;
private static final int FINISH_MESSAGE = 3;
private static final int NETWORK_MESSAGE = 4;
private static final int PROGRESS_CHANGED = 5;
private Handler handler;//完成线程见的通信
private int currentSize = 0;
private int progress = 0;
public UpdateDownRequestHandle() {
handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
handleSelfMessage(msg);
}
};
}
protected void handleSelfMessage(Message msg) {
Object[] response;
switch (msg.what) {
case FAILURE_MESSAGE:
response = (Object[]) msg.obj;
handlerFailureMessage((FailureCode) response[0]);
break;
case PROGRESS_CHANGED:
response = (Object[]) msg.obj;
int p = ((Integer) response[0]).intValue();
handlerProgressChangedMessage(p);
break;
case FINISH_MESSAGE:
onFinish();
break;
}
}
//各种消息的处理逻辑
protected void handlerProgressChangedMessage(int progress) {
mLoadListener.onProgressChanged(progress, "");
}
protected void handlerFailureMessage(FailureCode failureCode) {
onFailure(failureCode);
}
public void onFinish() {
mLoadListener.onFinished(currentSize, "");
}
public void onFailure(FailureCode failureCode) {
Log.d("TAG", "onFailure: " + failureCode);
mLoadListener.onFailure();
}
protected void sendFailureMsg(FailureCode code) {
sendMsg(obtainMessage(FAILURE_MESSAGE, new Object[]{code}));
}
protected void sendFinishMsg() {
sendMsg(obtainMessage(FINISH_MESSAGE, null));
}
protected void sendProgressChangedMsg(int progress) {
sendMsg(obtainMessage(PROGRESS_CHANGED, new Object[]{progress}));
}
protected void sendMsg(Message msg) {
if (handler != null) {
handler.sendMessage(msg);
} else {
handleSelfMessage(msg);
}
}
/**
* 获取一个消息对象
*
* @param responseMessage
* @param response
* @return
*/
protected Message obtainMessage(int responseMessage, Object response) {
Message msg;
if (handler != null) {
msg = handler.obtainMessage(responseMessage, response);
} else {
msg = Message.obtain();
msg.what = responseMessage;
msg.obj = response;
}
return msg;
}
public void sendResponseMessage(InputStream inputStream) {
RandomAccessFile acesFile = null;
currentSize = 0;
try {
acesFile = new RandomAccessFile(downloadPath, "rwd");
int limit = 0;
int length = -1;
byte[] bs = new byte[1024];
while ((length = inputStream.read(bs)) != -1) {
if (isDownloading) {
acesFile.write(bs, 0, length);
currentSize += length;
if (currentSize < contentLength) {
progress = (int) (currentSize * 100 / contentLength);
if (limit % 30 == 0 && progress <= 100) {
//为了限制一下notification的更新频率
sendProgressChangedMsg(progress);
}
if (progress >= 100) {
//下载完成
sendProgressChangedMsg(progress);
}
limit++;
}
}
}
sendFinishMsg();
} catch (IOException e) {
sendFailureMsg(FailureCode.IO);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (acesFile != null) {
acesFile.close();
}
} catch (IOException e) {
sendFailureMsg(FailureCode.IO);
}
}
}
}
}
这段代码有点长,简单来看就开启线程下载任务,根据现在的状态利用handle发送各种状态的消息,然后利用接口回调,调用接口,再让启动下载的类也就是我们后台下载的服务类去实现接口并处理相应的逻辑。
- UpdateDownManager 下载调度管理器,调用我们的UpdateDownLoadRequest,也是下载任务的入口,在这里我们为了为了健壮性加入一切判断。并将下载任务设置单例模式,并用线程池,方便管理闲扯避免僵尸线程。
- UpdateDownService这里就是我们启动下载任务的地方
public class UpdateDownService extends Service {
private static final String TAG = "UpdateDownService";
private String apkUrl;
private String filePath;
private NotificationManager mNotificationManager;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
filePath = Environment.getExternalStorageDirectory() + "/testDownload/test.apk";
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
notifyUser("下载失败", "下载失败原因", 0);
stopSelf();
}
apkUrl = intent.getStringExtra("apkUrl");
Log.i("TAG", "下载地址: " + apkUrl);
notifyUser(getString(R.string.update_download_start), getString(R.string.update_download_start), 0);
startDownload();
return super.onStartCommand(intent, flags, startId);
}
private void startDownload() {
UpdateDownManager.getInstance().startDownloads(apkUrl, filePath, new UpdateDownLoadListener() {
@Override
public void onStarted() {
}
@Override
public void onProgressChanged(int progress, String downloadUrl) {
Log.d(TAG, "onProgressChanged: "+progress);
notifyUser(getString(R.string.update_download_processing), getString(R.string.update_download_processing), progress);
}
@Override
public void onFinished(int completeSize, String downloadUrl) {
Log.d(TAG, "onProgressChanged: "+completeSize);
notifyUser(getString(R.string.update_download_finish), getString(R.string.update_download_finish), 100);
stopSelf();
}
@Override
public void onFailure() {
Log.d(TAG, "onProgressChanged: ");
notifyUser(getString(R.string.update_download_failed), getString(R.string.update_download_failed_msg), 0);
stopSelf();
}
});
}
/**
* 更新notification来告知用户下载进度
*
* @param result
* @param reason
* @param progress
*/
private void notifyUser(String result, String reason, int progress) {
Notification mNotification;
NotificationCompat.Builder build = new NotificationCompat.Builder(this);
build.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle(getString(R.string.app_name));
if (progress > 0 && progress < 100) {
build.setProgress(100, progress, false);
} else {
build.setProgress(0, 0, false);
}
build.setAutoCancel(false);
build.setWhen(System.currentTimeMillis());
build.setTicker(result);
build.setContentIntent(progress >= 100 ? getContentIntent() : PendingIntent.getActivity(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT));
mNotification = build.build();
mNotificationManager.notify(0, mNotification);
}
public PendingIntent getContentIntent() {
File apkFile = new File(filePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + apkFile.getAbsolutePath()), "application/vnd.android.package-archive");
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
public void onDestroy() {
super.onDestroy();
}
我们在onStartCommand()方法中启动下载,下载完成结束当前服务。然后用Notification通知用户,在用系统自带的api安装。最后就是在Activity启动服务下载任务就能进行了。篇幅较长Activity的代码我就不粘贴出来了。
结束
相比在第一行代码中的,这段代码多了做了一些逻辑上的处理,是代码更健壮性。原理都是相同的,如果你是在小范围应用或是自己做的练手应用想加入自动更新功能,就可以将这些代码封装到自己的工具类中,当然距离成熟框架还是有很大的距离,比如我们更新要和服务器版本对比。服务器推送新版本功能等等,但是思路都是这样的。在这里我只是抛砖引玉。身为小白的我,还需努力。 后续会更新在线更新等热修复的文章敬请期待。
写的不好大家多多谅解。如有错误真心希望大家提出来。最后希望大家一起进步。加油!!!
源码地址。