This commit is contained in:
Developer
2026-05-18 17:52:09 +08:00
commit cae04eead5
62 changed files with 11230 additions and 0 deletions

9
android/.gitignore vendored Normal file
View 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
View 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
}
}

Binary file not shown.

8
android/proguard-rules.pro vendored Normal file
View 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
View File

@@ -0,0 +1 @@
rootProject.name = 'printer'

View File

@@ -0,0 +1 @@
rootProject.name = 'printer'

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xiarui.printer">
</manifest>

View 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;
}
}

View 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();
}
}
}

View File

@@ -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;
}
}
}

View 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
}
}