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'])
}dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}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" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<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
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
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 com.zebra.sdk.printer.discovery.DiscoveredPrinter
interface ScanPrintersService {
fun discoveredPrinters(): List<DiscoveredPrinter>
fun startScan(printerScanCallback: PrinterScanCallback)
fun clearDiscoveredPrinterList
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
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
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
interface PrinterScanCallback {
fun onScanStarted()
fun onPrinterFound(discoveredPrinter: DiscoveredPrinter)
fun onScanFinished(discoveredPrinters: List<DiscoveredPrinter>)
fun onScanError(error
interface PrinterScanCallback {
fun onScanStarted()
fun onPrinterFound(discoveredPrinter: DiscoveredPrinter)
fun onScanFinished(discoveredPrinters: List<DiscoveredPrinter>)
fun onScanError(error
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
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
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
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
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
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
import android.net.Uri
import com.zebra.sdk.comm.BluetoothConnection
interface PrintDocumentService {
suspend fun printDocument(connection: BluetoothConnection, document: Uri
import android.net.Uri
import com.zebra.sdk.comm.BluetoothConnection
interface PrintDocumentService {
suspend fun printDocument(connection: BluetoothConnection, document: Uri
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
}
val printer = ZebraPrinterFactory.getLinkOsPrinter(connection)
val printerStatus = printer.currentStatus
if (printerStatus.isReadyToPrint) {
printer.sendFileContents(document.path)
true
} else {
false
}
} catch (e: Exception) {
false
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
}
val printer = ZebraPrinterFactory.getLinkOsPrinter(connection)
val printerStatus = printer.currentStatus
if (printerStatus.isReadyToPrint) {
printer.sendFileContents(document.path)
true
} else {
false
}
} catch (e: Exception) {
false
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
}
val printer = ZebraPrinterFactory.getLinkOsPrinter(connection)
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: