android 自动 检测 版本更新 自动下载 静默安装 Bmob 实现 (超详细)

前言:本文以Bmob作为数据存储的服务器来实现版本更新,使用其它服务器亦可,思路通用。数据的增删改查请查阅其官方文档:http://doc.bmob.cn/data/android/develop_doc/。涉及Bmob的数据部分不再赘述。文后有 demo
先上图,成功更新版本后的样子:
在这里插入图片描述

一、预备知识:VersionCode ,VersionName

Google为APK定义了两个关于版本属性:VersionCode和VersionName,用途各异:

VersionCode :版本号。对用户不可见,仅用于应用市场、程序内部识别版本,判断新旧等用途。
Integer类型,系统默认该值为1。每次发布更新版本时,递增该值
VersionName:版本名。展示给用户,用户通过它认知自己安装的版本
String类型,一般和VersionCode成对出现。

二、思路

每次进行版本更新时,VersionCode版本号递增1,需要开发者修改服务器后台版本数值。app从服务器上获取版本号,并与本地版本号进行比对,若大于本地,则提示用户升级。此属性值是版本更新的依据。
另一属性值versionName,用于向用户展示版本变化幅度。例如从1.0.1变到1.0.2,只是修改了一个很小的bug;若变到1.1.0,可能是修改了某些功能; 再如变到2.0.0,便是进行了大幅修改,比如UI界面改变,功能的增删等。此属性值不作为判断版本升级的依据,只是告知用户版本做了一定程度调整。

三、申请静态、动态各类权限,至少应该包括网络、读写权限

app判断需进行版本更新,访问服务器自动下载新版本apk文件到本地,然后进行静默安装。我的demo是copy以前代码,申请了一些无关的权限,请自行甄别

四、遇到的问题

调试中最易犯的错误是用debug版运行,版本如有更新,会自动下载release的新版本apk到本地。这样往往造成签名不一致,导致静默安装失败。解决方案:在app下build.gradle文件中,统一debug、release签名

//签名配置
    signingConfigs {
        config {
            keyAlias 'chat'
            keyPassword '123456'
            storeFile file('/src/main/jdk/mychat.jks')
            storePassword '123456'
        }
    }
    buildTypes {
        //打包配置
        release {
            //清理无用资源
            //shrinkResources true
            //是否启动ZipAlign压缩
            zipAlignEnabled true
            //是否混淆
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            //签名
            signingConfig signingConfigs.config
        }
        debug {
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            //签名
            signingConfig signingConfigs.config
        }          
        
    }

注意: 签名配置signingConfigs 时一定要放在打包配置buildTypes 之前,因为脚本是按顺序执行的

不重复造轮子了,部分方法参考网络,感谢分享!

五、关键代码实现

整体思路:手动申请权限成功后,获取后台最新版本号,并与本地版本号比对,大于则弹出对话框提示更新下载然后进行静默安装。

一> 模拟数据

1) 新建一个MyTestData 数据类,继承BmobObject ,根据需要自行增减字段

public class MyTestData extends BmobObject {
    private String desc;

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

2)BmobManger中增加批量上传数据的方法

 /**
     * 同时新增多条测试数据
     */
    public void uploadTestData( QueryListListener<BatchResult> listener) {

        BmobBatch batch = new BmobBatch();
        //批量添加
        List<BmobObject> categoriesSave = new ArrayList<>();
        for (int i = 0; i < 60; i++) {
            MyTestData category = new MyTestData();
            category.setDesc("这是测试: " + i);
            categoriesSave.add(category);
        }
        //执行批量操作
        batch.insertBatch(categoriesSave);
        batch.doBatch(listener);

    }

3)上传测试数据。该方法会自动在Bmob后台新建一个MyTestData表, 并产生模拟数据

BmobManager.getInstance().uploadTestData(new QueryListListener<BatchResult>(){
                @Override
                public void done(List<BatchResult> results, BmobException e) {
                    if (e == null) {
                        //返回结果的results和上面提交的顺序是一样的,请一一对应
                        for (int i = 0; i < results.size(); i++) {
                            BatchResult result = results.get(i);
                            BmobException ex = result.getError();
                            //只有批量添加才返回objectId
                            if (ex == null) {
                                Loggerr.i("第" + i + "个数据批量操作成功:" + result.getCreatedAt() + "," + result.getObjectId() + "," + result.getUpdatedAt());;
                                //模拟数据已写入,下次不会再写
                                SpUtils.getInstance().putBoolean("sp_is_first_send", false);
                                if (BmobManager.getInstance().isLogin()) {
                                    //跳转到登录页
                                    startActivity(new Intent(IndexActivity.this, LoginActivity.class));

                                } else {
                                    //跳转到注册页
                                    startActivity(new Intent(IndexActivity.this, RegisterActivity.class));
                                }
                                finish();
                            } else {
                                Loggerr.i("第" + i + "个数据批量操作失败:" + ex.getMessage() + "," + ex.getErrorCode());;
                            }
                        }
                    } else {
                        Loggerr.i("失败:" + e.getMessage() + "," + e.getErrorCode());
                    }
                }
            });

4)查看Bmob后台MyTestData表,新增了模拟测试数据

在这里插入图片描述

二) android 6.0后手动权限申请

1)清单文件 AdroidManifest.xml

  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STAT E" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.CALL_PHONE" />

2)BaseAcitity中申请。整个BaseAcitity类代码:

public class BaseActivity extends AppCompatActivity {

    //申请运行时权限的Code
    private static final int PERMISSION_REQUEST_CODE = 1000;
    //申请窗口权限的Code
    public static final int PERMISSION_WINDOW_REQUEST_CODE = 1001;

    //申明所需权限
    private String[] mStrPermission = {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
            Manifest.permission.READ_CONTACTS,
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.CALL_PHONE,
            Manifest.permission.ACCESS_FINE_LOCATION

    };
    //保存没有同意的权限
    private List<String> mPerList = new ArrayList<>();
    //保存没有同意的失败权限
    private List<String> mPerNoList = new ArrayList<>();
    private OnPermissionsResult permissionsResult;

    /**
     * 一个方法请求权限   
     */
    protected void request(OnPermissionsResult permissionsResult) {
        Log.i("register","base activity request 请求权限:" );
        if (!checkPermissionsAll()) {
            requestPermissionAll(permissionsResult);
        }
    }
    /**
     * 判断单个权限   
     */
    protected boolean checkPermissions(String permissions) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int check = checkSelfPermission(permissions);
            return check == PackageManager.PERMISSION_GRANTED;
        }
        return false;
    }
    /**
     * 判断是否需要申请权限 
     */
    protected boolean checkPermissionsAll() {
        mPerList.clear();
        for (int i = 0; i < mStrPermission.length; i++) {
            boolean check = checkPermissions(mStrPermission[i]);
            //如果不同意则请求
            if (!check) {
                mPerList.add(mStrPermission[i]);
            }
        }
        return mPerList.size() > 0 ? false : true;
    }
    /**
     * 请求权限   
     */
    protected void requestPermission(String[] mPermissions) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions(mPermissions, PERMISSION_REQUEST_CODE);
        }
    }
    /**
     * 申请所有权限   
     */
    protected void requestPermissionAll(OnPermissionsResult permissionsResult) {
        this.permissionsResult = permissionsResult;
        requestPermission((String[]) mPerList.toArray(new String[mPerList.size()]));
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        mPerNoList.clear();
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0) {
                for (int i = 0; i < grantResults.length; i++) {
                    if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
                        //你有失败的权限
                        mPerNoList.add(permissions[i]);
                    }
                }
                if (permissionsResult != null) {
                    if (mPerNoList.size() == 0) {
                        permissionsResult.OnSuccess();
                    } else {
                        permissionsResult.OnFail(mPerNoList);
                    }
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    protected interface OnPermissionsResult {
        void OnSuccess();

        void OnFail(List<String> noPermissions);
    }
    /**
     * 判断窗口权限    
     * @return
     */
    protected boolean checkWindowPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return Settings.canDrawOverlays(this);
        }
        return true;
    }
    /**
     * 请求窗口权限
     */
    protected void requestWindowPermissions() {
        Toast.makeText(this, "申请窗口权限,暂时没做UI交互", Toast.LENGTH_SHORT).show();
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION
                , Uri.parse("package:" + getPackageName()));
        startActivityForResult(intent, PERMISSION_WINDOW_REQUEST_CODE);
    }

}

3)TestAcivity继承BaseAcitivity,申请权限

/**
     * 权限申请成功后再去获取版本更新信息。因为是异步,会出现权限没有申请成功就去下载的情况,最终导致失败  
     */
    private void requestPermiss(){
        request(new BaseActivity.OnPermissionsResult(){
                    @Override
                    public void OnSuccess() {
                        Loggerr.i("权限成功:" );
                        //检查更新
                        new UpdateHelper(TestActivity.this).updateApp(new UpdateHelper.OnUpdateAppListener(){
                            @Override
                            public void OnUpdate(boolean isUpdate) {
                                if(isUpdate){
                                    Loggerr.i("版本更新了");
                                }else{
                                    Loggerr.i("版本没有更新了");
                                }
                            }
                        });
                    }
                    @Override
                    public void OnFail(List<String> noPermissions) {
                     
                    }
                }
        );
    }

三)版本更新数据类

1)数据类

public class UpdateSet extends BmobObject {
    //描述
    private String desc;
    //下载地址
    private String path;
    //版本号
    private int versionCode;
    public String getDesc() {
        return desc;
    }    
    public void setDesc(String desc) {
        this.desc = desc;
    }
    public String getPath() {
        return path;
    }
    public void setPath(String path) {
        this.path = path;
    }
    public int getVersionCode() {
        return versionCode;
    }
    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }
    
}

2)版本更新帮助类 - UpdateHelper类

public class UpdateHelper {
    private Context mContext;
    private DialogView mUpdateView;
    private TextView tv_desc;
    private TextView tv_confirm;
    private TextView tv_cancel;
    private String  TAG="register";
    private ProgressDialog mProgressDialog;
    public UpdateHelper(Context mContext) {
        this.mContext = mContext;
    }

    public void updateApp(final OnUpdateAppListener listener) {
        BmobManager.getInstance().queryUpdateSet(new FindListener<UpdateSet>() {
            @Override
            public void done(List<UpdateSet> list, BmobException e) {
                if (e == null) {
                    //每次只读取UpdateSet表中最新的一条数据
                    if (CommonUtils.isEmpty(list)) {                       
                        UpdateSet updateSet = list.get(0);
                        //获取自己的VersionCode
                        try {
                            int AppCode = mContext.getPackageManager().
                                    getPackageInfo(mContext.getPackageName(), 0).versionCode;
                            Loggerr.i("当前版本AppCod="+AppCode+",网络上versioncode="+updateSet.getVersionCode());
                            //有更新
                            if (listener != null) {
                                listener.OnUpdate(updateSet.getVersionCode() > AppCode ? true : false);
                            }
                            if (updateSet.getVersionCode() > AppCode) {
                                //检测到有更新比对版本
                                createUpdateDialog(updateSet);
                            }
                        } catch (PackageManager.NameNotFoundException ex) {
                            ex.printStackTrace();
                        }
                    }
                }else{
                    Loggerr.i("bmob后台没有版本更新数据");
                }
            }
        });
    }
    ```

四) 生成更新版realase .apk

versionCode 初始默认值为1,现将版本号递增1。每次更新,版本号都必须比前次大

 versionCode 2
 versionName "2.0 release版本测试"

创建存放.jks文件的文件夹,我的签名配置中需要创建jdk文件夹

//签名配置
    signingConfigs {
        config {
            keyAlias 'chat'
            keyPassword '123456'
            storeFile file('/src/main/jdk/mychat.jks')
            storePassword '123456'
        }
    }

限于篇幅,如何生成正式签名的apk,请看这篇:https://blog.csdn.net/u010475354/article/details/106899320
假设已生成app-release.apk文件
在这里插入图片描述

五) 上传更新版本信息到Bmob后台。绑定独立域名的可通过代码上传新版.apk文件,没有绑定的手动修改。下列1) ,2)方法,根据情况选择1种即可

1)已绑定的可直接上传.apk文件到Bmob后台

*将打包好的更新版 .apk 文件拷贝到手机中
*上传至Bmob服务器,成功后会返回真实下载地址
*上传版本信息、apk下载路径到后台UpdateSet 表中

//上传.apk文件,并将文件下载地址保存到后台UpdateSet表中 
  private void uploadApk(){
        //改成你手机存放需更新的 .apk文件地址
        String apkPath = "/sdcard/zzd/app-release.apk" ;
        File uploadFile = new File(apkPath);
        if (uploadFile != null) {
            //上传.apk文件
            final BmobFile bmobFile = new BmobFile(uploadFile);            
            bmobFile.uploadblock(new UploadFileListener() {
                @Override
                public void done(BmobException e) {
                    if (e == null) {
                        Loggerr.i("文件真实下载地址是:" + bmobFile.getFileUrl());
                        //更新版描述
                        String desc = "这是 2.0版本";
                        int versionCode = 2;
                        //后台修改UpdateSet表
                        BmobManager.getInstance().pushVersionUpdate(bmobFile.getFileUrl(), desc, versionCode, new SaveListener<String>() {
                            @Override
                            public void done(String s, BmobException e) {
                                if (e == null) {
                                    Loggerr.i("上传版本信息成功" );
                                }else{
                                    Loggerr.i("上传版本信息失败" );
                                }
                            }
                        });
                    }else{
                        Loggerr.i("上传apk失败" );
                    }
                }
                @Override
                public void onProgress(Integer value) {
                    // 返回的上传进度(百分比)
                }
            });
        }
    }

上传成功后,后台UpdateSet表:
在这里插入图片描述

2)未绑定独立域名的,手动操作后台:

1.后台新建UpdateSet表,表中字段:
desc - String类型
paht - String 类型
versionCode - Number类型
apk -file类型
在这里插入图片描述
2. 单击左上角添加行,单击空白行apk列,上传需更新的.apk文件 。
在这里插入图片描述
3. apk字段右击,将Copy link address 得到的apk真实下载地址填入到path字段。填写其它各个字段值,特别是versionCode,比上个版本多1

在这里插入图片描述
UpdateSet表最后的样子:
在这里插入图片描述

六)检测

将app下build.gradle 文件 版本属性值 改成默认值1

   versionCode 1
   versionName "1.0 debug版本测试"

运行,检测到版本更新:
在这里插入图片描述
下载后静默安装,会重启app。红色划线部分清晰显示当前版本为2,版本更新成功:
在这里插入图片描述
demo已上传csdn,不需要积分,等待核准。现把工程压缩,上传到Bmob,可以下载啦:

猜你喜欢

转载自blog.csdn.net/u010475354/article/details/106881625