Advanced Android

This section covers additional integration considerations and suggestions when developing audio applications that will use the BlueParrott Button on the Android platform

Sample Application

An additional sample application, blueParrottSDKaudiodemo, is included with the SDK that has code samples for the topics covered in this section.

This demo application demonstrates many of the techniques necessary to integrate the BlueParrott Button into a Push To Talk or other voice application. The example implements a simple voice-echo feature. When the user presses the BlueParrott Button, it records audio in a temporary file and when the BlueParrott Button is released, the audio is replayed.

The feature works in both foreground (with a full user interface) and background (with the user actions shown in the Notification Bar).

The application includes the following features:

  • Ensures required permissions are obtained
  • Automatically maintains a connection to the BlueParrott Button
  • Sets BlueParrott Button into SDK Mode
  • Users Services to run in background keeps foreground UI in synch with BlueParrott Button
  • Records audio when the BlueParrott Button is depressed
  • Playback of audio when BlueParrott Button is released
  • The Noficiation Bar is updated with an icon to indicate current app state (connected, recording, playing)

This is a sample screen from the application running in the foreground:

BlueParrott SDK Audio

This is a sample screen from the application running in background with the BlueParrott Head icon displayed in Notification Bar:

BlueParrott SDK Audio Back

Application Structure

The application has the following major Classes

  • MainActivity – This is the main user interface Activity. The user can invoke Record and Playback by touching the on-screen button. This Activity is also where the user is prompted to grant necessary permissions
  • SdkConnectionService – this Service is used to manage the connection to the BlueParrott Button. It watches for the connection of a headset and attempts to connect to the BlueParrott Button. Likewise, when it sees a Headset disconnect, it will release the connection to the BlueParrott Button
  • RecPlayAudioService – this Service is used to record and play back the audio. Audio is recorded to and played back from, a temporary file
  • StartServicesOnBoot – this is a BroadcastReceiver launched when the handset is booted. It is used to launch the SDKConnectionService and RecPlayAudioService

Connection Management

The job of the SdkConnectionService is to automatically maintain a connection to the BlueParrottButton when a BlueParrott headset is connected to the handset.

The service automatically detects when a bluetooth headset is connected (via the BluetoothDevice.ACTION_ACL_CONNECTED action) and sets up the connection to the BlueParrott Button via the SDK.

Likewise, when the headset is disconnected (via the BluetoothDevice.ACTION_ACL_DISCONNECTED action), the connection to the BlueParrott Button is torn down.

Setting up Receivers

When the Service is created it instantiates Receivers to do the work of connecting and disconnecting from the BlueParrott Button. These receivers will be triggered automatically by headset connection/disconnects. (The receivers are registered later).

//when service is created
public void onCreate() {
    super.onCreate();
    headsetSdk = BPSdk.getBPHeadset(this);
    headsetConnectReceiver = new HeadsetConnectReceiver();
    headsetDisconnectReceiver = new HeadsetDisconnectReceiver();
}

The HeadsetConnectReceiver will be triggered when a Bluetooth Headset is connected, and will result in an attempt to connect to the BlueParrott Button.

//React to connection of any Bluetooth headset - attempt to connect to SDK
 public class HeadsetConnectReceiver extends BroadcastReceiver {
     public void onReceive(Context context, final Intent intent) {
         String action = intent.getAction();
         BluetoothDevice bluetoothDevice;
         if (action.equals((BluetoothDevice.ACTION_ACL_CONNECTED))){
             bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
             Log.d(LOGTAG, " App received ACL_CONNECTED for "+ bluetoothDevice.getName() );
             deRegisterForConnect();

             //Add a listener to the headsetSDK object
             headsetSdk.addListener(SdkConnectionService.this);

             //use a 1s delay on the runnable to allow Bluetooth complete its business
             handler.postDelayed(runConnectSDK, 1000);
         }
     }
 }

The HeadsetDisconnectReceiver will be triggered when a Bluetooth Headset is diconnected, and will result in an attempt to disconnect from the BlueParrott Button. It will also register to recieve connect events in the future (so that when user re-connects the headset, the BlueParrott Button will automatically be connected)

//React to disconnection of any Bluetooth headset - disconnect from SDK
public class HeadsetDisconnectReceiver extends BroadcastReceiver {
    public void onReceive(Context context, final Intent intent) {
        String action = intent.getAction();
        BluetoothDevice bluetoothDevice;
        if (action.equals((BluetoothDevice.ACTION_ACL_DISCONNECTED))){
            bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            Log.d(LOGTAG, bluetoothDevice.getName() + " is disconnected");
            deRegisterForDisconnect();
            registerForConnect();
        }
    }

Registering the Receivers

When the SdkConnectionService starts it checks to see if there is a Headset already connected, and if there is, it attempts to connect to the BlueParrott Button via the SDK. It will also register to be notified when the headset disconnects by calling registerForDisconnect().

public int onStartCommand(Intent intent, int flags, int startId) {
   if (Utils.isBluetoothHeadsetConnected()) {
       if (!(headsetSdk.connected())) {
           headsetSdk.addListener(SdkConnectionService.this);
           handler.postDelayed(runConnectSDK, 1000);//delay for bluetooth to allow for timing issues (give OS a chance to catch up)
       }
       else{
           registerForDisconnect();
       }
   } else {
       registerForConnect();
   }
   return Service.START_STICKY;  //service will restart automatically
}

After that the recievers are registered and deregistered according to the following pattern.

  1. When a headset is connected

    • Deregister for connect receiver
    • Register for disconnect receiver
  2. Headset disconnected

    • Deregister for disconnect receiver
    • Register for connect receiver

There are some helper methods that will register these receivers based on the pattern described below


    private void registerForConnect() {
        IntentFilter fltr = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
        registerReceiver(headsetConnectReceiver, fltr);
    }


      private void registerForDisconnect() {
          IntentFilter fltr = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);
          registerReceiver(headsetDisconnectReceiver, fltr);
      }

      private void deRegisterForDisconnect() {
         try {
             unregisterReceiver(headsetDisconnectReceiver);
             } catch (IllegalArgumentException e) {
                 e.printStackTrace();
                }
      }

      private void deRegisterForConnect() {
         try {
             unregisterReceiver(headsetConnectReceiver);
             } catch (IllegalArgumentException e) {
                  e.printStackTrace();
                 }
      }

Connect to BlueParrott Button

The runConnectSDK runnable just calls the headsetSdk.connect() method.

    private Runnable runConnectSDK = new Runnable() {
       @Override
       public void run() {
           headsetSdk.connect();
       }
   };

Register for Disconnect Receiver

Once an attempt to connect to the BlueParrott Button has concluded, we can register for the disconnect broadcast receiver so that you listen for the Bluetooth ACL disconnect action again which will in turn trigger a disconnect from the BlueParrott Button.

@Override
    public void onConnect() {
        headsetSdk.enableSDKMode();
        //set up recevier to monitor for disconnects
        registerForDisconnect();
    }

    @Override
    public void onConnectFailure(int reasonCode) {
        //set up recevier to monitor for disconnects
        registerForDisconnect();
    }

On Boot Connection

Auto boot refers to automatically running your application when the handset is switched on or rebooted.

// Manifest changes are required to auto boot using a receiver
// which will start services in background.
// The receiver listens for BOOT_COMPLETED action which occurs with a cold start of the handset.
// The receiver also listens for QUICKBOT_POWERON action which occurs with a reboot of the handset
// Then within the broadcast receiver you may start the required services.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name="com.mysay.blueparrottsdkaudiodemo.StartServicesOnBoot"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.QUICKBOOT_POWERON" />
    </intent-filter>
</receiver>

Notification Icons

The BlueParrott Head Icon is not part of the BlueParrott SDK, however it is included in the Sample Apps.

It is designed to be displayed in the Notification Bar. It can be used to indicate when the BlueParrott Button is connected by displaying it. If used you should then clear it from the notification bar when disconnected from the BlueParrott Button. Through the use of the inverted BlueParrott Head the icon can be used to indicate to the user when an activity is taking place.

The BlueParrott Head Icon can be displayed while your application is in the foreground and is particularly useful in giving your user feedback when the application is running in the background.

//we are connected so show that in the notification bar
Utils.createNotification(this, RecPlayAudioService.AUDIO_STATE_IDLE);

//connection has failed, ensure we clear the notification bar
Utils.cancelHeadsetStatusNotification(this);

Icon Description
Parrott Head BlueParrott Head
Parrott Head invert BlueParrott Head inverted

Audio Record and Playback

There are various considerations for audio applications. It is beyond the scope of this document to cover all audio methods for recording and playback, however here are a couple of pointers you should consider when recording audio as a result of a BlueParrott Button Press.

SCO

Synchronous Connection Oriented (SCO) is for real-time narrow band signal which does not require retransmission. Generally applications recording over Bluetooth use SCO for VOIP and audio recording.

In order to record voice from the Bluetooth headset you must wait for the SCO channel to open. The Sample application uses a broadcast receiver to listen for this.

When recording is completed you should stop SCO.

If your application is recording from an alternative non-Bluetooth device you may record without reference to SCO.

//onCreate
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

public void startRecording() {
    if (Utils.isBluetoothHeadsetConnected()) {
    //Setup a receiver to notify us when SCO is connected
     registerReceiver(new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
             if (AudioManager.SCO_AUDIO_STATE_CONNECTED == state) {
                 broadcastAudioState(AUDIO_STATE_RECORDING);
                 isRecording = true;
                 recordThread = new RecordThread();
                 recordThread.start();
                 unregisterReceiver(this);
             }
         }
     }, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED));
     broadcastAudioState(AUDIO_STATE_WAITING_FOR_SCO);
     audioManager.startBluetoothSco();

public void stopRecording() {
    if (Utils.isBluetoothHeadsetConnected()) {
        audioManager.stopBluetoothSco();
        }

Record Thread

The sample application uses a thread to record to audio to a temporary .pcm file.

Recording is started when the BlueParrott Button is depressed.

A Buffer Output Stream is used in order to write to the temporary file.

 class RecordThread extends Thread {
     public void run() {
         try {
             byte[] buffer = new byte[mRecBufSize];
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(mPcmFile), mRecBufSize);
             audioRecord.startRecording();
             while (isRecording) {
                 int bufferReadResult = audioRecord.read(buffer, 0, mRecBufSize);
                 byte[] tmpBuf = new byte[bufferReadResult];
                 System.arraycopy(buffer, 0, tmpBuf, 0, bufferReadResult);
                 bos.write(tmpBuf, 0, tmpBuf.length);
             }
             bos.flush();
             bos.close();
             audioRecord.stop();
         } catch (Throwable t) {
             Log.d(LOGTAG, "error:" + t.getMessage());
         }
     }
 }

Stop Recording

When the BlueParrott Button is released the BlueParrott listener triggers the onButtonUp method, this is programmed to stop recording.

When it is time to stop recording you use a thread.join() to ensure that all the business of the record thread is completed before returning to calling class.

The method to start playing is called from the stop recording method.

   public void stopRecording() {
       // close the SCO channel
       if (Utils.isBluetoothHeadsetConnected()) {
           audioManager.stopBluetoothSco();
       }
       // wind down the thread and get in sync with it
       isRecording = false;
       try {
           if (recordThread != null) recordThread.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       startPlaying();
   }

Play Thread

The sample application uses a thread to play back the contents of the temporary .pcm file.

When the BlueParrott Button is released the BlueParrott listener triggers the onButtonUp method, in the sample program this is programmed to stop recording and start playback.

Recording is started when the BlueParrott Button is depressed.

A Buffer Input Stream is used in order to read from the temporary file.

 class PlayThread extends Thread {
     public void run() {
         try {
             sleep(500);//wait a bit
             byte[] buffer = new byte[mPlayBufSize];
             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(mPcmFile), mPlayBufSize);
             audioTrack.play();
             int readSize = -1;
             while (isPlaying && (readSize = bis.read(buffer)) != -1) {
                 audioTrack.write(buffer, 0, readSize);
             }
             audioTrack.stop();
             bis.close();
             broadcastAudioState(AUDIO_STATE_IDLE);
         } catch (Throwable t) {
             Log.d(LOGTAG, "error:" + t.getMessage());
         }
     }
 }

Broadcast Audio state to UI

A broadcast receiver is used in order to communicate to the UI the status of the audio activity.

In the sample application this audio state is used to determine which BlueParrott Head Icon to display in the Notification Bar, and the color of the Talk Button when the application is in the foreground.

public static final int AUDIO_STATE_IDLE = 0;
public static final int AUDIO_STATE_WAITING_FOR_SCO = 1;
public static final int AUDIO_STATE_RECORDING = 2;
public static final int AUDIO_STATE_PLAYING = 3;

//onCreate
//setup a localbroadcaster that we will use to tell the UI about state changed
broadcaster = LocalBroadcastManager.getInstance(this);

/*
  * Broadcast the audio state to the UI
  */
 public void broadcastAudioState(int audioState) {

     Utils.createNotification(this, audioState);
     Intent intent = new Intent(BP_AUDIO_STATE_BROADCAST);
     intent.putExtra(AUDIO_STATE_MESSAGE, audioState);
     broadcaster.sendBroadcast(intent);
 }