WS101 Programmer's Guide for Android

EMDK For Android 15.0

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
    }

image 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 legacy onScoStarted() 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 connection startScoConnection().
  • 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 using onScoFailed().

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:

  1. When the app first runs, grant the "Nearby devices" permission when prompted.
  2. If previously denied, go to Settings > Apps > [Your App Name] > Permissions and enable "Nearby devices."
  3. 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:

  1. Go to Settings > Connections (or) Connected Devices* and look for a WS101 device.
  2. 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.*
  3. 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