通过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]