本人新手,入行不到三个月,项目需要实现app自动更新。于是就想到抄书上这个下载示例,结果公司现在的板子用的8.0系统,运行起来直接ANR,自己又写了一个简易spring boot服务端,一个星期左右才完成的差不多。(初步翻了一下第3版好像没这个示例了,话说第2版我都没看完,第3版又出来了)。
趁周日(翘加班)把可以分享的分享一下。
首先android8.0服务的正确打开方式变了:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//android8.0以上通过startForegroundService启动service
startForegroundService(intent);
} else {
startService(intent);
}
bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务
然后是通知的适配,原:
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
需要增加一个channelId,下面的写法来源于搜索,郭神博客有更详细的写法(我没仔细看):
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel("channelid1","channelname",NotificationManager.IMPORTANCE_HIGH);
getNotificationManager().createNotificationChannel(notificationChannel);
}
//channelname 是会显示在通知权限页面的
NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"channelid1");
接着是解决ANR错误,搜索了好久发现有一个5秒内必须调用一次startForeground()的新特性
所以,重写onCreate,让服务在创建时就调用,果然问题解决。上代码:
@Override
public void onCreate() {
super.onCreate();
startForeground(1,getNotification("Waiting...",-1));
}
接下去就是项目功能,实现自主下载apk,下载前需要2个前提:
- app要知道有一个新版本存在
- app要知道新版本的url
于是我们要一个服务端项目,服务端先不说。书上的示例代码,是由button启动通过Binder往服务中传入一个url,然后通过url获取fileName。只有fileName里面带有版本信息,2个前提就都解决了。于是,扩展DownLoadTask(请忽略错误的大小写L):
fileName = url.substring(url.lastIndexOf("/"));
//可以指定别的路径
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
f (fileName.isEmpty()){
return TYPE_FAILED_FILENAME_NULL;
}else {
if (compareVersion(getVersion(fileName),packageName(MyApplication.getInstance())) != 1){
//APP 不需要更新
return TYPE_FAILED_IS_NEW;
}
File fileList = new File(directory + "/");
List<File> f = getFileList(fileList.listFiles());
int i = 0;
while (i < f.size()) {
File apkFile = f.get(i);
if (fileName.contains(apkFile.getName())){
return TYPE_FAILED_IS_NEW;
}else if (apkFile.getName().contains(".part")){
if (compareVersion(getVersion(fileName),packageName(MyApplication.getInstance())) == 1){
apkFile.delete();
}
}else {
apkFile.delete();
}
i ++;
}
fileName = fileName.substring(0,fileName.length()-3);//去掉apk
fileName = fileName + "part";//加上part
}
//这里还有其他代码
}
简单说一下上面的代码
- 通过url获取fileName(其实是/+fileName)
- 正确性检查,如果空返回一个失败信息,在DownLoadTask的onPostExecute中接受再用listener的onFailed打印错误信息(这里修改了书上的监听接口)
- 对比fileName的VersionName,判断是否需要更新。
- 需要更新,遍历下载目录下所有文件,如果有文件名被fileName包含(fileName多一个/),则不需要下载。如果有未完成的下载APK也就是.part文件,则核对版本。相等的留着续传,其他文件统统删除。
- 最后需要下载了,生成.part文件名
最后在下载完成后,重命名文件名为apk:
fileName = fileName.substring(0,fileName.length()-4);//去掉part
fileName = fileName + "apk";//加上apk
File newFile = new File(directory + fileName);
if (file.renameTo(newFile)) {
return TYPE_SUCCESS;
}else {
return TYPE_FAILED;
}
getVersion方法因为我固定的文明格式(app名称_V版本号_打包时间_备注信息),所以我的getVersion比较简单,大家可以根据自己格式或者更智能的抓出来。
private String getVersion(String name) {
int index=name.indexOf('_');
if (index==-1) return "";
int index2= name.indexOf('_',index+1);
if (index2 == -1){
return name.substring(index+1);
}else if (index2>index+1)
return name.substring(index+2,index2);
else
return "";
}
packageName(Context context)方法来源是搜索,MyApplication.getInstance()是全局获取的方式,书上有。
private String packageName(Context context) {
PackageManager manager = context.getPackageManager();
String name = null;
try {
PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
name = info.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return name;
}
compareVersion(String v1,String v2)也是面向百度编程的(如果有版权问题请留言):
public int compareVersion(String v1,String v2){
int i=0,j=0,x=0,y=0;
int v1Len=v1.length();
int v2Len=v2.length();
char c;
do {
while(i<v1Len){//计算出V1中的点之前的数字
c=v1.charAt(i++);
if(c>='0' && c<='9'){
x=x*10+(c-'0');//c-‘0’表示两者的ASCLL差值
}else if(c=='.'){
break;//结束
}else{
//无效的字符
}
}
while(j<v2Len){//计算出V2中的点之前的数字
c=v2.charAt(j++);
if(c>='0' && c<='9'){
y=y*10+(c-'0');
}else if(c=='.'){
break;//结束
}else{
//无效的字符
}
}
if(x<y){
return -1;
}else if(x>y){
return 1;
}else{
x=0;y=0;
continue;
}
} while ((i<v1Len) || (j<v2Len));
return 0;
}
遍历文件夹的方法:
public List<File> getFileList(File[] oriFile) {
List<File> fileList = new ArrayList<>();
for (File file : oriFile) {
if (file.isDirectory()) {
File[] childFileArr = file.listFiles();
Collections.addAll(fileList, childFileArr);
} else if (file.isFile()) {
Collections.addAll(fileList, file);
}
}
return fileList;
}
项目服务需要自启动,而不是要用户去点击,需要把原开始服务的代码放入ServiceConnection对象重写的onServiceConnected方法中去:
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownLoadService.DownloadBinder)service;
String url = "http://xxx.xxxx.com:8888/getVersionInfo";
downloadBinder.startDownload(url);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
最后是下载失败的处理改写了一下,1个小时重试下载(测试用的5秒,不知道1小时会不会被杀掉):
@Override
public void onFailed(int cause) {
downLoadTask = null;
//下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
String tip = "Download Failed";
switch (cause) {
case DownLoadTask.TYPE_FAILED_IS_NEW:
tip = "不需要更新";
break;
case DownLoadTask.TYPE_FAILED:
tip = "Download Failed";
break;
case DownLoadTask.TYPE_FAILED_URL_NULL:
tip = "失败,无法获取更新地址";
break;
case DownLoadTask.TYPE_FAILED_FILE_ERROR:
tip = "失败,资源错误";
break;
case DownLoadTask.TYPE_FAILED_FILENAME_NULL:
tip = "失败,获取不到文件名";
break;
default:
break;
}
getNotificationManager().notify(1, getNotification(tip, -1));
Toast.makeText(DownLoadService.this, tip, Toast.LENGTH_SHORT).show();
if (cause == DownLoadTask.TYPE_FAILED_IS_NEW) {
stopSelf();
}else {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3600000);
//Thread.sleep(5000);
downLoadTask = new DownLoadTask(listener);
downLoadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
最后的一个问题是url是写死的,下一次版本的名字是变动的,上面我传入的url是getVersionInfo。getVersionInfo是我写的服务端的一个Controller,返回的是一个url字符串。这样我们即时是写死一个url,但是可以通过服务端来实现新版本下载。具体代码参考原有的getContentLength(String downloadUrl)实现。
服务端另写,客户端完整源码下载:
https://github.com/markrenChina/ServiceBestPractice
快速调用的方法:
- 复制DownLoadListener.class , DownLoadService.class , DownLoadTask.class到自身项目
- 修改DownLoadService下的new Intent的第二个参数,点击通知想弹出的活动。
- 在DownLoadTask.class 适配你的全局Context
- 在AndroidManifest.xml 注册服务
- 在需要打开服务的活动中启动服务(onDestroy记得调用unbindService(connection))
- 自己看着修改