diff --git a/.gitignore b/.gitignore index 4587336..bf136d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Built application files *.apk *.aar +!app/libs/BluetoothBinding.aar *.ap_ *.aab diff --git a/app/build.gradle b/app/build.gradle index 2893805..023b81b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "com.sunmi.printerconfig" minSdk 21 targetSdk 35 - versionCode 2 - versionName "1.0.1" + versionCode 11 + versionName "1.0.10" } signingConfigs { @@ -56,6 +56,7 @@ android { } dependencies { + implementation files('libs/BluetoothBinding.aar') implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' diff --git a/app/libs/BluetoothBinding.aar b/app/libs/BluetoothBinding.aar new file mode 100644 index 0000000..75464c7 Binary files /dev/null and b/app/libs/BluetoothBinding.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 406e5bf..ee900f7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -10,3 +10,12 @@ # Keep printer configuration classes -keep class com.sunmi.printerconfig.** { *; } + +# Keep Sunmi SDK callback interfaces/classes used during Wi-Fi configuration. +-keep class com.sunmi.cloudprinter.** { *; } +-keep interface com.sunmi.cloudprinter.** { *; } + +# Keep callback implementers and SDK internals that may be invoked reflectively. +-keep class * implements com.sunmi.cloudprinter.presenter.SunmiPrinterClient$IPrinterClient { *; } +-keep class library.** { *; } +-keep class com.inuker.bluetooth.library.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87b4a00..2094e9d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,11 +13,14 @@ - + + diff --git a/app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinter.java b/app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinter.java new file mode 100644 index 0000000..84cf6e8 --- /dev/null +++ b/app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinter.java @@ -0,0 +1,38 @@ +package com.sunmi.printerconfig; + +import java.util.Objects; + +public final class DiscoveredPrinter { + private final String address; + private final String name; + + public DiscoveredPrinter(String address, String name) { + this.address = address == null ? "" : address.trim(); + this.name = name == null ? "" : name.trim(); + } + + public String getAddress() { + return address; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof DiscoveredPrinter)) { + return false; + } + DiscoveredPrinter that = (DiscoveredPrinter) other; + return Objects.equals(address, that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } +} diff --git a/app/src/main/java/com/sunmi/printerconfig/BluetoothDeviceAdapter.java b/app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinterAdapter.java similarity index 50% rename from app/src/main/java/com/sunmi/printerconfig/BluetoothDeviceAdapter.java rename to app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinterAdapter.java index a11dd47..11b7bbe 100644 --- a/app/src/main/java/com/sunmi/printerconfig/BluetoothDeviceAdapter.java +++ b/app/src/main/java/com/sunmi/printerconfig/DiscoveredPrinterAdapter.java @@ -1,29 +1,24 @@ package com.sunmi.printerconfig; -import android.Manifest; -import android.bluetooth.BluetoothDevice; -import android.content.pm.PackageManager; -import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.List; -public class BluetoothDeviceAdapter extends RecyclerView.Adapter { - private final List devices; +public class DiscoveredPrinterAdapter extends RecyclerView.Adapter { + private final List devices; private final OnDeviceClickListener listener; public interface OnDeviceClickListener { - void onDeviceClick(BluetoothDevice device); + void onDeviceClick(DiscoveredPrinter device); } - public BluetoothDeviceAdapter(List devices, OnDeviceClickListener listener) { + public DiscoveredPrinterAdapter(List devices, OnDeviceClickListener listener) { this.devices = devices; this.listener = listener; } @@ -38,24 +33,18 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - BluetoothDevice device = devices.get(position); - boolean hasPermission = true; + DiscoveredPrinter device = devices.get(position); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - hasPermission = ContextCompat.checkSelfPermission(holder.itemView.getContext(), - Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; - } + String baseName = device.getName().isEmpty() + ? holder.itemView.getContext().getString(R.string.unknown_device) + : device.getName(); - if (hasPermission) { - String name = device.getName(); - String address = device.getAddress(); - holder.deviceName.setText(name != null ? name : "Unknown Device"); - holder.deviceAddress.setText(address); - } else { - holder.deviceName.setText("Permission Required"); - holder.deviceAddress.setText(""); + if (PrinterDeviceClassifier.isLikelySunmi(device.getName())) { + baseName = baseName + " \u2022 " + holder.itemView.getContext().getString(R.string.likely_sunmi); } + holder.deviceName.setText(baseName); + holder.deviceAddress.setText(device.getAddress()); holder.itemView.setOnClickListener(v -> listener.onDeviceClick(device)); } @@ -65,8 +54,8 @@ public int getItemCount() { } static class ViewHolder extends RecyclerView.ViewHolder { - TextView deviceName; - TextView deviceAddress; + private final TextView deviceName; + private final TextView deviceAddress; ViewHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/com/sunmi/printerconfig/MainActivity.java b/app/src/main/java/com/sunmi/printerconfig/MainActivity.java index 556ee58..ef18ead 100644 --- a/app/src/main/java/com/sunmi/printerconfig/MainActivity.java +++ b/app/src/main/java/com/sunmi/printerconfig/MainActivity.java @@ -2,14 +2,12 @@ import android.Manifest; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; @@ -22,49 +20,53 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.sunmi.cloudprinter.bean.PrinterDevice; +import com.sunmi.cloudprinter.bean.Router; +import com.sunmi.cloudprinter.presenter.SunmiPrinterClient; + import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements SunmiPrinterClient.IPrinterClient { private static final int PERMISSION_REQUEST_CODE = 1; private static final int REQUEST_ENABLE_BT = 2; + private static final int PRINTER_SCAN_TIMEOUT_MS = 12_000; + private static final int PRINTER_CONNECTION_TIMEOUT_MS = 10_000; private BluetoothAdapter bluetoothAdapter; private Button scanButton; private ProgressBar progressBar; private TextView statusText; private RecyclerView devicesRecyclerView; - private BluetoothDeviceAdapter deviceAdapter; - private List deviceList; - - private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (BluetoothDevice.ACTION_FOUND.equals(action)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device != null && checkBluetoothPermission()) { - String deviceName = device.getName(); - // Filter for Sunmi printers - if (deviceName != null && (deviceName.startsWith("NT311") || - deviceName.contains("CloudPrinter") || - deviceName.contains("SUNMI"))) { - if (!deviceList.contains(device)) { - deviceList.add(device); - deviceAdapter.notifyDataSetChanged(); - } - } - } - } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { - progressBar.setVisibility(View.GONE); - scanButton.setEnabled(true); - scanButton.setText(R.string.scan_bluetooth); - if (deviceList.isEmpty()) { - statusText.setText(R.string.no_devices_found); - } else { - statusText.setText(""); - } - } + private DiscoveredPrinterAdapter deviceAdapter; + private final List deviceList = new ArrayList<>(); + private final Set discoveredAddresses = new HashSet<>(); + + private SunmiPrinterClient sunmiPrinterClient; + private final Handler scanTimeoutHandler = new Handler(Looper.getMainLooper()); + private final Handler connectionTimeoutHandler = new Handler(Looper.getMainLooper()); + + private String pendingPrinterAddress; + private String pendingPrinterName; + private boolean scanInProgress = false; + private boolean waitingForPrinterConnection = false; + + private final Runnable scanTimeoutRunnable = () -> { + if (!scanInProgress || waitingForPrinterConnection) { + return; + } + + stopPrinterScan(true); + if (deviceList.isEmpty()) { + statusText.setText(R.string.no_compatible_printers_found); + } + }; + + private final Runnable connectionTimeoutRunnable = () -> { + if (waitingForPrinterConnection) { + handlePrinterConnectionFailure(getString(R.string.printer_connection_timeout)); } }; @@ -78,9 +80,7 @@ protected void onCreate(Bundle savedInstanceState) { statusText = findViewById(R.id.statusText); devicesRecyclerView = findViewById(R.id.devicesRecyclerView); - deviceList = new ArrayList<>(); - deviceAdapter = new BluetoothDeviceAdapter(deviceList, this::onDeviceClick); - + deviceAdapter = new DiscoveredPrinterAdapter(deviceList, this::onDeviceClick); devicesRecyclerView.setLayoutManager(new LinearLayoutManager(this)); devicesRecyclerView.setAdapter(deviceAdapter); @@ -91,6 +91,11 @@ protected void onCreate(Bundle savedInstanceState) { return; } + sunmiPrinterClient = new SunmiPrinterClient( + new ReceiverSafeContext(getApplicationContext()), + this + ); + scanButton.setOnClickListener(v -> { if (checkPermissions()) { startBluetoothScan(); @@ -98,22 +103,16 @@ protected void onCreate(Bundle savedInstanceState) { requestPermissions(); } }); - - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothDevice.ACTION_FOUND); - filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); - registerReceiver(bluetoothReceiver, filter); } private boolean checkPermissions() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; - } else { - return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; } + return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; } private void requestPermissions() { @@ -122,60 +121,199 @@ private void requestPermissions() { new String[]{ Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT - }, PERMISSION_REQUEST_CODE); - } else { - ActivityCompat.requestPermissions(this, - new String[]{ - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.ACCESS_FINE_LOCATION - }, PERMISSION_REQUEST_CODE); + }, + PERMISSION_REQUEST_CODE); + return; } - } - private boolean checkBluetoothPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; - } - return true; + ActivityCompat.requestPermissions(this, + new String[]{ + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_FINE_LOCATION + }, + PERMISSION_REQUEST_CODE); } private void startBluetoothScan() { if (!bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - if (checkBluetoothPermission()) { - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); - } + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); return; } + waitingForPrinterConnection = false; + stopPrinterScan(false); + + scanInProgress = true; + scanTimeoutHandler.removeCallbacks(scanTimeoutRunnable); + connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable); + + discoveredAddresses.clear(); deviceList.clear(); deviceAdapter.notifyDataSetChanged(); statusText.setText(R.string.scanning); - if (checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) { - bluetoothAdapter.cancelDiscovery(); - } - scanButton.setEnabled(false); scanButton.setText(R.string.scanning); progressBar.setVisibility(View.VISIBLE); - if (checkBluetoothPermission()) { - bluetoothAdapter.startDiscovery(); + try { + sunmiPrinterClient.startScan(); + scanTimeoutHandler.postDelayed(scanTimeoutRunnable, PRINTER_SCAN_TIMEOUT_MS); + } catch (Throwable t) { + stopPrinterScan(true); + statusText.setText(getString(R.string.error, getString(R.string.printer_scan_failed))); + } + } + + private void stopPrinterScan(boolean resetUi) { + scanTimeoutHandler.removeCallbacks(scanTimeoutRunnable); + if (scanInProgress) { + scanInProgress = false; + try { + sunmiPrinterClient.stopScan(); + } catch (Throwable ignored) { + } + } + if (resetUi && !waitingForPrinterConnection) { + progressBar.setVisibility(View.GONE); + scanButton.setEnabled(true); + scanButton.setText(R.string.scan_bluetooth); } } - private void onDeviceClick(BluetoothDevice device) { - if (checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) { - bluetoothAdapter.cancelDiscovery(); + private void onDeviceClick(DiscoveredPrinter device) { + stopPrinterScan(false); + + pendingPrinterAddress = device.getAddress(); + pendingPrinterName = device.getName().isEmpty() + ? getString(R.string.unknown_device) + : device.getName(); + + if (pendingPrinterAddress.isEmpty()) { + Toast.makeText(this, R.string.printer_address_unavailable, Toast.LENGTH_LONG).show(); + progressBar.setVisibility(View.GONE); + scanButton.setEnabled(true); + scanButton.setText(R.string.scan_bluetooth); + return; } + waitingForPrinterConnection = true; + progressBar.setVisibility(View.VISIBLE); + scanButton.setEnabled(false); + statusText.setText(R.string.connecting_to_printer); + + connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable); + connectionTimeoutHandler.postDelayed(connectionTimeoutRunnable, PRINTER_CONNECTION_TIMEOUT_MS); + + try { + // Match the known stable flow: establish BLE session before Wi-Fi config screen. + sunmiPrinterClient.getPrinterSn(pendingPrinterAddress); + } catch (Throwable t) { + handlePrinterConnectionFailure(getString(R.string.printer_connection_failed)); + } + } + + private void openWifiConfigScreen() { Intent intent = new Intent(this, WifiConfigActivity.class); - intent.putExtra("device", device); + intent.putExtra("device_address", pendingPrinterAddress); + intent.putExtra("device_name", pendingPrinterName); startActivity(intent); } + private void handlePrinterConnectionFailure(String message) { + waitingForPrinterConnection = false; + connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable); + + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + scanButton.setEnabled(true); + scanButton.setText(R.string.scan_bluetooth); + statusText.setText(getString(R.string.error, message)); + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + }); + } + + @Override + public void onPrinterFount(PrinterDevice printerDevice) { + if (printerDevice == null) { + return; + } + + String address = printerDevice.getAddress(); + if (address == null || address.trim().isEmpty()) { + return; + } + + String name = printerDevice.getName(); + runOnUiThread(() -> { + if (!scanInProgress || !discoveredAddresses.add(address)) { + return; + } + + deviceList.add(new DiscoveredPrinter(address, name)); + deviceAdapter.notifyDataSetChanged(); + statusText.setText(getString(R.string.printers_found, deviceList.size())); + }); + } + + @Override + public void routerFound(Router router) { + // Not used in this activity. + } + + @Override + public void onGetWifiListFinish() { + // Not used in this activity. + } + + @Override + public void onGetWifiListFail() { + // Not used in this activity. + } + + @Override + public void onSetWifiSuccess() { + // Not used in this activity. + } + + @Override + public void wifiConfigSuccess() { + // Not used in this activity. + } + + @Override + public void onWifiConfigFail() { + // Not used in this activity. + } + + @Override + public void sendDataFail(int code, String msg) { + String failure = getString(R.string.wifi_push_error_with_code, code, msg == null ? "Unknown" : msg); + handlePrinterConnectionFailure(failure); + } + + @Override + public void getSnRequestSuccess() { + // Wait for onSnReceived callback. + } + + @Override + public void onSnReceived(String sn) { + stopPrinterScan(false); + waitingForPrinterConnection = false; + connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable); + + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + scanButton.setEnabled(true); + scanButton.setText(R.string.scan_bluetooth); + statusText.setText(""); + openWifiConfigScreen(); + }); + } + @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); @@ -198,13 +336,15 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in @Override protected void onDestroy() { super.onDestroy(); - try { - unregisterReceiver(bluetoothReceiver); - } catch (Exception e) { - e.printStackTrace(); - } - if (bluetoothAdapter != null && checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) { - bluetoothAdapter.cancelDiscovery(); + stopPrinterScan(false); + scanTimeoutHandler.removeCallbacks(scanTimeoutRunnable); + connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable); + + if (sunmiPrinterClient != null && pendingPrinterAddress != null && !pendingPrinterAddress.isEmpty()) { + try { + sunmiPrinterClient.disconnect(pendingPrinterAddress); + } catch (Throwable ignored) { + } } } } diff --git a/app/src/main/java/com/sunmi/printerconfig/PrinterConfigHelper.java b/app/src/main/java/com/sunmi/printerconfig/PrinterConfigHelper.java deleted file mode 100644 index 96eff1e..0000000 --- a/app/src/main/java/com/sunmi/printerconfig/PrinterConfigHelper.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.sunmi.printerconfig; - -import android.Manifest; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothSocket; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; -import android.util.Log; - -import androidx.core.content.ContextCompat; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.UUID; - -public class PrinterConfigHelper { - private static final String TAG = "PrinterConfigHelper"; - // Standard SPP UUID for Bluetooth Serial Port Profile - private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); - - private final Context context; - - public PrinterConfigHelper(Context context) { - this.context = context; - } - - /** - * Configure Sunmi printer Wi-Fi settings via Bluetooth - * @param device The Bluetooth device (printer) - * @param ssid Wi-Fi network SSID - * @param password Wi-Fi network password - * @return true if configuration was sent successfully - */ - public boolean configurePrinterWifi(BluetoothDevice device, String ssid, String password) { - BluetoothSocket socket = null; - OutputStream outputStream = null; - - try { - // Check Bluetooth permission - if (!checkBluetoothPermission()) { - Log.e(TAG, "Bluetooth permission not granted"); - return false; - } - - // Create Bluetooth socket - socket = device.createRfcommSocketToServiceRecord(SPP_UUID); - - // Connect to the device - Log.d(TAG, "Connecting to printer..."); - socket.connect(); - Log.d(TAG, "Connected successfully"); - - // Get output stream - outputStream = socket.getOutputStream(); - - // Send Wi-Fi configuration commands - boolean success = sendWifiConfig(outputStream, ssid, password); - - Log.d(TAG, "Configuration sent: " + success); - return success; - - } catch (IOException e) { - Log.e(TAG, "Error configuring printer: " + e.getMessage(), e); - return false; - } finally { - // Clean up resources - try { - if (outputStream != null) { - outputStream.close(); - } - if (socket != null) { - socket.close(); - } - } catch (IOException e) { - Log.e(TAG, "Error closing connection: " + e.getMessage()); - } - } - } - - /** - * Send Wi-Fi configuration to the printer - * This uses ESC/POS commands specific to Sunmi printers - */ - private boolean sendWifiConfig(OutputStream out, String ssid, String password) { - try { - // ESC/POS command header for Sunmi Wi-Fi configuration - // Format: ESC @ (initialize printer) - // Then: Custom command for Wi-Fi setup - - // Initialize printer - out.write(new byte[]{0x1B, 0x40}); // ESC @ - Thread.sleep(100); - - // Sunmi Wi-Fi configuration command format: - // Command structure: 0x1F 0x1B 0x1F [subcommand] [data length] [SSID] [password] - - // Method 1: Using Sunmi's proprietary Wi-Fi config command - // Header: 0x1F 0x1B 0x1F 0x91 (Wi-Fi config command) - byte[] header = new byte[]{0x1F, 0x1B, 0x1F, (byte) 0x91}; - out.write(header); - - // SSID length (1 byte) + SSID - byte[] ssidBytes = ssid.getBytes(StandardCharsets.UTF_8); - out.write(ssidBytes.length); - out.write(ssidBytes); - - // Password length (1 byte) + Password - byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); - out.write(passwordBytes.length); - out.write(passwordBytes); - - // Security type (1 byte): 3 = WPA2-PSK (most common) - out.write(0x03); - - out.flush(); - Thread.sleep(500); - - // Method 2: Alternative format using AT-style commands - // Some Sunmi printers accept AT commands for configuration - String atCommand = String.format("AT+WIFI_CONF=\"%s\",\"%s\"\r\n", ssid, password); - out.write(atCommand.getBytes(StandardCharsets.UTF_8)); - out.flush(); - Thread.sleep(500); - - // Method 3: ESC/POS extension command format - // Some models use this format - String configCommand = String.format("\u001B\u001F\u0010WIFI:%s,%s\n", ssid, password); - out.write(configCommand.getBytes(StandardCharsets.UTF_8)); - out.flush(); - Thread.sleep(500); - - // Send end marker - out.write(new byte[]{0x0A}); // Line feed - out.flush(); - - Log.d(TAG, "Wi-Fi configuration commands sent to printer"); - Log.d(TAG, "SSID: " + ssid); - - return true; - - } catch (IOException | InterruptedException e) { - Log.e(TAG, "Error sending Wi-Fi config: " + e.getMessage()); - return false; - } - } - - private boolean checkBluetoothPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return ContextCompat.checkSelfPermission(context, - Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; - } - return true; - } - - // Helper method to convert string to hex for debugging - private String bytesToHex(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02X ", b)); - } - return sb.toString(); - } -} diff --git a/app/src/main/java/com/sunmi/printerconfig/PrinterDeviceClassifier.java b/app/src/main/java/com/sunmi/printerconfig/PrinterDeviceClassifier.java new file mode 100644 index 0000000..efd8a7f --- /dev/null +++ b/app/src/main/java/com/sunmi/printerconfig/PrinterDeviceClassifier.java @@ -0,0 +1,20 @@ +package com.sunmi.printerconfig; + +import java.util.Locale; + +public final class PrinterDeviceClassifier { + private PrinterDeviceClassifier() { + } + + public static boolean isLikelySunmi(String deviceName) { + if (deviceName == null || deviceName.trim().isEmpty()) { + return false; + } + + String normalizedName = deviceName.toUpperCase(Locale.ROOT); + return normalizedName.startsWith("NT311") + || normalizedName.contains("CLOUDPRINTER") + || normalizedName.contains("CLOUD PRINT") + || normalizedName.contains("SUNMI"); + } +} diff --git a/app/src/main/java/com/sunmi/printerconfig/ReceiverSafeContext.java b/app/src/main/java/com/sunmi/printerconfig/ReceiverSafeContext.java new file mode 100644 index 0000000..0c24a91 --- /dev/null +++ b/app/src/main/java/com/sunmi/printerconfig/ReceiverSafeContext.java @@ -0,0 +1,50 @@ +package com.sunmi.printerconfig; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Handler; + +/** + * Wraps context receiver registration so legacy SDK code works on Android 13+. + */ +public class ReceiverSafeContext extends ContextWrapper { + public ReceiverSafeContext(Context base) { + super(base); + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return super.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + } + return super.registerReceiver(receiver, filter); + } + + @Override + public Intent registerReceiver( + BroadcastReceiver receiver, + IntentFilter filter, + String broadcastPermission, + Handler scheduler + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return super.registerReceiver( + receiver, + filter, + broadcastPermission, + scheduler, + Context.RECEIVER_NOT_EXPORTED + ); + } + return super.registerReceiver(receiver, filter, broadcastPermission, scheduler); + } +} diff --git a/app/src/main/java/com/sunmi/printerconfig/WifiConfigActivity.java b/app/src/main/java/com/sunmi/printerconfig/WifiConfigActivity.java index 31770ae..b70ec02 100644 --- a/app/src/main/java/com/sunmi/printerconfig/WifiConfigActivity.java +++ b/app/src/main/java/com/sunmi/printerconfig/WifiConfigActivity.java @@ -1,215 +1,373 @@ package com.sunmi.printerconfig; -import android.Manifest; import android.bluetooth.BluetoothDevice; -import android.content.Context; -import android.content.pm.PackageManager; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiManager; -import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; +import com.sunmi.cloudprinter.bean.PrinterDevice; +import com.sunmi.cloudprinter.bean.Router; +import com.sunmi.cloudprinter.presenter.SunmiPrinterClient; + +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -public class WifiConfigActivity extends AppCompatActivity { - private static final int PERMISSION_REQUEST_CODE = 3; +public class WifiConfigActivity extends AppCompatActivity implements SunmiPrinterClient.IPrinterClient { + private static final int WIFI_CONFIG_TIMEOUT_MS = 25_000; + + private String printerAddress; + private String printerName; - private BluetoothDevice device; private TextView printerNameText; private Spinner wifiSpinner; private EditText passwordInput; + private EditText manualSsidInput; + private LinearLayout manualSsidContainer; private Button configureButton; private ProgressBar progressBar; private TextView statusText; - private WifiManager wifiManager; - private PrinterConfigHelper printerHelper; + + private SunmiPrinterClient sunmiPrinterClient; + + private ArrayAdapter wifiAdapter; + private final List availableRouters = new ArrayList<>(); + private boolean waitingForWifiConfigResult = false; + + private final Handler wifiConfigTimeoutHandler = new Handler(Looper.getMainLooper()); + private final Runnable wifiConfigTimeoutRunnable = () -> { + if (waitingForWifiConfigResult) { + handleConfigurationFailure(getString(R.string.printer_wifi_config_timeout)); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_wifi_config); - device = getIntent().getParcelableExtra("device"); - if (device == null) { - Toast.makeText(this, "Error: No device selected", Toast.LENGTH_SHORT).show(); + printerAddress = getIntent().getStringExtra("device_address"); + printerName = getIntent().getStringExtra("device_name"); + + if ((printerAddress == null || printerAddress.isEmpty()) || printerName == null) { + BluetoothDevice device = getIntent().getParcelableExtra("device"); + if (device != null) { + printerAddress = safeGetDeviceAddress(device); + if (printerName == null || printerName.isEmpty()) { + printerName = safeGetDeviceName(device); + } + } + } + + if (printerAddress == null || printerAddress.isEmpty()) { + Toast.makeText(this, R.string.printer_address_unavailable, Toast.LENGTH_LONG).show(); finish(); return; } + if (printerName == null || printerName.isEmpty()) { + printerName = getString(R.string.unknown_device); + } + printerNameText = findViewById(R.id.printerNameText); wifiSpinner = findViewById(R.id.wifiSpinner); passwordInput = findViewById(R.id.passwordInput); + manualSsidInput = findViewById(R.id.manualSsidInput); + manualSsidContainer = findViewById(R.id.manualSsidContainer); configureButton = findViewById(R.id.configureButton); progressBar = findViewById(R.id.progressBar); statusText = findViewById(R.id.statusText); - wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - printerHelper = new PrinterConfigHelper(this); + sunmiPrinterClient = new SunmiPrinterClient( + new ReceiverSafeContext(getApplicationContext()), + this + ); - boolean hasPermission = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - hasPermission = ContextCompat.checkSelfPermission(this, - Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; - } - - if (hasPermission) { - String deviceName = device.getName(); - printerNameText.setText(getString(R.string.connected_to, deviceName != null ? deviceName : "Unknown")); - } + printerNameText.setText(getString(R.string.connected_to, printerName)); - loadWifiNetworks(); + setupWifiSpinner(); configureButton.setOnClickListener(v -> { - String selectedSsid = (String) wifiSpinner.getSelectedItem(); + Router selectedRouter = resolveSelectedRouter(); + String manualSsid = manualSsidInput.getText().toString().trim(); String password = passwordInput.getText().toString(); - if (selectedSsid == null || selectedSsid.isEmpty()) { - Toast.makeText(this, "Please select a Wi-Fi network", Toast.LENGTH_SHORT).show(); + if (selectedRouter == null) { + if (manualSsid.isEmpty()) { + Toast.makeText(this, R.string.select_or_enter_wifi_network, Toast.LENGTH_SHORT).show(); + return; + } + configurePrinter(buildManualRouter(manualSsid), password); return; } - if (password.isEmpty()) { - Toast.makeText(this, "Please enter Wi-Fi password", Toast.LENGTH_SHORT).show(); + if (selectedRouter.isHasPwd() && password.isEmpty()) { + Toast.makeText(this, R.string.enter_wifi_password, Toast.LENGTH_SHORT).show(); return; } - configurePrinter(selectedSsid, password); + configurePrinter(selectedRouter, password); }); } - private void loadWifiNetworks() { - if (!checkWifiPermissions()) { - requestWifiPermissions(); - return; + @Override + protected void onStart() { + super.onStart(); + loadNetworksFromPrinter(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + waitingForWifiConfigResult = false; + wifiConfigTimeoutHandler.removeCallbacks(wifiConfigTimeoutRunnable); + + if (sunmiPrinterClient != null && printerAddress != null && !printerAddress.isEmpty()) { + try { + sunmiPrinterClient.disconnect(printerAddress); + } catch (Throwable ignored) { + } + } + } + + private String safeGetDeviceAddress(BluetoothDevice device) { + try { + return device.getAddress() == null ? "" : device.getAddress(); + } catch (SecurityException e) { + return ""; } + } - List networkList = new ArrayList<>(); + private String safeGetDeviceName(BluetoothDevice device) { + try { + return device.getName() == null ? getString(R.string.unknown_device) : device.getName(); + } catch (SecurityException e) { + return getString(R.string.unknown_device); + } + } - // Get configured networks (the tablet's saved networks) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - boolean hasLocationPermission = ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; - boolean hasWifiStatePermission = ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_WIFI_STATE) == PackageManager.PERMISSION_GRANTED; + private void setupWifiSpinner() { + wifiAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, new ArrayList<>()); + wifiAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + wifiSpinner.setAdapter(wifiAdapter); - if (!hasLocationPermission) { - requestWifiPermissions(); - return; + wifiSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + boolean manualSelected = isManualEntrySelected(position); + manualSsidContainer.setVisibility(manualSelected ? View.VISIBLE : View.GONE); + if (!manualSelected) { + manualSsidInput.setText(""); + } } - if (hasWifiStatePermission) { - try { - List configs = wifiManager.getConfiguredNetworks(); - if (configs != null) { - for (WifiConfiguration config : configs) { - String ssid = config.SSID.replace("\"", ""); - if (!networkList.contains(ssid)) { - networkList.add(ssid); - } - } - } - } catch (SecurityException ignored) { - // Fall back to manual network entry below. - } + @Override + public void onNothingSelected(AdapterView parent) { + manualSsidContainer.setVisibility(View.GONE); } + }); + + updateWifiSpinner(); + } + + private Router resolveSelectedRouter() { + int selectedPosition = wifiSpinner.getSelectedItemPosition(); + + if (selectedPosition < 0 || selectedPosition >= availableRouters.size()) { + return null; } - // Add some common network detection - // Note: On Android 10+, scanning requires location and is limited - if (networkList.isEmpty()) { - // Add placeholder for manual entry if scanning fails - networkList.add("Enter manually below"); + if (isManualEntrySelected(selectedPosition)) { + return null; } - ArrayAdapter adapter = new ArrayAdapter<>(this, - android.R.layout.simple_spinner_item, networkList); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - wifiSpinner.setAdapter(adapter); + return availableRouters.get(selectedPosition); } - private boolean checkWifiPermissions() { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { - return true; + private boolean isManualEntrySelected(int position) { + return wifiAdapter != null && position == wifiAdapter.getCount() - 1; + } + + private void updateWifiSpinner() { + List options = new ArrayList<>(); + for (Router router : availableRouters) { + options.add(getRouterDisplayName(router)); + } + options.add(getString(R.string.manual_entry_option)); + + wifiAdapter.clear(); + wifiAdapter.addAll(options); + wifiAdapter.notifyDataSetChanged(); + + if (!availableRouters.isEmpty()) { + wifiSpinner.setSelection(0); + manualSsidContainer.setVisibility(View.GONE); + configureButton.setEnabled(true); + } else { + wifiSpinner.setSelection(wifiAdapter.getCount() - 1); + manualSsidContainer.setVisibility(View.VISIBLE); + configureButton.setEnabled(true); } - return ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; } - private void requestWifiPermissions() { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { - loadWifiNetworks(); - return; + private String getRouterDisplayName(Router router) { + String name = router.getName(); + if (name != null && !name.trim().isEmpty()) { + return name.trim(); } - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_CODE); + + byte[] essid = router.getEssid(); + if (essid == null || essid.length == 0) { + return getString(R.string.unknown_device); + } + + return new String(essid, StandardCharsets.UTF_8).trim(); + } + + private Router buildManualRouter(String ssid) { + Router router = new Router(); + router.setName(ssid); + router.setEssid(ssid.getBytes(StandardCharsets.UTF_8)); + router.setHasPwd(true); + return router; } - private void configurePrinter(String ssid, String password) { + private void loadNetworksFromPrinter() { configureButton.setEnabled(false); progressBar.setVisibility(View.VISIBLE); - statusText.setText(R.string.configuring); + statusText.setText(R.string.printer_wifi_scanning); - // Run configuration in background thread - new Thread(() -> { - try { - boolean success = printerHelper.configurePrinterWifi(device, ssid, password); - - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - configureButton.setEnabled(true); - - if (success) { - statusText.setText(R.string.success); - Toast.makeText(this, R.string.success, Toast.LENGTH_LONG).show(); - // Return to main activity after success - finish(); - } else { - statusText.setText(getString(R.string.error, "Configuration failed")); - Toast.makeText(this, "Configuration failed. Please try again.", - Toast.LENGTH_LONG).show(); - } - }); - } catch (Exception e) { - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - configureButton.setEnabled(true); - statusText.setText(getString(R.string.error, e.getMessage())); - Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show(); - }); - } - }).start(); + availableRouters.clear(); + updateWifiSpinner(); + + try { + sunmiPrinterClient.getPrinterWifiList(printerAddress); + } catch (Throwable t) { + progressBar.setVisibility(View.GONE); + statusText.setText(R.string.printer_wifi_scan_failed); + configureButton.setEnabled(true); + } + } + + private void configurePrinter(Router router, String password) { + waitingForWifiConfigResult = true; + wifiConfigTimeoutHandler.removeCallbacks(wifiConfigTimeoutRunnable); + wifiConfigTimeoutHandler.postDelayed(wifiConfigTimeoutRunnable, WIFI_CONFIG_TIMEOUT_MS); + + configureButton.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + statusText.setText(R.string.sending_wifi_to_printer); + + try { + sunmiPrinterClient.setPrinterWifi(printerAddress, router.getEssid(), password == null ? "" : password); + } catch (Throwable t) { + handleConfigurationFailure(getString(R.string.wifi_push_failed_try_24g)); + } + } + + private void handleConfigurationSuccess() { + waitingForWifiConfigResult = false; + wifiConfigTimeoutHandler.removeCallbacks(wifiConfigTimeoutRunnable); + + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + configureButton.setEnabled(true); + statusText.setText(R.string.success); + Toast.makeText(this, R.string.success, Toast.LENGTH_LONG).show(); + finish(); + }); + } + + private void handleConfigurationFailure(String message) { + waitingForWifiConfigResult = false; + wifiConfigTimeoutHandler.removeCallbacks(wifiConfigTimeoutRunnable); + + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + configureButton.setEnabled(true); + statusText.setText(getString(R.string.error, message)); + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + }); } @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == PERMISSION_REQUEST_CODE) { - boolean allGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - allGranted = false; - break; - } - } - if (allGranted) { - loadWifiNetworks(); + public void onPrinterFount(PrinterDevice printerDevice) { + // Not used in this activity. + } + + @Override + public void routerFound(Router router) { + runOnUiThread(() -> { + availableRouters.add(router); + updateWifiSpinner(); + }); + } + + @Override + public void onGetWifiListFinish() { + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + configureButton.setEnabled(true); + + if (availableRouters.isEmpty()) { + statusText.setText(R.string.printer_wifi_no_networks_found); + manualSsidContainer.setVisibility(View.VISIBLE); } else { - Toast.makeText(this, "Permissions required to scan Wi-Fi networks", - Toast.LENGTH_SHORT).show(); + statusText.setText(getString(R.string.wifi_networks_found, availableRouters.size())); } - } + }); + } + + @Override + public void onGetWifiListFail() { + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + configureButton.setEnabled(true); + statusText.setText(R.string.printer_wifi_scan_failed); + manualSsidContainer.setVisibility(View.VISIBLE); + }); + } + + @Override + public void onSetWifiSuccess() { + runOnUiThread(() -> statusText.setText(R.string.configuring)); + } + + @Override + public void wifiConfigSuccess() { + handleConfigurationSuccess(); + } + + @Override + public void onWifiConfigFail() { + handleConfigurationFailure(getString(R.string.wifi_push_failed_try_24g)); + } + + @Override + public void sendDataFail(int code, String msg) { + String failureMessage = getString(R.string.wifi_push_error_with_code, code, msg == null ? "Unknown" : msg); + handleConfigurationFailure(failureMessage); + } + + @Override + public void getSnRequestSuccess() { + // Not used in this activity. + } + + @Override + public void onSnReceived(String sn) { + // Not used in this activity. } } diff --git a/app/src/main/res/layout/activity_wifi_config.xml b/app/src/main/res/layout/activity_wifi_config.xml index 08f313c..b339b5d 100644 --- a/app/src/main/res/layout/activity_wifi_config.xml +++ b/app/src/main/res/layout/activity_wifi_config.xml @@ -46,13 +46,39 @@ android:layout_marginTop="8dp" android:minHeight="48dp"/> + + + + + + + Success! Printer configured. Error: %s Connected to: %s + Unknown Device + Likely Sunmi + Permission Required + Enter SSID manually + Wi-Fi SSID + Enter Wi-Fi network name + Scanning Wi-Fi networks... + Permissions are required to scan Wi-Fi networks + Turn on Wi-Fi to scan networks + Turn on Location to scan Wi-Fi networks + No Wi-Fi networks found. Enter SSID manually. + %1$d Wi-Fi networks found + Unable to refresh Wi-Fi scan results + Getting Wi-Fi networks from printer... + Failed to get Wi-Fi networks from printer. Use manual SSID. + No networks reported by printer. Use manual SSID. + Please select or enter a Wi-Fi network + Please enter Wi-Fi password + Connecting to printer... + Sending Wi-Fi credentials to printer... + Printer connection timed out. Keep the printer close and retry. + Failed to scan for compatible printers. + No compatible printers found. Make sure the printer is in pairing/config mode and retry. + %1$d compatible printer(s) found + Printer did not confirm Wi-Fi configuration in time. Check Wi-Fi (2.4GHz/WPA2) and retry. + Unable to connect to printer over Bluetooth. + Failed to configure Wi-Fi. Confirm 2.4GHz WPA/WPA2 network and retry. + Printer communication error (%1$d): %2$s + Printer Bluetooth address is unavailable.