Android alkalmazás fejlesztés Zebra címkenyomtatóhoz

A címkenyomtatás lépései az integrációtól a nyomtatás lebonyolításáig
LogiNet Mobile Dev Team

LogiNet Mobile Dev Team

Natív iOS, Android és cross-platform mobil szakértők

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!


A LogiNetnél natív és cross-platform mobil applikációk fejlesztésében is a segítségedre vagyunk. Professzionális szakembereink számos technológiát ismernek: natív iOS és Android applikációk készítése Kotlin, Swift technológiával, cross-platform megoldás Flutter technológiával. Mobil fejlesztői csapatunk erőforrás kiszervezési projektekben is részt vesz. Vedd fel kollégáinkkal a kapcsolatot!
LogiNet Mobile Dev Team

LogiNet Mobile Dev Team LogiNet Mobile Dev Team LinkedIn profilja

Natív iOS, Android és cross-platform mobil szakértők
Mobil app fejlesztői csapatunk tagjai komoly tapasztalattal rendelkeznek új, egyedi mobil applikáció készítésében, a meglévő mobil app továbbfejlesztésében, refaktorálásban. Naprakészek a legújabb technológiák terén. Céljuk, hogy a legmodernebb megoldásokat az elsők között építsék be a fejlesztett alkalmazásokba.