Android alkalmazás fejlesztés Zebra címkenyomtatóhoz
Bizonyos ügyfélkörben gyakori igény lehet a címkék gyors nyomtatása hordozható, kompakt hő nyomtatóval, mint arra cikksorozatunk első részében is rámutattunk. A Zebra ZQ szériás nyomtatók kiválóan alkalmasak erre a feladatra. Hogyan tudjuk integrálni a Zebra ZSDK android SDK-ját, milyen engedélyek szükségesek a nyomtatáshoz, milyen úton lehet megtalálni az elérhető nyomtatókat, illetve hogyan néz ki a nyomtatás? Cikkünkben választ kaphatsz ezekre a kérdésekre!
A LogiNet a Zebra tanúsított beszállítója.
ZSDK Android SDK integrálása
Mivel ez a könyvtár nincs fent egyik SDK megosztó platformon sem, így a Zebra hivatalos weboldaláról kell letölteni a .jar fájlt. A Google-ba beírva a “ZSDK Android SDK”-t könnyedén megtalálható az oldal, ahonnan letölthető.
Az SDK letöltését követően be kell másolni az app/libs mappába, viszont ez még nem elégséges ahhoz, hogy használni tudjuk. A következő sor hozzáadása is szükséges az app.gradle fájlban:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) }
Engedélyek adása
Mivel Bluetooth-al kapcsolódunk a címkenyomtatóhoz, ezért az eszközön szükség van a Bluetooth bekapcsolására, míg az alkalmazásban a következő engedélyeket kell hozzáadni az AndroidManifest.xml fájlban:
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Mielőtt elindítanánk a nyomtatók keresését, előtte “location permission” engedélyt szükséges kérni a felhasználótól.
import android.Manifest.permission import android.content.pm.PackageManager import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat class PermissionRequestExampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) checkPermissions() } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) } private fun checkPermissions() { if (!isPermissionsGranted()) { ActivityCompat.requestPermissions( this, PERMISSIONS, PERMISSIONS_REQUEST, ) return } } private fun isPermissionsGranted(): Boolean { var permissionsGranted = true for (permission in PERMISSIONS) { if (ContextCompat.checkSelfPermission( this, permission ) != PackageManager.PERMISSION_GRANTED ) { permissionsGranted = false break } } return permissionsGranted } companion object { private const val PERMISSIONS_REQUEST = 0 private val PERMISSIONS = arrayOf( permission.ACCESS_FINE_LOCATION, ) } }
A fenti kód részleten az látható, ahogy a képernyő létrejötte után megvizsgáljuk, hogy rendelkezünk-e a megfelelő engedélyekkel a felhasználótól. Ez jelen esetben az ACCESS_FINE_LOCATION permission. Ha nem, akkor elkérjük a felhasználótól.
Nyomtató keresés, cache-elés
A különböző nyomtatáshoz kapcsolódó service-eket érdemes leválasztani az Activity-ről és egy külön komponensben elhelyezni.
A keresés elindításához az SDK-ban lévő BluetoothDiscoverer osztály findPrinters() metódusát tudjuk használni.
A következő kódrészletben egy lehetséges megvalósítás szerepel.
import com.zebra.sdk.printer.discovery.DiscoveredPrinter interface ScanPrintersService { fun discoveredPrinters(): List<DiscoveredPrinter> fun startScan(printerScanCallback: PrinterScanCallback) fun clearDiscoveredPrinterList() }
import android.content.Context import com.zebra.sdk.comm.ConnectionException import com.zebra.sdk.printer.discovery.BluetoothDiscoverer import com.zebra.sdk.printer.discovery.DiscoveredPrinter import com.zebra.sdk.printer.discovery.DiscoveryHandler import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class ScanPrintersServiceImpl @Inject constructor( @ApplicationContext private val context: Context, ) : ScanPrintersService { private val printers = ArrayList<DiscoveredPrinter>() override fun discoveredPrinters() = printers override fun startScan(printerScanCallback: PrinterScanCallback) { try { printers.clear() printerScanCallback.onScanStarted() BluetoothDiscoverer.findPrinters(context, object : DiscoveryHandler { override fun foundPrinter(discoveredPrinter: DiscoveredPrinter) { printers.add(discoveredPrinter) printerScanCallback.onPrinterFound(discoveredPrinter) Timber.d("Bluetooth printer found: ${'$'}{discoveredPrinter.address}") } override fun discoveryFinished() { printerScanCallback.onScanFinished(printers) Timber.d("Bluetooth discovery finished") } override fun discoveryError(e: String) { Timber.d("Bluetooth discovery error: ${'$'}e") printerScanCallback.onScanError(e) } }) } catch (e: ConnectionException) { Timber.e(e) printerScanCallback.onScanError(e.localizedMessage ?: "Bluetooth discovery error") } } override fun clearDiscoveredPrinterList() { printers.clear() } }
interface PrinterScanCallback { fun onScanStarted() fun onPrinterFound(discoveredPrinter: DiscoveredPrinter) fun onScanFinished(discoveredPrinters: List<DiscoveredPrinter>) fun onScanError(error: String) }
A fenti kódrészletben látható egy ScanPrintersService interfész. Ezen interfész implementációja tartalmazza a nyomtató keresés funkciót, továbbá a megtalált nyomtatókat memóriába cache-eli. Ezt később el tudjunk érni, illetve a memória cache ürítését is lehetővé teszi. A keresés elindítása az adott igénytől függ, viszont az egyik lehetséges módszer egy UI-on elhelyezett Keresés gomb, melynek megnyomására meghívódik a startScan() metódus, amely a keresés eredményét a PrinterScanCallback-en keresztül adja vissza.
Miután a ScanPrintersService-től megkaptuk az elérhető nyomtatókat, azokat ki tudjuk listázni a képernyőre a felhasználó számára, így ki tudja választani, melyik nyomtatót szeretné csatlakoztatni.
Megjegyzés: A kódrészletek nyomokban a Hilt dependency injection library nyomait tartalmazzák (@Singleton, @Inject, @ApplicationContext). Természetesen ez nem szükséges a nyomtatók kereséséhez, viszont a dependency injection alkalmazása erősen ajánlott, és az egyik legelterjedtebb DI library jelenleg a Hilt.
Kapcsolódás a Zebra címkenyomtatóhoz
import com.zebra.sdk.comm.BluetoothConnection import kotlinx.coroutines.flow.StateFlow interface PrinterConnectionService { fun currentConnection(): StateFlow<BluetoothConnection?> suspend fun connectToPrinter(macAddress: String): Boolean suspend fun closeConnection(): Boolean }
import com.zebra.sdk.comm.BluetoothConnection import com.zebra.sdk.comm.Connection import com.zebra.sdk.comm.ConnectionException import com.zebra.sdk.printer.SGD import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class PrinterConnectionServiceImpl @Inject constructor() : PrinterConnectionService { private var currentConnection: MutableStateFlow<BluetoothConnection?> = MutableStateFlow(null) override fun currentConnection() = currentConnection override suspend fun connectToPrinter(macAddress: String): Boolean { return withContext(Dispatchers.Default) { val connection = BluetoothConnection(macAddress) try { Timber.d("Trying to connect to the printer") connection.open() Timber.d("Successfully connected to the printer") if (zebraPrinterSupportsPDF(connection)) { Timber.d("PDF print enabled") } else { Timber.d("PDF print not enabled") setPdfPrintingEnabled(connection) } currentConnection.value = connection true } catch (e: ConnectionException) { val error = "Error occurred while trying to connect to the printer" Timber.d(error) currentConnection.value = null false } } } override suspend fun closeConnection(): Boolean { return withContext(Dispatchers.Default) { Timber.d("Disconnect from the printer") currentConnection.value?.close() currentConnection.value = null true } } @Throws(ConnectionException::class) private fun zebraPrinterSupportsPDF(connection: Connection): Boolean { val printerInfo = SGD.GET("apl.enable", connection) return printerInfo == "pdf" } @Throws(ConnectionException::class) private fun setPdfPrintingEnabled(connection: Connection): Boolean { SGD.SET("apl.enable", "pdf", connection) val enabled = zebraPrinterSupportsPDF(connection) Timber.d("Trying to enable PDF print, success: ${'$'}enabled") return enabled } }
A fenti kódrészletben látható service-ben a connectToPrinter() metódussal tudunk kapcsolódni egy nyomtatóhoz. Jól látható, hogy a kapcsolódáshoz csak a nyomtatóhoz tartozó MAC address-re van szükség. A MAC address megtalálható az SDK-ban lévő DiscoveredPrinter osztályban. A nyomtató keresés eredménye egy ilyen DiscoveredPrinter lista.
Milyen formátumban lehet nyomtatni?
A ZQ szériába tartozó címkenyomtatókra több formátumban is el tudjuk juttatni amit nyomtatni szeretnénk.
ZPL vs. PDF
A ZPL (Zebra Programming Language) egy speciális nyelv, amelyet a Zebra fejlesztett ki a saját hőnyomtatóihoz. A ZPL széles körben használt nyelv címkék, vonalkódok és képek formázásához Zebra nyomtatókon. Lehetővé teszi, hogy megadjuk, hogyan kell nyomtatni a címkét, beleértve a szöveget, vonalkódokat, grafikákat és elrendezési részleteket.
A ZPL főbb jellemzői
Szöveg, vonalkódok és grafikák: A ZPL lehetővé teszi, hogy meghatározzuk a címkék elrendezését, beleértve a szöveg elhelyezését, a vonalkód létrehozását és a kép (logó) nyomtatását.
Sebesség és hatékonyság: A ZPL tömör és könnyű nyelv, amelyet gyors címke generálásra tervezték, ezáltal hatékony a nagy mennyiségű adat nyomtatásában.
Formátum tárolás: A ZPL parancsokat tárolni lehet a nyomtatón, így dinamikus tartalmú címkék gyors nyomtatása lehetséges, csupán a változó adatok elküldésével.
PDF nyomtatás Zebra címkenyomtatókon
A Zebra nyomtatók nemcsak ZPL-t (Zebra Programming Language) támogatnak, hanem PDF fájlok nyomtatását is lehetővé teszik. Ez különösen hasznos lehet olyan esetekben, amikor a címkék tartalmát nem kódolt ZPL-parancsok segítségével szeretnénk meghatározni, hanem közvetlenül PDF dokumentum formájában áll rendelkezésre az információ.
A fenti kódrészletek alapján már kiderülhetett, hogy a PDF nyomtatást választottuk a fejlesztés során. Ennek a részleteibe nem szeretnék itt most belemenni, mert egy PDF fájl összeállítása Androidban programkóddal egy külön cikket igényelne, viszont nagyvonalakban úgy kell elképzelni, hogy egy üres Canvas-ra, koordináták számolásával tudjuk pozicionálni a kívánt elemeket, mint például egy QR kód, vagy bármilyen más szöveges tartalmat.
A címkenyomtató beállításainak módosítása
A címkenyomtató beállításainak a megváltoztatására az SGD.GET és SGD.SET metódusok szolgálnak, ezek segítségével tudunk bizonyos beállításokat kiolvasni a nyomtatóról, illetve módosítani azokat. A nyomtatóhoz való csatlakozás után a zebraPrinterSupportsPDF() metódussal tudjuk megnézni, hogy a PDF nyomtatás van-e beállítva a nyomtatón, és ha nem, akkor a setPdfPrintingEnabled() metódussal tudjuk azt beállítani.
A nyomtató bővebb konfigurálására a Zebra Printer Setup Utility alkalmazás használható. Ez az alkalmazás általában előre fel van telepítve a Zebra android eszközökre.
Nyomtatás, adatküldés a címkenyomtatóra
Ha összeállítottuk a kinyomtatni kívánt pdf fájlt és a nyomtatót is sikeresen bekonfiguráltuk, akkor nincs más hátra, mint a nyomtatás.
import android.net.Uri import com.zebra.sdk.comm.BluetoothConnection interface PrintDocumentService { suspend fun printDocument(connection: BluetoothConnection, document: Uri): Boolean }
import android.net.Uri import com.zebra.sdk.comm.BluetoothConnection import com.zebra.sdk.printer.ZebraPrinterFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import java.lang.Exception import javax.inject.Inject class PrintDocumentServiceImpl @Inject constructor() : PrintDocumentService { override suspend fun printDocument(connection: BluetoothConnection, document: Uri): Boolean { return withContext(Dispatchers.IO + SupervisorJob()) { try { if (!connection.isConnected) { return@withContext false } // Get Instance of Printer val printer = ZebraPrinterFactory.getLinkOsPrinter(connection) // Verify Printer Status is Ready val printerStatus = printer.currentStatus if (printerStatus.isReadyToPrint) { printer.sendFileContents(document.path) true } else { false } } catch (e: Exception) { false } } } }
Ahogy a kódból is látszik, szükségünk van a pdf fájl URI-re, valamint a connection objektumra, ami a kapcsolódás után jön létre. Ezeknek a birtokában a printDocument() metódussal tudjuk elindítani a nyomtatást. Mivel ez egy long running task, ezért nem célszerű a Main thread-en futtatni. A fenti kódban coroutine-ok használatával oldjuk meg a szálváltást.
Ismerd meg hogyan támogatta a Zebra ökoszisztéma az app fejlesztéseinket QR kód beolvasás, Kiosk mód, offline működés, illetve PDF generálás esetén!