As highlighted in the first part of our series of articles, certain customer segments often need fast label printing using a portable, compact thermal printer. The Zebra ZQ printer series is an excellent choice for this task. How do you integrate the Zebra ZSDK Android SDK, what permissions do you need for printing, how do you find available printers, and what is the printing process like? You'll find the answers in this article!

Integrating the ZSDK Android SDK

As this library is unavailable on any SDK-sharing platform, the .jar file must be downloaded from Zebra's official website. Simply typing "ZSDK Android SDK" into Google will take you to the download page.
Once the SDK has been downloaded, it must be copied to the app/libs folder. However, this is not enough to use it. You also need to add the following line to your app.gradle file:
dependencies {
		implementation fileTree(dir: 'libs', include: ['*.jar'])
}
Grant permissions
As you are connecting to the label printer via Bluetooth, the Bluetooth functionality must be enabled on the device and the following permissions must be added to the AndroidManifest.xml file in the application:
 <uses-permission android:name="android.permission.BLUETOOTH" />
 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Before starting to search for printers, it is necessary to request "location permission" from the user.
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: Intv
        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,
        )
    }
}
In the code snippet above, you can see how to check that you have the necessary permissions from the user after the screen has been created. In this case, it's the ACCESS_FINE_LOCATION permission. If not, you request it from the user.
Printer search and caching
It is recommended to separate the various printing-related services from the Activity and place them in a separate component.
To start the search, you can use the findPrinters() method from the BluetoothDiscoverer class in the SDK.
The following code snippet shows a possible implementation.
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)

}
In the above code snippet you can see a ScanPrintersService interface. The implementation of this interface contains the printer search functionality and caches the found printers in memory. This allows them to be accessed later and also allows the memory cache to be cleared. How the search is initiated depends on the specific need, but one possible method is to place a search button on the UI, which triggers the startScan() method when pressed, and the search results are returned via the PrinterScanCallback.
Once you have received the available printers from the ScanPrintersService, you can display them on the screen for the user to choose which printer to connect to.
Note: The code fragments may contain elements of the Hilt dependency injection library (e.g. @Singleton, @Inject, @ApplicationContext). While this is not necessary for the printer search, the use of dependency injection is strongly recommended, and Hilt is currently one of the most widely used DI libraries.
Connecting to the Zebra label printer
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
    }
}
In the service shown in the code snippet above, you can connect to a printer using the connectToPrinter method. As you can see, you only need to know the MAC address of the printer. The MAC address can be found in the DiscoveredPrinter class provided by the SDK. The result of the printer search is a list of such DiscoveredPrinter objects.
What formats can you print?
ZQ Series label printers can receive print jobs in a variety of formats.

ZPL vs. PDF

ZPL (Zebra Programming Language) is a specialised language developed by Zebra for its thermal printers. ZPL is widely used to format labels, barcodes and images on Zebra printers. It allows you to specify how the label should be printed, including text, barcodes, graphics and layout details.

Key features of ZPL

  • Text, barcodes and graphics: ZPL allows you to define the layout of labels, including text placement, barcode creation and the printing of images (such as logos).
  • Speed and efficiency: ZPL is a compact and lightweight language designed for fast label creation, making it efficient for high-volume printing.
  • Format storage: ZPL commands can be stored on the printer, allowing fast printing of dynamic content labels by sending only the variable data.

PDF printing on Zebra label printers

As well as supporting ZPL (Zebra Programming Language), Zebra printers can also print PDF files. This can be particularly useful in cases where the content of the labels is not determined by encoded ZPL commands but is directly available in the form of a PDF document.
From the code snippets above, it should be clear that we chose to print via PDF during the development process.. I won't go into the details here, as creating a PDF file programmatically in Android would require a separate article. But in general, you can imagine it as positioning the desired elements, such as a QR code or any textual content, on a blank canvas by calculating coordinates.

Changing printer settings

The SGD.GET and SGD.SET methods are used to get and change certain settings on the printer. After connecting to the printer, the zebraPrinterSupportsPDF() method allows us to check if PDF printing is enabled on the printer, and if not, you can enable it using the setPdfPrintingEnabled() method.
For more advanced printer configuration, the Zebra Printer Setup Utility application can be used. This application is usually pre-installed on Zebra Android devices.

Printing and sending data to the label printer

Once you have created the PDF file you want to print and successfully configured the printer, the next step is to initiate the printing process.
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
            }
        }
    }
}
As shown in the code, you need the URI of the PDF file and the connection object that is created after the connection is established. With these in hand, you can start the printing process using the printDocument() method. As this is a long-running task, it is not advisable to run it on the main thread. In the code above, we handle thread switching by using coroutines.
Learn how Zebra devices simplify Android app development for specific hardware, including QR code scanning, Kiosk Mode, label printing and offline functionality.
At LogiNet, we assist you with both native and cross-platform mobile application development. Our professional experts are proficient in various technologies: creating native iOS and Android applications using Kotlin and Swift, and cross-platform solutions using Flutter. Our mobile development team also participates in resource outsourcing projects. Learn more about our mobile solutions!

Let's talk about

your project

Drop us a message about your digital product development project and we will get back to you within 2 days.
We'd love to hear the details about your ideas and goals, so that our experts can guide you from the first meeting.
John Radford
Client Services Director UK