应用升级的功能是每个App必备功能之一。
这篇文章介绍应用自动更新的原理以及封装实现一个在线升级的组件.
应用自动更新原理:
- apk下载
- 利用Notification通知用户进度消息
- 文件下载成功调用系统安装程序
用法:
Intent intent = new Intent(this,UpdateService.class);
intent.putExtra("apkUrl","apk的url");
intent.putExtra("filePath",filePath);
startService(intent);
传入两个参数,第一个是apk下载的url,第二个是apk存储的文件目录。
代码:
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();
}
UpdateService
开启apk文件下载,监听下载动作,显示通知栏样式,下载完成点击通知栏打开apk安装文件
public class UpdateService extends Service{
private String apkUrl;
private String filePath;
private NotificationManager notificationManager;
private Notification notification;
@Override
public void onCreate() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent == null){
notifyUser("下载失败","Intent 未空",0);
stopSelf();
}
apkUrl= intent.getStringExtra("apkUrl");
filePath = intent.getStringExtra("filePath");
notifyUser("下载开始了","下载开始了",0);
startDownLoad();
return super.onStartCommand(intent,flags,startId);
}
private void startDownLoad() {
UpdateManager.getInstance().startDownloads(apkUrl, filePath,
new UpdateDownloadListener() {
@Override
public void onStarted() {
}
@Override
public void onProgressChanged(int progress, String downloadUrl) {
notifyUser("正在下载","正在下载",progress);
}
@Override
public void onFinished(int completeSize, String downloadUrl) {
notifyUser("下载完成","下载完成",100);
stopSelf();
}
@Override
public void onFailure() {
notifyUser("下载失败","下载失败",0);
stopSelf();
}
});
}
/**
* 显示通知栏
* @param result
* @param reason
* @param progress
*/
private void notifyUser(String result ,String reason ,int progress) {
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(true);
build.setWhen(System.currentTimeMillis());
build.setTicker(result);
build.setContentIntent(progress >= 100 ? getContentIntent() :
PendingIntent.getActivity(this,0,new Intent() ,PendingIntent.FLAG_UPDATE_CURRENT));
notification = build.build();
notificationManager.notify(0,notification);
}
/**
* todo 这里需要针对7.0重新做适配,因为7.0访问url需要用FileProvider
* @return
*/
private PendingIntent getContentIntent() {
File apkFil = new File(filePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri apk;
if(Build.VERSION.SDK_INT>=24){
apk= FileProvider.getUriForFile(this,"sto.com.stocourier.fileprovider",apkFil);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}else {
apk=Uri.fromFile(apkFil);//低于7.0
}
intent.setDataAndType(apk, "application/vnd.android.package-archive");
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
startActivity(intent);
return pendingIntent;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
startDownLoad()方法调用UpdateManager开启apk文件下载动作。
UpdateManager
下载调度管理器
public class UpdateManager {
private static UpdateManager manager;
private ThreadPoolExecutor threadPoolExecutor;
private UpdateDownloadRequest request;
public UpdateManager() {
threadPoolExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
}
static {
manager = new UpdateManager();
}
public static UpdateManager getInstance(){
return manager;
}
public void startDownloads(String downUrl ,String localPath ,UpdateDownloadListener listener){
if(request != null){
return;
}
checkLocalFilePath(localPath);
request = new UpdateDownloadRequest(downUrl,localPath,listener);
Future<?> future = threadPoolExecutor.submit(request);
}
/**
* 用来检查文件路径是否已经存在
* @param path
*/
private void checkLocalFilePath(String path) {
File dir = new File(path.substring(0,path.lastIndexOf("/") + 1));
if(!dir.exists()){
dir.mkdir();
}
File file = new File(path);
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
checkLocalFilePath()方法检查文件目录是否存在。UpdateDownloadRequest类是下载动作的具体实现
UpdateDownloadRequest
真正的负责处理文件的下载和线程间通信
public class UpdateDownloadRequest implements Runnable{
private String downloadUrl;
private String localFilePath;
private UpdateDownloadListener downloadListener;
private boolean isDownLoading = false;
private long currentLength;
private DownloadResponseHandler downloadHandler;
public UpdateDownloadRequest(String downloadUrl, String localFilePath, UpdateDownloadListener downloadListener) {
this.downloadUrl = downloadUrl;
this.localFilePath = localFilePath;
this.downloadListener = downloadListener;
this.isDownLoading = true;
this.downloadHandler = new DownloadResponseHandler();
}
/**
* 真正的去建立连接的方法
*/
private void makeRequest() throws IOException ,InterruptedException{
if(!Thread.currentThread().isInterrupted()){
try {
URL url = new URL(downloadUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5*1000);
connection.setRequestProperty("Connection","Keep-Alive");
connection.connect();//阻塞我们当前的线程
currentLength = connection.getContentLength();
if(!Thread.currentThread().isInterrupted()){
//真正的完成文件的下载
downloadHandler.sendResponseMessage( connection.getInputStream());
}
}catch (IOException e){
throw e;
}
}
}
@Override
public void run() {
try {
makeRequest();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 格式化数字
* @param value
* @return
*/
private String getTwoPointFloat(float value){
DecimalFormat df = new DecimalFormat("0.00000000000");
return df.format(value);
}
/**
* 包号下载过程中所有可能出现的异常情况
*/
public enum FailureCode{
IO ,FileNotFound
}
/**
* 用来真正的去下载文件,并发送消息和回调的接口
*/
public class DownloadResponseHandler{
protected static final int SUCCESS_MESSAGE = 0;
protected static final int FAILURE_MESSAGE = 1;
protected static final int START_MESSAGE = 2;
protected static final int FINISH_MESSAGE = 3;
protected static final int NETWORK_OFF = 4;
protected static final int PROGRESS_CHANGED = 5;
private float mCompleteSize = 0;
private int progress = 0;
private Handler handler;//真正的完成线程间通信
public DownloadResponseHandler() {
handler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
handleSelfMessage(msg);
}
};
}
/**
* 用来发送不同的消息队象
*/
protected void sendFinishMessage(){
sendMessage(obtainMessage(FINISH_MESSAGE,null));
}
protected void sendProgressChangedMessage(int progress){
sendMessage(obtainMessage(PROGRESS_CHANGED,
new Object[]{progress}));
}
protected void sendFailureMessage(FailureCode failureCode){
sendMessage(obtainMessage(FAILURE_MESSAGE,
new Object[]{failureCode}));
}
protected void sendMessage(Message msg){
if(handler != null){
handler.sendMessage(msg);
}else {
handleSelfMessage(msg);
}
}
/**
* handler发送Message
* @param responseMessage
* @param response
* @return
*/
protected Message obtainMessage(int responseMessage ,Object response){
Message message = null;
if(handler != null){
message = handler.obtainMessage(responseMessage,response);
}else {
message = Message.obtain();
message.what = responseMessage;
message.obj = response;
}
return message;
}
protected void handleSelfMessage(Message message){
Object[] response;
switch (message.what){
case FAILURE_MESSAGE:
response = (Object[]) message.obj;
downloadListener.onFailure();
break;
case PROGRESS_CHANGED:
response = (Object[]) message.obj;
downloadListener.onProgressChanged(((Integer) response[0]).intValue(),"");
break;
case FINISH_MESSAGE:
downloadListener.onFinished((int) mCompleteSize,"");
break;
}
}
//文件下载的方法,会发送各种类型的事件
public void sendResponseMessage(InputStream is){
RandomAccessFile randomAccessFile = null;
mCompleteSize = 0;
try {
byte[] buffer = new byte[1024];
int length = -1;
int limit = 0;
randomAccessFile = new RandomAccessFile(localFilePath,"rwd");
while ((length = is.read(buffer)) != -1){
if(isDownLoading){
randomAccessFile.write(buffer,0,length);
mCompleteSize += length;
if(mCompleteSize < currentLength){
Log.e("tag", "completeSize="+mCompleteSize);
Log.e("tag", "currentLength="+currentLength);
progress = (int)(Float.parseFloat(getTwoPointFloat(mCompleteSize / currentLength))*100);
Log.e("tag", "下载进度:"+progress);
if(limit % 30 == 0 && progress <= 100){
//为了限制一下我们notification的更新频率
sendProgressChangedMessage(progress);
}
limit ++ ;
}
}
}
sendFinishMessage();
}catch (FileNotFoundException e) {
e.printStackTrace();
sendFailureMessage(FailureCode.FileNotFound);
} catch (IOException e) {
e.printStackTrace();
sendFailureMessage(FailureCode.IO);
}finally {
try {
if(is != null){
is.close();
}
if(randomAccessFile != null){
randomAccessFile.close();
}
}catch (IOException e){
sendFailureMessage(FailureCode.IO);
}
}
}
}
}
HttpURLConnection实现文件下载,DownloadResponseHandler实现异步下载线程跟主线程间数据通信。
注意:
- AndroidMainfest.xml中注册我们的服务
<service android:name=".UpdateService"/>
- Android7.0打开apk文件uri的方法
在上面Service中打开apk的方法,因为7.0系统访问系统Uri需要通过Provider实现
apk= FileProvider.getUriForFile(this,"sto.com.stocourier.fileprovider",apkFil);
第二个参数就是我们自定义的一个provider,在AndroidMainfest.xml中
<provider
android:authorities="sto.com.stocourier.fileprovider"
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path"/>
</provider>
resource引用xml文件目录下一个叫file_path的文件。在res目录下新建xml文件夹,然后在xml文件中新建file_path文件如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path=""/>
</paths>
关于provider的使用,不懂的可以看翔哥Android 7.0 行为变更 通过FileProvider在应用间共享文件吧
当然,你也可以利用OkHttp等网络框架替换HtppUrlConnection实现文件下载。
通过访问接口获取最新版本apk下载地址,调用我们的Service就可以实现apk的在线升级。
文章技术实现思路以及部分核心代码来源慕课网教程。