Commit 9f24575a by pye52

Merge branch 'fea-move_to_worker' into develop

parents 8c66dd9b 168208bd
...@@ -125,5 +125,4 @@ dependencies { ...@@ -125,5 +125,4 @@ dependencies {
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.liulishuo.filedownloader:library:1.7.7'
} }
package com.bgycc.smartcanteen.activity; package com.bgycc.smartcanteen.activity;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
...@@ -24,10 +31,11 @@ import android.widget.TextView; ...@@ -24,10 +31,11 @@ import android.widget.TextView;
import com.bgycc.smartcanteen.BuildConfig; import com.bgycc.smartcanteen.BuildConfig;
import com.bgycc.smartcanteen.Injection; import com.bgycc.smartcanteen.Injection;
import com.bgycc.smartcanteen.R; import com.bgycc.smartcanteen.R;
import com.bgycc.smartcanteen.command.CommandHelper;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.PayData; import com.bgycc.smartcanteen.entity.PayData;
import com.bgycc.smartcanteen.executor.SCTaskExecutor; import com.bgycc.smartcanteen.executor.SCTaskExecutor;
import com.bgycc.smartcanteen.socket.SCWebSocketClient; import com.bgycc.smartcanteen.socket.SCWebSocketClient;
import com.bgycc.smartcanteen.state.CommandState;
import com.bgycc.smartcanteen.state.ConnectState; import com.bgycc.smartcanteen.state.ConnectState;
import com.bgycc.smartcanteen.state.PayOfflineState; import com.bgycc.smartcanteen.state.PayOfflineState;
import com.bgycc.smartcanteen.state.PayOnlineState; import com.bgycc.smartcanteen.state.PayOnlineState;
...@@ -35,7 +43,7 @@ import com.bgycc.smartcanteen.state.QRCodeState; ...@@ -35,7 +43,7 @@ import com.bgycc.smartcanteen.state.QRCodeState;
import com.bgycc.smartcanteen.utils.NetworkUtils; import com.bgycc.smartcanteen.utils.NetworkUtils;
import com.bgycc.smartcanteen.utils.SmartCanteenUtils; import com.bgycc.smartcanteen.utils.SmartCanteenUtils;
import com.bgycc.smartcanteen.utils.TTSHelper; import com.bgycc.smartcanteen.utils.TTSHelper;
import com.bgycc.smartcanteen.utils.WorkerUtils; import com.bgycc.smartcanteen.utils.MonitorUtils;
import com.bgycc.smartcanteen.viewModel.CommandViewModel; import com.bgycc.smartcanteen.viewModel.CommandViewModel;
import com.bgycc.smartcanteen.viewModel.PayOfflineViewModel; import com.bgycc.smartcanteen.viewModel.PayOfflineViewModel;
import com.bgycc.smartcanteen.viewModel.PayOnlineViewModel; import com.bgycc.smartcanteen.viewModel.PayOnlineViewModel;
...@@ -77,6 +85,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -77,6 +85,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private TextView message; private TextView message;
private AudioManager audioManager; private AudioManager audioManager;
private WorkManager workManager;
private Handler handler = new Handler(); private Handler handler = new Handler();
private SimpleDateFormat payDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()); private SimpleDateFormat payDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
private SimpleDateFormat socketConnectedTimeDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); private SimpleDateFormat socketConnectedTimeDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
...@@ -110,6 +119,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -110,6 +119,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
workManager = WorkManager.getInstance(this);
ViewModelFactory factory = Injection.injectFactory(deviceSN); ViewModelFactory factory = Injection.injectFactory(deviceSN);
ViewModelProvider provider = new ViewModelProvider(this, factory); ViewModelProvider provider = new ViewModelProvider(this, factory);
payOnlineViewModel = provider.get(PayOnlineViewModel.class); payOnlineViewModel = provider.get(PayOnlineViewModel.class);
...@@ -243,23 +253,86 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -243,23 +253,86 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
}); });
commandViewModel.getCommandStateEvent().observe(this, event -> { commandViewModel.getCommandTask().observe(this, new Observer<Command>() {
switch (event.getState()) { private static final String WORKER_NAME = "smartcanteen_command";
case CommandState.IDLE: private LiveData<WorkInfo> runningLiveData;
settingLayout.animate().setDuration(300).alpha(0f); private Command runningCommand;
break; private OneTimeWorkRequest runningRequest;
case CommandState.WAIT:
settingText.setText(event.getMessage()); @Override
settingLayout.animate().setDuration(300).alpha(1f); public void onChanged(Command command) {
break; // 若为debug模式切换命令,则直接执行即可
case CommandState.SUCCESS: if (CommandHelper.isLogConfig(command)) {
case CommandState.FAILED:
settingText.setText(event.getMessage());
settingLayout.animate().setDuration(300).alpha(1f);
break;
case CommandState.TOGGLE_DEBUG:
toggleDebugLayout(); toggleDebugLayout();
break; commandViewModel.commandFinish(command);
return;
}
// 检查当前是否有在执行的任务
if (runningCommand != null) {
// 若已经有在执行的任务,则跳过该次响应
return;
}
reset();
runningCommand = command;
runningRequest = commandViewModel.getCommandWorker(command);
workManager.beginUniqueWork(WORKER_NAME, ExistingWorkPolicy.REPLACE, runningRequest)
.enqueue();
runningLiveData = workManager.getWorkInfoByIdLiveData(runningRequest.getId());
runningLiveData.observe(MainActivity.this, workInfoObserver);
}
private Observer<WorkInfo> workInfoObserver = new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
if (workInfo == null) {
settingLayout.animate().setDuration(50).alpha(0f);
return;
}
LogUtils.d(TAG, "\n任务: " + (runningCommand != null ? runningCommand.toString() : "null") +
"\n进度: " + workInfo.toString());
WorkInfo.State state = workInfo.getState();
if (state.isFinished()) {
Data data = workInfo.getOutputData();
switch (state) {
case SUCCEEDED:
case FAILED:
String message = CommandHelper.getMessage(data);
settingText.setText(message);
settingLayout.animate().setDuration(50).alpha(1f);
break;
}
// 任务完成后,隐藏提示语
settingLayout.animate()
.setStartDelay(50)
.setDuration(1300)
.alpha(0f);
// 通知数据库更新命令的执行状态
if (runningCommand != null) {
commandViewModel.commandFinish(runningCommand);
}
// 重置所有变量,使下次命令能成功执行
reset();
return;
}
if (workInfo.getState() == WorkInfo.State.RUNNING) {
Data data = workInfo.getProgress();
String message = CommandHelper.getMessage(data);
settingText.setText(message);
settingLayout.animate().setDuration(300).alpha(1f);
return;
}
}
};
private void reset() {
runningCommand = null;
runningRequest = null;
if (runningLiveData != null) {
runningLiveData.removeObserver(workInfoObserver);
runningLiveData = null;
}
} }
}); });
...@@ -270,8 +343,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -270,8 +343,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
commandViewModel.initialize(); commandViewModel.initialize();
handler.post(updateTimeRunnable); handler.post(updateTimeRunnable);
WorkerUtils.startLogFilesMonitor(this); MonitorUtils.startLogFilesMonitor(this);
WorkerUtils.startDatabaseMonitor(this); MonitorUtils.startDatabaseMonitor(this);
SCWebSocketClient.getInstance().tryConnect(); SCWebSocketClient.getInstance().tryConnect();
} }
...@@ -335,8 +408,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -335,8 +408,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
TTSHelper.release(); TTSHelper.release();
SCWebSocketClient.getInstance().realClose(); SCWebSocketClient.getInstance().realClose();
SCTaskExecutor.getInstance().quit(); SCTaskExecutor.getInstance().quit();
WorkerUtils.stopDatabaseMonitor(this); MonitorUtils.stopDatabaseMonitor(this);
WorkerUtils.stopLogFilesMonitor(this); MonitorUtils.stopLogFilesMonitor(this);
} }
private void initViews() { private void initViews() {
......
package com.bgycc.smartcanteen.command;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.google.gson.Gson;
public abstract class CommandHandler {
protected Command command;
protected Gson gson;
protected CommandProgressCallback commandProgressCallback;
public CommandHandler(Command command, Gson gson, CommandProgressCallback commandProgressCallback) {
this.command = command;
this.gson = gson;
this.commandProgressCallback = commandProgressCallback;
}
public abstract CommandResponse run() throws Exception;
void idle(String message, int progress) {
commandProgressCallback.idle(message, progress);
}
void wait(String message, int progress) {
commandProgressCallback.wait(message, progress);
}
void success(String message, int progress) {
commandProgressCallback.success(message, progress);
}
void failed(String message, int progress) {
commandProgressCallback.failed(message, progress);
}
CommandResponse failedResult(String reason) {
return CommandResponse.failed(reason);
}
CommandResponse successResult(String reason) {
return CommandResponse.success(reason);
}
}
package com.bgycc.smartcanteen.command;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import androidx.work.OneTimeWorkRequest;
import com.bgycc.smartcanteen.entity.Command;
import com.google.gson.Gson;
import java.util.HashSet;
import java.util.Set;
public class CommandHelper {
private static final String LOG_UPLOAD = "LOG_PULL";
private static final String APP_UPDATE = "CONFIG_UPDATE";
private static final String CONFIG_WIFI = "CONFIG_WIFI";
private static final String CONFIG_LOG = "CONFIG_LOG";
// 设备指令白名单
private static final Set<String> COMMAND_WHITELIST = new HashSet<String>() {
{
add(LOG_UPLOAD);
add(APP_UPDATE);
add(CONFIG_WIFI);
add(CONFIG_LOG);
}
};
public static String getMessage(Data data) {
return CommandWorker.getMessage(data);
}
public static boolean isLogConfig(Command command) {
return command.getAction().equals(CONFIG_LOG);
}
/**
* 根据action检查是否为指令类型
*/
public static boolean isCommand(String action) {
return COMMAND_WHITELIST.contains(action);
}
/**
* 该条指令是否在数据库中只能存在一条"未完成"状态的数据 <br/>
*
* @param command 指令
* @return <br/>
* true => 数据库已有"未完成"状态的该类指令,不应该重复插入 <br/>
* false => 数据库中该类指令都"已完成"或没有该类指令,可插入新的指令 <br/>
*/
public static boolean oneAtTime(Command command) {
return command.getAction().equals(APP_UPDATE);
}
public static OneTimeWorkRequest createWorker(Gson gson, Command command, String deviceSN) {
OneTimeWorkRequest worker = null;
Class<? extends ListenableWorker> workerClass = null;
switch (command.getAction()) {
case LOG_UPLOAD:
workerClass = LogCommandWorker.class;
break;
case APP_UPDATE:
workerClass = UpdateCommandWorker.class;
break;
case CONFIG_WIFI:
workerClass = WifiConfigCommandWorker.class;
break;
}
if (workerClass != null) {
Data inputData = CommandWorker.createBaseData(gson.toJson(command), deviceSN);
worker = new OneTimeWorkRequest.Builder(workerClass)
.setInputData(inputData)
.addTag(command.getAction())
.build();
}
return worker;
}
}
package com.bgycc.smartcanteen.command;
public interface CommandProgressCallback {
void idle(String message, int progress);
void wait(String message, int progress);
void success(String message, int progress);
void failed(String message, int progress);
}
package com.bgycc.smartcanteen.command;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.Injection;
import com.bgycc.smartcanteen.entity.Command;
import com.google.gson.Gson;
public abstract class CommandWorker extends Worker {
private static final String INPUT_DEVICESN = "input_devcesn";
private static final String INPUT_COMMAND = "input_command";
private static final String OUTPUT_MESSAGE = "output_message";
protected Gson gson;
protected Command command;
protected String deviceSN;
public static Data createBaseData(String jsonCommand, String deviceSN) {
return new Data.Builder()
.putString(INPUT_DEVICESN, deviceSN)
.putString(INPUT_COMMAND, jsonCommand)
.build();
}
public static String getMessage(Data data) {
return data.getString(OUTPUT_MESSAGE);
}
public CommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
gson = Injection.provideGson();
deviceSN = getInputData().getString(INPUT_DEVICESN);
command = gson.fromJson(getInputData().getString(INPUT_COMMAND), Command.class);
}
protected void inProgress(String message) {
Data data = new Data.Builder()
.putString(OUTPUT_MESSAGE, message)
.build();
setProgressAsync(data);
}
protected Result success() {
return Result.success();
}
protected Result success(String message) {
Data data = new Data.Builder()
.putString(OUTPUT_MESSAGE, message)
.build();
return Result.success(data);
}
protected Result failed() {
return Result.failure();
}
protected Result failed(String message) {
Data data = new Data.Builder()
.putString(OUTPUT_MESSAGE, message)
.build();
return Result.failure(data);
}
}
package com.bgycc.smartcanteen.command; package com.bgycc.smartcanteen.command;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.api.SCApi; import com.bgycc.smartcanteen.api.SCApi;
import com.bgycc.smartcanteen.api.SCRetrofit; import com.bgycc.smartcanteen.api.SCRetrofit;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandLog; import com.bgycc.smartcanteen.entity.CommandLog;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.blankj.utilcode.util.FileUtils; import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.LogUtils; import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils; import com.blankj.utilcode.util.PathUtils;
import com.blankj.utilcode.util.ZipUtils; import com.blankj.utilcode.util.ZipUtils;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
...@@ -33,49 +35,39 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; ...@@ -33,49 +35,39 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/** /**
* 设备指令: 上传日志 * 设备指令: 上传日志
*/ */
public class LogCommandHandler extends CommandHandler { public class LogCommandWorker extends CommandWorker {
private static final String BOOT_LOG = "system"; private static final String BOOT_LOG = "system";
private static final String ZIP_FILE = "log.zip"; private static final String ZIP_FILE = "log.zip";
private static final String UPLOAD_DIR = "upload_log"; private static final String UPLOAD_DIR = "upload_log";
private static final long DEFAULT_DELAY = 1000; private static final long DEFAULT_DELAY = 1000;
private volatile boolean start = false;
private SimpleDateFormat parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); private SimpleDateFormat parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
private SimpleDateFormat nameFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); private SimpleDateFormat nameFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
private String deviceSN;
private CommandLog commandLog; private CommandLog commandLog;
private File uploadDir; private File uploadDir;
private Date startTime; private Date startTime;
private Date endTime; private Date endTime;
private static LogCommandHandler instance; public LogCommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
public static synchronized LogCommandHandler getInstance(Command command, Gson gson, String deviceSN, CommandProgressCallback callback) {
if (instance == null) {
instance = new LogCommandHandler(command, gson, deviceSN, callback);
} else {
if (!instance.start) {
instance.init(command, gson, deviceSN, callback);
}
}
return instance;
}
private void init(Command command, Gson gson, String deviceSN, CommandProgressCallback callback) {
this.command = command;
this.gson = gson;
this.commandProgressCallback = callback;
this.deviceSN = deviceSN;
this.commandLog = gson.fromJson(command.getData(), CommandLog.class); this.commandLog = gson.fromJson(command.getData(), CommandLog.class);
parseDate();
} }
private LogCommandHandler(Command command, Gson gson, String deviceSN, CommandProgressCallback callback) { @NonNull
super(command, gson, callback); @Override
this.deviceSN = deviceSN; public Result doWork() {
this.commandLog = gson.fromJson(command.getData(), CommandLog.class);
parseDate(); parseDate();
if (!checkLogCommand()) {
return failed("日志上传指令不符合规范");
}
File logFile = getZipLogs();
if (logFile != null) {
inProgress("上传压缩文件至服务器");
upload(logFile);
return success();
}
return failed();
} }
private void parseDate() { private void parseDate() {
...@@ -101,14 +93,17 @@ public class LogCommandHandler extends CommandHandler { ...@@ -101,14 +93,17 @@ public class LogCommandHandler extends CommandHandler {
} }
// 获取指定类型及日期的日志文件 // 获取指定类型及日期的日志文件
private File getZipLogs() throws InterruptedException { private File getZipLogs() {
CommandLog.CommandLogData data = commandLog.getData(); CommandLog.CommandLogData data = commandLog.getData();
File logDir = getLogDirByType(data.getLogType()); File logDir = getLogDirByType(data.getLogType());
wait("建立临时目录", 0); inProgress("建立临时目录");
uploadDir = tempDirInit(); uploadDir = tempDirInit();
Thread.sleep(DEFAULT_DELAY); try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
}
if (uploadDir == null) return null; if (uploadDir == null) return null;
if (!copyTargetFiles(logDir, uploadDir)) { if (!copyTargetFiles(logDir, uploadDir)) {
...@@ -116,10 +111,13 @@ public class LogCommandHandler extends CommandHandler { ...@@ -116,10 +111,13 @@ public class LogCommandHandler extends CommandHandler {
return null; return null;
} }
Thread.sleep(DEFAULT_DELAY); try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
}
File zip = new File(PathUtils.getExternalStoragePath() + File.separator + ZIP_FILE); File zip = new File(PathUtils.getExternalStoragePath() + File.separator + ZIP_FILE);
try { try {
wait("开始压缩", 40); inProgress("开始压缩");
ZipUtils.zipFile(uploadDir, zip); ZipUtils.zipFile(uploadDir, zip);
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "压缩日志文件失败: " + e.getMessage(), e); LogUtils.e(TAG, "压缩日志文件失败: " + e.getMessage(), e);
...@@ -164,7 +162,7 @@ public class LogCommandHandler extends CommandHandler { ...@@ -164,7 +162,7 @@ public class LogCommandHandler extends CommandHandler {
}; };
List<File> logFiles = FileUtils.listFilesInDirWithFilter(src, filter, false, null); List<File> logFiles = FileUtils.listFilesInDirWithFilter(src, filter, false, null);
boolean copyResult = true; boolean copyResult = true;
wait("筛选目标日志文件", 10); inProgress("筛选目标日志文件");
int count = 0; int count = 0;
File descFile; File descFile;
boolean tempCopyResult; boolean tempCopyResult;
...@@ -200,26 +198,4 @@ public class LogCommandHandler extends CommandHandler { ...@@ -200,26 +198,4 @@ public class LogCommandHandler extends CommandHandler {
FileUtils.delete(zip); FileUtils.delete(zip);
} }
} }
@Override
public synchronized CommandResponse run() throws InterruptedException {
if (start) {
LogUtils.w(TAG, "日志上传任务已启动");
return null;
}
start = true;
if (!checkLogCommand()) {
start = false;
return failedResult("日志上传指令不符合规范");
}
File logFile = getZipLogs();
if (logFile != null) {
wait("上传压缩文件至服务器", 60);
upload(logFile);
start = false;
return successResult("");
}
start = false;
return failedResult("");
}
} }
package com.bgycc.smartcanteen.command;
import com.bgycc.smartcanteen.BuildConfig;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.bgycc.smartcanteen.entity.CommandUpdate;
import com.bgycc.smartcanteen.utils.DeviceProxy;
import com.blankj.utilcode.util.AppUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils;
import com.blankj.utilcode.util.Utils;
import com.google.gson.Gson;
import com.liulishuo.filedownloader.BaseDownloadTask;
import com.liulishuo.filedownloader.FileDownloadListener;
import com.liulishuo.filedownloader.FileDownloadSampleListener;
import com.liulishuo.filedownloader.FileDownloader;
import java.io.File;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/**
* 设备指令: 更新 <br/>
* 更新任务处理为单例,避免多个更新任务同时进行浪费资源
*/
public class UpdateCommandHandler extends CommandHandler {
private static final String UPDATE_APK = "SmartCanteen-update.apk";
private static final String UPDATE_APK_PATH = PathUtils.getExternalStoragePath() + File.separator + UPDATE_APK;
private static final long DEFAULT_DELAY = 5 * 1000;
// 保证更新任务不会在同一时间内重复执行
private volatile boolean start = false;
private CommandUpdate commandUpdate;
private static UpdateCommandHandler instance;
public static synchronized UpdateCommandHandler getInstance(Command command, Gson gson, CommandProgressCallback callback) {
if (instance == null) {
instance = new UpdateCommandHandler(command, gson, callback);
} else {
if (!instance.start) {
instance.init(command, gson, callback);
}
}
return instance;
}
private UpdateCommandHandler(Command command, Gson gson, CommandProgressCallback callback) {
super(command, gson, callback);
this.commandUpdate = gson.fromJson(command.getData(), CommandUpdate.class);
}
private void init(Command command, Gson gson, CommandProgressCallback callback) {
this.command = command;
this.gson = gson;
this.commandProgressCallback = callback;
this.commandUpdate = gson.fromJson(command.getData(), CommandUpdate.class);
}
@Override
public synchronized CommandResponse run() {
if (start) {
LogUtils.w(TAG, "更新任务已启动");
return null;
}
start = true;
if (commandUpdate.getData() == null || commandUpdate.getData().getUrl() == null) {
LogUtils.d(TAG, "更新包地址异常: " + commandUpdate.toString());
return failedResult("更新包地址异常");
}
String url = commandUpdate.getData().getUrl();
FileDownloader.setup(Utils.getApp());
FileDownloader.getImpl()
.create(url)
.setPath(UPDATE_APK_PATH)
.setAutoRetryTimes(3)
.setSyncCallback(true)
.setForceReDownload(true)
.setListener(listener)
.start();
return successResult("开始下载");
}
private FileDownloadListener listener = new FileDownloadSampleListener() {
@Override
protected void started(BaseDownloadTask task) {
LogUtils.d(TAG, "更新包开始下载: " + task.getUrl());
UpdateCommandHandler.this.wait("开始下载", 0);
}
@Override
protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {
int per = (int) (soFarBytes * 1f / totalBytes * 100);
UpdateCommandHandler.this.wait("下载进度: " + per + "%", per);
}
@Override
protected void error(BaseDownloadTask task, Throwable e) {
LogUtils.e(TAG, "下载失败: " + e.getMessage(), e);
failed("下载失败", 0);
try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
} finally {
idle("", 0);
start = false;
}
}
@Override
protected void completed(BaseDownloadTask task) {
File updateApk = new File(UPDATE_APK_PATH);
AppUtils.AppInfo info = AppUtils.getApkInfo(updateApk);
LogUtils.d(TAG, "更新包下载成功,开始安装: " + (info == null ? "null" : info.getPackageName()));
if (info == null || !info.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
FileUtils.delete(updateApk);
idle("", 0);
start = false;
return;
}
if (info.getVersionCode() == BuildConfig.VERSION_CODE) {
FileUtils.delete(updateApk);
LogUtils.w(TAG, "当前版本: " + BuildConfig.VERSION_CODE + ", 安装包版本: " + info.getVersionCode());
} else if (info.getVersionCode() < BuildConfig.VERSION_CODE) {
FileUtils.delete(updateApk);
LogUtils.d(TAG, "不允许安装低版本");
failed("不允许安装低版本", 0);
} else {
if (DeviceProxy.updateApp(updateApk)) {
success("开始安装", 100);
} else {
failed("安装文件权限修改失败", 0);
}
}
try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
} finally {
idle("", 0);
start = false;
}
}
};
}
package com.bgycc.smartcanteen.command;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.BuildConfig;
import com.bgycc.smartcanteen.api.SCRetrofit;
import com.bgycc.smartcanteen.entity.CommandUpdate;
import com.bgycc.smartcanteen.utils.DeviceProxy;
import com.bgycc.smartcanteen.utils.TrustAllCerts;
import com.blankj.utilcode.util.AppUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils;
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;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/**
* 设备指令: 更新 <br/>
* 更新任务处理为单例,避免多个更新任务同时进行浪费资源
*/
public class UpdateCommandWorker extends CommandWorker {
private static final String UPDATE_APK = "SmartCanteen-update.apk";
private static final String UPDATE_APK_PATH = PathUtils.getExternalStoragePath();
private static final int BUFFER_SIZE = 8 * 1024;
private static final long DEFAULT_DELAY = 3 * 1000;
// 保证更新任务不会在同一时间内重复执行
private CommandUpdate commandUpdate;
public UpdateCommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
commandUpdate = gson.fromJson(command.getData(), CommandUpdate.class);
}
@NonNull
@Override
public Result doWork() {
LogUtils.i(TAG, "开始执行更新任务");
if (commandUpdate.getData() == null || commandUpdate.getData().getUrl() == null) {
LogUtils.d(TAG, "更新包地址异常: " + commandUpdate.toString());
return failed("更新包地址异常");
}
File updateApk = new File(UPDATE_APK_PATH, UPDATE_APK);
if (updateApk.exists()) {
// 若文件存在,则检查安装文件版本是否与当前应用版本一致
// 若一致则说明已安装完毕,直接返回即可
AppUtils.AppInfo info = AppUtils.getApkInfo(updateApk);
if (info != null
&& info.getPackageName().equals(BuildConfig.APPLICATION_ID)
&& info.getVersionCode() == BuildConfig.VERSION_CODE) {
LogUtils.d(TAG, "版本" + info.getVersionCode() + "已安装完毕");
updateApk.deleteOnExit();
return success("已是最新版本");
}
}
updateApk.deleteOnExit();
String url = commandUpdate.getData().getUrl();
Request request = new Request.Builder()
.url(url)
.build();
OkHttpClient client = SCRetrofit.createOkHttpClientBuilder()
.hostnameVerifier(new TrustAllCerts.TrustAllHostnameVerifier())
.sslSocketFactory(TrustAllCerts.createSSLSocketFactory(), new TrustAllCerts())
.build();
InputStream is = null;
FileOutputStream fos = null;
try {
Response response = client.newCall(request).execute();
ResponseBody body = response.body();
if (body == null) {
LogUtils.w(TAG, "更新包数据为空");
return failed("更新包异常");
}
is = body.byteStream();
fos = new FileOutputStream(updateApk);
long total = body.contentLength();
float totalBytesRead = 0f;
BufferedSource source = body.source();
BufferedSink sink = Okio.buffer(Okio.sink(updateApk));
Buffer sinkBuffer = sink.getBuffer();
for (long bytesRead; (bytesRead = source.read(sinkBuffer, BUFFER_SIZE)) != -1; ) {
sink.emit();
totalBytesRead += bytesRead;
int progress = (int) ((totalBytesRead * 100) / total);
inProgress("下载进度: " + progress + "%");
}
sink.flush();
sink.close();
source.close();
body.close();
} catch (IOException e) {
updateApk.deleteOnExit();
LogUtils.w(TAG, "更新包下载失败: " + e.getMessage(), e);
return failed("更新包下载失败");
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ignored) {
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException ignored) {
}
}
}
// 强制显示一次100%进度,避免下载太快跳帧
inProgress("下载进度: 100%");
// 强制等待一段时间,避免下载太快跳帧
try {
Thread.sleep(DEFAULT_DELAY);
} catch (InterruptedException ignored) {
}
// 检查apk是否为合法安装文件并进入安装状态
AppUtils.AppInfo info = AppUtils.getApkInfo(updateApk);
LogUtils.d(TAG, "更新包下载成功,开始安装: " + (info == null ? "null" : info.getPackageName()));
if (info == null || !info.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
FileUtils.delete(updateApk);
LogUtils.w(TAG, "安装包包名异常");
return failed("安装包包名异常");
}
if (info.getVersionCode() == BuildConfig.VERSION_CODE) {
FileUtils.delete(updateApk);
LogUtils.w(TAG, "当前版本: " + BuildConfig.VERSION_CODE + ", 安装包版本: " + info.getVersionCode());
} else if (info.getVersionCode() < BuildConfig.VERSION_CODE) {
FileUtils.delete(updateApk);
LogUtils.d(TAG, "不允许安装低版本");
inProgress("不允许安装低版本");
} else {
LogUtils.d(TAG, "开始安装更新包: " + updateApk.getAbsolutePath());
if (DeviceProxy.updateApp(updateApk)) {
inProgress("开始安装");
} else {
inProgress("安装文件权限修改失败");
}
}
return success("下载成功,开始安装");
}
}
package com.bgycc.smartcanteen.command; package com.bgycc.smartcanteen.command;
import android.content.Context;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
import com.bgycc.smartcanteen.entity.Command; import androidx.annotation.NonNull;
import com.bgycc.smartcanteen.entity.CommandResponse; import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.entity.CommandWifiConfig; import com.bgycc.smartcanteen.entity.CommandWifiConfig;
import com.bgycc.smartcanteen.utils.NetworkUtils; import com.bgycc.smartcanteen.utils.NetworkUtils;
import com.blankj.utilcode.util.LogUtils; import com.blankj.utilcode.util.LogUtils;
import com.google.gson.Gson;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
public class WifiConfigCommandHandler extends CommandHandler { public class WifiConfigCommandWorker extends CommandWorker {
private static final long DEFAULT_DELAY = 3000; private static final long DEFAULT_DELAY = 3000;
private static final long POLLING_DELAY = 100; private static final long POLLING_DELAY = 200;
private CommandWifiConfig wifiConfig; private CommandWifiConfig wifiConfig;
private volatile boolean start = false; public WifiConfigCommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
private static WifiConfigCommandHandler instance; super(context, workerParams);
wifiConfig = gson.fromJson(command.getData(), CommandWifiConfig.class);
public static synchronized WifiConfigCommandHandler getInstance(Command command, Gson gson, CommandProgressCallback callback) {
if (instance == null) {
instance = new WifiConfigCommandHandler(command, gson, callback);
} else {
if (!instance.start) {
instance.init(command, gson, callback);
}
}
return instance;
}
private WifiConfigCommandHandler(Command command, Gson gson, CommandProgressCallback callback) {
super(command, gson, callback);
this.wifiConfig = gson.fromJson(command.getData(), CommandWifiConfig.class);
}
private void init(Command command, Gson gson, CommandProgressCallback callback) {
this.command = command;
this.gson = gson;
this.commandProgressCallback = callback;
this.wifiConfig = gson.fromJson(command.getData(), CommandWifiConfig.class);
} }
@NonNull
@Override @Override
public synchronized CommandResponse run() throws InterruptedException { public Result doWork() {
if (start) {
LogUtils.w(TAG, "Wifi配置任务已启动");
return null;
}
start = true;
CommandWifiConfig.CommandWifiConfigData data = wifiConfig.getData(); CommandWifiConfig.CommandWifiConfigData data = wifiConfig.getData();
if (data == null) { if (data == null) {
start = false; return failed();
return null;
} }
if (!NetworkUtils.isWifiEnabled()) { if (!NetworkUtils.isWifiEnabled()) {
wait("正在启动Wifi", 5); inProgress("正在启动Wifi");
if (!NetworkUtils.setEnable(true)) { if (!NetworkUtils.setEnable(true)) {
String failedMessage = "无法启动Wifi"; String failedMessage = "无法启动Wifi";
wait(failedMessage, 5); inProgress(failedMessage);
LogUtils.e(TAG, failedMessage); LogUtils.e(TAG, failedMessage);
Thread.sleep(DEFAULT_DELAY); try {
start = false; Thread.sleep(DEFAULT_DELAY);
return failedResult(failedMessage); } catch (Exception ignored) {
}
return failed(failedMessage);
} }
} }
// 断开当前在链接的wifi
WifiInfo currentWifiInfo = NetworkUtils.getWifiInfo();
if (currentWifiInfo != null) {
NetworkUtils.disconnect(currentWifiInfo.getNetworkId());
LogUtils.d(TAG, "断开当前wifi: " + currentWifiInfo.toString());
}
String ssid = data.getSsid(); String ssid = data.getSsid();
String identity = data.getIdentity(); String identity = data.getIdentity();
String pwd = data.getPwd(); String pwd = data.getPwd();
String type = data.getType(); String type = data.getType();
wait("正在配置Wifi", 10); inProgress("正在配置Wifi");
LogUtils.d(TAG, "开始配置wifi, ssid: " + ssid + ", identity: " + identity + ", pwd: " + pwd + ", type: " + type); LogUtils.d(TAG, "开始配置wifi, ssid: " + ssid + ", identity: " + identity + ", pwd: " + pwd + ", type: " + type);
try { try {
NetworkUtils.connect(ssid, identity, pwd, type); NetworkUtils.connect(ssid, identity, pwd, type);
// 轮询检查wifi是否链接成功 // 轮询检查wifi是否链接成功
for (int i = 0; i < 30; i++) { for (int i = 0; i < 50; i++) {
Thread.sleep(POLLING_DELAY); Thread.sleep(POLLING_DELAY);
WifiInfo info = NetworkUtils.getWifiInfo(); WifiInfo info = NetworkUtils.getWifiInfo();
if (info == null || info.getIpAddress() == 0) { if (info == null || info.getIpAddress() == 0) {
continue; continue;
} }
String message = "Wifi配置成功"; String message = "Wifi配置成功";
wait(message, 50); inProgress(message);
LogUtils.d(TAG, message); LogUtils.d(TAG, message);
Thread.sleep(DEFAULT_DELAY); Thread.sleep(DEFAULT_DELAY);
return successResult(message); return success(message);
} }
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "链接wifi过程出错: " + e.getMessage(), e); LogUtils.e(TAG, "链接wifi过程出错: " + e.getMessage(), e);
Thread.sleep(DEFAULT_DELAY); return failed(e.getMessage());
return failedResult(e.getMessage());
} finally {
start = false;
} }
return failedResult("无法连接Wifi"); return failed("无法连接Wifi");
} }
} }
...@@ -17,6 +17,10 @@ public interface CommandDao { ...@@ -17,6 +17,10 @@ public interface CommandDao {
@Query("select * from command where finish == 0 order by id asc") @Query("select * from command where finish == 0 order by id asc")
LiveData<List<Command>> queryUndoneCommand(); LiveData<List<Command>> queryUndoneCommand();
// 根据action搜索未完成的指令数
@Query("select count(*) from command where finish == 0 and `action` == :action")
int queryUndoneCommandCountByAction(String action);
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
long insertCommand(Command command); long insertCommand(Command command);
......
package com.bgycc.smartcanteen.worker; package com.bgycc.smartcanteen.monitor;
import android.content.Context; import android.content.Context;
......
package com.bgycc.smartcanteen.worker; package com.bgycc.smartcanteen.monitor;
import android.content.Context; import android.content.Context;
...@@ -30,7 +30,7 @@ public class LogFileMonitor extends Worker { ...@@ -30,7 +30,7 @@ public class LogFileMonitor extends Worker {
+ ", 总大小: " + totalSize); + ", 总大小: " + totalSize);
while (availableSize < (totalSize * 0.1f)) { while (availableSize < (totalSize * 0.1f)) {
// 当可用小于10%时,删除旧日志文件 // 当可用小于10%时,删除旧日志文件
List<File> logFiles = LogUtils.getLogFiles(); List<File> logFiles = FileUtils.listFilesInDir(logDirPath);
int size = logFiles.size(); int size = logFiles.size();
switch (size) { switch (size) {
case 0: case 0:
......
...@@ -18,6 +18,10 @@ public class CommandRepository { ...@@ -18,6 +18,10 @@ public class CommandRepository {
return dao.queryUndoneCommand(); return dao.queryUndoneCommand();
} }
public int queryUndoneCommandCountByAction(String action) {
return dao.queryUndoneCommandCountByAction(action);
}
public long insertCommand(Command command) { public long insertCommand(Command command) {
return dao.insertCommand(command); return dao.insertCommand(command);
} }
......
...@@ -2,16 +2,15 @@ package com.bgycc.smartcanteen.utils; ...@@ -2,16 +2,15 @@ package com.bgycc.smartcanteen.utils;
import android.content.Context; import android.content.Context;
import androidx.work.ExistingWorkPolicy;
import androidx.work.PeriodicWorkRequest; import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager; import androidx.work.WorkManager;
import com.bgycc.smartcanteen.worker.DatabaseMonitor; import com.bgycc.smartcanteen.monitor.DatabaseMonitor;
import com.bgycc.smartcanteen.worker.LogFileMonitor; import com.bgycc.smartcanteen.monitor.LogFileMonitor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class WorkerUtils { public class MonitorUtils {
private static final String LOG_MONITOR_WORKER = "worker_log_monitor"; private static final String LOG_MONITOR_WORKER = "worker_log_monitor";
private static final String DATABASE_MONITOR_WORKER = "database_log_monitor"; private static final String DATABASE_MONITOR_WORKER = "database_log_monitor";
......
...@@ -253,6 +253,10 @@ public class NetworkUtils { ...@@ -253,6 +253,10 @@ public class NetworkUtils {
return true; return true;
} }
public static boolean disconnect(int netId) {
return wifiManager.removeNetwork(netId);
}
private static WifiConfiguration createWifiConfiguration(String ssid, String identity, String pwd, String type) { private static WifiConfiguration createWifiConfiguration(String ssid, String identity, String pwd, String type) {
if(ssid == null || ssid.isEmpty()) if(ssid == null || ssid.isEmpty())
return null; return null;
......
package com.bgycc.smartcanteen.utils;
import android.annotation.SuppressLint;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
@SuppressLint("TrustAllX509TrustManager")
public class TrustAllCerts implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}
public static SSLSocketFactory createSSLSocketFactory() {
SSLSocketFactory ssfFactory = null;
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAllCerts() }, new SecureRandom());
ssfFactory = sc.getSocketFactory();
} catch (Exception ignored) {
}
return ssfFactory;
}
public static class TrustAllHostnameVerifier implements HostnameVerifier {
@SuppressLint("BadHostnameVerifier")
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
}
...@@ -4,27 +4,20 @@ import androidx.lifecycle.LiveData; ...@@ -4,27 +4,20 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import androidx.work.OneTimeWorkRequest;
import com.bgycc.smartcanteen.command.CommandProgressCallback; import com.bgycc.smartcanteen.command.CommandHelper;
import com.bgycc.smartcanteen.command.CommandHandler;
import com.bgycc.smartcanteen.command.LogCommandHandler;
import com.bgycc.smartcanteen.command.UpdateCommandHandler;
import com.bgycc.smartcanteen.command.WifiConfigCommandHandler;
import com.bgycc.smartcanteen.entity.Command; import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.bgycc.smartcanteen.executor.SCTaskExecutor; import com.bgycc.smartcanteen.executor.SCTaskExecutor;
import com.bgycc.smartcanteen.socket.SCWebSocketClient; import com.bgycc.smartcanteen.socket.SCWebSocketClient;
import com.bgycc.smartcanteen.socket.SCWebSocketListener; import com.bgycc.smartcanteen.socket.SCWebSocketListener;
import com.bgycc.smartcanteen.socket.SCWebSocketListenerAdapter; import com.bgycc.smartcanteen.socket.SCWebSocketListenerAdapter;
import com.bgycc.smartcanteen.state.CommandState;
import com.bgycc.smartcanteen.repository.CommandRepository; import com.bgycc.smartcanteen.repository.CommandRepository;
import com.blankj.utilcode.util.LogUtils; import com.blankj.utilcode.util.LogUtils;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
...@@ -33,31 +26,16 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; ...@@ -33,31 +26,16 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
* 监听Command数据库的变动,按如下流程进行处理: <br/> * 监听Command数据库的变动,按如下流程进行处理: <br/>
* 从数据库读取 -> 解析执行 -> 更新到数据库 * 从数据库读取 -> 解析执行 -> 更新到数据库
*/ */
public class CommandViewModel extends ViewModel implements CommandProgressCallback { public class CommandViewModel extends ViewModel {
private static final String LOG_UPLOAD = "LOG_PULL";
private static final String APP_UPDATE = "CONFIG_UPDATE";
private static final String CONFIG_WIFI = "CONFIG_WIFI";
private static final String CONFIG_LOG = "CONFIG_LOG";
// 设备指令白名单
private static final Set<String> COMMAND_WHITELIST = new HashSet<String>()
{
{
add(LOG_UPLOAD);
add(APP_UPDATE);
add(CONFIG_WIFI);
add(CONFIG_LOG);
}
};
private CommandRepository commandRepository; private CommandRepository commandRepository;
private Gson gson; private Gson gson;
private String deviceSN; private String deviceSN;
private MutableLiveData<CommandState> commandState = new MutableLiveData<>(); private MutableLiveData<Command> commandWorker = new MutableLiveData<>();
private LiveData<List<Command>> dataLiveData; private LiveData<List<Command>> dataLiveData;
public LiveData<CommandState> getCommandStateEvent() { public LiveData<Command> getCommandTask() {
return commandState; return commandWorker;
} }
public CommandViewModel(CommandRepository commandRepository, Gson gson, String deviceSN) { public CommandViewModel(CommandRepository commandRepository, Gson gson, String deviceSN) {
...@@ -67,7 +45,6 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -67,7 +45,6 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
// 监听数据库的变动,并执行未完成的指令 // 监听数据库的变动,并执行未完成的指令
this.dataLiveData = commandRepository.queryUndoneCommand(); this.dataLiveData = commandRepository.queryUndoneCommand();
this.dataLiveData.observeForever(dataObserver); this.dataLiveData.observeForever(dataObserver);
this.commandState.postValue(new CommandState(CommandState.IDLE));
} }
public void initialize() { public void initialize() {
...@@ -84,104 +61,40 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -84,104 +61,40 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
private Observer<List<Command>> dataObserver = commands -> { private Observer<List<Command>> dataObserver = commands -> {
if (commands == null || commands.isEmpty()) { if (commands == null || commands.isEmpty()) {
commandState.postValue(new CommandState(CommandState.IDLE));
return; return;
} }
Command first = commands.get(0); Command first = commands.get(0);
RequestRunnable runnable = new RequestRunnable(first); commandWorker.postValue(first);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
}; };
@Override public OneTimeWorkRequest getCommandWorker(Command command) {
public void idle(String message, int progress) { return CommandHelper.createWorker(gson, command, deviceSN);
commandState.postValue(new CommandState(CommandState.IDLE, message, progress));
} }
@Override public void commandFinish(Command command) {
public void wait(String message, int progress) { UpdateDatabaseRunnable runnable = new UpdateDatabaseRunnable(command);
commandState.postValue(new CommandState(CommandState.WAIT, message, progress)); SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
}
@Override
public void success(String message, int progress) {
commandState.postValue(new CommandState(CommandState.SUCCESS, message, progress));
}
@Override
public void failed(String message, int progress) {
commandState.postValue(new CommandState(CommandState.FAILED, message, progress));
} }
private SCWebSocketListener listener = new SCWebSocketListenerAdapter() { private SCWebSocketListener listener = new SCWebSocketListenerAdapter() {
private static final String RESPONSE_PAY_RESULT = "PAY_RESULT";
@Override @Override
public void onMessage(String action, JsonObject obj, String original) { public void onMessage(String action, JsonObject obj, String original) {
if (!COMMAND_WHITELIST.contains(action)) return; if (!CommandHelper.isCommand(action)) return;
LogUtils.d(TAG, "设备下发指令: " + original); LogUtils.d(TAG, "设备下发指令: " + original);
ResponseRunnable runnable = new ResponseRunnable(original, action); ResponseRunnable runnable = new ResponseRunnable(original, action);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable); SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
} }
}; };
private class RequestRunnable implements Runnable { private class UpdateDatabaseRunnable implements Runnable {
private Command command; private Command command;
RequestRunnable(Command command) { UpdateDatabaseRunnable(Command command) {
this.command = command; this.command = command;
} }
@Override @Override
public void run() { public void run() {
CommandHandler handler = null;
LogUtils.d(TAG, "开始执行指令: " + command.toString());
try {
switch (command.getAction()) {
case LOG_UPLOAD:
handler = LogCommandHandler.getInstance(command, gson, deviceSN,CommandViewModel.this);
break;
case APP_UPDATE:
handler = UpdateCommandHandler.getInstance(command, gson, CommandViewModel.this);
break;
case CONFIG_WIFI:
handler = WifiConfigCommandHandler.getInstance(command, gson, CommandViewModel.this);
break;
case CONFIG_LOG:
// CONFIG_LOG后直接return
commandState.postValue(new CommandState(CommandState.TOGGLE_DEBUG));
commandFinishAndUpdateDB();
return;
}
} catch (Exception e) {
commandState.postValue(new CommandState(CommandState.FAILED, e.getMessage()));
handler = null;
}
if (handler == null) {
LogUtils.w(TAG, "无法识别指令: " + command.toString());
commandFinishAndUpdateDB();
return;
}
try {
commandState.postValue(new CommandState(CommandState.WAIT));
CommandResponse response = handler.run();
if (response == null) {
LogUtils.d(TAG, "指令无返回结果: " + command.toString());
return;
}
if (response.success()) {
commandState.postValue(new CommandState(CommandState.SUCCESS, response.getMessage()));
} else {
commandState.postValue(new CommandState(CommandState.FAILED, response.getMessage()));
}
} catch (Exception e) {
commandState.postValue(new CommandState(CommandState.FAILED, e.getMessage()));
} finally {
commandFinishAndUpdateDB();
}
}
// 将执行完毕的指令更新到数据库,此时会触发dataObserver的动作(会继续搜索下一条未执行完毕的指令)
private void commandFinishAndUpdateDB() {
command.finish(); command.finish();
LogUtils.d(TAG, "指令执行完毕: " + command.toString()); LogUtils.d(TAG, "指令执行完毕: " + command.toString());
commandRepository.updateCommand(command); commandRepository.updateCommand(command);
...@@ -201,6 +114,14 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -201,6 +114,14 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
public void run() { public void run() {
// 指令插入到数据库,则会触发dataObserver // 指令插入到数据库,则会触发dataObserver
Command command = new Command(response, action); Command command = new Command(response, action);
// 若为更新指令,则需要检查是否有未完成的,避免同时存在多条更新指令重复执行
if (CommandHelper.oneAtTime(command)) {
int count = commandRepository.queryUndoneCommandCountByAction(action);
if (count > 0) {
LogUtils.d(TAG, "该指令已在数据库中记录并未执行完毕,不再插入重复指令");
return;
}
}
long lastInsertId = commandRepository.insertCommand(command); long lastInsertId = commandRepository.insertCommand(command);
if (lastInsertId == -1) { if (lastInsertId == -1) {
LogUtils.w(TAG, "指令插入到数据库失败: " + command.toString()); LogUtils.w(TAG, "指令插入到数据库失败: " + command.toString());
......
buildscript { buildscript {
ext { ext {
daemon_verson_code = 10 daemon_verson_code = 11
daemon_verson_name = "1.0" daemon_verson_name = "1.1"
} }
repositories { repositories {
google() google()
......
...@@ -100,5 +100,5 @@ dependencies { ...@@ -100,5 +100,5 @@ dependencies {
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.blankj:utilcodex:1.26.0' implementation 'com.blankj:utilcodex:1.28.4'
} }
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bgycc.smartcanteen.daemon"> package="com.bgycc.smartcanteen.daemon">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
......
...@@ -12,18 +12,20 @@ import java.io.File; ...@@ -12,18 +12,20 @@ import java.io.File;
public class ServiceApplication extends Application { public class ServiceApplication extends Application {
private static final String LOG_PREFIX = "daemon"; private static final String LOG_PREFIX = "daemon";
private static final String LOG_DIR = "log"; private static final String LOG_DIR = "log";
// 日志文件保留周期
private static final int LOG_SAVE_DAYS = 7;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Utils.init(getApplicationContext()); Utils.init(this);
CrashUtils.init();
String logDir = PathUtils.getExternalStoragePath() + File.separator + LOG_DIR; String logDir = PathUtils.getExternalStoragePath() + File.separator + LOG_DIR;
CrashUtils.init(logDir); CrashUtils.init(logDir);
LogUtils.getConfig() LogUtils.getConfig()
.setDir(logDir) .setDir(logDir)
.setLog2FileSwitch(true) .setLog2FileSwitch(true)
.setBorderSwitch(false) .setBorderSwitch(false)
.setFilePrefix(LOG_PREFIX); .setFilePrefix(LOG_PREFIX)
.setSaveDays(LOG_SAVE_DAYS);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment