Android+SpringBoot+Vue实现安装包前台上传,后台管理,移动端检测自动更新
场景
安卓app应用更新全流程如下
管理员登录后台系统,从浏览器上通过前端将apk以及版本号和更新说明等信息上传到后台。
后台提供app版本的管理的上传接口和增删改查的接口以及检测最新版本的接口。
app在启动后会首先调用检测最新版本的接口,获得最新版本的相关信息,如果最新版本的版本号大于当前应用的版本号则提示是否更新,点击更新后则会后后台提供的下载接口去下载最新的安装包并安装。
注:
博客主页:
https://blog.csdn.net/badao_liumang_qizhi
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
实现
Android使用Service+OKHttp实现应用后台检测与下载安装
新建一个Android项目,这里叫AppUpdateDemo
然后打开build.gradle,添加gson和okhttp的依赖
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
添加位置如下
然后打开AndroidManifest.xml添加相关权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 弹出系统对话框 因为要在Service中弹出对话框,故添加该权限,使得对话框独立运行,不依赖某个Activity -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!--安装文件-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
添加位置如下
因为在下载完apk之后需要打开下载的apk安装包进而调出安装,而在安卓7.0以上禁止在应用外部公开file://URL
所以需要在AndroidManifest.xml中做如下配置
<!-- 在安卓7.0以上禁止在应用外部公开 file://URI -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.badao.appupdatedemo.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
<!-- 上面的resource="xxx"指的是一个文件,file_path是文件名 -->
</provider>
配置位置如下
一定要注意这里代码中的包名要修改为跟自己的包名一致
然后上面的配置会指向一个res下xml下的file_path.xml的路径,此时在Android Studio中会报红色提示,将鼠标放在红色提示上,
根据提示新建此文件
回车之后,点击OK即可
建立成功之后的路径为
建立成功之后将其代码修改为
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="install_eric"
path="."/>
<root-path name="root_path" path="."/>
</paths>
这样在下载安装包之后就能调起安装
然后在包下新建config文件夹,然后新建一个UpdateConfig类
package com.badao.appupdatedemo.config;
public class UpdateConfig {
//文件存在且完整
public static final int FILE_EXIST_INTACT = 1;
//文件不存在
public static final int FILE_NOT_EXIST = 2;
//文件不完整
public static final int FILE_NOT_INTACT = 3;
//文件不完整 删除文件异常
public static final int FILE_DELETE_ERROR = 4;
//获取版本信息异常
public static final int CLIENT_INFO_ERROR = 5;
//需要弹出哪个对话框
public static final int DIALOG_MUST = 6;
public static final int DIALOG_CAN = 7;
//下载异常
public static final int DOWNLOAD_ERROR = 8;
//安装异常
public static final int INSTALL_ERROR = 9;
}
再新建一个update目录,此目录下新建三个类
第一个类是UpdateCheck
package com.badao.appupdatedemo.update;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import com.badao.appupdatedemo.bean.UpdateBean;
import com.google.gson.Gson;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class UpdateCheck {
/**
* 当前已连接的网络是否可用
* @param context
* @return
*/
public static boolean isNetWorkAvailable(Context context) {
if (context != null) {
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetworkInfo.isConnected()){
return activeNetworkInfo.isAvailable();
}else{
return false;
}
} else {
return false;
}
}
return false;
}
/**
* 网络是否已经连接
* @param context
* @return
*/
public static boolean isNetWorkConnected(Context context){
if (context!=null){
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager!=null){
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo.isConnected();
}else {
return false;
}
}
return false;
}
/**
* 检查版本 后台需要传的是版本名和包名, 可以根据自己需求更改
* @param client
* @param url
* @param packageName
* @param result
*/
public static void checkVersion(OkHttpClient client,
String url,
String versionName,
String packageName,
CheckVersionResult result){
if (TextUtils.isEmpty(url)){
result.fail(-1);
}else {
Log.i("EricLog", "url = \n" + url);
Request.Builder request = new Request.Builder();
request.url(url);
client.newCall(request.get().build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
result.error(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()){
Gson gson = new Gson();
String s = response.body().string();
//UpdateBean实体类, 根据自己需求写
UpdateBean info = gson.fromJson(s, UpdateBean.class);
//后台只给返回在服务器磁盘上的地址
String oldUrl = info.getData().getDownloadLink();
Log.i("oldUrl", "oldUrl = \n" + oldUrl);
//这里将下载apk的地址适配为自己的下载地址
String newUrl = "http://自己服务器的ip:8888/system/file/download?fileName="+oldUrl;
Log.i("newUrl", "newUrl = \n" + newUrl);
info.getData().setDownloadLink(newUrl);
result.success(info);
if (!call.isCanceled()){
call.cancel();
}
}else {
result.fail(response.code());
if (!call.isCanceled()) {
call.cancel();
}
}
}
});
}
}
/**
* 检查悬浮窗权限
* @param context
* @return
*/
public static boolean checkFloatPermission(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
return true;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
try {
Class cls = Class.forName("android.content.Context");
Field declaredField = cls.getDeclaredField("APP_OPS_SERVICE");
declaredField.setAccessible(true);
Object obj = declaredField.get(cls);
if (!(obj instanceof String)) {
return false;
}
String str2 = (String) obj;
obj = cls.getMethod("getSystemService", String.class).invoke(context, str2);
cls = Class.forName("android.app.AppOpsManager");
Field declaredField2 = cls.getDeclaredField("MODE_ALLOWED");
declaredField2.setAccessible(true);
Method checkOp = cls.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
int result = (Integer) checkOp.invoke(obj, 24, Binder.getCallingUid(), context.getPackageName());
return result == declaredField2.getInt(cls);
} catch (Exception e) {
return false;
}
} else {
return Settings.canDrawOverlays(context);
}
}
public interface CheckVersionResult{
//UpdateBean是实体类 自己替换就行
void success(UpdateBean info);
void fail(int code);
void error(Throwable throwable);
}
}
此类主要是一些工具类方法。
使用时需要将此类中下载apk的地址修改为自己后台服务器的下载地址
这里需要下载地址进行拼接并重新赋值的原因是,后台在上传apk时调用的是通用的apk上传接口
返回的是在服务器上的完整路径,而在下载时调用的是通用的文件下载接口,传递的是文件在服务器上的
全路径。
第二个类是UpdateDialog
package com.badao.appupdatedemo.update;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.view.WindowManager;
public class UpdateDialog {
/**
* 强制更新对话框
* @param context
* @param msg
* @param listener
* @return
*/
public static Dialog mustUpdate(Context context,
String msg,
DialogInterface.OnClickListener listener){
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("版本更新");
builder.setMessage(msg);
builder.setCancelable(false);
builder.setPositiveButton("更新", listener);
return builder.create();
}
/**
* 可以更新对话框
* @param context
* @param msg
* @param listener
* @param cancel
* @return
*/
public static Dialog canUpdate(Context context,
String msg,
DialogInterface.OnClickListener listener,
DialogInterface.OnCancelListener cancel){
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("版本更新");
builder.setMessage(msg);
builder.setCancelable(false);
builder.setPositiveButton("更新", listener);
builder.setNegativeButton("暂不更新", listener);
builder.setOnCancelListener(cancel);
return builder.create();
}
/**
* 版本获取异常对话框
* @param context
* @param msg
* @param listener
* @return
*/
public static Dialog errorDialog(Context context,
String msg,
DialogInterface.OnClickListener listener){
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("版本更新");
builder.setMessage(msg);
builder.setCancelable(false);
builder.setNegativeButton("确定", listener);
return builder.create();
}
/**
* 更新进度对话框
* @param context
* @return
*/
public static ProgressDialog durationDialog(Context context){
ProgressDialog dialog = new ProgressDialog(context);
dialog.setTitle("版本更新");
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setCancelable(false);
return dialog;
}
/**
* 设置系统对话框
* @param dialog
*/
public static void setType(Dialog dialog){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}else {
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
}
}
此类主要是声明一些弹窗。
第三个类是UpdateFile
package com.badao.appupdatedemo.update;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import androidx.core.content.FileProvider;
import com.badao.appupdatedemo.BuildConfig;
import com.badao.appupdatedemo.config.UpdateConfig;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class UpdateFile {
//异步校验网络文件 如果后台没有把文件长度传你可以使用这个判断
public static class CheckFile extends AsyncTask<String, Integer, Void> {
private File file;
private String fileUrl;
private Handler handler;
public CheckFile(File file, String fileUrl, Handler handler){
this.file = file;
Log.i("CheckFile-file","file="+file.toString());
this.fileUrl = fileUrl;
Log.i("CheckFile-fileUrl","fileUrl="+fileUrl.toString());
this.handler = handler;
}
@Override
protected Void doInBackground(String... strings) {
if (file.exists()){
if (verifyFile(fileUrl,file)){
//如果文件完整
handler.sendEmptyMessage(UpdateConfig.FILE_EXIST_INTACT);
}else {
//如果文件不完整则先删除现有文件,然后下载文件
if (!file.delete()) {
handler.sendEmptyMessage(UpdateConfig.FILE_DELETE_ERROR);
return null;
}
handler.sendEmptyMessage(UpdateConfig.FILE_NOT_INTACT);
}
}else {
handler.sendEmptyMessage(UpdateConfig.FILE_NOT_EXIST);
}
return null;
}
}
/**
* 校验网络文件
* @param mFile
* @param fileUrl
* @param handler
*/
public static void checkFile(File mFile, String fileUrl, Handler handler) {
Log.i("EricLog", "校验文件");
if (mFile.exists()){
if (verifyFile(fileUrl,mFile)){
//如果文件完整
handler.sendEmptyMessage(UpdateConfig.FILE_EXIST_INTACT);
}else {
//如果文件不完整则先删除现有文件,然后下载文件
if (!mFile.delete()) {
handler.sendEmptyMessage(UpdateConfig.FILE_DELETE_ERROR);
return;
}
handler.sendEmptyMessage(UpdateConfig.FILE_NOT_INTACT);
}
}else {
handler.sendEmptyMessage(UpdateConfig.FILE_NOT_EXIST);
}
}
/**
* 校验文件是否完整
* @param urlLoadPath
* @param file
* @return
*/
private static boolean verifyFile(String urlLoadPath, File file){
long length = file.length();//已下载的文件长度
long realLength = getFileLength(urlLoadPath);//网络获取的文件长度
Log.e("EricLog", "下载长度:" +length +"\t\t文件长度:" +realLength);
if (length != 0){
if (realLength != 0 && (realLength == length)){
return true;
}
}
return false;
}
/**
* 获取需要下载的文件长度
* @param url
* @return
*/
private static long getFileLength(String url) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response;
try {
response = client.newCall(request).execute();
if (response.isSuccessful()){
long length = response.body().contentLength();
response.body().close();
return length;
}
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}
//安装
public static void installApp(File file, Context context, Handler handler) {
if (!file.exists()) {
return;
}
// 跳转到新版本应用安装页面
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(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(context, BuildConfig.APPLICATION_ID+ ".fileProvider",file);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}catch (Throwable throwable){
Log.e("EricLog", "Error = " +throwable.getMessage());
handler.sendEmptyMessage(UpdateConfig.INSTALL_ERROR);
}
}
/**
* 异步网络文件下载并保存
*
*/
public static class DownloadAsync extends AsyncTask<String,Integer,Integer> {
private static final int DOWNLOAD_SUCCESS = 1;
private static final int DOWNLOAD_FAIL = 2;
private DownloadListener listener;
private int lastProgress;
private File file;
public DownloadAsync(File file, DownloadListener listener){
this.file = file;
this.listener = listener;
}
public interface DownloadListener{
void onProgress(int progress);
void onSuccess();
void onFail();
}
@Override
protected Integer doInBackground(String... strings) {
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
long contentLength = getFileLength(strings[0]);
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(strings[0])
.build();
Response response = client.newCall(request).execute();
if (response.code() == 200) {
inputStream = response.body().byteStream();
fileOutputStream = new FileOutputStream(file);
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = inputStream.read(b)) != -1) {
total += len;
fileOutputStream.write(b, 0, len);
//百分比的计算在这里
float pressent = (float) total / contentLength * 100;
int progress = (int) pressent;
publishProgress(progress);
}
response.body().close();
return DOWNLOAD_SUCCESS;
}else {
return DOWNLOAD_FAIL;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return DOWNLOAD_FAIL;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress > lastProgress){
listener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer integer) {
switch (integer){
case DOWNLOAD_SUCCESS:
listener.onSuccess();
break;
case DOWNLOAD_FAIL:
listener.onFail();
break;
default: break;
}
}
}
}
此类主要是用于校验文件、获取文件大小和下载与安装文件的一些方法
新建完这三个工具类之后,再在包下新建一个service目录,在此目录下新建一个Service
然后修改名字为UpdateService
点击Finish之后,会在AndroidManifest.xml中自动添加一个service
<service
android:name=".service.UpdateService"
android:enabled="true"
android:exported="true"></service>
然后修改UpdateService的代码
package com.badao.appupdatedemo.service;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.badao.appupdatedemo.bean.UpdateBean;
import com.badao.appupdatedemo.config.UpdateConfig;
import com.badao.appupdatedemo.update.UpdateCheck;
import com.badao.appupdatedemo.update.UpdateDialog;
import com.badao.appupdatedemo.update.UpdateFile;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public class UpdateService extends Service {
public static boolean isRunning = false;
public static String checkUrl = "http://你自己服务器的地址:8888/fzyscontrol/sys/version/getLastestVersion";
//当前版本
private int versionCode = -1;
//错误信息
private String error_msg = "";
//更新地址
private String updateUrl = "";
//更新描述
private String description = "";
//文件路径
private String filePath = "";
//文件名称
private String appName = "";
//目标文件
private File targetFile;
private static OkHttpClient client = getClient();
private Dialog mDialog;
private ProgressDialog mProgressDialog;
@SuppressLint("HandlerLeak")
private Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
int code = msg.what;
switch (code) {
case UpdateConfig.FILE_EXIST_INTACT:
Log.i("EricLog", "文件完整 需要安装");
UpdateFile.installApp(targetFile, getApplicationContext(), handler);
break;
case UpdateConfig.FILE_NOT_EXIST:
Log.i("EricLog", "文件不存在");
mProgressDialog = UpdateDialog.durationDialog(getApplicationContext());
//设置为系统对话框
UpdateDialog.setType(mProgressDialog);
mProgressDialog.show();
UpdateFile.DownloadAsync dFne = new UpdateFile.DownloadAsync(targetFile, listener);
dFne.execute(updateUrl);
break;
case UpdateConfig.FILE_DELETE_ERROR:
Log.i("EricLog", "文件删除异常");
error_msg = "文件删除出错了";
showDialog(DialogType.ERROR);
break;
case UpdateConfig.FILE_NOT_INTACT:
Log.i("EricLog", "文件不完整");
mProgressDialog = UpdateDialog.durationDialog(getApplicationContext());
UpdateDialog.setType(mProgressDialog);
mProgressDialog.show();
UpdateFile.DownloadAsync dFni = new UpdateFile.DownloadAsync(targetFile, listener);
dFni.execute(updateUrl);
break;
case UpdateConfig.DIALOG_MUST:
Log.i("EricLog", "弹出必须更新对话框");
showDialog(DialogType.MUST);
break;
case UpdateConfig.DIALOG_CAN:
Log.i("EricLog", "弹出可以更新对话框");
showDialog(DialogType.CAN);
break;
case UpdateConfig.CLIENT_INFO_ERROR:
Log.i("EricLog", "连接异常");
error_msg = "获取版本异常,请检查网络";
showDialog(DialogType.ERROR);
break;
case UpdateConfig.DOWNLOAD_ERROR:
Log.i("EricLog", "下载异常");
disProgress(false);
error_msg = "下载出错了,请重新下载";
showDialog(DialogType.ERROR);
break;
case UpdateConfig.INSTALL_ERROR:
Log.i("EricLog", "安装异常");
error_msg = "安装出错了,请手动安装";
showDialog(DialogType.ERROR);
break;
}
}
};
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.i("EricLog", "服务启动");
filePath = Environment.getExternalStorageDirectory()
.getAbsolutePath() + File.separator + "MyApp/";
Log.i("onCreate-filePath", "filePath="+filePath);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!isRunning) {
isRunning = true;
versionCode = getVersionCode(getApplicationContext());
String versionName = getVersionName(getApplicationContext());
if (versionCode == -1 || TextUtils.isEmpty(versionName)){
handler.sendEmptyMessage(UpdateConfig.CLIENT_INFO_ERROR);
}else {
if (UpdateCheck.isNetWorkConnected(getApplicationContext())
&& UpdateCheck.isNetWorkAvailable(getApplicationContext())) {
UpdateCheck.checkVersion(client,
checkUrl,
versionName,
getPackageName(),
result);
} else {
handler.sendEmptyMessage(UpdateConfig.CLIENT_INFO_ERROR);
}
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("EricLog", "服务销毁");
isRunning = false;
}
/**
* 获取版本号
* @return
*/
public static int getVersionCode(Context context){
PackageManager manager = context.getPackageManager();
PackageInfo info;
try {
info = manager.getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
return -1;
}
}
public static String getVersionName(Context context){
PackageManager manager = context.getPackageManager();
PackageInfo info;
try {
info = manager.getPackageInfo(context.getPackageName(), 0);
return info.versionName;
} catch (PackageManager.NameNotFoundException e) {
return "";
}
}
private static OkHttpClient getClient(){
return new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.build();
}
/**
* 检查是否需要更新 按需求写就行
*/
private void checkUpdate(UpdateBean info){
//目标版本号
int targetCode = info.getData().getVersionNum();
//是否强制更新
//int isCompulsory = info.getData().getIsCompulsory();
updateUrl = info.getData().getDownloadLink();
Log.i("updateUrl","updateUrl="+updateUrl);
appName = info.getData().getAppName();
description = info.getData().getUpdateInstructions();
//这里设置下载apk后存储在本地的目标路径的文件
//这里使用的是临时文件的路径
///data/data/com.badao.appupdatedemo/cache/badao79427110100998067.apk
String mPath = filePath + appName + ".apk";
try {
File tempPath = File.createTempFile("badao", ".apk");
mPath = tempPath.getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
}
Log.i("mPath","mPath="+mPath);
Log.i("EricLog", "目标文件:" + mPath);
targetFile = new File(mPath);
if (TextUtils.isEmpty(description)){
description = "修复了若干bug";
}
if (versionCode == targetCode){
stopSelf();
}else {
handler.sendEmptyMessage(UpdateConfig.DIALOG_CAN);
//其他的一些情况就按需求写吧
}
}
/**
* 应该展示哪个对话框
* @param type
*/
private void showDialog(DialogType type){
if (type == DialogType.MUST){
mDialog = UpdateDialog.mustUpdate(getApplicationContext(),
description,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UpdateFile.CheckFile checkFile = new UpdateFile.CheckFile(
targetFile, updateUrl, handler);
checkFile.execute();
}
});
}else if (type == DialogType.CAN){
mDialog = UpdateDialog.canUpdate(getApplicationContext(),
description,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE){
UpdateFile.CheckFile checkFile = new UpdateFile.CheckFile(
targetFile, updateUrl, handler);
checkFile.execute();
}else if (which == DialogInterface.BUTTON_NEGATIVE){
dismiss();
}
}
},
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dismiss();
}
});
}else if (type == DialogType.ERROR){
mDialog = UpdateDialog.errorDialog(getApplicationContext(),
error_msg,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dismiss();
}
});
}
UpdateDialog.setType(mDialog);
mDialog.show();
}
private void dismiss(){
if (mDialog != null && mDialog.isShowing()){
stopSelf();
mDialog.dismiss();
}
}
private void disProgress(boolean finishService){
if (finishService){
stopSelf();
}
if (mProgressDialog != null && mProgressDialog.isShowing()){
mProgressDialog.dismiss();
}
}
private UpdateCheck.CheckVersionResult result = new UpdateCheck.CheckVersionResult() {
@Override
public void success(UpdateBean info) {
if (info != null && info.getData() != null) {
checkUpdate(info);
}else {
handler.sendEmptyMessage(UpdateConfig.CLIENT_INFO_ERROR);
}
}
@Override
public void fail(int code) {
Log.e("EricLog", "Code = " + code);
handler.sendEmptyMessage(UpdateConfig.CLIENT_INFO_ERROR);
}
@Override
public void error(Throwable throwable) {
Log.e("EricLog", "Error = " +throwable.getMessage());
handler.sendEmptyMessage(UpdateConfig.CLIENT_INFO_ERROR);
}
};
private UpdateFile.DownloadAsync.DownloadListener listener =
new UpdateFile.DownloadAsync.DownloadListener() {
@Override
public void onProgress(int progress) {
mProgressDialog.setProgress(progress);
}
@Override
public void onSuccess() {
disProgress(true);
UpdateFile.installApp(targetFile, getApplicationContext(), handler);
}
@Override
public void onFail() {
handler.sendEmptyMessage(UpdateConfig.DOWNLOAD_ERROR);
}
};
/**
* 要展示的对话框类型
*/
public enum DialogType{
MUST, CAN, ERROR
}
}
将此service的checkUrl修改为自己的服务器的ip和端口
此service中用到的将服务端的远程apk下载到本地的路径为临时文件路径,在data/data/包名下cache目录下
然后还需要在包下新建bean包,在此包下新建版本更新接口返回的json数据对应的实体类
后台检测更新的接口返回的json数据为
然后根据此json数据生成bean的方式参考如下
AndroidStudio中安装GsonFormat插件并根据json文件生成JavaBean:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/110426851
然后根据json数据生成的UpdateBean为
package com.badao.appupdatedemo.bean;
public class UpdateBean {
/**
* msg : 操作成功
* code : 200
* data : {"id":9,"appName":"测试1","versionNum":16,"downloadLink":"D://fzys/file/2020/11/30/8a4ac525-8c28-45be-834b-6db0889b7aa9.jpg","updateInstructions":"测试11122","updateTime":"2020-11-30T16:51:09.000+08:00"}
*/
private String msg;
private int code;
private DataBean data;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public DataBean getData() {
return data;
}
public void setData(DataBean data) {
this.data = data;
}
public static class DataBean {
/**
* id : 9
* appName : 测试1
* versionNum : 16
* downloadLink : D://fzys/file/2020/11/30/8a4ac525-8c28-45be-834b-6db0889b7aa9.jpg
* updateInstructions : 测试11122
* updateTime : 2020-11-30T16:51:09.000+08:00
*/
private int id;
private String appName;
private int versionNum;
private String downloadLink;
private String updateInstructions;
private String updateTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public int getVersionNum() {
return versionNum;
}
public void setVersionNum(int versionNum) {
this.versionNum = versionNum;
}
public String getDownloadLink() {
return downloadLink;
}
public void setDownloadLink(String downloadLink) {
this.downloadLink = downloadLink;
}
public String getUpdateInstructions() {
return updateInstructions;
}
public void setUpdateInstructions(String updateInstructions) {
this.updateInstructions = updateInstructions;
}
public String getUpdateTime() {
return updateTime;
}
public void setUpdateTime(String updateTime) {
this.updateTime = updateTime;
}
}
}
最后项目的总目录为
然后打开MainActivity
在OnCreate方法中进行检测是否已经开启了悬浮窗的权限,如果已经开启了悬浮窗的权限
则直接通过
startService(new Intent(this,UpdateService.class));
的方式启动service进行是否更新的检测。
否则的话会通过
//否则跳转到开启悬浮窗的设置页面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M){
intent.setData(Uri.parse("package:" + getPackageName()));
}
//指定一个请求码,这样在重写的onActivityResult就能筛选到设置悬浮窗之后的结果
startActivityForResult(intent, 212);
}
跳转到开启悬浮窗权限的页面,并指定一个请求码为212,然后在MainActivity中重写onActivityResult方法
就能通过请求码获取到跳转到开启悬浮窗页面的返回结果
如果已经开启了则直接检测更新,否则的话会弹窗提示
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//跟开启悬浮窗的请求码一致
if (requestCode == 212){
//如果开启了悬浮窗的权限
if (UpdateCheck.checkFloatPermission(this)){
//直接检测更新
startService(new Intent(this,UpdateService.class));
}else {
//否则弹窗提示
Toast.makeText(this, "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
}
}
}
MainActivity完整示例代码
package com.badao.appupdatedemo;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.widget.Toast;
import com.badao.appupdatedemo.service.UpdateService;
import com.badao.appupdatedemo.update.UpdateCheck;
public class MainActivity extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//跟开启悬浮窗的请求码一致
if (requestCode == 212){
//如果开启了悬浮窗的权限
if (UpdateCheck.checkFloatPermission(this)){
//直接检测更新
startService(new Intent(this,UpdateService.class));
}else {
//否则弹窗提示
Toast.makeText(this, "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//应用启动后会先走此方法
//如果已经开启了悬浮窗的权限
if (UpdateCheck.checkFloatPermission(this))
{
//直接启动检测更新的service
startService(new Intent(this,UpdateService.class));
}else
{
//否则跳转到开启悬浮窗的设置页面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M){
intent.setData(Uri.parse("package:" + getPackageName()));
}
//指定一个请求码,这样在重写的onActivityResult就能筛选到设置悬浮窗之后的结果
startActivityForResult(intent, 212);
}
}
}
安卓端完整示例代码下载
https://download.csdn.net/download/BADAO_LIUMANG_QIZHI/13218755
然后就是搭建后台服务端。
前后端分离的方式搭建后台服务
这里使用了若依的前后端分离的版的框架搭建的后台服务。
若依前后端分离版手把手教你本地搭建环境并运行项目:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662
若依微服务版手把手教你本地搭建环境并运行前后端项目:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/109363303
上面是基于SpringBoot搭建的前后端分离的项目
下面是基于SpringCloud搭建的微服务版的项目
最终都是搭建一个前端项目和后台服务接口项目。
这里以后台微服务版的版的基础上去搭建后台接口
首先是新建通用的文件上传和下载的接口,注意此接口一定要做好权限验证与安全管理
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.system.utils.FileUtils;
import com.ruoyi.system.utils.UploadUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.ibatis.annotations.Param;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
/**
* 通用文件上传下载接口
* @author Chrisf
*/
@RestController
@RequestMapping("file")
@Api(tags = "文件通用上传下载")
public class FileController {
/**
* 上传文件
*
* @param file
* @return
*/
@PostMapping("upload")
@ApiOperation("上传")
public AjaxResult head_portrait(@Param("file") MultipartFile file) {
AjaxResult ajaxResult = AjaxResult.success();
try {
//文件夹路径
String path = "D://fzys/file/" + DateUtils.datePath() + "/";
FileUtils.check_folder(path);
// 上传后的文件名称
String auth_file_name = UploadUtil.save_file(file, path);
ajaxResult.put("code", 200);
ajaxResult.put("message", "成功");
ajaxResult.put("fileName", path + auth_file_name);
} catch (IOException e) {
ajaxResult.put("code", 400);
ajaxResult.put("message", "上传失败");
ajaxResult.put("head_portrait", null);
e.printStackTrace();
}
return ajaxResult;
}
/**
* 下载文件
* @param fileName
* @param response
* @throws IOException
*/
@GetMapping("download")
@ApiOperation("下载")
public void down_file(String fileName, HttpServletResponse response) throws IOException {
File file = new File(fileName);
// 清空response
response.reset();
// 设置response的Header 通知浏览器 已下载的方式打开文件 防止文本图片预览
response.addHeader("Content-Disposition",
"attachment;filename=" + new String(fileName.getBytes("gbk"), "iso-8859-1")); // 转码之后下载的文件不会出现中文乱码
response.addHeader("Content-Length", "" + file.length());
// 以流的形式下载文件
InputStream fis = new BufferedInputStream(new FileInputStream(fileName));
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
toClient.write(buffer);
toClient.flush();
toClient.close();
}
}
在这两个接口中用到的工具类方法有UploadUtil.save_file
public static String save_file(MultipartFile file, String path) throws IOException {
String filename=file.getOriginalFilename();
String suffix = filename.substring(filename.indexOf("."));
filename = UUID.randomUUID().toString() + suffix;
File file_temp=new File(path,filename);
if (!file_temp.getParentFile().exists()) {
file_temp.getParentFile().mkdir();
}
if (file_temp.exists()) {
file_temp.delete();
}
file_temp.createNewFile();
file.transferTo(file_temp);
return file_temp.getName();
}
和工具类FileUtils.check_folder
public static void check_folder(String path) {
File dir = new File(path);
// 判断文件夹是否存在
if (dir.isDirectory()) {
} else {
dir.mkdirs();
}
}
以及DateUtils.datePath(),是用来生成日期文件目录的方法
/**
* 日期路径 即年/月/日 如2018/08/08
*/
public static final String datePath()
{
Date now = new Date();
return DateFormatUtils.format(now, "yyyy/MM/dd");
}
通用的文件上传与下载的接口做好之后就是版本检测更新的接口
首先我们需要设计一个数据库来用来存储app的版本信息
然后使用若依自带的代码生成工具去生成前后端的代码,前端代码一会也要修改,这里先找到生成的Controller
@RestController
@RequestMapping("/sys/version")
@Api(tags = "APP版本管理")
public class SysAppVersionController extends BaseController {
@Autowired
private ISysAppVersionService sysAppVersionService;
@Autowired
private SysAppVersionMapper sysAppVersionMapper;
/**
* 查询版本更新记录列表
* @return
*/
@GetMapping("/getList")
@ApiOperation("查询版本更新记录列表")
public TableDataInfo getList(){
startPage();
List<SysAppVersion> list = sysAppVersionService.getList();
return getDataTable(list);
}
/**
* 新增版本更新记录
*/
@PostMapping("/add")
@ApiOperation("新增版本更新记录")
public AjaxResult addAppVersion(@RequestBody SysAppVersion sysAppVersion){
if (StringUtils.isNull(sysAppVersion.getVersionNum()) || StringUtils.isEmpty(sysAppVersion.getDownloadLink())){
return AjaxResult.error(400, "缺少必要参数");
}
return sysAppVersionService.insertSysAppVersion(sysAppVersion);
}
/**
* 修改版本更新记录
*/
@PostMapping("/edit")
@ApiOperation("修改版本更新记录")
public AjaxResult editAppVersion(@RequestBody SysAppVersion sysAppVersion){
if (sysAppVersion.getId() == null){
return AjaxResult.error(400, "缺少必要参数");
}
return sysAppVersionService.updateSysAppVersion(sysAppVersion);
}
@GetMapping("/getLastestVersion")
@ApiOperation("获取最新版本信息")
public AjaxResult getLastestVersion(){
SysAppVersion sysAppVersion = sysAppVersionMapper.getLast();
return AjaxResult.success(sysAppVersion);
}
}
下面调用的service和mapper都是生成的对单表的进行增删改的代码
这里主要是添加一个检测版本更新的接口,即上面的获取最新版本信息。
其最终执行mapper方法为
<!--查询最新的更新记录-->
<select id="getLast" resultMap="SysAppVersionResult">
<include refid="selectSysAppVersionVo"></include>
order by version_num desc
limit 1
</select>
此接口从数据库中查询出来版本号最高的那条记录并将此记录的相关信息返回给app端
app获取到版本好之后跟自己的当前的版本的版本号进行对比,如果高于当前版本则提示更新。
app端版本号的设置位置在
此接口的地址就是对应安卓端UpdateService中的checkUrl的地址。
然后就是修改前端页面,将vue页面修改如下
<template>
<div class="app-container">
<el-row :gutter="10" class="mb8">
<el-row class="btn_box">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-row>
<el-table
:data="tableData"
:height="tableHeight"
:loading="listLoding"
style="width: 100%">
<el-table-column
prop="appName"
label="应用名称"
width="180">
</el-table-column>
<el-table-column
prop="versionNum"
label="版本号"
width="180">
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间">
<template slot-scope='scope'>
{{ scope.row.updateTime | dataFormat }}
</template>
</el-table-column>
<el-table-column
prop="updateInstructions"
label="更新说明">
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="listData"
/>
<!-- 添加或修改通讯录对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="应用名称" prop="appName" v-if="editStatus ">
<el-input v-model="form.appName" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="版本号" prop="versionNum"
:rules="[
{ required: true, message: '版本号不能为空'},
{ type: 'number', message: '版本号必须为数字值'}
]"
v-if="editStatus"
>
<el-input v-model.number="form.versionNum" placeholder="请输入版本号" />
</el-form-item>
<el-form-item label="更新说明" prop="updateInstructions">
<el-input v-model="form.updateInstructions" placeholder="请输入更新说明" />
</el-form-item>
</el-form>
<el-col v-if="editStatus" class="upload_box">
<el-upload
:headers="headers"
:action="url"
:multiple="false"
:file-list="fileList"
:on-remove="fileRemove"
:on-success="uploadSuccess"
:on-error="uploadError"
:on-progress="uploadProgress"
:limit="1"
:on-exceed="beyond"
>
<el-button size="small">
上传
<i class="el-icon-upload el-icon--right"></i>
</el-button>
</el-upload>
</el-col>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm" :loading="btnLoding" v-show="editStatus">确 定</el-button>
<el-button type="primary" @click="editSubmit" v-show="!editStatus">确 定</el-button><!--修改按钮 -->
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</el-row>
</div>
</template>
<script>
import {upload,getList,add,edit} from "@/api/tool/edition.js"
import {getToken} from '@/utils/auth'
export default {
components: {},
props: {},
data() {
return {
//列表数据
tableData:[],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
},
title:"",
// 表单参数
form: {
id:null,
appName:null,
downloadLink:null,
updateInstructions:null,
versionNum:null
},
// 文件列表
fileList:[],
// 表格自适应高度
tableHeight: document.body.clientHeight - 230,
// 是否显示弹出层
open:false,
// 总条数
total: 0,
//返回的文件url
fileUrl: '',
// 文件列表
// 上传按钮闸口
btnLoding:false,
// 列表加载动画
listLoding:true,
// 表单校验
rules: {
appName: [
{ required: true, message: "应用名称不能为空", trigger: "blur" }
],
updateInstructions: [
{ required: true, message: "更新说明不能为空", trigger: "blur" }
],
},
// 修改字段显隐
editStatus:true,
// 修改id
editId:null,
progess: 0,
// 请求头
headers:{Authorization:"Bearer" +' ' + getToken()},
// 上传地址
url:process.env.VUE_APP_BASE_API + '/system/file/upload'
};
},
watch: {},
computed: {},
methods: {
listData(){
getList(this.queryParams).then(res=>{
this.tableData = res.rows;
this.total = res.total;
this.listLoding = false;
})
},
// 显现新增弹窗
handleAdd(){
this.title = '新增';
this.open = true;
this.editStatus = true
if(this.$refs['form']){
this.$refs['form'].resetFields();
}
this.fileList = [];
this.btnLoding = false;
},
// 文件上传成功
uploadSuccess(res,file,fileList){
console.log(fileList)
let fileParam={
name:null,
url:null
}
this.btnLoding = false;
this.form.downloadLink = res.fileName;
fileParam.url =res.fileName;
fileParam.name =res.name;
this.fileList= fileList;
this.$message(res.msg);
},
// 文件上传失败
uploadError(err){
this.btnLoding = false;
this.$message.error(res.msg);
},
// 上传中
uploadProgress(e){
this.btnLoding = true;
console.log(e,'上传中')
},
beyond(file, fileList){
this.$message({
message: '最多上传一个文件',
type: 'warning'
});
},
// 移除选择的文件
fileRemove(file, fileList) {
this.btnLoding = false;
console.log(file)
this.fileList = [];
this.form.downloadLink = null;
},
// 新增
submitForm(){
this.$refs["form"].validate(valid => {
if (valid) {
// console.log(this.form.fileName)
if(!this.form.downloadLink){
this.$notify({
title: '警告',
message: '请上传文件后在进行提交',
type: 'warning'
});
}else{
add(this.form).then(res =>{
if(res.code == 200){
this.$message(res.msg);
this.$refs['form'].resetFields();
this.fileList = [];
this.open = false;
this.listData();
}else{
this.$message.error(res.msg);
}
})
}
}
});
},
// 修改
handleUpdate(row){
this.editStatus = false;
this.title = '修改';
this.open = true;
this.form.updateInstructions = row.updateInstructions;
this.form.id = row.id;
},
// 修改提交
editSubmit(){
this.$refs["form"].validate(valid => {
if (valid) {
edit(this.form).then(res=>{
if(res.code == 200){
this.$message(res.msg);
this.$refs['form'].resetFields();
this.open = false;
this.listData();
}else{
this.$message.error(res.msg);
}
})
}
})
},
format(percentage) {
return percentage === 100 ? '上传完成' : `${percentage}%`;
},
cancel(){
this.open = false;
this.$refs['form'].resetFields();
this.fileList = [];
},
},
created() {
this.listData();
},
mounted() {}
};
</script>
<style lang="scss" scoped>
.upload_box{
min-height: 80px;
padding-bottom: 10px;
}
.btn_box{
margin-bottom: 20px;
}
</style>
除了自动生成的主要修改新增的页面,添加一个apk安装包上传的控件el-upload
调用的是前面的通用上传接口,会将apk安装包上传到服务器上并将在服务器上的地址返回,然后在点击新增页面的确认按钮后将
安装包地址一并提交到后台的新增接口,后台将其存储到数据库。
vue页面调用的js方法为
import request from '@/utils/request'
//上传文件
export function upload(query) {
return request({
url: '/system/file/upload',
method: 'post',
data:query
})
}
//查询列表
export function getList(query){
return request({
url:'/fzyscontrol/sys/version/getList',
method:'get',
params: query
})
}
//新增版本记录
export function add(query){
return request({
url:'/fzyscontrol/sys/version/add',
method:'post',
data: query
})
}
// 修改版本记录
export function edit(query){
return request({
url:'/fzyscontrol/sys/version/edit',
method:'post',
data: query
})
}
然后新增完一个版本之后就会在数据库中新增一个高版本的记录
就能实现后台将新版本的apk传递到后台,然后app在启动后会查询最新版本的信息,如果高于当前版本则会将apk下载与安装
然后点击更新,就会下载安装包并安装
本文地址:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/110524943
上一篇: IOS自动化第一篇:环境安装
下一篇: JSP中读文件和写文件的例子