前言:代码中账号只作展示之用,请勿上传信息,多谢!无法成功导入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的样子:
gif演示运行过程。看不到动图的,页面重新刷新下
demo的流程是这样的:
VersionCode 为1时,先生成.jks签名文件
生成版本更高的.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流程
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账号";
清单文件有关于网络和文件方面的,请予重视。权限过多,自行取舍(至少应保留网络、文件读取权限等):
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);
}
}
/**
* 权限申请成功后再去获取版本更新信息。因为是异步,会出现权限没有申请成功就去下载的情况,最终导致失败
*/
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
如果上传该类数据,会在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;
}
}
检测版本更新、下载、静默安装。使用其他方式更新的同学,比如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 AppCode ? true : false);
}
if (updateSet.getVersionCode() > AppCode) {
//检测到有更新比对版本
createUpdateDialog(updateSet);
}
} catch (PackageManager.NameNotFoundException ex) {
ex.printStackTrace();
}
}
}else{
Log.i(TAG,"bmob后台没有更新的数据是;");
}
}
});
}
/**
* 更新提示框
*/
private void createUpdateDialog(final UpdateSet updateSet) {
mUpdateView = DialogManager.getInstance().initView(mContext, R.layout.dialog_update_app);
tv_desc = mUpdateView.findViewById(R.id.tv_update_desc);
tv_confirm = mUpdateView.findViewById(R.id.tv_confirm);
tv_cancel = mUpdateView.findViewById(R.id.tv_cancel);
tv_desc.setText(updateSet.getDesc());
tv_confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DialogManager.getInstance().hide(mUpdateView);
Log.i(TAG,"下载;");
downloadApk(updateSet);
}
});
tv_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DialogManager.getInstance().hide(mUpdateView);
}
});
DialogManager.getInstance().show(mUpdateView);
initProgress();
}
private void initProgress() {
mProgressDialog = new ProgressDialog(mContext);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setCancelable(false);
}
/**
* 下载
*/
private void downloadApk(UpdateSet updateSet) {
if (TextUtils.isEmpty(updateSet.getPath())) {
return;
}
final String filePath = "/sdcard/lotuses/" + System.currentTimeMillis() + ".apk";
if (mProgressDialog != null) {
mProgressDialog.show();
}
//开始下载:
HttpManager.getInstance().download(updateSet.getPath(), filePath, new HttpManager.OnDownloadListener() {
@Override
public void onDownloadSuccess(String path) {
mProgressDialog.dismiss();
Log.i(TAG,"onDownloadSuccess:" + path);
if (!TextUtils.isEmpty(path)) {
installApk(path);
}
}
@Override
public void onDownloading(int progress) {
mProgressDialog.setProgress(progress);
Log.i(TAG,"onDownloading:" + progress);
/*
if(Utils.isRunOnUIThread()){
Log.i(TAG,"运行在主线程");
}else{
Log.i(TAG,"不是运行在主线程");
}
*/
}
@Override
public void onDownloadFailed(Exception e) {
mProgressDialog.dismiss();
Log.i(TAG,"onDownloadFailed:" + e.toString());
}
});
}
public interface OnUpdateAppListener {
void OnUpdate(boolean isUpdate);
}
/**
* 安装Apk
*/
public void installApk(String filePath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
File apkFile = new File(filePath);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 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);
}
}
每次更新,版本号都在前次基础上加1。versionCode 初始默认值为1,现将版本号改为2,以示此版为新版。
versionCode 2
versionName "2.0 release版本测试"
限于篇幅,如何生成正式签名的apk文件,请看这篇:https://blog.csdn.net/u010475354/article/details/106899320
这里假设已生成app-release.apk文件
这里以使用Bmob服务器为例。使用其它方法上传apk文件以及文件信息的同学,请跳过该步,自行解决。
在Bmob绑定好独立域名的可通过代码上传新版.apk文件,没有绑定的请手动修改。下列1) ,2)方法中,根据自身情况选择1种即可 ,当然方法1最方便
*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();
}
}
1.后台新建UpdateSet表,表中字段:
desc - String类型
paht - String 类型
versionCode - Number类型
apk -file类型
2. 单击左上角添加行,单击空白行apk列,上传需更新的.apk文件 。
3. apk字段右击,将Copy link address 得到的apk真实下载地址填入到path字段。填写其它各个字段值,特别是versionCode,比上个版本多1
以上生成并上传了最新版(2.0版)apk。作为测试,需改回旧版(1.0版),运行后会检测到需要更新弹出对话框。如果测试运行的是2及以上版本,则检测不到版本更新。将app下build.gradle 文件 版本属性值 改回默认值1
versionCode 1
versionName "1.0 debug版本测试"