通过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("&lt;p&gt;", "", $desc); $desc = str_replace("&lt;/p&gt;", "\n", $desc); if (preg_match("/(.)&lt;a\s+.?href=&quot;([^&quot;])&quot;/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 = &quot;/check_update&quot;;
private static final String LAST_CHECK = &quot;last_check&quot;;

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 &lt; 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 + &quot;v&quot; + 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]