通过RSS实现app的自动更新

时间:2022-12-01 05:59:07

缘起

其实这个想法早在十年前我还在做桌面应用的时候就想过了,而且还在一个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=\"([^\"]*)\"/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客户端库,还没有稳定,就不放出来了,请用自己喜欢的实现方式去实现。