init
This commit is contained in:
9
android/.gitignore
vendored
Normal file
9
android/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
91
android/build.gradle.kts
Normal file
91
android/build.gradle.kts
Normal file
@@ -0,0 +1,91 @@
|
||||
group = "com.xiarui.printer"
|
||||
version = "1.0"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
flatDir {
|
||||
dirs("libs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.xiarui.printer"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
pickFirsts += listOf(
|
||||
"lib/arm64-v8a/libjnidispatch.so",
|
||||
"lib/armeabi-v7a/libjnidispatch.so",
|
||||
"lib/x86/libjnidispatch.so",
|
||||
"lib/x86_64/libjnidispatch.so"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
it.outputs.upToDateWhen { false }
|
||||
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
implementation(files("libs/autoreplyprint.aar"))
|
||||
}
|
||||
|
||||
// Skip AAR bundling — local .aar dependency prevents bundle* tasks from succeeding.
|
||||
// The AAR is available at runtime via flatDir repo; intermediate outputs are sufficient.
|
||||
tasks.whenTaskAdded {
|
||||
if (name.startsWith("bundleDebugAar") || name.startsWith("bundleReleaseAar")) {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
BIN
android/libs/autoreplyprint.aar
Normal file
BIN
android/libs/autoreplyprint.aar
Normal file
Binary file not shown.
8
android/proguard-rules.pro
vendored
Normal file
8
android/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Keep Flutter plugin classes
|
||||
-keep class com.xiarui.printer.** { *; }
|
||||
-keep class io.flutter.embedding.engine.plugins.** { *; }
|
||||
|
||||
# Keep third-party SDKs
|
||||
-keep class com.caysn.autoreplyprint.** { *; }
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-keep class com.lvrenyang.** { *; }
|
||||
1
android/settings.gradle
Normal file
1
android/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'printer'
|
||||
1
android/settings.gradle.kts
Normal file
1
android/settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'printer'
|
||||
3
android/src/main/AndroidManifest.xml
Normal file
3
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.xiarui.printer">
|
||||
</manifest>
|
||||
186
android/src/main/java/com/xiarui/printer/PortHandleManager.java
Normal file
186
android/src/main/java/com/xiarui/printer/PortHandleManager.java
Normal file
@@ -0,0 +1,186 @@
|
||||
package com.xiarui.printer;
|
||||
|
||||
import com.caysn.autoreplyprint.AutoReplyPrint;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Manages port handles for the Flutter plugin.
|
||||
* <p>
|
||||
* Provides thread-safe registration, retrieval, and cleanup of native port handles.
|
||||
* Each registered Pointer is assigned a unique integer handle starting from 1.
|
||||
* </p>
|
||||
*/
|
||||
public class PortHandleManager {
|
||||
|
||||
/**
|
||||
* Interface for port operations to enable unit testing without native library.
|
||||
*/
|
||||
public interface PortOperations {
|
||||
/**
|
||||
* Close a port.
|
||||
*
|
||||
* @param handle native pointer to the port
|
||||
* @return true if successfully closed, false otherwise
|
||||
*/
|
||||
boolean closePort(Pointer handle);
|
||||
|
||||
/**
|
||||
* Check if a port is opened.
|
||||
*
|
||||
* @param handle native pointer to the port
|
||||
* @return true if port is open, false otherwise
|
||||
*/
|
||||
boolean isPortOpened(Pointer handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default PortOperations implementation using AutoReplyPrint native SDK.
|
||||
*/
|
||||
private static class NativePortOperations implements PortOperations {
|
||||
@Override
|
||||
public boolean closePort(Pointer handle) {
|
||||
return AutoReplyPrint.INSTANCE.CP_Port_Close(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPortOpened(Pointer handle) {
|
||||
return AutoReplyPrint.INSTANCE.CP_Port_IsOpened(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
private static volatile PortHandleManager instance;
|
||||
|
||||
// Handle registry: integer handle -> native Pointer
|
||||
private final ConcurrentHashMap<Integer, Pointer> handleRegistry = new ConcurrentHashMap<>();
|
||||
|
||||
// Atomic counter starting from 1
|
||||
private final AtomicInteger counter = new AtomicInteger(1);
|
||||
|
||||
// Port operations abstraction for testability
|
||||
private final PortOperations portOps;
|
||||
|
||||
/**
|
||||
* Private constructor. Use {@link #getInstance()} for production,
|
||||
* or inject custom PortOperations for testing.
|
||||
*/
|
||||
private PortHandleManager() {
|
||||
this(new NativePortOperations());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with injectable PortOperations for testing.
|
||||
*
|
||||
* @param portOps port operations implementation
|
||||
*/
|
||||
PortHandleManager(PortOperations portOps) {
|
||||
this.portOps = portOps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance using native AutoReplyPrint operations.
|
||||
*
|
||||
* @return singleton PortHandleManager
|
||||
*/
|
||||
public static PortHandleManager getInstance() {
|
||||
PortHandleManager localInstance = instance;
|
||||
if (localInstance == null) {
|
||||
synchronized (PortHandleManager.class) {
|
||||
localInstance = instance;
|
||||
if (localInstance == null) {
|
||||
instance = localInstance = new PortHandleManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return localInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a native port pointer and returns a unique integer handle.
|
||||
* <p>
|
||||
* If the pointer is null or has a zero native value, registration fails
|
||||
* and returns -1.
|
||||
* </p>
|
||||
*
|
||||
* @param pointer native port pointer to register
|
||||
* @return positive integer handle on success, -1 on failure
|
||||
*/
|
||||
public int registerHandle(Pointer pointer) {
|
||||
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
|
||||
return -1;
|
||||
}
|
||||
int handle = counter.getAndIncrement();
|
||||
handleRegistry.put(handle, pointer);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the native pointer for a registered handle.
|
||||
*
|
||||
* @param handle integer handle to look up
|
||||
* @return the registered Pointer, or null if not found
|
||||
*/
|
||||
public Pointer getHandle(int handle) {
|
||||
return handleRegistry.get(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a registered port and removes it from the registry.
|
||||
* <p>
|
||||
* Uses ConcurrentHashMap.compute() for atomic removal to prevent
|
||||
* race conditions (fixes C-04). The port is only closed if it was
|
||||
* successfully removed from the registry, preventing double-close.
|
||||
* </p>
|
||||
*
|
||||
* @param handle integer handle to close
|
||||
* @return true if port was found and closed, false otherwise
|
||||
*/
|
||||
public boolean closeHandle(int handle) {
|
||||
Pointer pointer = handleRegistry.compute(handle, (key, existing) -> null);
|
||||
if (pointer != null) {
|
||||
return portOps.closePort(pointer);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a handle is valid and the underlying port is still opened.
|
||||
*
|
||||
* @param handle integer handle to check
|
||||
* @return true if handle exists and port is opened, false otherwise
|
||||
*/
|
||||
public boolean isHandleValid(int handle) {
|
||||
Pointer pointer = handleRegistry.get(handle);
|
||||
if (pointer == null) {
|
||||
return false;
|
||||
}
|
||||
return portOps.isPortOpened(pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all registered ports and clears the registry.
|
||||
* <p>
|
||||
* Iterates through all registered handles, closing each one.
|
||||
* Called during engine detachment to clean up resources.
|
||||
* </p>
|
||||
*/
|
||||
public void closeAll() {
|
||||
ConcurrentHashMap<Integer, Pointer> snapshot = new ConcurrentHashMap<>(handleRegistry);
|
||||
for (Integer handle : snapshot.keySet()) {
|
||||
closeHandle(handle);
|
||||
}
|
||||
handleRegistry.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying registry for testing purposes only.
|
||||
*
|
||||
* @return the handle registry map
|
||||
*/
|
||||
ConcurrentHashMap<Integer, Pointer> getRegistry() {
|
||||
return handleRegistry;
|
||||
}
|
||||
}
|
||||
887
android/src/main/java/com/xiarui/printer/PrinterPlugin.java
Normal file
887
android/src/main/java/com/xiarui/printer/PrinterPlugin.java
Normal file
@@ -0,0 +1,887 @@
|
||||
package com.xiarui.printer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.caysn.autoreplyprint.AutoReplyPrint;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.WString;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
|
||||
/**
|
||||
* Flutter plugin for printer communication via serial/USB ports.
|
||||
* <p>
|
||||
* Provides MethodChannel handlers for opening, closing, and enumerating
|
||||
* printer ports using the autoreplyprint AAR SDK.
|
||||
* </p>
|
||||
*/
|
||||
public class PrinterPlugin implements FlutterPlugin, MethodCallHandler {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
/// when the Flutter Engine is detached from the Activity
|
||||
private MethodChannel channel;
|
||||
private PortHandleManager portHandleManager;
|
||||
|
||||
/**
|
||||
* Valid range for baud rate: 1200 to 921600.
|
||||
*/
|
||||
private static final int MIN_BAUD_RATE = 1200;
|
||||
private static final int MAX_BAUD_RATE = 921600;
|
||||
|
||||
/**
|
||||
* Valid range for data bits: 5 to 8.
|
||||
*/
|
||||
private static final int MIN_DATA_BITS = 5;
|
||||
private static final int MAX_DATA_BITS = 8;
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
|
||||
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "printer");
|
||||
channel.setMethodCallHandler(this);
|
||||
portHandleManager = PortHandleManager.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
|
||||
switch (call.method) {
|
||||
case "getPlatformVersion":
|
||||
result.success("Android " + android.os.Build.VERSION.RELEASE);
|
||||
break;
|
||||
case "openComPort":
|
||||
handleOpenComPort(call, result);
|
||||
break;
|
||||
case "openUsbPort":
|
||||
handleOpenUsbPort(call, result);
|
||||
break;
|
||||
case "closePort":
|
||||
handleClosePort(call, result);
|
||||
break;
|
||||
case "isPortOpened":
|
||||
handleIsPortOpened(call, result);
|
||||
break;
|
||||
case "enumComPorts":
|
||||
handleEnumComPorts(call, result);
|
||||
break;
|
||||
case "enumUsbPorts":
|
||||
handleEnumUsbPorts(call, result);
|
||||
break;
|
||||
case "setMultiByteMode":
|
||||
handleSetMultiByteMode(call, result);
|
||||
break;
|
||||
case "setMultiByteEncoding":
|
||||
handleSetMultiByteEncoding(call, result);
|
||||
break;
|
||||
case "printText":
|
||||
handlePrintText(call, result);
|
||||
break;
|
||||
case "setAlignment":
|
||||
handleSetAlignment(call, result);
|
||||
break;
|
||||
case "setTextScale":
|
||||
handleSetTextScale(call, result);
|
||||
break;
|
||||
case "setTextBold":
|
||||
handleSetTextBold(call, result);
|
||||
break;
|
||||
case "setTextUnderline":
|
||||
handleSetTextUnderline(call, result);
|
||||
break;
|
||||
case "feedLine":
|
||||
handleFeedLine(call, result);
|
||||
break;
|
||||
case "feedDot":
|
||||
handleFeedDot(call, result);
|
||||
break;
|
||||
case "halfCutPaper":
|
||||
handleHalfCutPaper(call, result);
|
||||
break;
|
||||
case "fullCutPaper":
|
||||
handleFullCutPaper(call, result);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles openComPort method call.
|
||||
* <p>
|
||||
* Opens a serial port with the specified parameters on a background thread
|
||||
* and returns the result on the main thread via Handler.
|
||||
* Validates portName (non-empty), baudRate (1200-921600), and dataBits (5-8).
|
||||
* Implements PORT-01, PORT-02, T-01-01.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing port parameters
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleOpenComPort(@NonNull MethodCall call, @NonNull Result result) {
|
||||
String portName = call.argument("portName");
|
||||
Integer baudRate = call.argument("baudRate");
|
||||
Integer dataBits = call.argument("dataBits");
|
||||
Integer parity = call.argument("parity");
|
||||
Integer stopBits = call.argument("stopBits");
|
||||
Integer flowControl = call.argument("flowControl");
|
||||
Integer autoReplyMode = call.argument("autoReplyMode");
|
||||
|
||||
// Validate port name (T-01-01)
|
||||
if (portName == null || portName.isEmpty()) {
|
||||
result.error("INVALID_ARGUMENT", "portName is required and cannot be empty", null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate baud rate
|
||||
if (baudRate == null || baudRate < MIN_BAUD_RATE || baudRate > MAX_BAUD_RATE) {
|
||||
result.error(
|
||||
"INVALID_ARGUMENT",
|
||||
"baudRate must be between " + MIN_BAUD_RATE + " and " + MAX_BAUD_RATE,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data bits (C-02 fix)
|
||||
int db = dataBits != null ? dataBits : 8;
|
||||
if (db < MIN_DATA_BITS || db > MAX_DATA_BITS) {
|
||||
result.error(
|
||||
"INVALID_ARGUMENT",
|
||||
"dataBits must be between " + MIN_DATA_BITS + " and " + MAX_DATA_BITS,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Defaults
|
||||
int p = parity != null ? parity : 0;
|
||||
int sb = stopBits != null ? stopBits : 0;
|
||||
int fc = flowControl != null ? flowControl : 0;
|
||||
int arm = autoReplyMode != null ? autoReplyMode : 1;
|
||||
|
||||
// Execute on background thread (C-01 fix)
|
||||
final String finalPortName = portName;
|
||||
final int finalBaudRate = baudRate;
|
||||
final int finalDataBits = db;
|
||||
final int finalParity = p;
|
||||
final int finalStopBits = sb;
|
||||
final int finalFlowControl = fc;
|
||||
final int finalAutoReplyMode = arm;
|
||||
final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Pointer pointer = AutoReplyPrint.INSTANCE.CP_Port_OpenCom(
|
||||
finalPortName,
|
||||
finalBaudRate,
|
||||
finalDataBits,
|
||||
finalParity,
|
||||
finalStopBits,
|
||||
finalFlowControl,
|
||||
finalAutoReplyMode
|
||||
);
|
||||
|
||||
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
|
||||
final Pointer finalPointer = pointer;
|
||||
mainHandler.post(() ->
|
||||
result.error("PORT_OPEN_FAILED", "Failed to open port: " + finalPortName, null)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int dartHandle = portHandleManager.registerHandle(pointer);
|
||||
mainHandler.post(() -> result.success(dartHandle));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
mainHandler.post(() ->
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
mainHandler.post(() ->
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null)
|
||||
);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles openUsbPort method call.
|
||||
* <p>
|
||||
* Opens a USB port on a background thread and returns the result on the main thread.
|
||||
* Validates portName (non-empty) and autoReplyMode (0 or 1).
|
||||
* Implements T-01-02.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing port parameters
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleOpenUsbPort(@NonNull MethodCall call, @NonNull Result result) {
|
||||
String portName = call.argument("portName");
|
||||
Integer autoReplyMode = call.argument("autoReplyMode");
|
||||
|
||||
// Validate port name (T-01-02)
|
||||
if (portName == null || portName.isEmpty()) {
|
||||
result.error("INVALID_ARGUMENT", "portName is required and cannot be empty", null);
|
||||
return;
|
||||
}
|
||||
|
||||
int arm = autoReplyMode != null ? autoReplyMode : 1;
|
||||
|
||||
final String finalPortName = portName;
|
||||
final int finalAutoReplyMode = arm;
|
||||
final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Pointer pointer = AutoReplyPrint.INSTANCE.CP_Port_OpenUsb(
|
||||
finalPortName,
|
||||
finalAutoReplyMode
|
||||
);
|
||||
|
||||
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
|
||||
mainHandler.post(() ->
|
||||
result.error("PORT_OPEN_FAILED", "Failed to open USB port: " + finalPortName, null)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int dartHandle = portHandleManager.registerHandle(pointer);
|
||||
mainHandler.post(() -> result.success(dartHandle));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
mainHandler.post(() ->
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
mainHandler.post(() ->
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null)
|
||||
);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles closePort method call.
|
||||
* <p>
|
||||
* Closes a port by its integer handle.
|
||||
* Implements PORT-03, T-01-03.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleClosePort(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean success = portHandleManager.closeHandle(handle);
|
||||
result.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles isPortOpened method call.
|
||||
* <p>
|
||||
* Checks if a port is currently opened by its integer handle.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleIsPortOpened(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isValid = portHandleManager.isHandleValid(handle);
|
||||
result.success(isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles enumComPorts method call.
|
||||
* <p>
|
||||
* Enumerates available serial ports and returns a list of port names.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call (no arguments)
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleEnumComPorts(@NonNull MethodCall call, @NonNull Result result) {
|
||||
try {
|
||||
String[] ports = AutoReplyPrint.CP_Port_EnumCom_Helper.EnumCom();
|
||||
List<String> portList = (ports != null) ? Arrays.asList(ports) : new ArrayList<>();
|
||||
result.success(portList);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles enumUsbPorts method call.
|
||||
* <p>
|
||||
* Enumerates available USB ports and returns a list of port names.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call (no arguments)
|
||||
* @param result the result callback
|
||||
*/
|
||||
private void handleEnumUsbPorts(@NonNull MethodCall call, @NonNull Result result) {
|
||||
try {
|
||||
String[] ports = AutoReplyPrint.CP_Port_EnumUsb_Helper.EnumUsb();
|
||||
List<String> portList = (ports != null) ? Arrays.asList(ports) : new ArrayList<>();
|
||||
result.success(portList);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setMultiByteMode method call.
|
||||
* <p>
|
||||
* Sets the printer to multi-byte encoding mode.
|
||||
* Must be called before setMultiByteEncoding and printText for Chinese/multibyte text.
|
||||
* Implements PRINT-01.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetMultiByteMode(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetMultiByteMode(pointer);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setMultiByteMode failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setMultiByteEncoding method call.
|
||||
* <p>
|
||||
* Sets the multi-byte character encoding for the printer.
|
||||
* Valid encoding values: 0=GBK, 1=UTF8, 3=BIG5, 4=ShiftJIS, 5=EUC_KR.
|
||||
* Implements PRINT-01.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and encoding (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetMultiByteEncoding(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer encoding = call.argument("encoding");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding == null) {
|
||||
result.error("INVALID_ARGUMENT", "encoding is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding != 0 && encoding != 1 && encoding != 3 && encoding != 4 && encoding != 5) {
|
||||
result.error("INVALID_ARGUMENT",
|
||||
"encoding must be one of {0=GBK, 1=UTF8, 3=BIG5, 4=ShiftJIS, 5=EUC_KR}", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetMultiByteEncoding(pointer, encoding);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setMultiByteEncoding failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles printText method call.
|
||||
* <p>
|
||||
* Prints text using UTF-8 encoding (WString).
|
||||
* Requires multi-byte mode and UTF-8 encoding to be set beforehand for correct Chinese character output.
|
||||
* Implements PRINT-02, T-02-03.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and text (String)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handlePrintText(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
String text = call.argument("text");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text == null || text.isEmpty()) {
|
||||
result.error("INVALID_ARGUMENT", "text is required and cannot be empty", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_PrintTextInUTF8(pointer, new WString(text));
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "printText failed (length: " + text.length() + ")", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setAlignment method call.
|
||||
* <p>
|
||||
* Sets the print alignment.
|
||||
* Valid values: 0=left, 1=center, 2=right.
|
||||
* Implements PRINT-04.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and alignment (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetAlignment(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer alignment = call.argument("alignment");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (alignment == null) {
|
||||
result.error("INVALID_ARGUMENT", "alignment is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (alignment != 0 && alignment != 1 && alignment != 2) {
|
||||
result.error("INVALID_ARGUMENT", "alignment must be one of {0=left, 1=center, 2=right}", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetAlignment(pointer, alignment);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setAlignment failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setTextScale method call.
|
||||
* <p>
|
||||
* Sets text width and height scale factors.
|
||||
* Both widthScale and heightScale must be in range [1, 8].
|
||||
* Implements PRINT-04, T-02-02.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle, widthScale (int), heightScale (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetTextScale(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer widthScale = call.argument("widthScale");
|
||||
Integer heightScale = call.argument("heightScale");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widthScale == null) {
|
||||
result.error("INVALID_ARGUMENT", "widthScale is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (heightScale == null) {
|
||||
result.error("INVALID_ARGUMENT", "heightScale is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widthScale < 1 || widthScale > 8) {
|
||||
result.error("INVALID_ARGUMENT", "widthScale must be in range [1, 8]", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (heightScale < 1 || heightScale > 8) {
|
||||
result.error("INVALID_ARGUMENT", "heightScale must be in range [1, 8]", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextScale(pointer, widthScale, heightScale);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setTextScale failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setTextBold method call.
|
||||
* <p>
|
||||
* Sets text bold printing.
|
||||
* Valid values: 0=off, 1=on.
|
||||
* Implements PRINT-04.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and bold (int, 0 or 1)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetTextBold(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer bold = call.argument("bold");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bold == null) {
|
||||
result.error("INVALID_ARGUMENT", "bold is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bold != 0 && bold != 1) {
|
||||
result.error("INVALID_ARGUMENT", "bold must be one of {0=off, 1=on}", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextBold(pointer, bold);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setTextBold failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setTextUnderline method call.
|
||||
* <p>
|
||||
* Sets text underline style.
|
||||
* Valid values: 0=none, 1=1-dot, 2=2-dot.
|
||||
* Implements PRINT-04, T-02-02.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and underline (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleSetTextUnderline(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer underline = call.argument("underline");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (underline == null) {
|
||||
result.error("INVALID_ARGUMENT", "underline is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (underline != 0 && underline != 1 && underline != 2) {
|
||||
result.error("INVALID_ARGUMENT", "underline must be one of {0=none, 1=1-dot, 2=2-dot}", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextUnderline(pointer, underline);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "setTextUnderline failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles feedLine method call.
|
||||
* <p>
|
||||
* Feeds paper by specified number of lines.
|
||||
* numLines must be greater than 0.
|
||||
* Implements PRINT-05, T-02-02.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and numLines (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleFeedLine(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer numLines = call.argument("numLines");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numLines == null) {
|
||||
result.error("INVALID_ARGUMENT", "numLines is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numLines <= 0) {
|
||||
result.error("INVALID_ARGUMENT", "numLines must be greater than 0", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FeedLine(pointer, numLines);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "feedLine failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles feedDot method call.
|
||||
* <p>
|
||||
* Feeds paper by specified number of dots.
|
||||
* numDots must be greater than 0.
|
||||
* Implements PRINT-05, T-02-02.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle and numDots (int)
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleFeedDot(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
Integer numDots = call.argument("numDots");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numDots == null) {
|
||||
result.error("INVALID_ARGUMENT", "numDots is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numDots <= 0) {
|
||||
result.error("INVALID_ARGUMENT", "numDots must be greater than 0", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FeedDot(pointer, numDots);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "feedDot failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles halfCutPaper method call.
|
||||
* <p>
|
||||
* Triggers half-cut on the paper cutter.
|
||||
* Implements PRINT-06.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleHalfCutPaper(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_HalfCutPaper(pointer);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "halfCutPaper failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fullCutPaper method call.
|
||||
* <p>
|
||||
* Triggers full-cut on the paper cutter.
|
||||
* Implements PRINT-06.
|
||||
* </p>
|
||||
*
|
||||
* @param call the method call containing handle
|
||||
* @param result the result callback, returns true on success
|
||||
*/
|
||||
private void handleFullCutPaper(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Integer handle = call.argument("handle");
|
||||
|
||||
if (handle == null) {
|
||||
result.error("INVALID_ARGUMENT", "handle is required", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Pointer pointer = portHandleManager.getHandle(handle);
|
||||
if (pointer == null) {
|
||||
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FullCutPaper(pointer);
|
||||
if (!success) {
|
||||
result.error("PRINT_FAILED", "fullCutPaper failed", null);
|
||||
return;
|
||||
}
|
||||
result.success(success);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("UNKNOWN_ERROR", e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
channel.setMethodCallHandler(null);
|
||||
// Clean up all open ports when engine detaches (T-01-05)
|
||||
if (portHandleManager != null) {
|
||||
portHandleManager.closeAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.xiarui.printer;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for PortHandleManager.
|
||||
* Uses a mock PortOperations to avoid dependency on native library.
|
||||
*/
|
||||
public class PortHandleManagerTest {
|
||||
|
||||
private PortHandleManager manager;
|
||||
private MockPortOperations mockOps;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockOps = new MockPortOperations();
|
||||
manager = new PortHandleManager(mockOps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: registerHandle with valid pointer returns positive handle.
|
||||
*/
|
||||
@Test
|
||||
public void testRegisterHandle_ValidPointer_ReturnsPositiveHandle() {
|
||||
// Create a pointer with non-zero native value
|
||||
Pointer pointer = new Pointer(0x1234L);
|
||||
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
assertTrue("Handle should be > 0", handle > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2: registerHandle with null pointer returns -1.
|
||||
*/
|
||||
@Test
|
||||
public void testRegisterHandle_NullPointer_ReturnsMinusOne() {
|
||||
int handle = manager.registerHandle(null);
|
||||
|
||||
assertEquals("Null pointer should return -1", -1, handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2b: registerHandle with zero native value pointer returns -1.
|
||||
*/
|
||||
@Test
|
||||
public void testRegisterHandle_ZeroNativeValue_ReturnsMinusOne() {
|
||||
Pointer pointer = new Pointer(0L);
|
||||
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
assertEquals("Zero native value should return -1", -1, handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 3: getHandle returns the registered pointer.
|
||||
*/
|
||||
@Test
|
||||
public void testGetHandle_RegisteredHandle_ReturnsPointer() {
|
||||
Pointer pointer = new Pointer(0x5678L);
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
Pointer retrieved = manager.getHandle(handle);
|
||||
|
||||
assertEquals("Retrieved pointer should match registered pointer", pointer, retrieved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 4: getHandle for unregistered handle returns null.
|
||||
*/
|
||||
@Test
|
||||
public void testGetHandle_UnregisteredHandle_ReturnsNull() {
|
||||
Pointer retrieved = manager.getHandle(999);
|
||||
|
||||
assertNull("Unregistered handle should return null", retrieved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 5: closeHandle closes the port and removes registration.
|
||||
*/
|
||||
@Test
|
||||
public void testCloseHandle_RegisteredHandle_ReturnsTrue() {
|
||||
Pointer pointer = new Pointer(0xABCDL);
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
boolean result = manager.closeHandle(handle);
|
||||
|
||||
assertTrue("closeHandle should return true for registered handle", result);
|
||||
assertTrue("Mock close should have been called", mockOps.closeCalled);
|
||||
assertNull("Handle should be removed after close", manager.getHandle(handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 6: closeHandle for unregistered handle returns false.
|
||||
*/
|
||||
@Test
|
||||
public void testCloseHandle_UnregisteredHandle_ReturnsFalse() {
|
||||
boolean result = manager.closeHandle(999);
|
||||
|
||||
assertFalse("closeHandle should return false for unregistered handle", result);
|
||||
assertFalse("Mock close should NOT have been called", mockOps.closeCalled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 7: closeAll closes all registered ports and clears registry.
|
||||
*/
|
||||
@Test
|
||||
public void testCloseAll_ClosesAllPorts() {
|
||||
Pointer p1 = new Pointer(0x1111L);
|
||||
Pointer p2 = new Pointer(0x2222L);
|
||||
int h1 = manager.registerHandle(p1);
|
||||
int h2 = manager.registerHandle(p2);
|
||||
|
||||
manager.closeAll();
|
||||
|
||||
assertNull("Handle 1 should be cleared", manager.getHandle(h1));
|
||||
assertNull("Handle 2 should be cleared", manager.getHandle(h2));
|
||||
assertTrue("Mock close should have been called", mockOps.closeCalled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 8: Concurrent registrations return unique handles.
|
||||
*/
|
||||
@Test
|
||||
public void testRegisterHandle_Concurrent_ReturnsUniqueHandles() throws InterruptedException {
|
||||
final int threadCount = 10;
|
||||
final int[] handles = new int[threadCount];
|
||||
Thread[] threads = new Thread[threadCount];
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
final int index = i;
|
||||
threads[i] = new Thread(() -> {
|
||||
Pointer pointer = new Pointer(0x1000L + index);
|
||||
handles[index] = manager.registerHandle(pointer);
|
||||
});
|
||||
}
|
||||
|
||||
for (Thread t : threads) t.start();
|
||||
for (Thread t : threads) t.join();
|
||||
|
||||
// All handles should be unique
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
for (int j = i + 1; j < threadCount; j++) {
|
||||
assertNotEquals(
|
||||
"Handles should be unique: " + handles[i] + " vs " + handles[j],
|
||||
handles[i], handles[j]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 9: isHandleValid returns true for valid opened handle.
|
||||
*/
|
||||
@Test
|
||||
public void testIsHandleValid_OpenedHandle_ReturnsTrue() {
|
||||
Pointer pointer = new Pointer(0xEEEEL);
|
||||
int handle = manager.registerHandle(pointer);
|
||||
mockOps.isOpenedResult = true;
|
||||
|
||||
boolean valid = manager.isHandleValid(handle);
|
||||
|
||||
assertTrue("Opened handle should be valid", valid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 10: isHandleValid returns false for unregistered handle.
|
||||
*/
|
||||
@Test
|
||||
public void testIsHandleValid_UnregisteredHandle_ReturnsFalse() {
|
||||
boolean valid = manager.isHandleValid(999);
|
||||
|
||||
assertFalse("Unregistered handle should not be valid", valid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock PortOperations for testing without native library.
|
||||
*/
|
||||
static class MockPortOperations implements PortHandleManager.PortOperations {
|
||||
boolean closeCalled = false;
|
||||
boolean isOpenedResult = true;
|
||||
|
||||
@Override
|
||||
public boolean closePort(Pointer handle) {
|
||||
closeCalled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPortOpened(Pointer handle) {
|
||||
return isOpenedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
android/src/test/java/com/xiarui/printer/PrinterPluginTest.java
Normal file
178
android/src/test/java/com/xiarui/printer/PrinterPluginTest.java
Normal file
@@ -0,0 +1,178 @@
|
||||
package com.xiarui.printer;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import org.junit.Test;
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Unit tests for PrinterPlugin.
|
||||
* Tests parameter validation and method routing.
|
||||
*/
|
||||
public class PrinterPluginTest {
|
||||
|
||||
private PrinterPlugin plugin;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
plugin = new PrinterPlugin();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
|
||||
final MethodCall call = new MethodCall("getPlatformVersion", null);
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_openComPort_emptyPortName_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
|
||||
put("portName", "");
|
||||
put("baudRate", 115200);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_openComPort_nullPortName_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
|
||||
put("portName", null);
|
||||
put("baudRate", 115200);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_openComPort_invalidBaudRate_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
|
||||
put("portName", "/dev/ttyS0");
|
||||
put("baudRate", 100); // Below minimum 1200
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("baudRate"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_openComPort_invalidDataBits_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
|
||||
put("portName", "/dev/ttyS0");
|
||||
put("baudRate", 115200);
|
||||
put("dataBits", 9); // Above maximum 8
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("dataBits"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_openUsbPort_emptyPortName_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("openUsbPort", new java.util.HashMap<String, Object>() {{
|
||||
put("portName", "");
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_closePort_nullHandle_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
|
||||
put("handle", null);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("handle"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_isPortOpened_nullHandle_returnsInvalidArgument() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("isPortOpened", new java.util.HashMap<String, Object>() {{
|
||||
put("handle", null);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("handle"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_unknownMethod_returnsNotImplemented() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("unknownMethod", null);
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).notImplemented();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_closePort_validHandle_callsPortHandleManager() {
|
||||
// Register a handle first to test close
|
||||
PortHandleManager manager = PortHandleManager.getInstance();
|
||||
com.sun.jna.Pointer pointer = new com.sun.jna.Pointer(0x1234L);
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
|
||||
put("handle", handle);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).success(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_closePort_invalidHandle_returnsFalse() {
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
|
||||
put("handle", 99999); // Non-existent handle
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
verify(mockResult).success(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMethodCall_isPortOpened_validHandle_callsPortHandleManager() {
|
||||
PortHandleManager manager = PortHandleManager.getInstance();
|
||||
com.sun.jna.Pointer pointer = new com.sun.jna.Pointer(0x5678L);
|
||||
int handle = manager.registerHandle(pointer);
|
||||
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
MethodCall call = new MethodCall("isPortOpened", new java.util.HashMap<String, Object>() {{
|
||||
put("handle", handle);
|
||||
}});
|
||||
|
||||
plugin.onMethodCall(call, mockResult);
|
||||
|
||||
// Note: Without native library, isPortOpened will fail due to UnsatisfiedLinkError
|
||||
// but the test verifies the routing logic
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user