Overview
Zebra's WS101 Bluetooth Communication Badge is a rugged Bluetooth comm device that can add hands-free voice capabilities to Zebra mobile computers and tablets. Small and light enough to be worn or pocketed, WS101-series devices provide workers with a means to easily communicate with fellow employees through Zebra's voice-enabled apps or a company's own. Staff can make calls, look up inventory items, hear responses and receive instructions, all without touching a mobile device or tablet.
![]() |
![]() |
![]() |
|||
| The WS101 in healthcare, pharma and retail. |
Main Features
- Programmable Button Events: P1, P2, P3 (press/release/long-press)
- Programmable Voice Triggers: Wake Word, Duress Word
- Audio and Haptic Feedback: Play tones and trigger vibration on the device (new in
SDK v1.0.6) - Connection Events: Access Control List (ACL) dis/connect
- SCO Audio: Start/stop mic capture
There's also a WS101 Programmer's Guide for iOS
App-dev Requirements
- Android Studio with Java 11 or later
- Android API level 30 or greater
- Zebra Bluetooth-equipped target device(s) running Android 11 (or later)
- If targeting Android 12 or later: Bluetooth runtime permissions (see Permissions section)
- WS101 connected via Bluetooth for "Phone calls" and "Media audio"
What is SCO Audio?
Synchronous Connection-oriented (SCO) Audio is a Bluetooth protocol designed for usage scenarios that can benefit from low-latency communications, such as those for voice. SCO Audio delivers its payload with less delay than other protocols by using reserved time slots. The trade-off is lower bandwidth, making SCO Audio unsuitable for music streaming and other high-fidelity audio content.
WS101 Series Controls
Front View
| Control | Description | |
|---|---|---|
| 1 | Microphone | Captures voice and ambient sounds. |
| 2 | Badge clip tower bar | Enables the attachment of a lanyard and/or badge clip. |
| 3 | Power | Performs multiple functions: Power: Press and hold to turn device on or off. Device enters pairing mode each time it's turned on. Pairing: Hold to re-enter pairing mode after device has been powered up. Incoming telephony call: Press once to answer; press twice to reject. On a call: Press twice to hang up. |
| 4 | P1 programmable button | Sends an event to a registered app (unprogrammed by default). Zebra recommends programming this button for push-to-talk function. |
| 5 | LED ring | Multi-colored visual indicator for multiple functions, including pairing, battery charging and level, incoming call, call on hold. |
Side Views
| Control | Description | |
|---|---|---|
| 1 | Volume up/down | Adjusts volume of the Bluetooth audio stream when Bluetooth audio is active. Volume level can change only when an audio stream is active. |
| 2 | P2 programmable button | Sends an event to a registered app. By default, mutes/unmutes the mic during telephony calls. |
| 3 | Barcode | Enables the "scan-to-pair" Bluetooth function. |
| 4 | NFC "N-Mark" | Location for the "tap-to-pair" Bluetooth function. |
Top View
| Control | Description | |
|---|---|---|
| 1 | 3.5mm audio jack* | Input for headphones or headset; supports push-to-talk functions. |
| 2 | Top LED | Visual status indicator for incoming calls, call on hold. |
| 3 | Speaker | Projects audio upward, toward device user. |
| 4 | P3 programmable button (red) | Sends an event to a registered app (unprogrammed by default). |
* Feature not present on "-H" (blue) models.
Installation
Android Archive (AAR, local)
1. Copy libs/zebra-audio-connect-sdk-release.aar to the app/libs/ directory.
2. Modify settings.gradle to include:
dependencyResolutionManagement {
repositories {
flatDir { dirs 'libs' }
// ... other repos
}
}
3. Modify app/build.gradle to include:
implementation files('libs/zebra-audio-connect-sdk-release.aar')
If the app is to enable R8 (formerly ProGuard) minification
(minifyEnabled = true), the consumer R8 Rules (below) also must be included.
Permissions
The SDK does not request permissions automatically. The app must declare and request them as below.
Manifest entries
// Bluetooth Classic and SCO
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
// Android 12+ Bluetooth control
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
// Microphone for SCO audio capture
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Runtime requests (example)
// Permissions array
private val requiredPermissions = arrayOf(
Manifest.permission.RECORD_AUDIO,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
} else {
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
}
)
// Launcher
private val permLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
if (results.values.all { it }) initSdk() else onPermsDenied()
}
// In Activity.onCreate():
if (hasAllPermissions()) initSdk() else
permLauncher.launch(requiredPermissions)
Data Structures
Button Event Listener:
interface ZWAButtonEventListener {
fun onButtonEvent(button: ZWAButton, state: ButtonState)
}
enum class ZWAButton {
P1, //Programmable button 1
P2, //Programmable button 2
P3 //Programmable button 3
}
enum class ButtonState {
PRESS, // Button pressed down
RELEASE, // Button released
LONG_PRESS // Button held down for longer than one (1) second
}
WS101 and WS101-H programmable button identifiers.
Click image to enlarge; ESC to exit.
Connection Event Listener Interface:
interface ZWAConnectionListener {
fun onDeviceConnected(device: ZWABluetoothDevice)
fun onDeviceDisconnected(device: ZWABluetoothDevice)
}
data class ZWABluetoothDevice(
val name: String, // Device name (e.g., "Zebra WS101")
val address: String, // Bluetooth MAC address("00:11:22:33:AA:BB")
val deviceType: DeviceType, // CLASSIC or BLE
val isConnected: Boolean // Current connection status
)
enum class DeviceType {
CLASSIC,
BLE
}
Voice Trigger Listener Interface:
interface ZWAVoiceTriggerListener {
fun onVoiceTriggerDetected(triggerType: VoiceTriggerType)
}
enum class VoiceTriggerType {
WAKE_WORD,
DURESS_WORD
}
- WAKE_WORD: Triggers app-defined actions, such as launching a voice assistant app.
- DURESS_WORD: Triggers app-defined actions, such calling for emergency services.
SCO Connection Listener Interface:
interface ScoConnectionListener {
fun onScoStarted() // Note: Audio path may not be ready yet.
fun onScoFailed(error: ScoError)
// NEW: Called when SCO audio path is connected and ready for use.
fun onScoAudioConnected()
// NEW: Called when SCO audio path is disconnected.
fun onScoAudioDisconnected()
}
enum class ScoError {
BLUETOOTH_DISABLED,
DISCONNECTED,
UNKNOWN_ERROR
}
Initialization
Application-level:
Recommended for apps that can declare manifest permissions ahead of runtime.
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
ZebraAudioConnectSDK.initialize(
this,
setOf(
ZebraAudioConnectSDK.Module.BUTTONS,
ZebraAudioConnectSDK.Module.VOICE,
ZebraAudioConnectSDK.Module.CONNECTION,
ZebraAudioConnectSDK.Module.SCO,
ZebraAudioConnectSDK.Module.COMMANDS
)
)
}
override fun onTerminate() {
ZebraAudioConnectSDK.shutdown()
super.onTerminate()
}
}
Activity-level (after permissions):
fun initSdk() {
ZebraAudioConnectSDK.initialize(
applicationContext,
setOf(
ZebraAudioConnectSDK.Module.BUTTONS,
ZebraAudioConnectSDK.Module.VOICE,
ZebraAudioConnectSDK.Module.CONNECTION,
ZebraAudioConnectSDK.Module.SCO
)
)
versionTextView.text = "SDK v${ZebraAudioConnectSDK.getVersion()}"
}
R8 Rules:
Required only if the consuming app enables minifyEnabled = true.
Add these lines to the R8 (formerly ProGuard) configuration (e.g. in consumer-rules.pro):
# Keep all SDK classes and members
-keep class com.zebra.audio.connect.sdk.** { *; }
# Keep enum values for correct dispatch in listeners
-keepclassmembers enum com.zebra.audio.connect.sdk.** { *; }
# Ensure the module’s build.gradle already references this file:
android {
defaultConfig {
// ...
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
consumerProguardFiles("consumer-rules.pro")
}
}
}
Public API
// New Listener for Command success/failure callbacks
interface CommandListener {
fun onToneSuccess()
fun onToneFailure()
fun onVibrationSuccess(duration: Long)
fun onVibrationFailure(duration: Long)
}
// Set the listener
ZebraAudioConnectSDK.setCommandListener(object : CommandListener {
// Override methods for success and failure
})
// New Command API Methods
// Play a tone on the WS101 device
ZebraAudioConnectSDK.playTone()
// Trigger vibration on the WS101 device (Vibration Duration: 100ms to 3000ms)
ZebraAudioConnectSDK.triggerVibration(timeInMillis: Long = 500L)
:::java
// Version
val sdkVersion = ZebraAudioConnectSDK.getVersion()
// Listeners
ZebraAudioConnectSDK.setButtonEventListener { button, state -> /* ... */ }
ZebraAudioConnectSDK.setVoiceTriggerListener { triggerType -> /* ... */ }
ZebraAudioConnectSDK.setConnectionListener(object : ZWAConnectionListener {
override fun onDeviceConnected(device: ZWABluetoothDevice) { /* ... */ }
override fun onDeviceDisconnected(device: ZWABluetoothDevice, reason:
DisconnectReason) { /* ... */ }
})
ZebraAudioConnectSDK.setScoListener(object : ScoConnectionListener {
override fun onScoStarted() = /* ... */
override fun onScoFailed(error: ScoError) = /* ... */
})
// SCO control
ZebraAudioConnectSDK.startScoConnection()
ZebraAudioConnectSDK.stopScoConnection()
Usage Example (MainActivity)
class MainActivity : AppCompatActivity() {
// UI elements
private lateinit var btnRecord: Button
private lateinit var btnPlay: Button
private lateinit var txtLog: TextView
private var isRecording = false
private var isPlaying = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// init views
btnRecord = findViewById(R.id.btn_start_recording)
btnPlay = findViewById(R.id.btn_playback)
txtLog = findViewById(R.id.textLog)
// permissions & SDK init
if (hasAllPermissions()) initSdkAndListeners()
else permLauncher.launch(requiredPermissions)
updateUi()
btnRecord.setOnClickListener {
if (!isRecording) startScoAndRecord() else stopRecording()
}
btnPlay.setOnClickListener { if (!isPlaying) startPlayback() else
stopPlayback() }
}
private fun initSdkAndListeners() {
ZebraAudioConnectSDK.initialize(...)
// Delay before recording to allow SCO setup
val scoDelayMs = 50L // Adjust per device; can be 0-200 ms. See Best Practices and Error Handling (below)
// Button events
ZebraAudioConnectSDK.setButtonEventListener { btn, st -> log("Btn $btn →
$st") }
// Voice triggers
ZebraAudioConnectSDK.setVoiceTriggerListener { tr -> log("Voice $tr") }
// Connection events
ZebraAudioConnectSDK.setConnectionListener(...)
// SCO events
ZebraAudioConnectSDK.setScoListener(...)
}
// ... record, playback, log, updateUi implementations ...
}
Best Practices and Error Handling
- IMPORTANT: For reliable Bluetooth audio operations, always use the
onScoAudioConnected()callback to verify that the audio path is fully established before routing stream data. Avoid using the legacyonScoStarted()callback, as it could trigger before the channel is ready to transmit audio. - Permission denial and UI state management: Disable or transition the Record UI once a session begins to prevent concurrent streams. If recording starts successfully, disable the "Record" action or toggle the button to a "Stop" state.
- ScoError cases:
Handle ScoError.BLUETOOTH_DISABLED, prompts user to enable Bluetooth. Retry or abort gracefully. - Multi-app: Initiate only one SCO session at a time. Use Android Audio Focus or custom locking if multiple apps integrate the SDK.
- System Alert Window permission: Request this permission during runtime; allows
activities to be started from the background, for example, if an activity must be started from the
onReceive()method after a reboot.
// create a request code
const val REQUEST_CODE_SYSTEM_ALERT_WINDOW = 1001
// Check if SYSTEM_ALERT_WINDOW permission is granted
fun hasSystemAlertWindowPermission(): Boolean {
return Settings.canDrawOverlays(context)
}
/**
* Request for SYSTEM_ALERT_WINDOW permission, and handle the activity result
* in onActivityResult method.
*/
fun requestSystemAlertWindowPermission(activity: Activity) {
if (!hasSystemAlertWindowPermission()) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context.packageName}")
)
activity.startActivityForResult(intent, REQUEST_CODE_SYSTEM_ALERT_WINDOW)
}
}
Usage Example (with forground service)
- Make sure to have declared all the Foreground service-related permissions in the manifest and also requested at runtime in the MainActivity.
- Provided that this service is running in the background, when the WAKE WORD is triggered in
the listener
ZebraAudioConnectSDK.setVoiceTriggerListener, then start the SCO connectionstartScoConnection(). - Once the SCO is established, call the APIs to play tone or vibrate (or both) as an indication to start recording further (use some cache file to store the recording if needed inside the service, so that it can be played once the app is opened).
class WakeWordService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
initSDK()
}
private fun initSDK() {
// Connection events is called before initializing the SDK so that we
// can capture the initial connection state
ZebraAudioConnectSDK.setConnectionListener(...)
ZebraAudioConnectSDK.initialize(
applicationContext,
setOf(
ZebraAudioConnectSDK.Module.BUTTONS,
ZebraAudioConnectSDK.Module.VOICE,
ZebraAudioConnectSDK.Module.CONNECTION,
ZebraAudioConnectSDK.Module.SCO,
ZebraAudioConnectSDK.Module.COMMANDS
)
)
// Button events
ZebraAudioConnectSDK.setButtonEventListener { btn, st -> log("Btn $btn →
$st") }
// Voice triggers
ZebraAudioConnectSDK.setVoiceTriggerListener { tr ->
log("Voice $tr")
startScoConnection()
}
// To play tone and trigger vibration
ZebraAudioConnectSDK.setCommandListener(...)
// SCO events
ZebraAudioConnectSDK.setScoListener(object : ScoConnectionListener {
override fun onScoStarted() {
log("SCO Connection Initiated")
// Don't start recording here - audio path not ready yet!
}
override fun onScoAudioConnected() {
log("SCO Audio Path Ready")
// ✅ NOW it's safe to:
// 1. Play tone/vibrate to indicate recording will start
playTone()
triggerVibration(1000L) // 1 second
// 2. Start audio recording (audio path is fully established)
startAudioRecording()
}
override fun onScoAudioDisconnected() {
log("SCO Audio Disconnected")
stopAudioRecording()
}
override fun onScoFailed(error: ScoError) {
log("SCO Connection Failed: $error")
}
})
}
}
Multi-app Considerations
- Button/Voice events: Broadcast to all registered listeners across apps.
- SCO sessions: Only one active session is allowed at a time. Subsequent
startScoConnection()calls fail; handle usingonScoFailed().
Frequently Asked Questions
Why isn't the WS101 mic working?
The WS101 requires exclusive use of the microphone via Synchronous Connection-oriented (SCO) audio connection for voice capture. Some of the most common causes of mic failure are listed below.
Causes for mic failure:
- Wrong connection type: SCO requires the Bluetooth "Hands-free" or "Headset" profile to be active. The WS101 must be connected for "Phone calls" and "Media audio" in the Android System settings panel of the connected WS101 device.
- Another app using the microphone: Only one app can use Bluetooth SCO audio at a time. Quit other app(s) that use the mic (e.g. phone dialer, voice recorder) before running yours.
- Missing permissions: If the device is running Android 12 or later, the problem could be that your app lacks special Bluetooth permissions required at runtime.
- Also see next question.
Why doesn't my app work on Android 12?
In addition to a declaration in the app manifest (as indicated under Permissions above), devices running Android 12 (or later) enforce additional Bluetooth permissions that must be requested at runtime. Heightened security introduced with Android 12 requires that apps be granted permission for "Nearby devices" or "Bluetooth" when they start up. To address the issue, try the steps below:
- When the app first runs, grant the "Nearby devices" permission when prompted.
- If previously denied, go to Settings > Apps > [Your App Name] > Permissions and enable "Nearby devices."
- Restart the app to activate the permission changes.
Lack of background permission also might be hindering the app. Try granting the app background permission if the issue persists.
Why isn't my app receiving WS101 button presses or wake-word events?
Button presses and voice commands use the Bluetooth "Hands-free Profile" control channel, which is enabled only when Bluetooth is configured for audio and for calls. If the device is configured only for audio, the channel that carries button and voice signals is not available for your app to use.
Check WS101 settings:
- Go to Settings > Connections (or) Connected Devices* and look for a WS101 device.
- If there's none, tap "Pair new device" and/or follow the normal process for Bluetooth pairing.
The message "Connected for Phone calls and Media audio" should appear.* - Otherwise, tap the gear icon." The "Phone calls" and "Media audio" toggle controls should both be active.
* Exact processes and wording vary by Android version.
Other things to check:
- Pairing vs. Active Connection - Check that the device is not only paired but also connected.
Previously paired (and remembered) devices don't always reconnect when a host device comes into range. - Wrong button or voice trigger mappings - Zebra recommends using the Zebra Bluetooth Comms Utility to create and verify button mappings to configure wake- and duress-word settings.
- For Android, the Zebra BT Comms Utility is available:
Can I check whether a WS101 is connected before initializing the SDK?
There's no need; SDK 1.0.6 provides automatic device detection. The Zebra Audio Connect SDK automatically detects already-connected WS101 devices upon initialization. The onDeviceConnected() callback is triggered for any devices that were connected before the SDK was initialized, eliminating the need for manual Bluetooth device scanning.
Also See
- Zebra WS101 Product Reference Guide (HTML) | Complete usage and technical reference in searchable online format
- Zebra WS101 Product Reference Guide (PDF) | Complete usage and technical reference in portable document format (PDF)


