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

前言:代码中账号只作展示之用,请勿上传信息,多谢!无法成功导入demo的同学,请下载as4.0。先前无法导入的同学,是因为之前demo的签名文件没有存放到本工程中。最新demo已经更改,感谢同学反馈。
本文版本更新的.apk文件存放于Bmob服务器,使用其它方式存放亦可,思路通用。本文提供:权限申请、异步下载、静默安装以及版本更新核心思路。
Bmob数据上传请查阅官方文档:http://doc.bmob.cn/data/android/develop_doc/,文中不再赘述。最新demo下载地址:
链接:https://pan.baidu.com/s/1DAAiqQzm8O_Y4pjy_5ny5w
提取码:5ivc

demo基于 androidx,最新版AS4.0导入它完全没问题 。固执使用V4,V7包的同学,自行导包,代码基本一致。建议官网下载AS 4.0,无需,速度很快: https://developer.android.google.cn/studio 。安装请参考这篇: https://blog.csdn.net/qq_41976613/article/details/91432304
demo从版本号1, 成功更新到版本号 2的样子:

android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第1张图片

gif演示运行过程。看不到动图的,页面重新刷新下


demo的流程是这样的:

  1. VersionCode 为1时,先生成.jks签名文件

  2. 生成版本更高的.apk文件
    将VersionCode 改为 2,双击右侧工具栏的Gradle->项目名->app->Tasks->build->assemble, 生 成.apk文件
    3 将此.apk文件拷贝到手机,准备上传服务器用(如果你的Bmob账号允许上传文件)
    4 开始测试。将VersionCode 改回1,并运行。
    5 点击主页面按钮上传apk
    6 退出app重新运行,这时能检测到版本更新
    7 根据提示下载apk,然后自动安装运行新版本

    若首次运行demo弹出版本更新对话框,那是因为Bmob服务器上存有测试的更新版本。我会及时清理,方便大家测试。使用BMOB的同学最好还是注册自己的账号,完整走一遍demo流程

一、预备知识:VersionCode ,VersionName

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

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

二、思路

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

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

app检测出需进行版本更新,则访问服务器自动下载新版本.apk文件到本地,然后进行静默安装。

四、遇到的问题

调试时运行的是debug版,检测到版本有更新,然后自动下载存放在服务器的release版apk到本地。这样往往会由于两个版本签名不一致,导致静默安装失败。解决方案:在app下build.gradle文件中,统一debug、release签名,杜绝不一致。签名文件.jks最好放在本项目目录下,避免他人导入你的项目出现q签名文件不存在的错误

//签名配置
    signingConfigs {
        config {
            keyAlias 'myapkupdate'
            keyPassword '123456'
            storeFile file('/src/main/myupdate.jks')
            storePassword '123456'
        }
    }
    buildTypes {
        //打包配置
        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 之前,因为脚本是按顺序执行的

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

五、关键代码实现

整体思路:手动申请权限成功后,获取后台最新版本号,并与本地版本号比对,大于则弹出对话框提示更新下载然后进行静默安装。
特别重要
确定用Bmob来版本更新的同学,请使用自己的Bmob账号,demo账号仅供测试用:

public class BmobManager {
    private static final String BMOB_SDK_ID = "改成自己的bmob账号";

一) android 6.0后手动权限申请

1)清单文件 AdroidManifest.xml

清单文件有关于网络和文件方面的,请予重视。权限过多,自行取舍(至少应保留网络、文件读取权限等):

 

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    

        
            
                

                
            
         
        
        
        
        
        
        
    


2)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 mPerList = new ArrayList<>();
    //保存没有同意的失败权限
    private List 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 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 noPermissions) {

                    }
                }
        );
    }

二)版本更新数据类

使用其他方式更新的同学,比如xml,json,也应有类似的Bean

1) 自定义UpdateSet数据类

如果上传该类数据,会在Bmob后台自动生成一张同名的表

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类

检测版本更新、下载、静默安装。使用其他方式更新的同学,比如xml,json,请用自己的数据Bean解析,然后酌情使用该方法:

/**
 * 如何更新?
 * 1.将Apk上传至Bmob获得Url
 * 2.修改UpdateSet中的属性
 * versionCode +1
 */
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() {
            @Override
            public void done(List list, BmobException e) {
                if (e == null) {
                    if (CommonUtils.isEmpty(list)) {
                        //倒序
                        Collections.reverse(list);
                        for(int j=0;j= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri uri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", apkFile);
            intent.setDataAndType(uri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
        }
        mContext.startActivity(intent);
    }
}

    ```

三) 下载管理类

okHttp异步下载 .apk文件。可拿到其它项目用

public class HttpManager {

    private static volatile HttpManager mInstnce = null;
    private OkHttpClient mOkHttpClient;
    private HttpManager() {
        mOkHttpClient = new OkHttpClient();
    }

    public static HttpManager getInstance() {
        if (mInstnce == null) {
            synchronized (HttpManager.class) {
                if (mInstnce == null) {
                    mInstnce = new HttpManager();
                }
            }
        }
        return mInstnce;
    }

    /**
     * 下载
     */
    public void download(final String url, final String saveDir, final OnDownloadListener listener) {
        Request request = new Request.Builder().url(url).build();
        mOkHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                listener.onDownloadFailed(e);
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                //储存下载文件的目录
                //String savePath = isExistDir(saveDir);
                try {
                    is = response.body().byteStream();
                    long total = response.body().contentLength();
                    //不用从url 直接从path
                    File file = new File(saveDir);
                    fos = new FileOutputStream(file);
                    long sum = 0;
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        listener.onDownloading(progress);
                    }
                    fos.flush();
                    //下载完成
                    listener.onDownloadSuccess(file.getAbsolutePath());
                } catch (Exception e) {
                    listener.onDownloadFailed(e);
                } finally {
                    try {
                        if (is != null)
                            is.close();
                    } catch (IOException e) {

                    }
                    try {
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {

                    }
                }
            }
        });
    }

    /**
     * 判断文件下载目录是否存在
     */
    private String isExistDir(String saveDir) throws IOException {
        File downloadFile = new File(saveDir);
        if (!downloadFile.mkdirs()) {
            downloadFile.createNewFile();
        }
        String savePath = downloadFile.getAbsolutePath();
        return savePath;
    }

    /**
     * 从路径获取文件名
     */
    private String getNameFromUrl(String url) {
        return url.substring(url.lastIndexOf("/") + 1);
    }

    /**
     * 下载进度监听
     */
    public interface OnDownloadListener {
        /**
         * 下载成功
         */
        void onDownloadSuccess(String path);

        /**
         *  * 下载进度
         */
        void onDownloading(int progress);
        /**
         * 下载失败
         */
        void onDownloadFailed(Exception e);
    }

}

四) 生成新版 .apk 文件(release版)

每次更新,版本号都在前次基础上加1。versionCode 初始默认值为1,现将版本号改为2,以示此版为新版。

 versionCode 2
 versionName "2.0 release版本测试"

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

五) 上传最新的版本信息到Bmob后台

这里以使用Bmob服务器为例。使用其它方法上传apk文件以及文件信息的同学,请跳过该步,自行解决。
在Bmob绑定好独立域名的可通过代码上传新版.apk文件,没有绑定的请手动修改。下列1) ,2)方法中,根据自身情况选择1种即可 ,当然方法1最方便

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

*1 将打包好的最新版 .apk 文件拷贝到手机中,并请在代码中填上该地址
*2 文件上传会返回真实的apk下载路径
*3 上传版本信息、apk下载路径到后台UpdateSet 表中

//上传.apk文件,并将文件下载地址保存到后台UpdateSet表中
    private void uploadApk(){
        //改成你手机存放需更新的 .apk文件地址
        String apkPath = "/sdcard/zzd/app-release.apk" ;
        File uploadFile = new File(apkPath);
        if (uploadFile != null) {
            //进度条显示
            if (mProgressDialog != null) {
                mProgressDialog.show();
            }
            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() {
                            @Override
                            public void done(String s, BmobException e) {
                                mProgressDialog.dismiss();
                                if (e == null) {
                                    Loggerr.i("上传版本信息成功" );
                                    Toast.makeText(TestActivity.this, "上传版本信息成功", Toast.LENGTH_SHORT).show();
                                }else{
                                    Loggerr.i("上传版本信息失败:"+e.toString() );
                                    Toast.makeText(TestActivity.this, "上传版本信息失败:"+e.toString(), Toast.LENGTH_SHORT).show();
                                }
                            }
                        });
                    }else{
                        mProgressDialog.dismiss();
                        Loggerr.i("上传apk失败:" +e.toString());
                    }
                }
                @Override
                public void onProgress(Integer progress) {
                    mProgressDialog.setProgress(progress);
                }
            });
        }else{
            Toast.makeText(TestActivity.this, "文件不存在", Toast.LENGTH_SHORT).show();
        }
    }

点击apk上传按钮上传文件的样子:
android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第2张图片

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

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

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

android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第3张图片
UpdateSet表最后的样子:
在这里插入图片描述

六)检测

以上生成并上传了最新版(2.0版)apk。作为测试,需改回旧版(1.0版),运行后会检测到需要更新弹出对话框。如果测试运行的是2及以上版本,则检测不到版本更新。将app下build.gradle 文件 版本属性值 改回默认值1

   versionCode 1
   versionName "1.0 debug版本测试"

运行,当前版本为1,检测到有新版本:
android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第4张图片
自动下载:
android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第5张图片
静默安装:
android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第6张图片
成功更新到版本 2:
android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo)_第7张图片

你可能感兴趣的:(android 自动 检测 版本更新 自动下载 静默安装 (超详细 有demo))