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

开篇

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

版本更新的步骤

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

效果截屏

Android 教你如何在GitHub上做app版本更新_第1张图片

立即体验

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

Android 教你如何在GitHub上做app版本更新_第2张图片

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

详细实施步骤

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

    Android 教你如何在GitHub上做app版本更新_第3张图片

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

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

    Android 教你如何在GitHub上做app版本更新_第4张图片

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

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

    Android 教你如何在GitHub上做app版本更新_第5张图片

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

    Android 教你如何在GitHub上做app版本更新_第6张图片

  • 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 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(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 grantedPermissions, @Nullable List deniedPermissions, @Nullable List 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

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

你可能感兴趣的:(Android 教你如何在GitHub上做app版本更新)