Android 一个简单的版本更新下载apk小示例
一、简介:
1.运用 okhttp + notification 通知栏带进度的下载apk,下载完毕后并自动安装,如果用户取消可在通知栏点击安装,点击一次通知栏移除,同时支持自动静默下载(后台默默下载) 并解决UI卡顿问题,支持忽略此版本。
2.另一个服务里用的downloadManager进行下载效果基本一样,大家下载源码酌情考虑看那个更合心意吧。
废话不多说,我们来点实际的看看效果图。
二、简介
写这篇文章的目的也是一个小小的总结,因为项目需要。集成别人的势必冗杂,本着简单易维护的原则,自己动手写一个更符合要求。文章末尾会奉献源码,有兴趣的盆友可以下载看看。当然欢迎大家的指正交流。
1.通常情况下,用户进入主页面,我们会连接服务器查看用户当前apk版本是否是最新版本,不是则显示dialog提示用户下载最新版本客户端。
2.当用户点击下载后启动服务,通知栏显示下载进度。下载完毕后自动安装。同时,如果用户点击错误,取消安装。则可以继续查看通知栏,点击也可实现安装。
3.我们也需要一个开关。提供用户选择wifi下自动更新。这样当用户进入主页面有新版本后自动后台下载(静默下载)。完毕后显示安装界面提供用户安装。
三、代码实现
- 准备工作
这些工具类就不一一给出了 需要大家下载Demo自行提取吧
1.需要SP存储工具类 SPUtils
2.获取apk版本,并比较的工具类 VersionUtils
3.判断当前网络的工具列 NetworkTypeUtils
- MainActivity关键代码:
1.进入主页面,联网请求服务器比较当前apk版本与服务器端apk版本信息,如果不小于,则不做任何操作。如果小于服务版本信息则进行下一步判断,判断WiFi下自动更新的开关状态,网络状态。SP存储的版本状态。
2.这里我们用到了VersionUtils 版本比较工具类的方法:
VersionUtils.compareVersion(String.valueOf(VersionUtils.getVersionCode(this)), "1.2.0"); 返回值第一个值 < 第二个值则返回 -1 (1.2.0是我伪造的服务器版本号)。
3.VersionUtils.compareVersion()当返回值为-1时,我们要对用户进行提示 ,显示自定义的dialog,其中忽略当前版本是根据服务器端的版本号,用户选中ChecBox后将服务器版本号保存到SP储存中,下次进入判断SP储存的版本号于服务器版本号是否相等,相等则不再提示。
4.启动 DownloadService服务下载apk
package com.aaron.download_demo;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import com.aaron.download_demo.service.DownloadService;
import com.aaron.download_demo.utils.NetworkTypeUtils;
import com.aaron.download_demo.utils.SPUtils;
import com.aaron.download_demo.utils.VersionUtils;
/**
* 作者:哇牛Aaron
* 作者简书文章地址: http://www.jianshu.com/users/07a8b5386866/latest_articles
* 时间: 2016/11/18
* 功能描述: 主页面
* 判断当前apk版本与服务器apk版本是否一致,不一致则弹出对话框,提示用户下载最新版本apk。
* 如果wifi下自动更新开关处于关闭状态,用户点击更新,则启动服务下载apk。并在前台显示进度,下载完成后自动安装。
* 如果wifi下自动更新开关处于打开状态,则在后台服务默认下载,下载完成后自动安装。
* 如果用户点击忽略此版本,则当前版本将不再提示用户更新。
*/
public class MainActivity extends Activity {
private static final String TAG = MainActivity.class.getSimpleName();
private Button btn_wifi;
private Boolean isCheck = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initListener();
progressVersion();
}
/**
* 说明:
* 比较服务器版本与当前apk版本,如果低于服务器版本
* 1.wifi下自动更新开关打开 启动服务,后台静默下载apk,下载完毕后自动弹出安装界面
* 2.wifi下自动更新开关关闭 启动服务,下载apk,并用notification通知栏显示下载进度等,下载完毕后自动弹出安装界面
* --
* 目前我没有线程的接口,去比较服务的版本,所以写一个假的服务器版本 用到的朋友自行显示比较即可
*/
private void progressVersion() {
//VersionUtils.getVersionCode(this)工具类里获取当前安装的apk版本号
int version = VersionUtils.compareVersion(String.valueOf(VersionUtils.getVersionCode(this)),
"1.2.0");//这里 1.2.0使我们伪造的 你完全可以得到自己服务器接口里的版本号 然后进行比对
/**
* 比较版本大小 version1为当前所安装的版本
* version1 < version2 则 返回 -1
* version1 > version2 则 返回 1
* version1 == version2 则 返回 0
*/
if (version == -1) {
//判断 用户是否进入app主页面
Intent intent = new Intent();
intent.setClassName("com.aaron.download_demo", "MainActivity");
if (intent.resolveActivity(getPackageManager()) == null) {
Log.e(TAG, "不存在MainActivity");
// 说明系统MainActivity没有被打开
return;
} else {
Log.e(TAG, "存在MainActivity");
/**
* wifi状态下自动下载
*/
if ((boolean) SPUtils.get(this, SPUtils.WIFI_DOWNLOAD_SWITCH, false)
&& NetworkTypeUtils.getCurrentNetType(MainActivity.this).equals("wifi")) {
startService(new Intent(MainActivity.this, DownloadService.class));
//startService(new Intent(MainActivity.this, DownloadService2.class));
//Log.e("TAG", "startService");
} else { //提示dialog
//判断 忽略的版本sp信息是否与当前版本相等 如果不相等 则显示更新的dialog
String spVersion = (String) SPUtils.get(this, SPUtils.APK_VERSION, "");
if (!spVersion.equals("1.2.0")) {//服务器版本 依旧填假数据 1.2.0
//下面是自定义dialog
View view = View.inflate(this, R.layout.download_layout, null);
final Dialog dialog = new AlertDialog.Builder(this).create();
dialog.show();
dialog.setContentView(view);
TextView content = (TextView) view.findViewById(R.id.tv_content);
content.setText("解决不能及时..." + "等其它版本信息");
//取消
TextView cancel = (TextView) view.findViewById(R.id.btn_cancel);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//当true时 保存版本信息
if (isCheck) {
SPUtils.put(MainActivity.this, SPUtils.APK_VERSION, "1.2.0");
}
//Log.e("TAG","isCheck == " + isCheck);
dialog.dismiss();
}
});
//确定
TextView Sure = (TextView) view.findViewById(R.id.btn_ok);
Sure.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startService(new Intent(MainActivity.this, DownloadService.class));
//startService(new Intent(MainActivity.this, DownloadService2.class));
//当true时 保存版本信息
if (isCheck) {
SPUtils.put(MainActivity.this, SPUtils.APK_VERSION, "1.2.0");
}
dialog.dismiss();
//Log.e("TAG", "isCheck == " + isCheck);
}
});
//忽略该版本
CheckBox checkBox = (CheckBox) view.findViewById(R.id.cb_ignore);
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
isCheck = true;
} else {
isCheck = false;
}
}
});
}
}
}
}
}
private void initListener() {
btn_wifi.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SettingActivity.class));
}
});
}
private void initView() {
btn_wifi = (Button) findViewById(R.id.btn_wifi);
}
}
3 . DownloadService服务:
1.用hongyang大神封装好的okhttp下载工具下载apk,改工具包含下2载进度。hongyang大神封装库:okhttputils
2.关于notification不甚了解的盆友可以看看这篇文章
Android 通知栏Notification的整合
3.说明:
其中,服务中通过handler发送消息,区分是静默下载还是显示通知栏下载。
联网下载中重写inProgress()方法中,参数progress为下载进度。但我们 直接拿来用会出现卡顿问题,严重时会报异常。notification多次快速刷新进度会导致卡顿。这里我们也已经解决,通过handler + 判断语言去除小于或等于当前进度的数据,不在重复发送数据。具体看代码,很好理解。
package com.aaron.download_demo.service;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import com.aaron.download_demo.R;
import com.aaron.download_demo.utils.SPUtils;
import com.zhy.http.okhttp.OkHttpUtils;
import com.zhy.http.okhttp.callback.FileCallBack;
import java.io.File;
import okhttp3.Call;
/**
* 作者:哇牛Aaron
* 作者简书文章地址: http://www.jianshu.com/users/07a8b5386866/latest_articles
* 时间: 2016/11/18
* 功能描述: OkHttp + notification 下载的服务类
* OkHttp下载框架hongyang大神封装的好的库 链接:https://github.com/hongyangAndroid/okhttputils
*/
public class DownloadService extends Service {
/* private static final String DOWN_APK_URL = "http://219.238.2.164/apk.r1.market.hiapk.com/" +
"data/upload/apkres/2016/11_10/11/com.ss.android.article.news_115902.apk?" +
"wsiphost=local";*/
private String DOWN_APK_URL = "http://111.204.176.254:8081/api/basic/apk";
private static final String TAG = DownloadService.class.getSimpleName();
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
//显示 Notification
showNotificationProgress(msg.arg1);
break;
case 2:
//安装apk
installApk();
break;
}
}
};
private Boolean autoDownLoad;
private int currentProgress = 0;
private NotificationManager manager;
private static final String APK_NAME = "downloadDemo.apk";
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//hongyang大神封装好的okHttp网络请求
//启动分线程下载
new Thread(new Runnable() {
@Override
public void run() {
okHttpDownLoadApk(DOWN_APK_URL);
}
}).start();
}
/**
* 联网下载最新版本apk
*/
private void okHttpDownLoadApk(final String url) {
OkHttpUtils
.get()
.url(url)
.build()// Environment.getExternalStorageDirectory().getAbsolutePath() 存储路径
.execute(new FileCallBack(Environment.getExternalStorageDirectory().getAbsolutePath(), APK_NAME) {
@Override
public void onError(Call call, Exception e, int id) {
Log.e(TAG, "onError :" + e.getMessage());
}
@Override
public void onResponse(File response, int id) {
//Log.e(TAG, "onResponse() 当前线程 == " + Thread.currentThread().getName());
//Log.e(TAG, "onResponse :" + response.getAbsolutePath());
}
@Override
public void inProgress(final float progress, long total, int id) {
super.inProgress(progress, total, id);
//Log.e(TAG, "inProgress() 当前线程 == " + Thread.currentThread().getName());
autoDownLoad = (Boolean) SPUtils.get(DownloadService.this, SPUtils.WIFI_DOWNLOAD_SWITCH, false);
//判断开关状态 开 则静默下载
if (autoDownLoad) {
//说明自动更新 这里服务在后台默默运行下载 只能看日志了
//Log.e(TAG, "自动安装的进度 == " + (100 * progress));
if ((100 * progress) == 100.0) {
//Log.e(TAG, "网络请求 自动安装 当前线程 == " + Thread.currentThread().getName());
handler.sendEmptyMessage(2);
}
} else {//否则 进度条下载
int pro = (int) (100 * progress);
//解决pro进度重复传递 progress的问题 这里解决UI界面卡顿问题
if (currentProgress < pro && pro <= 100) {
//Log.e(TAG, "进入");
currentProgress = pro;
Message msg = new Message();
msg.what = 1;
msg.arg1 = currentProgress;
handler.sendMessage(msg);
}
}
}
});
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void showNotificationProgress(int progress) {
//Log.e(TAG, "进度 == " + progress);
String message = "当前下载进度: " + progress + "%";
int AllProgress = 100;
Intent intent = null;
if (progress == 100) {
message = "下载完毕,点击安装";
//Log.e(TAG, "下载完成 " + progress);
//安装apk
installApk();
if (manager != null) {
manager.cancel(0);//下载完毕 移除通知栏
}
//当进度为100%时 传入安装apk的intent
File fileLocation = new File(Environment.getExternalStorageDirectory(), APK_NAME);
intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory("android.intent.category.DEFAULT");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(fileLocation), "application/vnd.android.package-archive");
}
if (intent == null){
intent = new Intent(); //暂时没有更好的办法 保证不能为空即可
}
//表示返回的PendingIntent仅能执行一次,执行完后自动取消
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.icon)//App小的图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))//App大图标
.setContentTitle("自定义内容")//设置通知的信息
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setContentText(message)
.setAutoCancel(false)//用户点击后自动删除
//.setDefaults(Notification.DEFAULT_LIGHTS)//灯光
.setPriority(Notification.PRIORITY_DEFAULT)//设置优先级
.setOngoing(true)
.setProgress(AllProgress, progress, false) //AllProgress最大进度 //progress 当前进度
.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
manager.notify(0, notification);
}
/**
* 安装apk
*/
private void installApk() {
//Environment.getExternalStorageDirectory() 保存的路径
Log.e(TAG, "installApk运行了");
File fileLocation = new File(Environment.getExternalStorageDirectory(), APK_NAME);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory("android.intent.category.DEFAULT");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(fileLocation), "application/vnd.android.package-archive");
startActivity(intent);
//停止服务
stopSelf();
}
@Override
public void onDestroy() {
//Log.e(TAG, "onDestroy()");
super.onDestroy();
/*if (manager != null) {
manager.cancel(0);//下载完毕 移除通知栏
}*/
}
}
PendingIntent 设置通知栏的点击意图,当进度为100%时才让其点击意图进行。
PendingIntent.FLAG_ONE_SHOT 表示返回的PendingIntent仅能执行一次,执行完后自动取消
4.DownloadService2
用downloadManager进行下载
参考资料:Android DownloadManager 的使用
package com.aaron.download_demo.service;
import android.app.DownloadManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import com.aaron.download_demo.utils.SPUtils;
import java.io.File;
/**
* 作者:哇牛Aaron
* 作者简书文章地址: http://www.jianshu.com/users/07a8b5386866/latest_articles
* 时间: 2016/11/18
* 功能描述: 运用DownloadManager实现下载 一样通知栏会显示
*
*/
public class DownloadService2 extends Service {
public static final String DOWNLOAD_FOLDER_NAME = "Trinea";
public static final String DOWNLOAD_FILE_NAME = "123456Aaron.apk";
private DownloadManager downloadManager;
private String DOWN_APK_URL = "http://219.238.2.164/apk.r1.market.hiapk.com/" +
"data/upload/apkres/2016/11_10/11/com.ss.android.article.news_115902.apk?" +
"wsiphost=local";
private long downloadId;
private DownloadFinish downloadFinish;
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
Log.e("TAG", "onCreate() 启动服务");
downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
//下载之前先移除上一个 不然会导致 多次下载不成功问题
long id = (long) SPUtils.get(DownloadService2.this, SPUtils.KEY, (long) 0);
if (id != 0) {
downloadManager.remove(id);
}
initData();
downloadFinish = new DownloadFinish();
//动态注册广播接收器
registerReceiver(downloadFinish, new IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
private void initData() {
Log.e("TAG", "initData() 执行了~");
//判断文件是否存在 不存在则创建
File folder = new File(DOWNLOAD_FOLDER_NAME);
if (!folder.exists() || !folder.isDirectory()) {
folder.mkdirs();
}
//设置下载的URL
DownloadManager.Request request = new DownloadManager.Request(
Uri.parse(DOWN_APK_URL));
request.setDestinationInExternalPublicDir(DOWNLOAD_FOLDER_NAME,
DOWNLOAD_FILE_NAME);
//设置样式 貌似必须用 getString() 如果不用 下载完毕后会显示 下载的路径
//request.setTitle(getString(R.string.download_notification_title));
request.setTitle("下载标题XX");
request.setDescription("自定义随便填");
//判断开关状态
Boolean isOpen = (Boolean) SPUtils.get(this, SPUtils.WIFI_DOWNLOAD_SWITCH, false);
//自动下载开关是否打开 如果打开
if (isOpen) {
Log.e("TAG", "下载完才显示");
/**
* 在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,直到用户点击该
* Notification或者消除该Notification。
*/
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
} else {
Log.e("TAG", "正在下载时显示");
/**
* 在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,
* 直到用户点击该Notification或者消除该Notification。
*/
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
//是否显示下载用户接口
request.setVisibleInDownloadsUi(false);
//request.setMimeType("application/cn.trinea.download.file");
request.setMimeType("application/vnd.android.package-archive");//设置此Type不然点击通知栏无法安装
downloadId = downloadManager.enqueue(request);
SPUtils.put(this, SPUtils.KEY, downloadId);
}
/**
* 接受下载完成的广播
*/
class DownloadFinish extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("TAG", "DownloadFinish 广播接受完毕");
//此ID为下载完成的ID
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
//如果完成的ID 于 我们下载的ID 一致则表示下载完成
if (downloadId == completeDownloadId) {
Log.e("TAG", "DownloadFinish downloadId == completeDownloadId");
//安装apk
String apkFilePath = new StringBuilder(Environment
.getExternalStorageDirectory().getAbsolutePath())
.append(File.separator)
.append(DOWNLOAD_FOLDER_NAME)
.append(File.separator).append(DOWNLOAD_FILE_NAME)
.toString();
install(context, apkFilePath);
}
}
}
/**
* 安装APK
*
* @param context
* @param filePath
*/
private void install(Context context, String filePath) {
Log.e("TAG", "install() 安装");
Intent i = new Intent(Intent.ACTION_VIEW);
File file = new File(filePath);
if (file != null && file.length() > 0 && file.exists() && file.isFile()) {
i.setDataAndType(Uri.parse("file://" + filePath),
"application/vnd.android.package-archive");
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
//停止服务
stopSelf();
}
//打印看看有没有停止服务 带完善的地方 下载完成以后通知栏应该移除掉啊 还没弄不知道行不行 这里只能用户手动移除
//而且也没有做用户点击前台服务就启动安装 以前做过貌似报错 记不清了
@Override
public void onDestroy() {
Log.e("TAG", "onDestroy()");
super.onDestroy();
if (downloadFinish != null) {
//解注册
unregisterReceiver(downloadFinish);
}
}
}
源码下载点这里↓
源码下载
欢迎盆友们纠正知错,互相交流学习。谢谢
纠错 晚上检测了下发现DownloadService空指针 上方文章代码已经更改,DownloadService类中174行添加这句即可 if (intent == null){ intent = new Intent(); }