通过RSS实现应用的自动更新
缘起
其实这个想法早在十年前我还在做桌面应用的时候就想过了,而且还在一个DELPHI程序里尝试了一下,但是因为后来没再做桌面应用,这事也就放下了。
最近在做移动应用,又开始觉得需要这样一个功能。本来这种事情交给Google Play处理就好了,但是因为国内的奇葩环境,完全依赖Google Play并不现实,所以大部分国产应用都实现了自己的自动更新功能。
我不知道别人是怎么实现应用的自动更新的,但基本功能不外就是:服务端提供一个API,客户端定期调用取得最新版本信息,与当前版本比较,如果更新则提示下载更新。
问题在于发布更新的过程我希望能简单化,所以选中了RSS作为服务端的API。这样每次发布一个新版本,我只需要简单地发一篇BLOG即可——只要在其中提供必要的版本信息,客户端即可从RSS输出中得到版本更新的信息。
本来是只需要在客户端实现一个RSS解析功能即可,但是因为我已经实现了一套REST的客户端库,所以为了统一访问,又做了个服务端,把RSS转成JSON返回。
服务端
是一个简单的PHP页面,功能就是用CURL读取RSS的内容,然后通过正则表达式解析(懒得用XML了),转成JSON格式返回。
代码如下:
<?php header('Content-type: application/json; charset=utf-8');define('USER_AGENT', 'auto update'); define('RSS_URL', 'http://yourdomain.com/rss');
function rss_process($url) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_VERBOSE, 0); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, USER_AGENT); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch); $response_info = curl_getinfo($ch); curl_close($ch);
switch(intval($response_info['http_code'])) { case 200: return $response; default: return ""; } }
function get_entry() { $content = rss_process(RSS_URL); $result = array( 'name' => '', 'version' => '', 'desc' => '', 'link' => ''); if (preg_match("/<item>(.?)</item>/is", $content, $matches)) { $item = $matches[1]; if (preg_match("/<title>(.?)</title>/is", $item, $matches)) { $title = $matches[1]; if (preg_match("/(.)\s+([0-9.])/", $title, $matches)) { $result['name'] = $matches[1]; $result['version'] = $matches[2]; } } if (preg_match("/<description>(.?)</description>/is", $item, $matches)) { $desc = $matches[1]; $desc = str_replace("<p>", "", $desc); $desc = str_replace("</p>", "\n", $desc); if (preg_match("/(.)<a\s+.?href="([^"])"/is", $desc, $matches)) { $result['desc'] = $matches[1]; $result['link'] = $matches[2]; } } } return $result; }
$entry = get_entry(); echo json_encode($entry); ?>
用法就是一个简单的REST调用: GET http://yourdomain.com/check_update.php 返回一个JSON对象,内容为:name, version, desc, link。分别为应用名,版本号,更新说明和下载链接。
发布更新BLOG的格式为:
标题为“应用名 x.x.x.x”,内容为更新说明,最后一行放一个a tag,href指向下载链接,链接文本随意(不会出现在JSON里)。其中x.x.x.x格式的版本号必须与下载链接里的应用版本号严格相同,否则会导致反复更新。
Android客户端
功能就是执行一次异步的REST调用,取得返回的JSON对象,然后判断版本,如果不同(只要不同就认为服务端的版本更新,比判断大小简单)则弹出对话框,确认下载则打开默认的下载工具开始下载。
Java代码如下:
public class UpdateChecker implements AsyncRestCallListener {public static class Latest implements Serializable { private static final long serialVersionUID = 1L; public String name; public String version; public String desc; public String link; } private static final String LATEST = "/check_update"; private static final String LAST_CHECK = "last_check"; private Context mContext; private AsyncRestCall mUpdater; private String mTitle; private String mVersion; private SharedPreferences mPref; public UpdateChecker(Context context, String updateURL, String title) { super(); mContext = context; mUpdater = new AsyncRestCall(null, updateURL, this, null); mTitle = title; getVersion(); mPref = PreferenceManager.getDefaultSharedPreferences(context); } public String getVersion() { if (TextUtils.isEmpty(mVersion)) { ComponentName comp = new ComponentName(mContext, getClass()); PackageInfo pinfo = null; try { pinfo = mContext.getPackageManager().getPackageInfo(comp.getPackageName(), 0); } catch (NameNotFoundException e) { e.printStackTrace(); } mVersion = pinfo.versionName; } return mVersion; } public void checkNow(int interval) { long now = (new Date()).getTime(); long lastcheck = mPref.getLong(LAST_CHECK, 0); if (lastcheck + interval*60*1000 < now) { mUpdater.get(LATEST, null, Latest.class); SharedPreferences.Editor pref = mPref.edit(); pref.putLong(LAST_CHECK, now); pref.commit(); } } public void checkNow() { checkNow(1); } public void checkDaily() { checkNow(24*60); } @Override public void onSuccess(Object result) { final Latest latest = (Latest) result; if (! mVersion.equals(latest.version)) { AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle(mTitle + "v" + latest.version); builder.setMessage(latest.desc); builder.setPositiveButton(mContext.getString(android.R.string.ok), new AlertDialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(latest.link)); mContext.startActivity(intent); } }); builder.setNegativeButton(mContext.getString(android.R.string.cancel), new AlertDialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.show(); } } @Override public void onErrorOrCancel(Throwable e) { }
}
使用方法很简单,在MainActivity里调用checkDaily,它的功能是如果距离上次检查超过24小时,则自动检查一 次。然后在About页面里放一个立即检查的按钮,在里面调用checkNow,不过它也不是每次都立即检查的,最快的检查频率被限制在一分钟。
例子代码如下:
// MainActivity mChecker = new UpdateChecker(this, UPDATE_URL, this.getString(R.string.new_version)); mChecker.checkDaily();// About OnCreate mChecker = new UpdateChecker(view.getContext(), UPDATE_URL, this.getString(R.string.new_version));
@Override public void onClick(View v) { mChecker.checkNow(); }
基本上就是这样。其中AsyncRestCall是我自己实现的一套REST客户端库,还没有稳定,就不放出来了,请用自己喜欢的实现方式去实现。
推送到[go4pro.org]