开篇
之前的项目版本更新一直都需要后台开发人员来插一脚,虽然写一个版本更新的接口并不费多大的力气,但是每一个项目都要做重复的工作,你要知道后台开发挺忙的,我不想后台人员分心,所以有了这篇文章。
版本更新的步骤
- 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、上传
apk
和output.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;
}
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和关注,谢谢!我是JustinRoom,QQ:1006368252。
在一个崇高的目标支持下,不停地工作,即使慢,也一定会获得成功。 —— 爱因斯坦