珍惜生命,多做笔记
android应用如果是发布到豌豆荚/应用宝等平台上,是不需要自己做更新功能的,只需要在相关平台上更新就行.不过如果希望能够通过应用本身来检查和更新自己,就需要自己来开发相应的功能了.
很显然,为了完成这一功能,一个服务端是少不了的,我们需要下载服务端上面的新版本的apk文件来更新自己,同时为了检测版本信息,需要另一个文件来进行版本控制,这里我使用的是xml文件.
客户端先读取服务器上面配置的xml文件来比较自己和服务器上的版本号,如果有更高的版本,就可以下载服务器上面的apk文件了.
使用myeclipse新建一个工程,在WebRoot目录下新建一个xml文件,我命名为VersionInfo.xml,内容如下:
<pre name="code" class="html"><?xml version="1.0" encoding="UTF-8"?>
<update>
<version>8</version>
<name>1.8</name>
<url>http://106.2.199.70:7007/YDPlatform/Light.apk</url>
<info> .版本号:1.8 \n .修复按下返回键没有退出的问题 \n.优化传感器算法 \n</info>
</update>
version标签内容为服务器上apk文件的版本,name标签为该版本的版本号,url为新的apk文件地址,域名也可以直接写为ip地址+端口号,我们需要在服务器上将这个apk文件放到WebRoot目录下.info标签写明了新版本的修改信息,其中\n用来换行.
Android应用程序通过Manifest.xml文件来配置应用版本信息:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="phy.light" android:versionCode="7" android:versionName="1.7" >
可以看到,我们在当前应用中的版本为7,而服务器上面配置的版本为8,有了更高的版本,我们就可以对应用进行更新了.
检测当前应用版本的方法如下:
<pre name="code" class="java">private int getVersion(){ int version=0; try{ version=context.getPackageManager().getPackageInfo("phy.light", 0).versionCode; }catch(Exception e){ Log.d("NameNotFound","XX X"); return 9999; } return version; }
这里通过PackageManager来获取应用信息,"phy.light"为应用所在的包.需要注意的是异常的处理,如果没有检测到异常,必须要返回一个值,否则将会在后续流程中出现严重错误!这里我们直接返回9999,保证如果出现异常的话,应用仍然正常执行.因为正常情况下,应用的版本不会达到9999,程序将不会继续更新自己.
下面,我们需要到服务器上面读取新版本的信息,这里我直接下载了他的xml文件.
private InputStream DownXml(){ URL url=null; InputStream stream=null; HttpURLConnection conn; try { url=new URL("http://www.xxxxx.com/YDPlatform/VersionInfo.xml"); conn=(HttpURLConnection) url.openConnection(); stream=conn.getInputStream(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } return stream; }
这里仍然需要注意异常的处理,若不作任何处理,当产生异常的时候会导致应用崩溃,这里返回null即可.
接下来是解析读取的xml文件:
private void ParseXml(InputStream stream){ HashMap<String,String> map=null; if(stream!=null){ try{ map=new HashMap<String,String>(); DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance(); DocumentBuilder builder=factory.newDocumentBuilder(); Document document=builder.parse(stream); Element root=document.getDocumentElement(); NodeList childs=root.getChildNodes(); for(int i=0;i<childs.getLength();i++){ Node child=(Node)childs.item(i); if(child.getNodeType()==Node.ELEMENT_NODE){ if("version".equals(child.getNodeName())){ map.put("version", child.getFirstChild().getNodeValue()); }else if("name".equals(child.getNodeName())){ map.put("name", child.getFirstChild().getNodeValue()); }else if("url".equals(child.getNodeName())){ map.put("url", child.getFirstChild().getNodeValue()); }else if("info".equals(child.getNodeName())){ map.put("info", child.getFirstChild().getNodeValue()); }; } } stream.close(); mHashMap=map; handler.sendEmptyMessage(READVERSION_FINISH); }catch(Exception e){} } }
也可以采用另一种方式,就是服务器端先自己读取VersionInfo.xml文件,然后使用servlet返回xml字符串,客户端再以Map的形式读取读取,不过这样就需要额外的使用 org.dom4j 的包.
public static Map<String,String> parseXml(String xmlStr) throws DocumentException{ Map<String,String> map=new HashMap<String,String>(); Document dom=DocumentHelper.parseText(xmlStr); Element root=dom.getRootElement(); List<Element> elementList=root.elements(); for(Element e:elementList){ map.put(e.getName(), e.getText()); } return map; }
注意网络通信需要使用后台线程来处理:
private class DownVersionInfo extends Thread{//读取版本信息 @Override public void run() { // TODO Auto-generated method stub ParseXml(DownXml()); } };
完成这些之后,就可以来比较版本信息了,如果有新版本,则弹出升级对话框,反对静默升级,流量也是RMB买的!
private int getNewVersion(HashMap<String,String> map){ int version=0; if(null!=map){ version=Integer.valueOf(map.get("version")); } return version; }
private void IsUpdate(int old_version,int new_version){//检查是否有新版本 if(new_version>old_version){ showDownloadDialog(); } }
创建升级对话框,这里发现"\n"居然无法使文字换行!经过多次试验(欲死欲活的折腾!),发现需要将其替换为"\\n":
private void showDownloadDialog(){//升级对话框 AlertDialog.Builder builder=new Builder(context); builder.setTitle(context.getString(R.string.bt_update_title)); String temp=mHashMap.get("info").toString().replace("\\n","\n"); builder.setMessage(temp); builder.setPositiveButton(context.getString(R.string.bt_update1), new OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub t2=new DownLoadThread(); t2.start(); dialog.dismiss(); }}); builder.setNegativeButton(context.getString(R.string.bt_update2), new OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub dialog.dismiss(); }}); builder.create().show(); }
用户确认更新后,就可以愉快的下载apk了,这里产生的异常不必进行处理.
private class DownLoadThread extends Thread{//下载apk文件 @Override public void run() { // TODO Auto-generated method stub super.run(); try{ path=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+File.separator; URL url=new URL(mHashMap.get("url")); HttpURLConnection conn=(HttpURLConnection)url.openConnection(); conn.connect(); int length=conn.getContentLength(); InputStream stream=conn.getInputStream(); File f=new File(path); if(!f.exists()){ f.mkdirs(); } File apk=new File(path,mHashMap.get("name")); FileOutputStream fos=new FileOutputStream(apk); int count=0; byte buf[]=new byte[1024]; do{ if((count=stream.read(buf))!=-1){ fos.write(buf,0,count); handler.sendEmptyMessage(DOWNLOAD); }else{ handler.sendEmptyMessage(DOWNLOAD_FINISH); break; } }while(!cancelUpdate); stream.close();fos.close(); }catch(Exception e){} } }
这里因为文件比较小,没有做更多的处理,最好的做法是写一个通知,更新的时候显示出下载进度.下面是安装新的apk,旧的apk和新的apk签名必须一致,否则无法更新.
private void installAPK(){ File apkfile=new File(path,mHashMap.get("name")); if(!apkfile.exists()){ return; } Intent i=new Intent(Intent.ACTION_VIEW); i.setDataAndType(Uri.parse("file://"+apkfile.toString()), "application/vnd.android.package-archive"); context.startActivity(i); }
上面这个方法是从网络上找到的,经过查询,"application/vnd.android.package-archive"表示apk文件.
最后,通过一个handler来控制整个流程:
private Handler handler=new Handler(){ @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub super.handleMessage(msg); switch(msg.what){ case DOWNLOAD: break; case DOWNLOAD_FINISH: installAPK(); break; case READVERSION_FINISH: IsUpdate(getVersion(), getNewVersion(mHashMap)); break; } } };
当我们需要升级的时候,就可以初始化并启动一个DownVersionInfo线程了.
public void init(){ t=new DownVersionInfo(); t.start(); }
完整的应用端代码如下:
package phy.update; import java.io.File; import java.io.InputStream; import java.io.FileOutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import phy.light.R; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.util.Log; public class UpdateAPK { Context context; DownVersionInfo t; DownLoadThread t2; /* 下载中 */ private static final int DOWNLOAD = 1; /* 下载结束 */ private static final int DOWNLOAD_FINISH = 2; private static final int READVERSION_FINISH = 3; /* 保存解析的XML信息 */ HashMap<String, String> mHashMap; /* 下载保存路径 */ private String path; /* 是否取消更新 */ private boolean cancelUpdate = false; private Handler handler=new Handler(){ @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub super.handleMessage(msg); switch(msg.what){ case DOWNLOAD: break; case DOWNLOAD_FINISH: installAPK(); break; case READVERSION_FINISH: IsUpdate(getVersion(), getNewVersion(mHashMap)); break; } } }; public UpdateAPK(Context _context){ context=_context; } public void init(){ t=new DownVersionInfo(); t.start(); } private int getVersion(){ int version=0; try{ version=context.getPackageManager().getPackageInfo("phy.light", 0).versionCode; }catch(Exception e){ Log.d("NameNotFound","XX X"); return 9999; } return version; } private int getNewVersion(HashMap<String,String> map){ int version=0; if(null!=map){ version=Integer.valueOf(map.get("version")); } return version; } private void IsUpdate(int old_version,int new_version){//检查是否有新版本 if(new_version>old_version){ showDownloadDialog(); } } private void showDownloadDialog(){//升级对话框 AlertDialog.Builder builder=new Builder(context); builder.setTitle(context.getString(R.string.bt_update_title)); String temp=mHashMap.get("info").toString().replace("\\n","\n"); builder.setMessage(temp); builder.setPositiveButton(context.getString(R.string.bt_update1), new OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub t2=new DownLoadThread(); t2.start(); dialog.dismiss(); }}); builder.setNegativeButton(context.getString(R.string.bt_update2), new OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub dialog.dismiss(); }}); builder.create().show(); } private InputStream DownXml(){ URL url=null; InputStream stream=null; HttpURLConnection conn; try { url=new URL("http://www.xxxxx.com/YDPlatform/VersionInfo.xml"); conn=(HttpURLConnection) url.openConnection(); stream=conn.getInputStream(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } return stream; } private void ParseXml(InputStream stream){ HashMap<String,String> map=null; if(stream!=null){ try{ map=new HashMap<String,String>(); DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance(); DocumentBuilder builder=factory.newDocumentBuilder(); Document document=builder.parse(stream); Element root=document.getDocumentElement(); // Log.d("root",""+root.getTagName()); NodeList childs=root.getChildNodes(); for(int i=0;i<childs.getLength();i++){ Node child=(Node)childs.item(i); if(child.getNodeType()==Node.ELEMENT_NODE){ if("version".equals(child.getNodeName())){ map.put("version", child.getFirstChild().getNodeValue()); // Log.d("version",""+child.getFirstChild().getNodeValue()); }else if("name".equals(child.getNodeName())){ map.put("name", child.getFirstChild().getNodeValue()); }else if("url".equals(child.getNodeName())){ map.put("url", child.getFirstChild().getNodeValue()); }else if("info".equals(child.getNodeName())){ map.put("info", child.getFirstChild().getNodeValue()); }; } } stream.close(); mHashMap=map; handler.sendEmptyMessage(READVERSION_FINISH); }catch(Exception e){} } } private class DownVersionInfo extends Thread{//读取版本信息 @Override public void run() { // TODO Auto-generated method stub super.run(); ParseXml(DownXml()); } }; private class DownLoadThread extends Thread{//下载apk文件 @Override public void run() { // TODO Auto-generated method stub super.run(); try{ path=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+File.separator; URL url=new URL(mHashMap.get("url")); HttpURLConnection conn=(HttpURLConnection)url.openConnection(); conn.connect(); int length=conn.getContentLength(); InputStream stream=conn.getInputStream(); File f=new File(path); if(!f.exists()){ f.mkdirs(); } File apk=new File(path,mHashMap.get("name")); FileOutputStream fos=new FileOutputStream(apk); int count=0; byte buf[]=new byte[1024]; do{ if((count=stream.read(buf))!=-1){ fos.write(buf,0,count); handler.sendEmptyMessage(DOWNLOAD); }else{ handler.sendEmptyMessage(DOWNLOAD_FINISH); break; } }while(!cancelUpdate); stream.close();fos.close(); }catch(Exception e){} } } private void installAPK(){ File apkfile=new File(path,mHashMap.get("name")); if(!apkfile.exists()){ return; } Intent i=new Intent(Intent.ACTION_VIEW); i.setDataAndType(Uri.parse("file://"+apkfile.toString()), "application/vnd.android.package-archive"); context.startActivity(i); } }