Commit 090ceca0 by pye52

Merge branch 'develop' into fea-phone

# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/bgycc/smartcanteen/activity/MainActivity.java
#	build.gradle
parents 1b9fdb3b 9f24575a
......@@ -7,28 +7,14 @@ android {
applicationId "com.bgycc.smartcanteen"
minSdkVersion 22
targetSdkVersion 29
versionCode 13
versionName "1.3.3"
versionCode 16
versionName "1.3.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters "armeabi", "armeabi-v7a", "x86", "mips"
}
buildConfigField "int", "DaemonVersion", String.valueOf(rootProject.ext.daemon_verson_code)
}
signingConfigs {
debug {
keyAlias 'town'
keyPassword 'maxrocky'
storeFile file('canteen.jks')
storePassword 'maxrocky'
}
release {
keyAlias 'town'
keyPassword 'maxrocky'
storeFile file('canteen.jks')
storePassword 'maxrocky'
}
}
buildTypes {
debug {
minifyEnabled false
......@@ -67,6 +53,11 @@ android {
buildConfigField "String", "MainWebSocketServerUrl", '"wss://diningservice.bgy.com.cn/websocket/%s/V%s"'
buildConfigField "String", "MainHttpServerHost", '"https://diningback.bgy.com.cn"'
}
pro_cloud {
dimension "smart_canteen"
buildConfigField "String", "MainWebSocketServerUrl", '"wss://diningservice-cloud.bgy.com.cn/websocket/%s/V%s"'
buildConfigField "String", "MainHttpServerHost", '"https://diningback-cloud.bgy.com.cn"'
}
}
signingConfigs {
canteen {
......@@ -111,26 +102,29 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
def work_version = "2.3.4"
implementation "androidx.work:work-runtime:$work_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.java-websocket:Java-WebSocket:1.4.0"
implementation 'com.blankj:utilcodex:1.26.0'
implementation "org.java-websocket:Java-WebSocket:1.5.1"
implementation 'com.blankj:utilcodex:1.28.4'
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.king.zxing:zxing-lite:1.1.6-androidx'
implementation 'com.squareup.okhttp3:okhttp:4.3.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1'
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation 'com.squareup.retrofit2:converter-gson:2.7.1'
implementation 'com.squareup.retrofit2:converter-scalars:2.7.1'
def okhttp_version = "4.6.0"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
def retrofit_version = "2.8.1"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.google.code.gson:gson:2.8.6'
def room_version = "2.2.4"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation 'com.liulishuo.filedownloader:library:1.7.7'
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.bgycc.smartcanteen">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
......@@ -24,7 +25,8 @@
android:theme="@style/AppTheme">
<activity android:name=".activity.MainActivity"
android:launchMode="singleInstance"
android:screenOrientation="landscape">
android:screenOrientation="landscape"
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......@@ -51,6 +53,11 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove"
android:exported="false" />
</application>
</manifest>
\ No newline at end of file
......@@ -2,6 +2,10 @@ package com.bgycc.smartcanteen;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.work.Configuration;
import com.bgycc.smartcanteen.executor.SCTaskExecutor;
import com.blankj.utilcode.util.CrashUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils;
......@@ -9,20 +13,31 @@ import com.blankj.utilcode.util.Utils;
import java.io.File;
public class RootApp extends Application {
public class RootApp extends Application implements Configuration.Provider {
private static final String LOG_PREFIX = "app";
private static final String LOG_DIR = "log";
// 日志文件保留周期
private static final int LOG_SAVE_DAYS = 7;
@Override
public void onCreate() {
super.onCreate();
Utils.init(getApplicationContext());
Utils.init(this);
String logDir = PathUtils.getExternalStoragePath() + File.separator + LOG_DIR;
CrashUtils.init(logDir);
LogUtils.getConfig()
.setDir(logDir)
.setLog2FileSwitch(true)
.setBorderSwitch(false)
.setFilePrefix(LOG_PREFIX);
.setFilePrefix(LOG_PREFIX)
.setSaveDays(LOG_SAVE_DAYS);
}
@NonNull
@Override
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setExecutor(SCTaskExecutor.getIOThreadExecutor())
.build();
}
}
package com.bgycc.smartcanteen.activity;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.constraintlayout.widget.ConstraintLayout;
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.pm.PackageInfo;
......@@ -28,10 +35,11 @@ import android.widget.TextView;
import com.bgycc.smartcanteen.BuildConfig;
import com.bgycc.smartcanteen.Injection;
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.executor.SCTaskExecutor;
import com.bgycc.smartcanteen.socket.SCWebSocketClient;
import com.bgycc.smartcanteen.state.CommandState;
import com.bgycc.smartcanteen.state.ConnectState;
import com.bgycc.smartcanteen.state.PayOfflineState;
import com.bgycc.smartcanteen.state.PayOnlineState;
......@@ -39,6 +47,7 @@ import com.bgycc.smartcanteen.state.QRCodeState;
import com.bgycc.smartcanteen.utils.NetworkUtils;
import com.bgycc.smartcanteen.utils.SmartCanteenUtils;
import com.bgycc.smartcanteen.utils.TTSHelper;
import com.bgycc.smartcanteen.utils.MonitorUtils;
import com.bgycc.smartcanteen.viewModel.CommandViewModel;
import com.bgycc.smartcanteen.viewModel.PayOfflineViewModel;
import com.bgycc.smartcanteen.viewModel.PayOnlineViewModel;
......@@ -84,6 +93,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private TextView message;
private AudioManager audioManager;
private WorkManager workManager;
private Handler handler = new Handler();
private SimpleDateFormat payDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
private SimpleDateFormat socketConnectedTimeDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
......@@ -120,6 +130,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
workManager = WorkManager.getInstance(this);
ViewModelFactory factory = Injection.injectFactory(deviceSN);
ViewModelProvider provider = new ViewModelProvider(this, factory);
payOnlineViewModel = provider.get(PayOnlineViewModel.class);
......@@ -259,23 +270,86 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
});
commandViewModel.getCommandStateEvent().observe(this, event -> {
switch (event.getState()) {
case CommandState.IDLE:
settingLayout.animate().setDuration(300).alpha(0f);
break;
case CommandState.WAIT:
settingText.setText(event.getMessage());
settingLayout.animate().setDuration(300).alpha(1f);
break;
case CommandState.SUCCESS:
case CommandState.FAILED:
settingText.setText(event.getMessage());
settingLayout.animate().setDuration(300).alpha(1f);
break;
case CommandState.TOGGLE_DEBUG:
commandViewModel.getCommandTask().observe(this, new Observer<Command>() {
private static final String WORKER_NAME = "smartcanteen_command";
private LiveData<WorkInfo> runningLiveData;
private Command runningCommand;
private OneTimeWorkRequest runningRequest;
@Override
public void onChanged(Command command) {
// 若为debug模式切换命令,则直接执行即可
if (CommandHelper.isLogConfig(command)) {
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;
}
}
});
......@@ -286,6 +360,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
commandViewModel.initialize();
handler.post(updateTimeRunnable);
MonitorUtils.startLogFilesMonitor(this);
MonitorUtils.startDatabaseMonitor(this);
SCWebSocketClient.getInstance().tryConnect();
}
......@@ -362,6 +438,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
TTSHelper.release();
SCWebSocketClient.getInstance().realClose();
SCTaskExecutor.getInstance().quit();
MonitorUtils.stopDatabaseMonitor(this);
MonitorUtils.stopLogFilesMonitor(this);
}
@Override
......
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;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.api.SCApi;
import com.bgycc.smartcanteen.api.SCRetrofit;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandLog;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils;
import com.blankj.utilcode.util.ZipUtils;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
......@@ -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 ZIP_FILE = "log.zip";
private static final String UPLOAD_DIR = "upload_log";
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 nameFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
private String deviceSN;
private CommandLog commandLog;
private File uploadDir;
private Date startTime;
private Date endTime;
private static LogCommandHandler instance;
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;
public LogCommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.commandLog = gson.fromJson(command.getData(), CommandLog.class);
parseDate();
}
private LogCommandHandler(Command command, Gson gson, String deviceSN, CommandProgressCallback callback) {
super(command, gson, callback);
this.deviceSN = deviceSN;
this.commandLog = gson.fromJson(command.getData(), CommandLog.class);
@NonNull
@Override
public Result doWork() {
parseDate();
if (!checkLogCommand()) {
return failed("日志上传指令不符合规范");
}
File logFile = getZipLogs();
if (logFile != null) {
inProgress("上传压缩文件至服务器");
upload(logFile);
return success();
}
return failed();
}
private void parseDate() {
......@@ -101,14 +93,17 @@ public class LogCommandHandler extends CommandHandler {
}
// 获取指定类型及日期的日志文件
private File getZipLogs() throws InterruptedException {
private File getZipLogs() {
CommandLog.CommandLogData data = commandLog.getData();
File logDir = getLogDirByType(data.getLogType());
wait("建立临时目录", 0);
inProgress("建立临时目录");
uploadDir = tempDirInit();
Thread.sleep(DEFAULT_DELAY);
try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
}
if (uploadDir == null) return null;
if (!copyTargetFiles(logDir, uploadDir)) {
......@@ -116,10 +111,13 @@ public class LogCommandHandler extends CommandHandler {
return null;
}
Thread.sleep(DEFAULT_DELAY);
try {
Thread.sleep(DEFAULT_DELAY);
} catch (Exception ignored) {
}
File zip = new File(PathUtils.getExternalStoragePath() + File.separator + ZIP_FILE);
try {
wait("开始压缩", 40);
inProgress("开始压缩");
ZipUtils.zipFile(uploadDir, zip);
} catch (IOException e) {
LogUtils.e(TAG, "压缩日志文件失败: " + e.getMessage(), e);
......@@ -164,7 +162,7 @@ public class LogCommandHandler extends CommandHandler {
};
List<File> logFiles = FileUtils.listFilesInDirWithFilter(src, filter, false, null);
boolean copyResult = true;
wait("筛选目标日志文件", 10);
inProgress("筛选目标日志文件");
int count = 0;
File descFile;
boolean tempCopyResult;
......@@ -200,26 +198,4 @@ public class LogCommandHandler extends CommandHandler {
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;
import android.content.Context;
import android.net.wifi.WifiInfo;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandResponse;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.entity.CommandWifiConfig;
import com.bgycc.smartcanteen.utils.NetworkUtils;
import com.blankj.utilcode.util.LogUtils;
import com.google.gson.Gson;
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 POLLING_DELAY = 100;
private static final long POLLING_DELAY = 200;
private CommandWifiConfig wifiConfig;
private volatile boolean start = false;
private static WifiConfigCommandHandler instance;
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);
public WifiConfigCommandWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
wifiConfig = gson.fromJson(command.getData(), CommandWifiConfig.class);
}
@NonNull
@Override
public synchronized CommandResponse run() throws InterruptedException {
if (start) {
LogUtils.w(TAG, "Wifi配置任务已启动");
return null;
}
start = true;
public Result doWork() {
CommandWifiConfig.CommandWifiConfigData data = wifiConfig.getData();
if (data == null) {
start = false;
return null;
return failed();
}
if (!NetworkUtils.isWifiEnabled()) {
wait("正在启动Wifi", 5);
inProgress("正在启动Wifi");
if (!NetworkUtils.setEnable(true)) {
String failedMessage = "无法启动Wifi";
wait(failedMessage, 5);
inProgress(failedMessage);
LogUtils.e(TAG, failedMessage);
Thread.sleep(DEFAULT_DELAY);
start = false;
return failedResult(failedMessage);
try {
Thread.sleep(DEFAULT_DELAY);
} 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 identity = data.getIdentity();
String pwd = data.getPwd();
String type = data.getType();
wait("正在配置Wifi", 10);
inProgress("正在配置Wifi");
LogUtils.d(TAG, "开始配置wifi, ssid: " + ssid + ", identity: " + identity + ", pwd: " + pwd + ", type: " + type);
try {
NetworkUtils.connect(ssid, identity, pwd, type);
// 轮检查wifi是否链接成功
for (int i = 0; i < 30; i++) {
// 轮检查wifi是否链接成功
for (int i = 0; i < 50; i++) {
Thread.sleep(POLLING_DELAY);
WifiInfo info = NetworkUtils.getWifiInfo();
if (info == null || info.getIpAddress() == 0) {
continue;
}
String message = "Wifi配置成功";
wait(message, 50);
inProgress(message);
LogUtils.d(TAG, message);
Thread.sleep(DEFAULT_DELAY);
return successResult(message);
return success(message);
}
} catch (Exception e) {
LogUtils.e(TAG, "链接wifi过程出错: " + e.getMessage(), e);
Thread.sleep(DEFAULT_DELAY);
return failedResult(e.getMessage());
} finally {
start = false;
return failed(e.getMessage());
}
return failedResult("无法连接Wifi");
return failed("无法连接Wifi");
}
}
package com.bgycc.smartcanteen.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
import androidx.room.Database;
......@@ -11,7 +8,6 @@ import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import com.bgycc.smartcanteen.data.dao.CommandDao;
......@@ -20,25 +16,17 @@ import com.bgycc.smartcanteen.data.dao.PayResponseDao;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.PayData;
import com.bgycc.smartcanteen.entity.PayResponse;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.Utils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@Database(
entities = {
Command.class,
PayData.class,
PayResponse.class
},
version = 1,
version = 2,
exportSchema = false)
public abstract class DatabaseManager extends RoomDatabase {
private static final String TAG = "SmartCanteen_Database";
private static final int UPDATE_MAX_TIMES = 5;
@NonNull
@Override
......@@ -72,70 +60,15 @@ public abstract class DatabaseManager extends RoomDatabase {
context.getApplicationContext(),
DatabaseManager.class,
DB_NAME)
.addCallback(new Callback() {
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
// 数据库启动时,将标记为"在线支付"的订单统一改为"离线支付"
// 避免某个在线支付订单在支付过程中App因特殊状况强制退出导致漏支付
Cursor cursor = db.query("select * from " + PayData.TABLE_NAME + " where payState == 0");
if (cursor == null || cursor.getCount() <= 0) return;
cursor.moveToFirst();
List<PayData> list = selectPayOnlineData(cursor);
forceToPayOffline(db, list);
}
})
.addCallback(new OldDataCompatible())
.addCallback(new ForceToPayOffline())
.addMigrations(new _1To_2Migration())
// 一旦版本不兼容则清空数据库
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build();
}
@NotNull
private static List<PayData> selectPayOnlineData(Cursor cursor) {
List<PayData> list = new ArrayList<>();
do {
PayData payData = new PayData();
payData.setEquipmentNo(cursor.getString(cursor.getColumnIndex("equipmentNo")));
payData.setPayCode(cursor.getString(cursor.getColumnIndex("payCode")));
payData.setTerminalType(cursor.getString(cursor.getColumnIndex("terminalType")));
payData.setTime(cursor.getLong(cursor.getColumnIndex("time")));
payData.setPayState(cursor.getInt(cursor.getColumnIndex("payState")));
list.add(payData);
} while (cursor.moveToNext());
return list;
}
private static void forceToPayOffline(@NonNull SupportSQLiteDatabase db, List<PayData> list) {
StringBuilder ids = new StringBuilder();
db.beginTransaction();
for (PayData payData : list) {
payData.payOffline();
ContentValues values = new ContentValues();
values.put("payState", -1);
int r;
int count = 0;
do {
r = db.update(PayData.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values,
"payCode=?", new String[]{payData.getPayCode()});
count++;
if (count == UPDATE_MAX_TIMES) {
LogUtils.file(TAG, "订单: " + payData.toString() + "在线支付异常");
}
} while (r <= 0 && count < UPDATE_MAX_TIMES);
ids.append(payData.getPayCode())
.append(",");
}
db.setTransactionSuccessful();
db.endTransaction();
if (ids.length() > 0) {
LogUtils.d(TAG, "订单号: " + ids.substring(0, ids.length() - 1) + " 未能接收后台支付结果");
}
}
public abstract CommandDao getCommandDao();
public abstract PayDataDao getPayDataDao();
......
package com.bgycc.smartcanteen.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.bgycc.smartcanteen.entity.PayData;
import com.blankj.utilcode.util.LogUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
// 数据库启动时,将标记为"在线支付"的订单统一改为"离线支付"
// 避免某个在线支付订单在支付过程中App因特殊状况强制退出导致漏支付
public class ForceToPayOffline extends RoomDatabase.Callback {
private static final String TAG = "ForceToPayOffline";
private static final int UPDATE_MAX_TIMES = 5;
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
Cursor cursor = db.query("select * from " + PayData.TABLE_NAME + " where payState == 0");
if (cursor == null || cursor.getCount() <= 0) return;
cursor.moveToFirst();
List<PayData> list = selectPayOnlineData(cursor);
forceToPayOffline(db, list);
}
@NotNull
private static List<PayData> selectPayOnlineData(Cursor cursor) {
List<PayData> list = new ArrayList<>();
do {
PayData payData = new PayData();
payData.setEquipmentNo(cursor.getString(cursor.getColumnIndex("equipmentNo")));
payData.setPayCode(cursor.getString(cursor.getColumnIndex("payCode")));
payData.setTerminalType(cursor.getString(cursor.getColumnIndex("terminalType")));
payData.setTime(cursor.getLong(cursor.getColumnIndex("time")));
payData.setPayState(cursor.getInt(cursor.getColumnIndex("payState")));
list.add(payData);
} while (cursor.moveToNext());
return list;
}
private static void forceToPayOffline(@NonNull SupportSQLiteDatabase db, List<PayData> list) {
StringBuilder ids = new StringBuilder();
db.beginTransaction();
try {
for (PayData payData : list) {
payData.payOffline();
ContentValues values = new ContentValues();
values.put("payState", payData.getPayState());
int r;
int count = 0;
do {
r = db.update(PayData.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values,
"payCode=?", new String[]{payData.getPayCode()});
count++;
if (count == UPDATE_MAX_TIMES) {
LogUtils.file(TAG, "订单: " + payData.toString() + "支付状态修改异常");
}
} while (r <= 0 && count < UPDATE_MAX_TIMES);
ids.append(payData.getPayCode())
.append(",");
}
db.setTransactionSuccessful();
} catch (Exception e) {
LogUtils.e(TAG, "异常在线支付订单处理失败: " + e.getMessage(), e);
}
db.endTransaction();
if (ids.length() > 0) {
LogUtils.d(TAG, "由于设备异常关闭,订单号: " + ids.substring(0, ids.length() - 1) + "已强制标记为离线支付");
}
}
}
package com.bgycc.smartcanteen.data;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.bgycc.smartcanteen.entity.PayData;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.SPUtils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.util.List;
public class OldDataCompatible extends RoomDatabase.Callback {
private static final String TAG = "OldDataCompatible";
private static final String SP_NAME = "PayStorage";
private static final String DATA_KEY = "PAY_LIST";
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
// 搜索是否存在1.3.1版本以前的离线支付清单
String data = SPUtils.getInstance(SP_NAME).getString(DATA_KEY, "");
if (TextUtils.isEmpty(data)) {
return;
}
db.beginTransaction();
// 将其解析并插入到当前数据库
List<PayData> list = new Gson().fromJson(data, new TypeToken<List<PayData>>() {}.getType());
try {
for (PayData payData : list) {
payData.setUploadTime(0);
payData.payOffline();
ContentValues contentValues = new ContentValues();
contentValues.put("equipmentNo", payData.getEquipmentNo());
contentValues.put("payCode", payData.getPayCode());
contentValues.put("terminalType", payData.getTerminalType());
contentValues.put("time", payData.getTime());
contentValues.put("uploadTime", payData.getUploadTime());
contentValues.put("payState", payData.getPayState());
db.insert(PayData.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, contentValues);
}
db.setTransactionSuccessful();
LogUtils.d(TAG, "成功恢复" + list.size() + "条旧版本离线订单数据");
} catch (Exception e) {
LogUtils.e(TAG, "旧离线订单文件解析失败: " + e.getMessage(), e);
} finally {
db.endTransaction();
// 数据插入完成后清理sp的离线订单数据
SPUtils.getInstance(SP_NAME).clear();
}
}
}
package com.bgycc.smartcanteen.data;
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.bgycc.smartcanteen.entity.PayData;
public class _1To_2Migration extends Migration {
public _1To_2Migration() {
super(1, 2);
}
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE " + PayData.TABLE_NAME +
" ADD COLUMN uploadTime INTEGER NOT NULL DEFAULT 0");
}
}
......@@ -17,9 +17,21 @@ public interface CommandDao {
@Query("select * from command where finish == 0 order by id asc")
LiveData<List<Command>> queryUndoneCommand();
// 根据action搜索未完成的指令数
@Query("select count(*) from command where finish == 0 and `action` == :action")
int queryUndoneCommandCountByAction(String action);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertCommand(Command command);
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateCommand(Command command);
@Query("select count(*) from command")
int countOldData();
// 删除执行完毕的指令
@Query("delete from command where id in " +
"(select id from command order by id asc limit :limit)")
int clearOldData(int limit);
}
......@@ -13,10 +13,12 @@ import java.util.List;
@Dao
public interface PayDataDao {
/**
* 获取所有需要离线支付的订单
* 获取需要离线支付的订单(同时1h内未上传过的)
*/
@Query("select * from paydata where payState == -1")
List<PayData> queryPayOfflineData();
@Query("select * from paydata " +
"where payState == -1 and (:currentTim - uploadTime) > (1 * 60 * 60 * 1000) " +
"limit :limit")
List<PayData> queryPayOfflineData(long currentTim, int limit);
/**
* 根据支付码获取指定订单
......@@ -32,4 +34,12 @@ public interface PayDataDao {
@Update(onConflict = OnConflictStrategy.REPLACE)
int updatePayData(List<PayData> data);
@Query("select count(*) from paydata where payState == 1 or payState == -2")
int countOldData();
// 删除"支付成功"和"支付失败"的旧订单
@Query("delete from paydata where payCode in " +
"(select payCode from paydata where payState == 1 or payState == -2 order by time asc limit :limit)")
int clearOldData(int limit);
}
......@@ -3,6 +3,7 @@ package com.bgycc.smartcanteen.data.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.bgycc.smartcanteen.entity.PayResponse;
......@@ -10,4 +11,12 @@ import com.bgycc.smartcanteen.entity.PayResponse;
public interface PayResponseDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertPayResponse(PayResponse response);
@Query("select count(*) from payresponse")
int countOldData();
// 删除旧的服务器通知
@Query("delete from payresponse where id in " +
"(select id from payresponse order by id asc limit :limit)")
int clearOldData(int limit);
}
package com.bgycc.smartcanteen.entity;
import androidx.annotation.StringDef;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
......@@ -9,14 +8,6 @@ import java.util.Objects;
@Entity(tableName = Command.TABLE_NAME)
public class Command {
public static final String TABLE_NAME = "command";
public static final String LOG_UPLOAD = "LOG_PULL";
public static final String APP_UPDATE = "CONFIG_UPDATE";
public static final String CONFIG_WIFI = "CONFIG_WIFI";
public static final String CONFIG_LOG = "CONFIG_LOG";
@StringDef(value = {LOG_UPLOAD, APP_UPDATE, CONFIG_WIFI, CONFIG_LOG})
public @interface COMMAND_ACTION {
}
@PrimaryKey(autoGenerate = true)
private long id;
......@@ -28,7 +19,7 @@ public class Command {
}
public Command(String command, @COMMAND_ACTION String action) {
public Command(String command, String action) {
this.data = command;
this.action = action;
this.finish = false;
......
......@@ -28,6 +28,8 @@ public class PayData {
private String payCode = "";
private String terminalType;
private long time;
// 记录订单发到后台的时间
private long uploadTime;
/**
* 1 -> 支付成功(在线支付/离线支付成功后标记)
* 0 -> 在线支付(任务创建时默认标记)
......@@ -46,6 +48,7 @@ public class PayData {
this.terminalType = terminalType;
this.time = System.currentTimeMillis() / 1000;
this.payState = PAY_ONLINE;
this.uploadTime = 0;
}
public boolean success() {
......@@ -112,6 +115,14 @@ public class PayData {
this.time = time;
}
public long getUploadTime() {
return uploadTime;
}
public void setUploadTime(long uploadTime) {
this.uploadTime = uploadTime;
}
public int getPayState() {
return payState;
}
......@@ -124,17 +135,18 @@ public class PayData {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PayData data = (PayData) o;
return time == data.time &&
payState == data.payState &&
Objects.equals(equipmentNo, data.equipmentNo) &&
Objects.equals(payCode, data.payCode) &&
Objects.equals(terminalType, data.terminalType);
PayData payData = (PayData) o;
return time == payData.time &&
uploadTime == payData.uploadTime &&
payState == payData.payState &&
Objects.equals(equipmentNo, payData.equipmentNo) &&
payCode.equals(payData.payCode) &&
Objects.equals(terminalType, payData.terminalType);
}
@Override
public int hashCode() {
return Objects.hash(equipmentNo, payCode, terminalType, time, payState);
return Objects.hash(equipmentNo, payCode, terminalType, time, uploadTime, payState);
}
@Override
......@@ -144,6 +156,7 @@ public class PayData {
", payCode='" + payCode + '\'' +
", terminalType='" + terminalType + '\'' +
", time=" + time +
", uploadTime=" + uploadTime +
", payState=" + payState +
'}';
}
......
......@@ -22,7 +22,7 @@ public class DefaultTaskExecutor extends TaskExecutor {
private final Object mLock = new Object();
private final ScheduledExecutorService mThread = Executors.newScheduledThreadPool(6, new ThreadFactory() {
private static final String THREAD_NAME_STEM = "mqtt_thread_%d";
private static final String THREAD_NAME_STEM = "SmartCanteen_Thread_%d";
private final AtomicInteger mThreadId = new AtomicInteger(0);
......
package com.bgycc.smartcanteen.monitor;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.bgycc.smartcanteen.Injection;
import com.bgycc.smartcanteen.repository.CommandRepository;
import com.bgycc.smartcanteen.repository.PayDataRepository;
import com.bgycc.smartcanteen.repository.PayResponseRepository;
import com.blankj.utilcode.util.LogUtils;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
public class DatabaseMonitor extends Worker {
private CommandRepository commandRepository;
private PayDataRepository payDataRepository;
private PayResponseRepository payResponseRepository;
// 数据清理阈值
private static final int DATABASE_LIMIT = 50 * 10000;
public DatabaseMonitor(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
commandRepository = Injection.provideCommandRepositorySingleton();
payDataRepository = Injection.providePayDataRepositorySingleton();
payResponseRepository = Injection.providePayResponseRepositorySingleton();
}
@NonNull
@Override
public Result doWork() {
StringBuilder builder = new StringBuilder("清理阈值: " + DATABASE_LIMIT);
int commandCount = commandRepository.countOldData();
builder.append("\n")
.append("command数据库当前旧数据量: ")
.append(commandCount);
if (commandCount > DATABASE_LIMIT) {
int clearCount = commandRepository.clearOldData(commandCount - DATABASE_LIMIT);
builder.append("\n")
.append("command数据库已清理旧数据: ")
.append(clearCount);
}
int payDataCount = payDataRepository.countOldData();
builder.append("\n")
.append("payData数据库当前旧数据量: ")
.append(payDataCount);
if (payDataCount > DATABASE_LIMIT) {
int clearCount = payDataRepository.clearOldData(payDataCount - DATABASE_LIMIT);
builder.append("\n")
.append("payData数据库已清理旧数据: ")
.append(clearCount);
}
int payResponseCount = payResponseRepository.countOldData();
builder.append("\n")
.append("payResponse数据库当前旧数据量: ")
.append(payResponseCount);
if (payResponseCount > DATABASE_LIMIT) {
int clearCount = payResponseRepository.clearOldData(payResponseCount - DATABASE_LIMIT);
builder.append("\n")
.append("payResponse数据库已清理旧数据: ")
.append(clearCount);
}
LogUtils.d(TAG, builder);
return Result.success();
}
}
package com.bgycc.smartcanteen.monitor;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.LogUtils;
import java.io.File;
import java.util.Collections;
import java.util.List;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
public class LogFileMonitor extends Worker {
public LogFileMonitor(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
String logDirPath = LogUtils.getConfig().getDir();
long totalSize = FileUtils.getFsTotalSize(logDirPath);
long availableSize = FileUtils.getFsAvailableSize(logDirPath);
LogUtils.d(TAG, "日志文件系统, 可用大小: " + availableSize
+ ", 总大小: " + totalSize);
while (availableSize < (totalSize * 0.1f)) {
// 当可用小于10%时,删除旧日志文件
List<File> logFiles = FileUtils.listFilesInDir(logDirPath);
int size = logFiles.size();
switch (size) {
case 0:
LogUtils.w(TAG, "没有日志文件,但内存空间已接近满");
return Result.success();
case 1:
// 只有一个日志文件时,直接删除
FileUtils.delete(logFiles.get(0));
return Result.success();
default:
// 有多个文件时,先排序,后删除最旧的日志
Collections.sort(logFiles, (f1, f2) -> {
if (f1.lastModified() == f2.lastModified()) {
return 0;
}
return (f1.lastModified() > f2.lastModified()) ? 1 : -1;
});
for (File f : logFiles.subList(0, 2)) {
FileUtils.delete(f);
}
totalSize = FileUtils.getFsTotalSize(logDirPath);
availableSize = FileUtils.getFsAvailableSize(logDirPath);
break;
}
}
return Result.success();
}
}
\ No newline at end of file
......@@ -18,6 +18,10 @@ public class CommandRepository {
return dao.queryUndoneCommand();
}
public int queryUndoneCommandCountByAction(String action) {
return dao.queryUndoneCommandCountByAction(action);
}
public long insertCommand(Command command) {
return dao.insertCommand(command);
}
......@@ -25,4 +29,12 @@ public class CommandRepository {
public int updateCommand(Command command) {
return dao.updateCommand(command);
}
public int countOldData() {
return dao.countOldData();
}
public int clearOldData(int limit) {
return dao.clearOldData(limit);
}
}
......@@ -12,8 +12,8 @@ public class PayDataRepository {
this.dao = dao;
}
public List<PayData> queryPayOfflineData() {
return dao.queryPayOfflineData();
public List<PayData> queryPayOfflineData(long currentTime, int limit) {
return dao.queryPayOfflineData(currentTime, limit);
}
public PayData queryPayDataByPayCode(String payCode) {
......@@ -31,4 +31,12 @@ public class PayDataRepository {
public int updatePayData(PayData data) {
return dao.updatePayData(data);
}
public int countOldData() {
return dao.countOldData();
}
public int clearOldData(int limit) {
return dao.clearOldData(limit);
}
}
......@@ -13,4 +13,12 @@ public class PayResponseRepository {
public long insertPayResponse(PayResponse response) {
return dao.insertPayResponse(response);
}
public int countOldData() {
return dao.countOldData();
}
public int clearOldData(int limit) {
return dao.clearOldData(limit);
}
}
......@@ -30,6 +30,8 @@ import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLParameters;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/**
......@@ -157,6 +159,9 @@ public class SCWebSocketClient extends WebSocketClient {
@Override
public void onMessage(String response) {
// 由于项目设计阶段没有规范返回的结果
// 导致做统一处理比较麻烦(需要考虑到很多情况)
// 因此目前只采取通过JsonObject来逐个字段判断的低效方案
JsonObject jsonObject;
String action = "";
try {
......@@ -193,7 +198,7 @@ public class SCWebSocketClient extends WebSocketClient {
@Override
public void onClose(int code, String reason, boolean remote) {
LogUtils.w(TAG, "socket已关闭, 原因: " + reason + ", 是否服务器断开链接: " + remote);
LogUtils.w(TAG, "socket已关闭: " + code + ", 原因: " + reason + ", 是否服务器断开链接: " + remote);
connectState.postValue(new ConnectState(ConnectState.OFFLINE));
for (SCWebSocketListener l : listener) {
l.onClose(code, reason, remote);
......@@ -241,6 +246,9 @@ public class SCWebSocketClient extends WebSocketClient {
Heartbeat request = new Heartbeat(deviceSN);
String requestStr = gson.toJson(request);
send(requestStr);
for (SCWebSocketListener l : listener) {
l.onHeartbeat();
}
}
};
......@@ -265,4 +273,10 @@ public class SCWebSocketClient extends WebSocketClient {
NetworkUtils.bindProcessToNetwork(network);
}
};
// 该方法在1.5.1时必须为空,否则会报错
// https://github.com/TooTallNate/Java-WebSocket/issues/1011
@Override
protected void onSetSSLParameters(SSLParameters sslParameters) {
}
}
......@@ -11,6 +11,8 @@ public interface SCWebSocketListener {
void onClose(int code, String reason, boolean remote);
void onHeartbeat();
void onError(Exception ex);
void onReconnect();
......
......@@ -22,6 +22,11 @@ public class SCWebSocketListenerAdapter implements SCWebSocketListener {
}
@Override
public void onHeartbeat() {
}
@Override
public void onError(Exception ex) {
}
......
package com.bgycc.smartcanteen.utils;
import android.content.Context;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.bgycc.smartcanteen.monitor.DatabaseMonitor;
import com.bgycc.smartcanteen.monitor.LogFileMonitor;
import java.util.concurrent.TimeUnit;
public class MonitorUtils {
private static final String LOG_MONITOR_WORKER = "worker_log_monitor";
private static final String DATABASE_MONITOR_WORKER = "database_log_monitor";
private static final int LOG_REPEAT_INTERVAL = 4;
private static final int DATABASE_REPEAT_INTERVAL = 12;
// 每隔一定时间检查日志文件夹并进行清理
public static void startLogFilesMonitor(Context context) {
stopLogFilesMonitor(context);
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(LogFileMonitor.class, LOG_REPEAT_INTERVAL, TimeUnit.HOURS)
.addTag(LOG_MONITOR_WORKER)
.build();
WorkManager.getInstance(context)
.enqueue(request);
}
public static void stopLogFilesMonitor(Context context) {
WorkManager.getInstance(context)
.cancelAllWorkByTag(LOG_MONITOR_WORKER);
}
// 每隔一定时间检查数据库并进行清理
public static void startDatabaseMonitor(Context context) {
stopDatabaseMonitor(context);
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(DatabaseMonitor.class, DATABASE_REPEAT_INTERVAL, TimeUnit.HOURS)
.addTag(DATABASE_MONITOR_WORKER)
.build();
WorkManager.getInstance(context)
.enqueue(request);
}
public static void stopDatabaseMonitor(Context context) {
WorkManager.getInstance(context)
.cancelAllWorkByTag(DATABASE_MONITOR_WORKER);
}
}
......@@ -253,6 +253,10 @@ public class NetworkUtils {
return true;
}
public static boolean disconnect(int netId) {
return wifiManager.removeNetwork(netId);
}
private static WifiConfiguration createWifiConfiguration(String ssid, String identity, String pwd, String type) {
if(ssid == null || ssid.isEmpty())
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;
}
}
}
package com.bgycc.smartcanteen.viewModel;
import android.text.TextUtils;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import androidx.work.OneTimeWorkRequest;
import com.bgycc.smartcanteen.command.CommandProgressCallback;
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.command.CommandHelper;
import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.CommandResponse;
import com.bgycc.smartcanteen.executor.SCTaskExecutor;
import com.bgycc.smartcanteen.socket.SCWebSocketClient;
import com.bgycc.smartcanteen.socket.SCWebSocketListener;
import com.bgycc.smartcanteen.socket.SCWebSocketListenerAdapter;
import com.bgycc.smartcanteen.state.CommandState;
import com.bgycc.smartcanteen.repository.CommandRepository;
import com.blankj.utilcode.util.LogUtils;
import com.google.gson.Gson;
......@@ -30,21 +23,19 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/**
* 监听本地扫码、服务器下发的设置指令 <br/>
* 设备指令需要匹配以下规则: <br/>
* 1、action非"PAY_RESULT" <br/><br/>
* 监听Command数据库的变动,按如下流程进行处理: <br/>
* 从数据库读取 -> 解析执行 -> 更新到数据库
*/
public class CommandViewModel extends ViewModel implements CommandProgressCallback {
public class CommandViewModel extends ViewModel {
private CommandRepository commandRepository;
private Gson gson;
private String deviceSN;
private MutableLiveData<CommandState> commandState = new MutableLiveData<>();
private MutableLiveData<Command> commandWorker = new MutableLiveData<>();
private LiveData<List<Command>> dataLiveData;
public LiveData<CommandState> getCommandStateEvent() {
return commandState;
public LiveData<Command> getCommandTask() {
return commandWorker;
}
public CommandViewModel(CommandRepository commandRepository, Gson gson, String deviceSN) {
......@@ -54,7 +45,6 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
// 监听数据库的变动,并执行未完成的指令
this.dataLiveData = commandRepository.queryUndoneCommand();
this.dataLiveData.observeForever(dataObserver);
this.commandState.postValue(new CommandState(CommandState.IDLE));
}
public void initialize() {
......@@ -71,106 +61,40 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
private Observer<List<Command>> dataObserver = commands -> {
if (commands == null || commands.isEmpty()) {
commandState.postValue(new CommandState(CommandState.IDLE));
return;
}
Command first = commands.get(0);
RequestRunnable runnable = new RequestRunnable(first);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
commandWorker.postValue(first);
};
@Override
public void idle(String message, int progress) {
commandState.postValue(new CommandState(CommandState.IDLE, message, progress));
}
@Override
public void wait(String message, int progress) {
commandState.postValue(new CommandState(CommandState.WAIT, message, progress));
}
@Override
public void success(String message, int progress) {
commandState.postValue(new CommandState(CommandState.SUCCESS, message, progress));
public OneTimeWorkRequest getCommandWorker(Command command) {
return CommandHelper.createWorker(gson, command, deviceSN);
}
@Override
public void failed(String message, int progress) {
commandState.postValue(new CommandState(CommandState.FAILED, message, progress));
public void commandFinish(Command command) {
UpdateDatabaseRunnable runnable = new UpdateDatabaseRunnable(command);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
}
private SCWebSocketListener listener = new SCWebSocketListenerAdapter() {
private static final String RESPONSE_PAY_RESULT = "PAY_RESULT";
@Override
public void onMessage(String action, JsonObject obj, String original) {
// 设备指令需要匹配以下规则:
// 1、action非"PAY_RESULT"
if (TextUtils.isEmpty(action) || action.equals(RESPONSE_PAY_RESULT)) return;
if (!CommandHelper.isCommand(action)) return;
LogUtils.d(TAG, "设备下发指令: " + original);
ResponseRunnable runnable = new ResponseRunnable(original, action);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
}
};
private class RequestRunnable implements Runnable {
private class UpdateDatabaseRunnable implements Runnable {
private Command command;
RequestRunnable(Command command) {
UpdateDatabaseRunnable(Command command) {
this.command = command;
}
@Override
public void run() {
CommandHandler handler = null;
LogUtils.d(TAG, "开始执行指令: " + command.toString());
try {
switch (command.getAction()) {
case Command.LOG_UPLOAD:
handler = LogCommandHandler.getInstance(command, gson, deviceSN,CommandViewModel.this);
break;
case Command.APP_UPDATE:
handler = UpdateCommandHandler.getInstance(command, gson, CommandViewModel.this);
break;
case Command.CONFIG_WIFI:
handler = WifiConfigCommandHandler.getInstance(command, gson, CommandViewModel.this);
break;
case Command.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();
LogUtils.d(TAG, "指令执行完毕: " + command.toString());
commandRepository.updateCommand(command);
......@@ -190,6 +114,14 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba
public void run() {
// 指令插入到数据库,则会触发dataObserver
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);
if (lastInsertId == -1) {
LogUtils.w(TAG, "指令插入到数据库失败: " + command.toString());
......
......@@ -37,7 +37,7 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
* 支付状态(空闲、发送订单信息、支付中、支付成功、支付失败)都会通过{@link PayOnlineState}发出通知 <br/><br/>
*/
public class PayOnlineViewModel extends ViewModel {
private static final long TIMEOUT = 5 * 1000;
private static final long TIMEOUT = 10 * 1000;
// 在线支付延迟150ms执行,留出时间给扫码反馈
private static final long REQUEST_DELAY = 150;
private static final long DEFAULT_DELAY = 3 * 1000;
......
buildscript {
ext {
daemon_verson_code = 10
daemon_verson_name = "1.0"
daemon_verson_code = 11
daemon_verson_name = "1.1"
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath 'com.android.tools.build:gradle:3.6.3'
}
}
......@@ -21,6 +21,28 @@ allprojects {
}
}
// 保证daemon会先打包到app的assets文件夹内
task clean(type: Delete) {
delete rootProject.buildDir
Task appPreBuildTask
Task daemonBuildTask
subprojects.forEach { project ->
if (project.name == "app") {
Set<Task> appPreBuildTasks = project.getTasksByName("preBuild", false)
if (!appPreBuildTasks.isEmpty()) {
appPreBuildTask = appPreBuildTasks[0]
}
} else if (project.name == "daemon") {
Set<Task> daemonAssembleReleaseTasks = project.getTasksByName("assembleRelease", false)
if (!daemonAssembleReleaseTasks.isEmpty()) {
daemonBuildTask = daemonAssembleReleaseTasks[0]
}
}
}
if (appPreBuildTask != null && daemonBuildTask != null) {
appPreBuildTask.dependsOn(daemonBuildTask)
}
}
......@@ -100,5 +100,5 @@ dependencies {
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
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"
package="com.bgycc.smartcanteen.daemon">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
......
......@@ -12,18 +12,20 @@ import java.io.File;
public class ServiceApplication extends Application {
private static final String LOG_PREFIX = "daemon";
private static final String LOG_DIR = "log";
// 日志文件保留周期
private static final int LOG_SAVE_DAYS = 7;
@Override
public void onCreate() {
super.onCreate();
Utils.init(getApplicationContext());
CrashUtils.init();
Utils.init(this);
String logDir = PathUtils.getExternalStoragePath() + File.separator + LOG_DIR;
CrashUtils.init(logDir);
LogUtils.getConfig()
.setDir(logDir)
.setLog2FileSwitch(true)
.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