前不久接到个任务,在我们的app里面添加更新模块,在之前的版本中,我们的更新都是直接通过浏览器下载apk包来安装更新的,我想各位很大一部分应用的更新方法都是这样,因为它简单、方便,但是他也有许多不好的地方,比如需要用户跳转到浏览器页面、下载不可控、网络不好的情况的下失败无法续传,退出浏览器就无法接着下了等。。
于是我们这个更新模块的需求就来了
1.下载后台进行,退出我们应用下载任务依旧能继续执行操作
2.下载文件支持断点续传
3.下载任务支持没有安装sdcard时也可下载更新
4.notify栏提示操作
对几个需求稍作分析,解决方法如下:
1.下载更新的线程放到一个service中去,service的好处是不易被系统回收,而且也容易操作。我们需要先在AndroidMainfest.xml文件中去注册这个service
<service android:name="com.dj.app.UpdateService"/>
2.断点续传,请求头中有个重要的参数range,代表的意思的我要取的数据的范围,这个需要服务器支持,默认情况都是支持的。我的思路是下载前先获取文件包大小,然后检查是否已经有下载好的部分了,没有就从头开始,有就接着下。
request.addHeader("Range", "bytes=" + downLength + "-");
为了支持断点续传,我将下载好的文件版本号、文件长度都以prefrence的形似保存下来,下次更新如果还是这个版本就接着下,如果又有更新的了就删掉重下。
private void checkTemFile() {
existTemFileVersionCode = preferences().getInt(
UPDATE_FILE_VERSIONCODE, 0);
if (newestVersionCode == existTemFileVersionCode
|| newestVersionCode == TEST) {
File temFile = new File(context.getFilesDir(),
Integer.valueOf(newestVersionCode) + ".apk");
if (!temFile.exists()) {
saveLogFile(newestVersionCode, 0);
}
} else {
deleteApkFile(existTemFileVersionCode);
saveLogFile(newestVersionCode, 0);
}
}
3.无sdcard时也能下载,那只能将apk包下载到系统内存中
context.getFilesDir();
这样创建的文件在/data/data/应用包名/files
伪代码:
if (downLength > 0) {//接着上次的下载
outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",
Context.MODE_APPEND+ Context.MODE_WORLD_READABLE);
} else {//从头开始下载
outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",Context.MODE_WORLD_READABLE);
}
4.notify,我设定了一个更新notify的线程专门去观察下载线程的进度,每一分钟更新一次notify中的进度条。
new Thread() {
public void run() {
try {
boolean notFinish = true;
while (notFinish) {
Thread.sleep(1000);
notFinish = false;
if (downloadThread == null) {
break;
}
if (!downloadThread.downFinish) {
notFinish = true;
}
downloadThread.showNotification();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
stopSelf();
}
};
}.start();
downloadThread就是我的下载线程了因为要对不同进度时有不同的控制,这个可以通过notification.contentIntent来进行设定,比如100%的时候,我想要用户点击通知栏,即可进行安装,则应该这样做
notification.tickerText = "下载完成";
notification.when = System.currentTimeMillis();
Intent notificationIntent = new Intent();
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
notificationIntent
.setAction(android.content.Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
notificationIntent.setDataAndType(
Uri.parse("file:///data/data/"
+ context.getPackageName()
+ "/files/"
+ (Integer.valueOf(newestVersionCode)
.toString()) + ".apk"), type);
notification.contentView = rv;
notification.flags = Notification.FLAG_AUTO_CANCEL;
notification.contentView
.setProgressBar(
R.id.update_notification_progressbar, 100,
p, false);
notification.contentView.setTextViewText(
R.id.update_notification_progresstext, p + "%");
notification.contentView.setTextViewText(
R.id.update_notification_title, "下载完成,点击安装");
PendingIntent contentIntent = PendingIntent.getActivity(
context, 0, notificationIntent, 0);
notification.contentIntent = contentIntent;
下面我将下载线程完整的代码贴出来
class DownloadThread extends Thread {
private static final String UPDATE_FILE_VERSIONCODE = "updateTemFileVersionCode";
private static final String UPDATE_FILE_LENGTH = "updateTemFileLength";
private static final String TEST_UPDATE_FILE_LENGTH = "testupdatefilelength";
private static final int BUFFER_SIZE = 1024;
private static final int NETWORK_CONNECTION_TIMEOUT = 15000;
private static final int NETWORK_SO_TIMEOUT = 15000;
private int newestVersionCode, existTemFileVersionCode;
private String downUrl;
private int fileLength = Integer.MAX_VALUE;
private int downLength;
private boolean downFinish;
private static final int CHECK_FAILED = -1;
private static final int CHECK_SUCCESS = 0;
private static final int CHECK_RUNNING = 1;
private int checkStatus;
private boolean isChecking = false;
private Context context;
private boolean isPercentZeroRunning = false;
private boolean stop;
private Object block = new Object();
private boolean receiverRegistered = false;
private NotificationManager mNM;
private RemoteViews rv;
public DownloadThread(Context context, String downUrl, int versionCode) {
super("DownloadThread");
this.downUrl = downUrl;
this.newestVersionCode = versionCode;
this.context = context;
this.mNM = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
this.rv = new RemoteViews(context.getPackageName(), R.layout.notify);
this.downFinish = false;
}
private void checkTemFile() {
existTemFileVersionCode = preferences().getInt(
UPDATE_FILE_VERSIONCODE, 0);
if (newestVersionCode == existTemFileVersionCode
|| newestVersionCode == TEST) {
File temFile = new File(context.getFilesDir(),
Integer.valueOf(newestVersionCode) + ".apk");
if (!temFile.exists()) {
saveLogFile(newestVersionCode, 0);
}
} else {
deleteApkFile(existTemFileVersionCode);
saveLogFile(newestVersionCode, 0);
}
}
private void deleteApkFile(int existVersionCode) {
File temFile = new File(context.getFilesDir(),
Integer.valueOf(existVersionCode) + ".apk");
if (temFile.exists()) {
temFile.delete();
}
}
private SharedPreferences preferences() {
return context.getSharedPreferences(context.getPackageName(),
Context.MODE_WORLD_READABLE | Context.MODE_WORLD_WRITEABLE);
}
private void saveLogFile(int versionCode, int downloadLength) {
SharedPreferences.Editor edit = preferences().edit();
if (versionCode == TEST) {
edit.putInt(TEST_UPDATE_FILE_LENGTH, downloadLength);
} else {
edit.putInt(UPDATE_FILE_VERSIONCODE, versionCode);
edit.putInt(UPDATE_FILE_LENGTH, downloadLength);
}
edit.commit();
}
@Override
public void run() {
checkTemFile();
this.stop = false;
while (!downFinish) {
Log.i(TAG, "download thread start : while()");
if (newestVersionCode == TEST) {
downLength = preferences().getInt(TEST_UPDATE_FILE_LENGTH,
0);
} else {
downLength = preferences().getInt(UPDATE_FILE_LENGTH, 0);
}
InputStream is = null;
FileOutputStream outStream = null;
try {
// check the network
ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo();
boolean con = ni == null ? false : ni
.isConnectedOrConnecting();
synchronized (block) {
if (!con) {
context.registerReceiver(
receiver,
new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION));
receiverRegistered = true;
try {
Log.i(TAG, "network is not ok : block.wait()");
block.wait();
} catch (InterruptedException e1) {
}
}
}
if (fileLength == Integer.MAX_VALUE) {
URL url = new URL(downUrl);
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
if (conn.getResponseCode() / 100 == 2
&& conn.getContentLength() != -1) {
fileLength = conn.getContentLength();
Log.i(TAG, "getContentLength == " + fileLength);
} else {
Thread.sleep(5000);
Log.i(TAG, "getContentLength failed : retry in 5 second later");
continue;
}
}
HttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet(downUrl);
request.addHeader("Range", "bytes=" + downLength + "-");
if (downLength < fileLength) {
HttpHost proxy = HttpBase.globalProxy();
HttpParams httpParams = request.getParams();
HttpConnectionParams.setConnectionTimeout(httpParams,
NETWORK_CONNECTION_TIMEOUT);
HttpConnectionParams.setSoTimeout(httpParams,
NETWORK_SO_TIMEOUT);
ConnRouteParams.setDefaultProxy(request.getParams(),
proxy);
HttpResponse response = httpClient.execute(request);
Log.i(TAG, "getContent's response status == "
+ response.getStatusLine().getStatusCode());
if (response.getStatusLine().getStatusCode() / 100 != 2) {
continue;
}
HttpEntity entity = response.getEntity();
is = entity.getContent();
byte[] buffer = new byte[BUFFER_SIZE];
int offset = 0;
if (downLength > 0) {
outStream = context
.openFileOutput(
Integer.valueOf(newestVersionCode)
+ ".apk",
Context.MODE_APPEND
+ Context.MODE_WORLD_READABLE);
} else {
outStream = context
.openFileOutput(
Integer.valueOf(newestVersionCode)
+ ".apk",
Context.MODE_WORLD_READABLE);
}
while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
&& !stop) {
outStream.write(buffer, 0, offset);
downLength += offset;
}
}
if (downLength == fileLength) {
File apkFile = new File(context.getFilesDir(),
Integer.valueOf(newestVersionCode) + ".apk");
if (isApkFileOK(apkFile)) {
checkStatus = CHECK_SUCCESS;
} else {
deleteApkFile(newestVersionCode);
saveLogFile(existTemFileVersionCode, 0);
checkStatus = CHECK_FAILED;
}
this.downFinish = true;
}
} catch (Exception e) {
} finally {
saveLogFile(newestVersionCode, downLength);
if (receiverRegistered) {
context.unregisterReceiver(receiver);
receiverRegistered = false;
}
if (stop || downFinish) {
break;
}
try {
if (outStream != null) {
outStream.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
}
}
}
}
@Override
public void interrupt() {
synchronized (block) {
Log.i(TAG, "block.notify()");
block.notify();
}
}
private void cancel() {
stop = true;
mNM.cancel(MOOD_NOTIFICATIONS);
}
private boolean isApkFileOK(File file) {
checkStatus = CHECK_RUNNING;
// first check the file header
/*if (file.isDirectory() || !file.canRead() || file.length() < 4) {
return false;
}
DataInputStream in = null;
try {
in = new DataInputStream(new BufferedInputStream(
new FileInputStream(file)));
int test = in.readInt();
if (test != 0x504b0304)
return false;
} catch (IOException e) {
return false;
} finally {
try {
in.close();
} catch (IOException e) {
}
}*/
// second unZip file to check(without saving)
boolean result = unzip(file);
isChecking = false;
return result;
}
private boolean unzip(File unZipFile) {
boolean succeed = true;
ZipInputStream zin = null;
ZipEntry entry = null;
try {
zin = new ZipInputStream(new FileInputStream(unZipFile));
boolean first = true;
while (true) {
if ((entry = zin.getNextEntry()) == null) {
if (first)
succeed = false;
break;
}
first = false;
if (entry.isDirectory()) {
zin.closeEntry();
continue;
}
if (!entry.isDirectory()) {
byte[] b = new byte[1024];
@SuppressWarnings("unused")
int len = 0;
while ((len = zin.read(b)) != -1) {
}
zin.closeEntry();
}
}
} catch (IOException e) {
succeed = false;
} finally {
if (null != zin) {
try {
zin.close();
} catch (IOException e) {
}
}
}
return succeed;
}
public void showNotification() {
float result = (float) downLength / (float) fileLength;
int p = (int) (result * 100);
if (p == 0 && isPercentZeroRunning || p == 100 && isChecking) {
return;
} else if (p != 0) {
isPercentZeroRunning = false;
}
Notification notification = new Notification(R.drawable.icon, null,
0);
if (p == 100) {
if (checkStatus == CHECK_RUNNING) {
notification.tickerText = "开始检查下载文件";
notification.when = System.currentTimeMillis();
notification.flags = notification.flags
| Notification.FLAG_ONGOING_EVENT
| Notification.FLAG_NO_CLEAR;
notification.contentView = rv;
notification.contentView.setProgressBar(
R.id.update_notification_progressbar, 100, p, true);
notification.contentView.setTextViewText(
R.id.update_notification_title, "正在检查下载文件...");
PendingIntent contentIntent = PendingIntent.getActivity(
context, 0, null, 0);
notification.contentIntent = contentIntent;
isChecking = true;
} else if (checkStatus == CHECK_FAILED) {
notification.tickerText = "文件验证失败!";
notification.when = System.currentTimeMillis();
notification.flags = notification.flags
| Notification.FLAG_AUTO_CANCEL;
notification.contentView = rv;
notification.contentView.setProgressBar(
R.id.update_notification_progressbar, 100, p, false);
notification.contentView.setTextViewText(
R.id.update_notification_title, "文件验证失败,请重新下载");
Intent notificationIntent = new Intent(context,
UpdateService.class);
notificationIntent.setAction(ACTION_STOP);
PendingIntent contentIntent = PendingIntent.getService(
context, 0, notificationIntent, 0);
notification.contentIntent = contentIntent;
} else {
notification.tickerText = "下载完成";
notification.when = System.currentTimeMillis();
Intent notificationIntent = new Intent();
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
notificationIntent
.setAction(android.content.Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
notificationIntent.setDataAndType(
Uri.parse("file:///data/data/"
+ context.getPackageName()
+ "/files/"
+ (Integer.valueOf(newestVersionCode)
.toString()) + ".apk"), type);
notification.contentView = rv;
notification.flags = Notification.FLAG_AUTO_CANCEL;
notification.contentView
.setProgressBar(
R.id.update_notification_progressbar, 100,
p, false);
notification.contentView.setTextViewText(
R.id.update_notification_progresstext, p + "%");
notification.contentView.setTextViewText(
R.id.update_notification_title, "下载完成,点击安装");
PendingIntent contentIntent = PendingIntent.getActivity(
context, 0, notificationIntent, 0);
notification.contentIntent = contentIntent;
}
} else if (p == 0) {
notification.tickerText = "准备下载";
notification.when = System.currentTimeMillis();
Intent notificationIntent = new Intent(context,
UpdateService.class);
notificationIntent.setAction(ACTION_STOP);
notification.flags = notification.flags
| Notification.FLAG_ONGOING_EVENT;
notification.contentView = rv;
notification.contentView.setProgressBar(
R.id.update_notification_progressbar, 100, p, true);
notification.contentView.setTextViewText(
R.id.update_notification_title, "正在准备下载(点击取消)");
PendingIntent contentIntent = PendingIntent.getService(context,
0, notificationIntent, 0);
notification.contentIntent = contentIntent;
isPercentZeroRunning = true;
} else {
notification.tickerText = "开始下载";
notification.when = System.currentTimeMillis();
Intent notificationIntent = new Intent(context,
UpdateService.class);
notificationIntent.setAction(ACTION_STOP);
notification.contentView = rv;
notification.flags = notification.flags
| Notification.FLAG_ONGOING_EVENT;
notification.contentView.setProgressBar(
R.id.update_notification_progressbar, 100, p, false);
notification.contentView.setTextViewText(
R.id.update_notification_progresstext, p + "%");
notification.contentView.setTextViewText(
R.id.update_notification_title, "正在下载(点击取消)");
PendingIntent contentIntent = PendingIntent.getService(context,
0, notificationIntent, 0);
notification.contentIntent = contentIntent;
}
mNM.notify(MOOD_NOTIFICATIONS, notification);
}
}
可以看到,我的下载是包裹在一个while循环中的,假如没有下载完成,我会一直重复这个循环,可以注意到,我在取数据的时候有个标志位stop
while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
&& !stop) {
outStream.write(buffer, 0, offset);
downLength += offset;
}
有了这个就可以在外面控制强制停止下载。
在下载开始阶段我最先做的时就是检查网络情况,如果没有网络,我就使用一个block让这个线程阻塞掉,有人会问那什么时候恢复呢?我在service里面加了个广播
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent
.getAction())) {
NetworkInfo info = (NetworkInfo) intent
.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
boolean hasConnectivity = (info != null && info.isConnected()) ? true
: false;
if (hasConnectivity && downloadThread != null) {
downloadThread.interrupt();
}
}
}
};
可以监听系统网络情况,如果连上了,就调用interrupt()来唤醒线程。这样做的好处时,在没有网络时,这个线程不会无限循环的去获取数据。
最后在这个版本发布后因为代码一些bug导致,如果网络数据获取有问题,则用户下载下来的安装包就会解析错误,而且这是个死胡同,除非去手动下载个好的安装包来装,否则我们软件会一直提示,一直完成,但是一直装不上。。。哎,我们用户可是百万级啊,这个bug很致命,还好当时做了备用方案,可以由服务器控制客户端更新方法,于是改为以前的更新。
这也是我在上面的代码中下载完成时加入了最后一步,校验安装包的过程。我们都知道apk就是一个zip文件,通常对一个zip文件的校验,最简单的是校验文件头是不是0x504b0304,但是这只是文件格式的判断,加入是文件内容字节损坏还是查不出来,只能通过去unzip这个文件来捕获异常。在上面unzip(File unZipFile)方法中,我尝试去解压软件,对ZipInputStream流我没做任何处理,仅仅是看这个解压过程是否正常,以此判断这个zip文件是否正常,暂时也没想到更好的办法。
private boolean unzip(File unZipFile) {
boolean succeed = true;
ZipInputStream zin = null;
ZipEntry entry = null;
try {
zin = new ZipInputStream(new FileInputStream(unZipFile));
boolean first = true;
while (true) {
if ((entry = zin.getNextEntry()) == null) {
if (first)
succeed = false;
break;
}
first = false;
if (entry.isDirectory()) {
zin.closeEntry();
continue;
}
if (!entry.isDirectory()) {
byte[] b = new byte[1024];
@SuppressWarnings("unused")
int len = 0;
while ((len = zin.read(b)) != -1) {
}
zin.closeEntry();
}
}
} catch (IOException e) {
succeed = false;
} finally {
if (null != zin) {
try {
zin.close();
} catch (IOException e) {
}
}
}
return succeed;
}