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 { ...@@ -7,28 +7,14 @@ android {
applicationId "com.bgycc.smartcanteen" applicationId "com.bgycc.smartcanteen"
minSdkVersion 22 minSdkVersion 22
targetSdkVersion 29 targetSdkVersion 29
versionCode 13 versionCode 16
versionName "1.3.3" versionName "1.3.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk { ndk {
abiFilters "armeabi", "armeabi-v7a", "x86", "mips" abiFilters "armeabi", "armeabi-v7a", "x86", "mips"
} }
buildConfigField "int", "DaemonVersion", String.valueOf(rootProject.ext.daemon_verson_code) 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 { buildTypes {
debug { debug {
minifyEnabled false minifyEnabled false
...@@ -67,6 +53,11 @@ android { ...@@ -67,6 +53,11 @@ android {
buildConfigField "String", "MainWebSocketServerUrl", '"wss://diningservice.bgy.com.cn/websocket/%s/V%s"' buildConfigField "String", "MainWebSocketServerUrl", '"wss://diningservice.bgy.com.cn/websocket/%s/V%s"'
buildConfigField "String", "MainHttpServerHost", '"https://diningback.bgy.com.cn"' 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 { signingConfigs {
canteen { canteen {
...@@ -111,26 +102,29 @@ android { ...@@ -111,26 +102,29 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) 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.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
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 "org.java-websocket:Java-WebSocket:1.4.0" implementation "org.java-websocket:Java-WebSocket:1.5.1"
implementation 'com.blankj:utilcodex:1.26.0' implementation 'com.blankj:utilcodex:1.28.4'
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'
implementation 'com.king.zxing:zxing-lite:1.1.6-androidx' implementation 'com.king.zxing:zxing-lite:1.1.6-androidx'
implementation 'com.squareup.okhttp3:okhttp:4.3.1' def okhttp_version = "4.6.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1' implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation 'com.squareup.retrofit2:retrofit:2.7.1' implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation 'com.squareup.retrofit2:converter-gson:2.7.1' def retrofit_version = "2.8.1"
implementation 'com.squareup.retrofit2:converter-scalars:2.7.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' 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"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.bgycc.smartcanteen"> package="com.bgycc.smartcanteen">
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
...@@ -24,7 +25,8 @@ ...@@ -24,7 +25,8 @@
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".activity.MainActivity" <activity android:name=".activity.MainActivity"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:screenOrientation="landscape"> android:screenOrientation="landscape"
tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
...@@ -51,6 +53,11 @@ ...@@ -51,6 +53,11 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove"
android:exported="false" />
</application> </application>
</manifest> </manifest>
\ No newline at end of file
...@@ -2,6 +2,10 @@ package com.bgycc.smartcanteen; ...@@ -2,6 +2,10 @@ package com.bgycc.smartcanteen;
import android.app.Application; 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.CrashUtils;
import com.blankj.utilcode.util.LogUtils; import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PathUtils; import com.blankj.utilcode.util.PathUtils;
...@@ -9,20 +13,31 @@ import com.blankj.utilcode.util.Utils; ...@@ -9,20 +13,31 @@ import com.blankj.utilcode.util.Utils;
import java.io.File; 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_PREFIX = "app";
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);
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);
}
@NonNull
@Override
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setExecutor(SCTaskExecutor.getIOThreadExecutor())
.build();
} }
} }
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.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
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;
...@@ -28,10 +35,11 @@ import android.widget.TextView; ...@@ -28,10 +35,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;
...@@ -39,6 +47,7 @@ import com.bgycc.smartcanteen.state.QRCodeState; ...@@ -39,6 +47,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.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;
...@@ -84,6 +93,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -84,6 +93,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());
...@@ -120,6 +130,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -120,6 +130,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);
...@@ -259,23 +270,86 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -259,23 +270,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;
}
} }
}); });
...@@ -286,6 +360,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -286,6 +360,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
commandViewModel.initialize(); commandViewModel.initialize();
handler.post(updateTimeRunnable); handler.post(updateTimeRunnable);
MonitorUtils.startLogFilesMonitor(this);
MonitorUtils.startDatabaseMonitor(this);
SCWebSocketClient.getInstance().tryConnect(); SCWebSocketClient.getInstance().tryConnect();
} }
...@@ -362,6 +438,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -362,6 +438,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();
MonitorUtils.stopDatabaseMonitor(this);
MonitorUtils.stopLogFilesMonitor(this);
} }
@Override @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; 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");
} }
} }
package com.bgycc.smartcanteen.data; package com.bgycc.smartcanteen.data;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.room.Database; import androidx.room.Database;
...@@ -11,7 +8,6 @@ import androidx.room.DatabaseConfiguration; ...@@ -11,7 +8,6 @@ import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker; import androidx.room.InvalidationTracker;
import androidx.room.Room; import androidx.room.Room;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.SupportSQLiteOpenHelper;
import com.bgycc.smartcanteen.data.dao.CommandDao; import com.bgycc.smartcanteen.data.dao.CommandDao;
...@@ -20,25 +16,17 @@ import com.bgycc.smartcanteen.data.dao.PayResponseDao; ...@@ -20,25 +16,17 @@ import com.bgycc.smartcanteen.data.dao.PayResponseDao;
import com.bgycc.smartcanteen.entity.Command; import com.bgycc.smartcanteen.entity.Command;
import com.bgycc.smartcanteen.entity.PayData; import com.bgycc.smartcanteen.entity.PayData;
import com.bgycc.smartcanteen.entity.PayResponse; import com.bgycc.smartcanteen.entity.PayResponse;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.Utils; import com.blankj.utilcode.util.Utils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@Database( @Database(
entities = { entities = {
Command.class, Command.class,
PayData.class, PayData.class,
PayResponse.class PayResponse.class
}, },
version = 1, version = 2,
exportSchema = false) exportSchema = false)
public abstract class DatabaseManager extends RoomDatabase { public abstract class DatabaseManager extends RoomDatabase {
private static final String TAG = "SmartCanteen_Database";
private static final int UPDATE_MAX_TIMES = 5;
@NonNull @NonNull
@Override @Override
...@@ -72,70 +60,15 @@ public abstract class DatabaseManager extends RoomDatabase { ...@@ -72,70 +60,15 @@ public abstract class DatabaseManager extends RoomDatabase {
context.getApplicationContext(), context.getApplicationContext(),
DatabaseManager.class, DatabaseManager.class,
DB_NAME) DB_NAME)
.addCallback(new Callback() { .addCallback(new OldDataCompatible())
@Override .addCallback(new ForceToPayOffline())
public void onOpen(@NonNull SupportSQLiteDatabase db) { .addMigrations(new _1To_2Migration())
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);
}
})
// 一旦版本不兼容则清空数据库 // 一旦版本不兼容则清空数据库
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.allowMainThreadQueries() .allowMainThreadQueries()
.build(); .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 CommandDao getCommandDao();
public abstract PayDataDao getPayDataDao(); 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 { ...@@ -17,9 +17,21 @@ 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);
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)
int updateCommand(Command command); 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; ...@@ -13,10 +13,12 @@ import java.util.List;
@Dao @Dao
public interface PayDataDao { public interface PayDataDao {
/** /**
* 获取所有需要离线支付的订单 * 获取需要离线支付的订单(同时1h内未上传过的)
*/ */
@Query("select * from paydata where payState == -1") @Query("select * from paydata " +
List<PayData> queryPayOfflineData(); "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 { ...@@ -32,4 +34,12 @@ public interface PayDataDao {
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)
int updatePayData(List<PayData> data); 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; ...@@ -3,6 +3,7 @@ package com.bgycc.smartcanteen.data.dao;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.OnConflictStrategy; import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.bgycc.smartcanteen.entity.PayResponse; import com.bgycc.smartcanteen.entity.PayResponse;
...@@ -10,4 +11,12 @@ import com.bgycc.smartcanteen.entity.PayResponse; ...@@ -10,4 +11,12 @@ import com.bgycc.smartcanteen.entity.PayResponse;
public interface PayResponseDao { public interface PayResponseDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
long insertPayResponse(PayResponse response); 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; package com.bgycc.smartcanteen.entity;
import androidx.annotation.StringDef;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
...@@ -9,14 +8,6 @@ import java.util.Objects; ...@@ -9,14 +8,6 @@ import java.util.Objects;
@Entity(tableName = Command.TABLE_NAME) @Entity(tableName = Command.TABLE_NAME)
public class Command { public class Command {
public static final String TABLE_NAME = "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) @PrimaryKey(autoGenerate = true)
private long id; private long id;
...@@ -28,7 +19,7 @@ public class Command { ...@@ -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.data = command;
this.action = action; this.action = action;
this.finish = false; this.finish = false;
......
...@@ -28,6 +28,8 @@ public class PayData { ...@@ -28,6 +28,8 @@ public class PayData {
private String payCode = ""; private String payCode = "";
private String terminalType; private String terminalType;
private long time; private long time;
// 记录订单发到后台的时间
private long uploadTime;
/** /**
* 1 -> 支付成功(在线支付/离线支付成功后标记) * 1 -> 支付成功(在线支付/离线支付成功后标记)
* 0 -> 在线支付(任务创建时默认标记) * 0 -> 在线支付(任务创建时默认标记)
...@@ -46,6 +48,7 @@ public class PayData { ...@@ -46,6 +48,7 @@ public class PayData {
this.terminalType = terminalType; this.terminalType = terminalType;
this.time = System.currentTimeMillis() / 1000; this.time = System.currentTimeMillis() / 1000;
this.payState = PAY_ONLINE; this.payState = PAY_ONLINE;
this.uploadTime = 0;
} }
public boolean success() { public boolean success() {
...@@ -112,6 +115,14 @@ public class PayData { ...@@ -112,6 +115,14 @@ public class PayData {
this.time = time; this.time = time;
} }
public long getUploadTime() {
return uploadTime;
}
public void setUploadTime(long uploadTime) {
this.uploadTime = uploadTime;
}
public int getPayState() { public int getPayState() {
return payState; return payState;
} }
...@@ -124,17 +135,18 @@ public class PayData { ...@@ -124,17 +135,18 @@ public class PayData {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
PayData data = (PayData) o; PayData payData = (PayData) o;
return time == data.time && return time == payData.time &&
payState == data.payState && uploadTime == payData.uploadTime &&
Objects.equals(equipmentNo, data.equipmentNo) && payState == payData.payState &&
Objects.equals(payCode, data.payCode) && Objects.equals(equipmentNo, payData.equipmentNo) &&
Objects.equals(terminalType, data.terminalType); payCode.equals(payData.payCode) &&
Objects.equals(terminalType, payData.terminalType);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(equipmentNo, payCode, terminalType, time, payState); return Objects.hash(equipmentNo, payCode, terminalType, time, uploadTime, payState);
} }
@Override @Override
...@@ -144,6 +156,7 @@ public class PayData { ...@@ -144,6 +156,7 @@ public class PayData {
", payCode='" + payCode + '\'' + ", payCode='" + payCode + '\'' +
", terminalType='" + terminalType + '\'' + ", terminalType='" + terminalType + '\'' +
", time=" + time + ", time=" + time +
", uploadTime=" + uploadTime +
", payState=" + payState + ", payState=" + payState +
'}'; '}';
} }
......
...@@ -22,7 +22,7 @@ public class DefaultTaskExecutor extends TaskExecutor { ...@@ -22,7 +22,7 @@ public class DefaultTaskExecutor extends TaskExecutor {
private final Object mLock = new Object(); private final Object mLock = new Object();
private final ScheduledExecutorService mThread = Executors.newScheduledThreadPool(6, new ThreadFactory() { 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); 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 { ...@@ -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);
} }
...@@ -25,4 +29,12 @@ public class CommandRepository { ...@@ -25,4 +29,12 @@ public class CommandRepository {
public int updateCommand(Command command) { public int updateCommand(Command command) {
return dao.updateCommand(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 { ...@@ -12,8 +12,8 @@ public class PayDataRepository {
this.dao = dao; this.dao = dao;
} }
public List<PayData> queryPayOfflineData() { public List<PayData> queryPayOfflineData(long currentTime, int limit) {
return dao.queryPayOfflineData(); return dao.queryPayOfflineData(currentTime, limit);
} }
public PayData queryPayDataByPayCode(String payCode) { public PayData queryPayDataByPayCode(String payCode) {
...@@ -31,4 +31,12 @@ public class PayDataRepository { ...@@ -31,4 +31,12 @@ public class PayDataRepository {
public int updatePayData(PayData data) { public int updatePayData(PayData data) {
return dao.updatePayData(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 { ...@@ -13,4 +13,12 @@ public class PayResponseRepository {
public long insertPayResponse(PayResponse response) { public long insertPayResponse(PayResponse response) {
return dao.insertPayResponse(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; ...@@ -30,6 +30,8 @@ import java.util.List;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLParameters;
import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/** /**
...@@ -157,6 +159,9 @@ public class SCWebSocketClient extends WebSocketClient { ...@@ -157,6 +159,9 @@ public class SCWebSocketClient extends WebSocketClient {
@Override @Override
public void onMessage(String response) { public void onMessage(String response) {
// 由于项目设计阶段没有规范返回的结果
// 导致做统一处理比较麻烦(需要考虑到很多情况)
// 因此目前只采取通过JsonObject来逐个字段判断的低效方案
JsonObject jsonObject; JsonObject jsonObject;
String action = ""; String action = "";
try { try {
...@@ -193,7 +198,7 @@ public class SCWebSocketClient extends WebSocketClient { ...@@ -193,7 +198,7 @@ public class SCWebSocketClient extends WebSocketClient {
@Override @Override
public void onClose(int code, String reason, boolean remote) { 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)); connectState.postValue(new ConnectState(ConnectState.OFFLINE));
for (SCWebSocketListener l : listener) { for (SCWebSocketListener l : listener) {
l.onClose(code, reason, remote); l.onClose(code, reason, remote);
...@@ -241,6 +246,9 @@ public class SCWebSocketClient extends WebSocketClient { ...@@ -241,6 +246,9 @@ public class SCWebSocketClient extends WebSocketClient {
Heartbeat request = new Heartbeat(deviceSN); Heartbeat request = new Heartbeat(deviceSN);
String requestStr = gson.toJson(request); String requestStr = gson.toJson(request);
send(requestStr); send(requestStr);
for (SCWebSocketListener l : listener) {
l.onHeartbeat();
}
} }
}; };
...@@ -265,4 +273,10 @@ public class SCWebSocketClient extends WebSocketClient { ...@@ -265,4 +273,10 @@ public class SCWebSocketClient extends WebSocketClient {
NetworkUtils.bindProcessToNetwork(network); 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 { ...@@ -11,6 +11,8 @@ public interface SCWebSocketListener {
void onClose(int code, String reason, boolean remote); void onClose(int code, String reason, boolean remote);
void onHeartbeat();
void onError(Exception ex); void onError(Exception ex);
void onReconnect(); void onReconnect();
......
...@@ -22,6 +22,11 @@ public class SCWebSocketListenerAdapter implements SCWebSocketListener { ...@@ -22,6 +22,11 @@ public class SCWebSocketListenerAdapter implements SCWebSocketListener {
} }
@Override @Override
public void onHeartbeat() {
}
@Override
public void onError(Exception ex) { 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 { ...@@ -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;
}
}
}
package com.bgycc.smartcanteen.viewModel; package com.bgycc.smartcanteen.viewModel;
import android.text.TextUtils;
import androidx.lifecycle.LiveData; 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;
...@@ -30,21 +23,19 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; ...@@ -30,21 +23,19 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/** /**
* 监听本地扫码、服务器下发的设置指令 <br/> * 监听本地扫码、服务器下发的设置指令 <br/>
* 设备指令需要匹配以下规则: <br/>
* 1、action非"PAY_RESULT" <br/><br/>
* 监听Command数据库的变动,按如下流程进行处理: <br/> * 监听Command数据库的变动,按如下流程进行处理: <br/>
* 从数据库读取 -> 解析执行 -> 更新到数据库 * 从数据库读取 -> 解析执行 -> 更新到数据库
*/ */
public class CommandViewModel extends ViewModel implements CommandProgressCallback { public class CommandViewModel extends ViewModel {
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) {
...@@ -54,7 +45,6 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -54,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() {
...@@ -71,106 +61,40 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -71,106 +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 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));
} }
@Override public void commandFinish(Command command) {
public void failed(String message, int progress) { UpdateDatabaseRunnable runnable = new UpdateDatabaseRunnable(command);
commandState.postValue(new CommandState(CommandState.FAILED, message, progress)); SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
} }
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 (!CommandHelper.isCommand(action)) return;
// 1、action非"PAY_RESULT"
if (TextUtils.isEmpty(action) || action.equals(RESPONSE_PAY_RESULT)) 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 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(); command.finish();
LogUtils.d(TAG, "指令执行完毕: " + command.toString()); LogUtils.d(TAG, "指令执行完毕: " + command.toString());
commandRepository.updateCommand(command); commandRepository.updateCommand(command);
...@@ -190,6 +114,14 @@ public class CommandViewModel extends ViewModel implements CommandProgressCallba ...@@ -190,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());
......
...@@ -30,14 +30,14 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; ...@@ -30,14 +30,14 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
/** /**
* 负责处理离线支付订单 <br/> * 负责处理离线支付订单 <br/>
* 每当WebSocket建立链接时,都搜索数据库所有标记需要"离线支付"的订单 <br/> * 每当WebSocket建立链接时,都会搜索数据库标记为"离线支付"(即{@link PayData#getPayState()})为{@link PayData#offline()}的订单 <br/>
* 将其{@link PayData#getPayCode()}打包发送到后台 <br/> * 将所有搜索到的订单信息组装为{@link PayRequest}发送到后台 <br/>
* 当后台响应操作后,更新待处理离线订单状态为"支付成功" <br/> * 后台会在接收到订单时下发"操作完成"通知,此时更新订单的{@link PayData#getUploadTime()} <br/>
* 支付状态(标记成功、空闲、发送订单信息、支付成功、支付失败)都会通过{@link PayOfflineState}发出通知 <br/><br/> * 后台会在离线订单写入到数据库时下发"写入成功"通知,此时更新订单状态为"支付成功" <br/>
* "标记成功"状态: 在WebSocket未链接时,"在线支付"订单直接标记为"离线支付",并更新到数据库成功的状态
*/ */
public class PayOfflineViewModel extends ViewModel { public class PayOfflineViewModel extends ViewModel {
private static final long TIMEOUT = 5 * 1000; // 离线支付的超时时长需要被在线支付更长(因为发送的订单量更多)
private static final long TIMEOUT = 30 * 1000;
// 在线支付延迟150ms执行,留出时间给扫码反馈 // 在线支付延迟150ms执行,留出时间给扫码反馈
private static final long REQUEST_DELAY = 150; private static final long REQUEST_DELAY = 150;
private static final long DEFAULT_DELAY = 3 * 1000; private static final long DEFAULT_DELAY = 3 * 1000;
...@@ -53,6 +53,7 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -53,6 +53,7 @@ public class PayOfflineViewModel extends ViewModel {
} }
private PayRequest payRequest; private PayRequest payRequest;
private ScheduledFuture<?> payOfflineFuture;
private ScheduledFuture<?> timeoutFuture; private ScheduledFuture<?> timeoutFuture;
public PayOfflineViewModel(PayDataRepository payDataRepository, public PayOfflineViewModel(PayDataRepository payDataRepository,
...@@ -74,6 +75,21 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -74,6 +75,21 @@ public class PayOfflineViewModel extends ViewModel {
SCTaskExecutor.getInstance().schedule(runnable, REQUEST_DELAY, TimeUnit.MILLISECONDS); SCTaskExecutor.getInstance().schedule(runnable, REQUEST_DELAY, TimeUnit.MILLISECONDS);
} }
private void traversalPayOfflineData() {
cancelPayOffline();
cancelTimeout();
TimeoutRunnable timeoutRunnable = new TimeoutRunnable();
timeoutFuture = SCTaskExecutor.getInstance().schedule(timeoutRunnable, TIMEOUT, TimeUnit.MILLISECONDS);
RequestRunnable runnable = new RequestRunnable();
payOfflineFuture = SCTaskExecutor.getInstance().schedule(runnable, 0, TimeUnit.MILLISECONDS);
}
private void cancelPayOffline() {
if (payOfflineFuture == null) return;
payOfflineFuture.cancel(true);
payOfflineFuture = null;
}
private void cancelTimeout() { private void cancelTimeout() {
if (timeoutFuture == null) return; if (timeoutFuture == null) return;
timeoutFuture.cancel(true); timeoutFuture.cancel(true);
...@@ -83,34 +99,69 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -83,34 +99,69 @@ public class PayOfflineViewModel extends ViewModel {
private SCWebSocketListener listener = new SCWebSocketListenerAdapter() { private SCWebSocketListener listener = new SCWebSocketListenerAdapter() {
private static final String RESPONSE_MESSAGE = "message"; private static final String RESPONSE_MESSAGE = "message";
private static final String RESPONSE_OFFLINE_RESULT = "操作完成"; private static final String RESPONSE_OFFLINE_RESULT = "操作完成";
private static final String RESPONSE_PAY_OFFLINE_RESULT = "PAY_OFFLINE_RESULT";
private static final String RESPONSE_SUCCESS = "1";
// 每心跳多少次后进行一次离线订单检测
private static final int PAY_OFFLINE_CHECK = 30;
// 当链接频繁断开时,给予离线订单检测一定缓冲时间
private static final long PAY_OFFLINE_INTERVAL = 30 * 60 * 1000;
private long lastPayTime = -1;
private int heartbeatCount = 0;
@Override @Override
public void onOpen(ServerHandshake data) { public void onOpen(ServerHandshake data) {
// 只要Socket链接成功,则搜索数据库需要离线支付的订单,并发送给后台 heartbeatCount = 0;
cancelTimeout(); long currentTime = System.currentTimeMillis();
TimeoutRunnable timeoutRunnable = new TimeoutRunnable(); if ((currentTime - lastPayTime) < PAY_OFFLINE_INTERVAL) {
timeoutFuture = SCTaskExecutor.getInstance().schedule(timeoutRunnable, TIMEOUT, TimeUnit.MILLISECONDS); LogUtils.w(TAG, "离线检测过于频繁,可能是链接频繁断开导致");
RequestRunnable runnable = new RequestRunnable(); return;
SCTaskExecutor.getInstance().executeOnDiskIO(runnable); }
lastPayTime = currentTime;
traversalPayOfflineData();
}
@Override
public void onHeartbeat() {
// 在一定心跳次数后开始检查离线订单是否已处理完毕
heartbeatCount++;
if (heartbeatCount >= PAY_OFFLINE_CHECK) {
heartbeatCount = 0;
traversalPayOfflineData();
}
} }
@Override @Override
public void onMessage(String action, JsonObject obj, String original) { public void onMessage(String action, JsonObject obj, String original) {
if (action.equals(RESPONSE_PAY_OFFLINE_RESULT)) {
String code = null;
// 这里为了兼容新的消息体需要单独对code作判断
if (obj.has("code")) {
code = obj.get("code").getAsString();
}
if (code != null && code.equals(RESPONSE_SUCCESS)) {
// 离线订单已写入到服务器数据库,可以标记为支付成功
LogUtils.d(TAG, "离线支付结果响应: " + original);
ResponseRunnable runnable = new ResponseRunnable(original);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
}
return;
}
String message = ""; String message = "";
if (obj.has(RESPONSE_MESSAGE)) { if (obj.has(RESPONSE_MESSAGE)) {
message = obj.get(RESPONSE_MESSAGE).getAsString(); message = obj.get(RESPONSE_MESSAGE).getAsString();
} }
if (!TextUtils.isEmpty(action) || !message.equals(RESPONSE_OFFLINE_RESULT)) return; if (!TextUtils.isEmpty(action) || !message.equals(RESPONSE_OFFLINE_RESULT)) return;
// 离线支付结果需要匹配以下规则: // 离线订单只上传到服务器,未写入到服务器数据库
// 1、没有action // 此时只需要更改其uploadTime(保证一定时间内不会频繁发送到服务器)
// 2、message为"操作完成" UpdateUploadTimeRunnable runnable = new UpdateUploadTimeRunnable();
LogUtils.d(TAG, "离线支付结果响应: " + original);
cancelTimeout();
ResponseRunnable runnable = new ResponseRunnable(original);
SCTaskExecutor.getInstance().executeOnDiskIO(runnable); SCTaskExecutor.getInstance().executeOnDiskIO(runnable);
} }
}; };
// 当支付时WebSocket未链接,则走离线支付流程
// 此时直接标记订单为"离线支付"状态
private class MarkRunnable implements Runnable { private class MarkRunnable implements Runnable {
private PayData payData; private PayData payData;
...@@ -124,7 +175,7 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -124,7 +175,7 @@ public class PayOfflineViewModel extends ViewModel {
int result = payDataRepository.updatePayData(payData); int result = payDataRepository.updatePayData(payData);
if (result <= 0) { if (result <= 0) {
payOfflineState.postValue(new PayOfflineState(PayOfflineState.MARK_FAILED)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.MARK_FAILED));
LogUtils.w(TAG, "WebSocket未链接, 订单标记失败: " + payData.toString()); LogUtils.w(TAG, "WebSocket未链接, 订单标记失败: " + payData.toString());
return; return;
} }
payOfflineState.postValue(new PayOfflineState(PayOfflineState.MARK)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.MARK));
...@@ -139,16 +190,21 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -139,16 +190,21 @@ public class PayOfflineViewModel extends ViewModel {
} }
private class RequestRunnable implements Runnable { private class RequestRunnable implements Runnable {
// 离线支付每一次上传的订单量(若上传数据量过大服务器会拒绝)
private static final int PAY_OFFLINE_PER_LIMIT = 10;
@Override @Override
public void run() { public void run() {
long currentTime = System.currentTimeMillis();
// 获取需要离线支付的订单 // 获取需要离线支付的订单
List<PayData> payOfflineData = payDataRepository.queryPayOfflineData(); List<PayData> payOfflineData = payDataRepository.queryPayOfflineData(currentTime, PAY_OFFLINE_PER_LIMIT);
if (payOfflineData == null || payOfflineData.isEmpty()) { if (payOfflineData == null || payOfflineData.isEmpty()) {
cancelTimeout(); cancelTimeout();
payRequest = null; payRequest = null;
LogUtils.d(TAG, "所有离线订单处理完毕");
return; return;
} }
payRequest = new PayRequest(deviceSN, payOfflineData); payRequest = new PayRequest(deviceSN, payOfflineData);
String requestStr = gson.toJson(payRequest); String requestStr = gson.toJson(payRequest);
payOfflineState.postValue(new PayOfflineState(PayOfflineState.SEND, requestStr)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.SEND, requestStr));
SCWebSocketClient.getInstance().send(requestStr); SCWebSocketClient.getInstance().send(requestStr);
...@@ -156,6 +212,28 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -156,6 +212,28 @@ public class PayOfflineViewModel extends ViewModel {
} }
} }
private class UpdateUploadTimeRunnable implements Runnable {
@Override
public void run() {
if (payRequest == null || payRequest.getData().isEmpty()) {
LogUtils.w(TAG, "后台返回离线支付上传结果,但没有待处理任务");
return;
}
long currentTime = System.currentTimeMillis();
List<PayData> dataList = payRequest.getData();
StringBuilder ids = new StringBuilder();
for (PayData data : dataList) {
ids.append(data.getPayCode())
.append(",");
data.setUploadTime(currentTime);
}
payDataRepository.updatePayData(dataList);
if (ids.length() > 0) {
LogUtils.d(TAG, "已上传的离线支付订单号: " + ids.substring(0, ids.length() - 1));
}
}
}
private class ResponseRunnable implements Runnable { private class ResponseRunnable implements Runnable {
private String response; private String response;
...@@ -165,6 +243,7 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -165,6 +243,7 @@ public class PayOfflineViewModel extends ViewModel {
@Override @Override
public void run() { public void run() {
cancelTimeout();
if (payRequest == null || payRequest.getData().isEmpty()) { if (payRequest == null || payRequest.getData().isEmpty()) {
LogUtils.w(TAG, "后台返回离线支付结果,但没有待处理任务"); LogUtils.w(TAG, "后台返回离线支付结果,但没有待处理任务");
payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE));
...@@ -189,7 +268,7 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -189,7 +268,7 @@ public class PayOfflineViewModel extends ViewModel {
} }
payDataRepository.updatePayData(dataList); payDataRepository.updatePayData(dataList);
if (ids.length() > 0) { if (ids.length() > 0) {
LogUtils.d(TAG, "待上传的所有离线支付订单号: " + ids.substring(0, ids.length() - 1)); LogUtils.d(TAG, "离线支付订单号: " + ids.substring(0, ids.length() - 1) + " 已保存到服务器数据库");
} }
payRequest = null; payRequest = null;
...@@ -198,6 +277,8 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -198,6 +277,8 @@ public class PayOfflineViewModel extends ViewModel {
} catch (Exception ignored) { } catch (Exception ignored) {
} finally { } finally {
payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE));
// 继续处理下一批离线订单
traversalPayOfflineData();
} }
} }
} }
...@@ -211,6 +292,7 @@ public class PayOfflineViewModel extends ViewModel { ...@@ -211,6 +292,7 @@ public class PayOfflineViewModel extends ViewModel {
payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE)); payOfflineState.postValue(new PayOfflineState(PayOfflineState.IDLE));
return; return;
} }
LogUtils.w(TAG, "离线支付超时: 订单已重新标记为离线支付");
List<PayData> payDataList = payRequest.getData(); List<PayData> payDataList = payRequest.getData();
for (PayData d : payDataList) { for (PayData d : payDataList) {
d.payOffline(); d.payOffline();
......
...@@ -37,7 +37,7 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG; ...@@ -37,7 +37,7 @@ import static com.bgycc.smartcanteen.utils.SmartCanteenUtils.TAG;
* 支付状态(空闲、发送订单信息、支付中、支付成功、支付失败)都会通过{@link PayOnlineState}发出通知 <br/><br/> * 支付状态(空闲、发送订单信息、支付中、支付成功、支付失败)都会通过{@link PayOnlineState}发出通知 <br/><br/>
*/ */
public class PayOnlineViewModel extends ViewModel { public class PayOnlineViewModel extends ViewModel {
private static final long TIMEOUT = 5 * 1000; private static final long TIMEOUT = 10 * 1000;
// 在线支付延迟150ms执行,留出时间给扫码反馈 // 在线支付延迟150ms执行,留出时间给扫码反馈
private static final long REQUEST_DELAY = 150; private static final long REQUEST_DELAY = 150;
private static final long DEFAULT_DELAY = 3 * 1000; private static final long DEFAULT_DELAY = 3 * 1000;
......
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()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.1' classpath 'com.android.tools.build:gradle:3.6.3'
} }
} }
...@@ -21,6 +21,28 @@ allprojects { ...@@ -21,6 +21,28 @@ allprojects {
} }
} }
// 保证daemon会先打包到app的assets文件夹内
task clean(type: Delete) { task clean(type: Delete) {
delete rootProject.buildDir 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 { ...@@ -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