Android 教你如何在GitHub上做app版本更新

开篇

  之前的项目版本更新一直都需要后台开发人员来插一脚,虽然写一个版本更新的接口并不费多大的力气,但是每一个项目都要做重复的工作,你要知道后台开发挺忙的,我不想后台人员分心,所以有了这篇文章。

版本更新的步骤

  • 1、访问接口获取最新版本信息
  • 2、比较最新版本信息与本地版本信息
  • 3、下载最新版本apk安装文件
  • 4、安装apk

效果截屏

立即体验

扫描以下二维码下载体验App(从0.2.3版本开始,体验App内嵌版本更新检测功能):


JSCKit库传送门:https://github.com/JustinRoom/JSCKit

详细实施步骤

  • 1、准备发布的apk和相对应的版本信息文件。
    我们在使用Android studio打包发布版apk时会同时生成相对应的版本信息文件output.json,如下图:


    当然你可以编写自定义的版本信息文件,我偷懒,就用打包时生成的版本信息文件。

  • 2、上传apkoutput.json到GitHub上(如何上传我就不写了,百度一下很多相关资料)。这是我上传路径截图:


    这里我们要知道两个资源路径:

  • JSCKitDemo.apk——https://raw.githubusercontent.com/JustinRoom/JSCKit/master/capture/JSCKitDemo.apk
    注意是资源路径,并不是网页路径,仔细看下图:

  • output.json——https://raw.githubusercontent.com/JustinRoom/JSCKit/master/capture/output.json
    注意是资源路径,并不是网页路径,仔细看下图:


  • 3、根据output.json里的json字符串编写java bean:
    VersionEntity.java

public class VersionEntity {
    private OutputType outputType;
    private ApkInfo apkInfo;
    private String path;

    public OutputType getOutputType() {
        return outputType;
    }

    public void setOutputType(OutputType outputType) {
        this.outputType = outputType;
    }

    public ApkInfo getApkInfo() {
        return apkInfo;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public void setApkInfo(ApkInfo apkInfo) {
        this.apkInfo = apkInfo;
    }

    public static VersionEntity fromJson(String json) {
        try {
            JSONObject jsonObject = new JSONObject(json);
            VersionEntity entity = new VersionEntity();

            JSONObject outputTypeObject = jsonObject.getJSONObject("outputType");
            OutputType outputType = new OutputType();
            outputType.setType(outputTypeObject.optString("type"));
            entity.setOutputType(outputType);

            JSONObject apkInfoObject = jsonObject.getJSONObject("apkInfo");
            ApkInfo apkInfo = new ApkInfo();
            apkInfo.setType(apkInfoObject.optString("type"));
            apkInfo.setVersionCode(apkInfoObject.optInt("versionCode"));
            apkInfo.setVersionName(apkInfoObject.optString("versionName"));
            apkInfo.setEnabled(apkInfoObject.optBoolean("enabled"));
            apkInfo.setOutputFile(apkInfoObject.optString("outputFile"));
            apkInfo.setFullName(apkInfoObject.optString("fullName"));
            apkInfo.setBaseName(apkInfoObject.getString("baseName"));
            entity.setApkInfo(apkInfo);

            entity.setPath(jsonObject.optString("path"));

            return entity;
        } catch (JSONException e) {
            e.printStackTrace();
        }

        return null;
    }

    public String toJson() {
        JSONObject jsonObject = new JSONObject();
        try {
            JSONObject outputTypeObject = new JSONObject();
            outputTypeObject.put("type", outputType.getType());
            jsonObject.put("outputType", outputTypeObject);

            JSONObject apkInfoObject = new JSONObject();
            apkInfoObject.put("type", apkInfo.getType());
            apkInfoObject.put("versionCode", apkInfo.getVersionCode());
            apkInfoObject.put("versionName", apkInfo.getVersionName());
            apkInfoObject.put("enabled", apkInfo.isEnabled());
            apkInfoObject.put("outputFile", apkInfo.getOutputFile());
            apkInfoObject.put("fullName", apkInfo.getFullName());
            apkInfoObject.put("baseName", apkInfo.getBaseName());
            jsonObject.put("apkInfo", apkInfoObject);

            jsonObject.put("path", getPath());
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return jsonObject.toString();
    }
}

OutputType.java

public class OutputType {
    private String type;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

ApkInfo.java

public class ApkInfo {
    private String type;
    private int versionCode;
    private String versionName;
    private boolean enabled;
    private String outputFile;
    private String fullName;
    private String baseName;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getVersionCode() {
        return versionCode;
    }

    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    public String getVersionName() {
        return versionName;
    }

    public void setVersionName(String versionName) {
        this.versionName = versionName;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getOutputFile() {
        return outputFile;
    }

    public void setOutputFile(String outputFile) {
        this.outputFile = outputFile;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public String getBaseName() {
        return baseName;
    }

    public void setBaseName(String baseName) {
        this.baseName = baseName;
    }
}
  • 4、编写版本更新逻辑。网络框架用的是:Retrofit2 + RxAndroid
    用GET方法请求
public interface ApiService {
    @GET("JustinRoom/JSCKit/master/capture/output.json")
    Observable<String> getVersionInfo();

}

a、读取网络文件output.json的内容

private void loadVersionInfo() {
        OkHttpClient client = new CustomHttpClient()
                .setConnectTimeout(5_000)
                .setShowLog(true)
                .createOkHttpClient();
        Retrofit retrofit = new CustomRetrofit()
                //我在app的build.gradle文件的defaultConfig标签里定义了BASE_URL
                .setBaseUrl("https://raw.githubusercontent.com/")
                .setOkHttpClient(client)
                .createRetrofit();
        retrofit.create(ApiService.class)
                .getVersionInfo()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new LoadingDialogObserver<String>(createLoadingDialog()) {
                    @Override
                    public void onNext(String s) {
                        //output.json文件里是JSONArrary, 我们取第一个JSONObject就好
                        s = s.substring(1, s.length() - 1);
                        VersionEntity entity = VersionEntity.fromJson(s);
                        showUpdateTipsDialog(entity);
                    }

                    @Override
                    public void onNetStart(Disposable disposable) {
                        Log.i("MainActivity", "onNetStart: ");
                    }

                    @Override
                    public void onNetError(Throwable e) {

                    }

                    @Override
                    public void onNetFinish(Disposable disposable) {

                    }
                });
    }

b、比较最新版本与本地版本:如果最新版本的versionCode大于本地版本的versionCode,弹窗提示。

    private void showUpdateTipsDialog(final VersionEntity entity) {
        if (entity == null)
            return;

        int curVersionCode = 0;
        String curVersionName = "";
        try {
            PackageManager manager = getPackageManager();
            PackageInfo info = manager.getPackageInfo(getPackageName(), 0);
            curVersionCode = info.versionCode;
            curVersionName = info.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        if (curVersionCode > 0 && entity.getApkInfo().getVersionCode() > curVersionCode)
            new AlertDialog.Builder(this)
                    .setTitle("更新提示")
                    .setMessage("1、当前版本:" + curVersionName + "\n2、最新版本:" + entity.getApkInfo().getVersionName())
                    .setPositiveButton("更新", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            checkPermissionBeforeDownloadApk(entity.getApkInfo().getVersionName());
                        }
                    })
                    .setNegativeButton("取消", null)
                    .show();
    }

c、有新版本,我们下载新版本:这里主要用系统自带的DownloadManager下载文件,我的库中已经封装好了。不懂DownloadManager的请参阅这篇文章:app 在线更新那点事儿(适配Android6.0、7.0、8.0),也可以参考我Demo中的代码。

    private void checkPermissionBeforeDownloadApk(final String versionName){
        checkPermissions(0, new CustomPermissionChecker.OnCheckListener() {
            @Override
            public void onResult(int requestCode, boolean isAllGranted, @NonNull List<String> grantedPermissions, @Nullable List<String> deniedPermissions, @Nullable List<String> shouldShowPermissions) {
                if (isAllGranted){
                    downloadApk(versionName);
                    return;
                }

                if (shouldShowPermissions != null && shouldShowPermissions.size() > 0){
                    String message = "当前应用需要以下权限:\n\n" + getAllPermissionDes(shouldShowPermissions);
                    showPermissionRationaleDialog("温馨提示", message, "设置", "知道了");
                }
            }

            @Override
            public void onFinally(int requestCode) {
                recyclePermissionChecker();
            }
        }, Manifest.permission.WRITE_EXTERNAL_STORAGE);
    }

    public void downloadApk(String versionName){
        registerDownloadCompleteReceiver();
        DownloadEntity entity = new DownloadEntity();
        entity.setUrl("https://raw.githubusercontent.com/JustinRoom/JSCKit/master/capture/JSCKitDemo.apk");
        entity.setSubPath("JSCKitDemo"+ versionName + ".apk");
        entity.setTitle("JSCKitDemo"+ versionName + ".apk");
        entity.setDesc("JSCKit Library");
        entity.setMimeType("application/vnd.android.package-archive");
        downloadFile(entity);
    }

    public final long downloadFile(DownloadEntity downloadEntity) {
        String url = downloadEntity.getUrl();
        if (TextUtils.isEmpty(url))
            return -1;

        Uri uri = Uri.parse(url);
        String subPath = downloadEntity.getSubPath();
        if (subPath == null || subPath.trim().length() == 0) {
            subPath = uri.getLastPathSegment();
        }

        File destinationDirectory = downloadEntity.getDestinationDirectory();
        if (destinationDirectory == null) {
            destinationDirectory = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
        }

        File file = new File(destinationDirectory, subPath);
        File directory = file.getParentFile();
        if (!directory.exists()){//创建文件保存目录
            boolean result = directory.mkdirs();
            if (!result)
                Log.e("APermissionCheck", "Failed to make directories.");
        }

        if (file.exists()){
//            boolean result = file.delete();
//            if (!result)
//                Log.e("APermissionCheck", "Failed to delete file.");
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        DownloadManager.Request request = new DownloadManager.Request(uri);
        //设置title
        request.setTitle(downloadEntity.getTitle());
        // 设置描述
        request.setDescription(downloadEntity.getDesc());
        // 完成后显示通知栏
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        //
        Uri destinationUri = Uri.withAppendedPath(Uri.fromFile(destinationDirectory), subPath);
//        Uri destinationUri = FileProviderCompat.getUriForFile(this, file);
        request.setDestinationUri(destinationUri);
//        request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, subPath);
        request.setMimeType(downloadEntity.getMimeType());
        request.setVisibleInDownloadsUi(true);

        DownloadManager mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
        return mDownloadManager == null ? -1 : mDownloadManager.enqueue(request);
    }

/**
     * 注册下载完成监听
     */
    private void registerDownloadCompleteReceiver(){
        if (downloadReceiver == null)
            downloadReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())){
                        unRegisterDownloadCompleteReceiver();
                        long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                        findDownloadFileUri(downloadId);
                    }
                }
            };
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
        registerReceiver(downloadReceiver, intentFilter);
    }

/**
     * 注销下载完成监听
     */
    private void unRegisterDownloadCompleteReceiver(){
        if (downloadReceiver != null){
            unregisterReceiver(downloadReceiver);
            downloadReceiver = null;
        }
    }

d、获取下载好的apk文件的Uri路径:关于文件的Uri获取在7.0之前和7.0之后的版本有差异。7.0之后的版本的主要用FileProvider共享文件方式获取。不懂FileProvider的请参阅这篇文章:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

    public final void findDownloadFileUri(long completeDownLoadId) {
        Uri uri;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            // 6.0以下
            DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
            assert  downloadManager != null;
            uri = downloadManager.getUriForDownloadedFile(completeDownLoadId);
        } else {
            File file = queryDownloadedFile(completeDownLoadId);
            uri = FileProviderCompat.getUriForFile(this, file);
        }
        onDownloadCompleted(uri);
    }

    private File queryDownloadedFile(long downloadId) {
        File targetFile = null;
        DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
        if (downloadId != -1) {
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(downloadId);
            query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
            assert downloadManager != null;
            Cursor cur = downloadManager.query(query);
            if (cur != null) {
                if (cur.moveToFirst()) {
                    String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                    if (!TextUtils.isEmpty(uriString)) {
                        targetFile = new File(Uri.parse(uriString).getPath());
                    }
                }
                cur.close();
            }
        }
        return targetFile;
    }

查看FileProviderCompat.java

e、安装下载好的apk

    @Override
    protected void onDownloadCompleted(Uri uri) {
        if (uri == null)
            return;

        //8.0有未知应用安装请求权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            //先获取是否有安装未知来源应用的权限
            if (getPackageManager().canRequestPackageInstalls())
                installApk(uri);
        } else {
            installApk(uri);
        }
    }

    public final void installApk(Uri uri){
        Intent intentInstall = new Intent();
        intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intentInstall.setAction(Intent.ACTION_VIEW);
        FileProviderCompat.setDataAndType(intentInstall, uri, "application/vnd.android.package-archive", true);
        startActivity(intentInstall);
    }

注意:8.0系统中安装应用需要安装未知来源应用请求权限,Demo中只做了简单处理,童鞋们请自己做好兼容性处理。

以后的app版本更新再也不需要后台做额外的开发了,后台你给我滚,劳资再也不需要你了!----哈哈哈!

Demo链接

请详细参考我的Demo:
https://github.com/JustinRoom/JSCKit/blob/master/app/src/main/java/jsc/exam/jsckit/ui/MainActivity.java

篇尾

  如果你觉得我写得还可以的,请给我你的star和关注,谢谢!我是JustinRoomQQ:1006368252

在一个崇高的目标支持下,不停地工作,即使慢,也一定会获得成功。 —— 爱因斯坦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容