美文网首页Android 进阶技术篇专题我爱编程android技术
Android 教你如何在GitHub上做app版本更新

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

作者: SwitchLife | 来源:发表于2018-04-25 16:43 被阅读34次

    开篇

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

    版本更新的步骤

    • 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和关注,谢谢!我是JustinEoyQQ:1006368252

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

    相关文章

      网友评论

      • crazyone33:其实还是需要后台的,因为Github流量有限,肯定会慢。还是自己的服务器体验好
        SwitchLife:@1feng 😂我那只是调侃下后台而已。后台肯定是不可或缺的。只要看懂这个更新流程就好!

      本文标题:Android 教你如何在GitHub上做app版本更新

      本文链接:https://www.haomeiwen.com/subject/bahylftx.html