PandoraServer: report full stacktrace through gRPC

Test: avatar run
Change-Id: I867aabb80a4f280563a15db181ce683a166c8819
diff --git a/android/pandora/server/src/com/android/pandora/A2dp.kt b/android/pandora/server/src/com/android/pandora/A2dp.kt
index cce3715..7e5f426 100644
--- a/android/pandora/server/src/com/android/pandora/A2dp.kt
+++ b/android/pandora/server/src/com/android/pandora/A2dp.kt
@@ -28,6 +28,8 @@
 import io.grpc.Status
 import io.grpc.stub.StreamObserver
 import java.io.Closeable
+import java.io.PrintWriter
+import java.io.StringWriter
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.cancel
@@ -91,8 +93,7 @@
             .first()
 
         if (state == BluetoothProfile.STATE_DISCONNECTED) {
-          Log.e(TAG, "openSource failed, A2DP has been disconnected")
-          throw Status.UNKNOWN.asException()
+          throw RuntimeException("openSource failed, A2DP has been disconnected")
         }
       }
 
@@ -124,8 +125,7 @@
             .first()
 
         if (state == BluetoothProfile.STATE_DISCONNECTED) {
-          Log.e(TAG, "waitSource failed, A2DP has been disconnected")
-          throw Status.UNKNOWN.asException()
+          throw RuntimeException("waitSource failed, A2DP has been disconnected")
         }
       }
 
@@ -146,8 +146,7 @@
       Log.i(TAG, "start: device=$device")
 
       if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot start")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot start")
       }
 
       audioTrack!!.play()
@@ -171,13 +170,11 @@
       Log.i(TAG, "suspend: device=$device")
 
       if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot suspend")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot suspend")
       }
 
       if (!bluetoothA2dp.isA2dpPlaying(device)) {
-        Log.e(TAG, "Device is already suspended, cannot suspend")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is already suspended, cannot suspend")
       }
 
       val a2dpPlayingStateFlow =
@@ -201,8 +198,7 @@
       Log.i(TAG, "isSuspended: device=$device")
 
       if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot get suspend state")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot get suspend state")
       }
 
       val isSuspended = bluetoothA2dp.isA2dpPlaying(device)
@@ -216,8 +212,7 @@
       Log.i(TAG, "close: device=$device")
 
       if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot close")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot close")
       }
 
       val a2dpConnectionStateChangedFlow =
@@ -270,9 +265,13 @@
           )
         }
       }
-      override fun onError(t: Throwable?) {
-        Log.e(TAG, t.toString())
-        responseObserver.onError(t)
+      override fun onError(t: Throwable) {
+        t.printStackTrace()
+        val sw = StringWriter()
+        t.printStackTrace(PrintWriter(sw))
+        responseObserver.onError(
+          Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
+        )
       }
       override fun onCompleted() {
         responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
@@ -290,8 +289,7 @@
       Log.i(TAG, "getAudioEncoding: device=$device")
 
       if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot getAudioEncoding")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot getAudioEncoding")
       }
 
       // For now, we only support 44100 kHz sampling rate.
diff --git a/android/pandora/server/src/com/android/pandora/A2dpSink.kt b/android/pandora/server/src/com/android/pandora/A2dpSink.kt
index 1b941f2..5785e7c 100644
--- a/android/pandora/server/src/com/android/pandora/A2dpSink.kt
+++ b/android/pandora/server/src/com/android/pandora/A2dpSink.kt
@@ -25,7 +25,6 @@
 import android.content.IntentFilter
 import android.media.*
 import android.util.Log
-import io.grpc.Status
 import io.grpc.stub.StreamObserver
 import java.io.Closeable
 import kotlinx.coroutines.CoroutineScope
@@ -85,8 +84,7 @@
             .first()
 
         if (state == BluetoothProfile.STATE_DISCONNECTED) {
-          Log.e(TAG, "waitStream failed, A2DP has been disconnected")
-          throw Status.UNKNOWN.asException()
+          throw RuntimeException("waitStream failed, A2DP has been disconnected")
         }
       }
 
@@ -100,8 +98,7 @@
       val device = request.sink.connection.toBluetoothDevice(bluetoothAdapter)
       Log.i(TAG, "close: device=$device")
       if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
-        Log.e(TAG, "Device is not connected, cannot close")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot close")
       }
 
       val a2dpConnectionStateChangedFlow =
diff --git a/android/pandora/server/src/com/android/pandora/Gatt.kt b/android/pandora/server/src/com/android/pandora/Gatt.kt
index 60d7ba5..77b6c88 100644
--- a/android/pandora/server/src/com/android/pandora/Gatt.kt
+++ b/android/pandora/server/src/com/android/pandora/Gatt.kt
@@ -26,7 +26,6 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.util.Log
-import io.grpc.Status
 import io.grpc.stub.StreamObserver
 import java.io.Closeable
 import java.util.UUID
@@ -77,8 +76,7 @@
       val mtu = request.mtu
       Log.i(TAG, "exchangeMTU MTU=$mtu")
       if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) {
-        Log.e(TAG, "Error on requesting MTU $mtu")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Error on requesting MTU $mtu")
       }
       ExchangeMTUResponse.newBuilder().build()
     }
@@ -383,8 +381,7 @@
 
   private suspend fun tryDiscoverServices(gattInstance: GattInstance) {
     if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) {
-      Log.e(TAG, "Error on discovering services for $gattInstance")
-      throw Status.UNKNOWN.asException()
+      throw RuntimeException("Error on discovering services for $gattInstance")
     } else {
       gattInstance.waitForDiscoveryEnd()
     }
diff --git a/android/pandora/server/src/com/android/pandora/GattInstance.kt b/android/pandora/server/src/com/android/pandora/GattInstance.kt
index edf5111..efc938e 100644
--- a/android/pandora/server/src/com/android/pandora/GattInstance.kt
+++ b/android/pandora/server/src/com/android/pandora/GattInstance.kt
@@ -56,11 +56,7 @@
   ) {}
   private var mGattInstanceValuesRead = arrayListOf<GattInstanceValueRead>()
 
-  class GattInstanceValueWrote(
-    var uuid: UUID?,
-    var handle: Int,
-    var status: AttStatusCode
-  ) {}
+  class GattInstanceValueWrote(var uuid: UUID?, var handle: Int, var status: AttStatusCode) {}
   private var mGattInstanceValueWrote = GattInstanceValueWrote(null, 0, AttStatusCode.UNKNOWN_ERROR)
 
   companion object GattManager {
@@ -320,7 +316,8 @@
       characteristic.getInstanceId(),
       AttStatusCode.UNKNOWN_ERROR
     )
-    if (mGatt.writeCharacteristic(
+    if (
+      mGatt.writeCharacteristic(
         characteristic,
         value,
         BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
@@ -329,7 +326,6 @@
       waitForWriteEnd()
     }
     return mGattInstanceValueWrote
-
   }
 
   public suspend fun writeDescriptorBlocking(
@@ -341,15 +337,10 @@
       descriptor.getInstanceId(),
       AttStatusCode.UNKNOWN_ERROR
     )
-    if (mGatt.writeDescriptor(
-        descriptor,
-        value
-      ) == BluetoothStatusCodes.SUCCESS
-    ) {
+    if (mGatt.writeDescriptor(descriptor, value) == BluetoothStatusCodes.SUCCESS) {
       waitForWriteEnd()
     }
     return mGattInstanceValueWrote
-
   }
 
   public fun disconnectInstance() {
diff --git a/android/pandora/server/src/com/android/pandora/GattServerManager.kt b/android/pandora/server/src/com/android/pandora/GattServerManager.kt
index 3a33d64..b6d3c52 100644
--- a/android/pandora/server/src/com/android/pandora/GattServerManager.kt
+++ b/android/pandora/server/src/com/android/pandora/GattServerManager.kt
@@ -68,7 +68,13 @@
             ByteArray(negociatedMtu)
           )
         } else {
-          server.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, ByteArray(512 - offset))
+          server.sendResponse(
+            device,
+            requestId,
+            BluetoothGatt.GATT_SUCCESS,
+            offset,
+            ByteArray(512 - offset)
+          )
         }
       }
 
diff --git a/android/pandora/server/src/com/android/pandora/Host.kt b/android/pandora/server/src/com/android/pandora/Host.kt
index 389f19e..aaf7481 100644
--- a/android/pandora/server/src/com/android/pandora/Host.kt
+++ b/android/pandora/server/src/com/android/pandora/Host.kt
@@ -41,10 +41,10 @@
 import android.util.Log
 import com.google.protobuf.ByteString
 import com.google.protobuf.Empty
-import io.grpc.Status
 import io.grpc.stub.StreamObserver
-import java.nio.ByteBuffer
 import java.io.Closeable
+import java.lang.IllegalArgumentException
+import java.nio.ByteBuffer
 import java.time.Duration
 import java.util.UUID
 import kotlinx.coroutines.CoroutineScope
@@ -68,9 +68,8 @@
 
 object ByteArrayOps {
   public fun getUShortAt(input: ByteArray, index: Int): UShort {
-    return (
-      ((input[index + 1].toUInt() and 0xffU) shl 8) or
-       (input[index].toUInt() and 0xffU)).toUShort()
+    return (((input[index + 1].toUInt() and 0xffU) shl 8) or (input[index].toUInt() and 0xffU))
+      .toUShort()
   }
 
   public fun getShortAt(input: ByteArray, index: Int): Short {
@@ -78,11 +77,10 @@
   }
 
   public fun getUIntAt(input: ByteArray, index: Int): UInt {
-    return (
-      ((input[index + 3].toUInt() and 0xffU) shl 24) or
+    return (((input[index + 3].toUInt() and 0xffU) shl 24) or
       ((input[index + 2].toUInt() and 0xffU) shl 16) or
       ((input[index + 1].toUInt() and 0xffU) shl 8) or
-       (input[index].toUInt() and 0xffU))
+      (input[index].toUInt() and 0xffU))
   }
 
   public fun getIntAt(input: ByteArray, index: Int): Int {
@@ -90,10 +88,9 @@
   }
 
   public fun getUInt24At(input: ByteArray, index: Int): UInt {
-    return (
-      ((input[index + 2].toUInt() and 0xffU) shl 16) or
+    return (((input[index + 2].toUInt() and 0xffU) shl 16) or
       ((input[index + 1].toUInt() and 0xffU) shl 8) or
-       (input[index].toUInt() and 0xffU))
+      (input[index].toUInt() and 0xffU))
   }
 
   public fun getInt24At(input: ByteArray, index: Int): Int {
@@ -280,14 +277,14 @@
     responseObserver: StreamObserver<WaitConnectionResponse>
   ) {
     grpcUnary(scope, responseObserver) {
-      if (request.address.isEmpty()) throw Status.UNKNOWN.asException()
+      if (request.address.isEmpty())
+        throw IllegalArgumentException("Request address field must be set")
       var bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
 
       Log.i(TAG, "waitConnection: device=$bluetoothDevice")
 
       if (!bluetoothAdapter.isEnabled) {
-        Log.e(TAG, "Bluetooth is not enabled, cannot waitConnection")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Bluetooth is not enabled, cannot waitConnection")
       }
 
       if (!bluetoothDevice.isConnected() || waitedAclConnection.contains(bluetoothDevice)) {
@@ -312,8 +309,7 @@
       val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter)
       Log.i(TAG, "waitDisconnection: device=$bluetoothDevice")
       if (!bluetoothAdapter.isEnabled) {
-        Log.e(TAG, "Bluetooth is not enabled, cannot waitDisconnection")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Bluetooth is not enabled, cannot waitDisconnection")
       }
       if (bluetoothDevice.bondState != BluetoothDevice.BOND_NONE) {
         flow
@@ -327,6 +323,8 @@
 
   override fun connect(request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>) {
     grpcUnary(scope, responseObserver) {
+      if (request.address.isEmpty())
+        throw IllegalArgumentException("Request address field must be set")
       val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
 
       Log.i(TAG, "connect: address=$bluetoothDevice")
@@ -360,8 +358,7 @@
       Log.i(TAG, "disconnect: device=$bluetoothDevice")
 
       if (!bluetoothDevice.isConnected()) {
-        Log.e(TAG, "Device is not connected, cannot disconnect")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("Device is not connected, cannot disconnect")
       }
 
       when (request.connection.transport) {
@@ -381,16 +378,14 @@
               instance
             }
           if (gattInstance.isDisconnected()) {
-            Log.e(TAG, "Device is not connected, cannot disconnect")
-            throw Status.UNKNOWN.asException()
+            throw RuntimeException("Device is not connected, cannot disconnect")
           }
 
           bluetoothDevice.disconnect()
           gattInstance.disconnectInstance()
         }
         else -> {
-          Log.e(TAG, "Device type UNKNOWN")
-          throw Status.UNKNOWN.asException()
+          throw RuntimeException("Device type UNKNOWN")
         }
       }
       flow
@@ -412,8 +407,7 @@
         ownAddressType != OwnAddressType.RANDOM &&
           ownAddressType != OwnAddressType.RESOLVABLE_OR_RANDOM
       ) {
-        Log.e(TAG, "connectLE: Unsupported OwnAddressType: $ownAddressType")
-        throw Status.UNKNOWN.asException()
+        throw RuntimeException("connectLE: Unsupported OwnAddressType: $ownAddressType")
       }
       val (address, type) =
         when (request.getAddressCase()!!) {
@@ -425,7 +419,8 @@
             Pair(request.publicIdentity, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
           ConnectLERequest.AddressCase.RANDOM_STATIC_IDENTITY ->
             Pair(request.randomStaticIdentity, BluetoothDevice.ADDRESS_TYPE_RANDOM)
-          ConnectLERequest.AddressCase.ADDRESS_NOT_SET -> throw Status.UNKNOWN.asException()
+          ConnectLERequest.AddressCase.ADDRESS_NOT_SET ->
+            throw IllegalArgumentException("Request address field must be set")
         }
       Log.i(TAG, "connectLE: $address")
       val bluetoothDevice = scanLeDevice(address.decodeAsMacAddressToString(), type)!!
@@ -493,8 +488,7 @@
             !dataTypesRequest.getIncompleteServiceClassUuids32List().isEmpty() or
             !dataTypesRequest.getIncompleteServiceClassUuids128List().isEmpty()
         ) {
-          Log.e(TAG, "Incomplete Service Class Uuids not supported")
-          throw Status.UNKNOWN.asException()
+          throw RuntimeException("Incomplete Service Class Uuids not supported")
         }
 
         // Handle service uuids
@@ -600,43 +594,47 @@
               var dataTypesBuilder =
                 DataTypes.newBuilder().setTxPowerLevel(scanRecord.getTxPowerLevel())
 
-                scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_SHORT]?.let {
+              scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_SHORT]?.let {
                 dataTypesBuilder.setShortenedLocalName(it.decodeToString())
               }
                 ?: run { dataTypesBuilder.setIncludeShortenedLocalName(false) }
 
-                scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_COMPLETE]?.let {
+              scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_COMPLETE]?.let {
                 dataTypesBuilder.setCompleteLocalName(it.decodeToString())
               }
                 ?: run { dataTypesBuilder.setIncludeCompleteLocalName(false) }
 
-                scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL]?.let {
+              scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL]?.let {
                 dataTypesBuilder.setAdvertisingInterval(ByteArrayOps.getShortAt(it, 0).toInt())
               }
 
-                scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL_LONG]?.let {
+              scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL_LONG]?.let {
                 dataTypesBuilder.setAdvertisingInterval(ByteArrayOps.getIntAt(it, 0))
               }
 
-                scanData[ScanRecord.DATA_TYPE_APPEARANCE]?.let {
+              scanData[ScanRecord.DATA_TYPE_APPEARANCE]?.let {
                 dataTypesBuilder.setAppearance(ByteArrayOps.getShortAt(it, 0).toInt())
               }
 
-                scanData[ScanRecord.DATA_TYPE_CLASS_OF_DEVICE]?.let {
+              scanData[ScanRecord.DATA_TYPE_CLASS_OF_DEVICE]?.let {
                 dataTypesBuilder.setClassOfDevice(ByteArrayOps.getInt24At(it, 0))
               }
 
-                scanData[ScanRecord.DATA_TYPE_URI]?.let {
+              scanData[ScanRecord.DATA_TYPE_URI]?.let {
                 dataTypesBuilder.setUri(it.decodeToString())
               }
 
-                scanData[ScanRecord.DATA_TYPE_LE_SUPPORTED_FEATURES]?.let {
+              scanData[ScanRecord.DATA_TYPE_LE_SUPPORTED_FEATURES]?.let {
                 dataTypesBuilder.setLeSupportedFeatures(ByteString.copyFrom(it))
               }
 
-                scanData[ScanRecord.DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE]?.let {
-                dataTypesBuilder.setPeripheralConnectionIntervalMin(ByteArrayOps.getShortAt(it, 0).toInt())
-                dataTypesBuilder.setPeripheralConnectionIntervalMax(ByteArrayOps.getShortAt(it, 2).toInt())
+              scanData[ScanRecord.DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE]?.let {
+                dataTypesBuilder.setPeripheralConnectionIntervalMin(
+                  ByteArrayOps.getShortAt(it, 0).toInt()
+                )
+                dataTypesBuilder.setPeripheralConnectionIntervalMax(
+                  ByteArrayOps.getShortAt(it, 2).toInt()
+                )
               }
 
               for (serviceDataEntry in serviceData) {
@@ -711,8 +709,7 @@
                   .put(manufacturerSpecificDatas.get(id))
               }
               dataTypesBuilder.setManufacturerSpecificData(
-                ByteString.copyFrom(manufacturerData.array(), 0,
-                  manufacturerData.position())
+                ByteString.copyFrom(manufacturerData.array(), 0, manufacturerData.position())
               )
               val primaryPhy =
                 when (result.getPrimaryPhy()) {
diff --git a/android/pandora/server/src/com/android/pandora/Main.kt b/android/pandora/server/src/com/android/pandora/Main.kt
index 5f34a12..9881d1b 100644
--- a/android/pandora/server/src/com/android/pandora/Main.kt
+++ b/android/pandora/server/src/com/android/pandora/Main.kt
@@ -20,8 +20,8 @@
 import android.os.Bundle
 import android.os.Debug
 import android.util.Log
-import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.runner.MonitoringInstrumentation
 
 @kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt b/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt
index 8ba4618..d64aea1 100644
--- a/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt
+++ b/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt
@@ -79,7 +79,8 @@
   }
 
   fun play() {
-    if (currentTrack == -1 || currentTrack == QUEUE_SIZE) currentTrack = QUEUE_START_INDEX else currentTrack += 1
+    if (currentTrack == -1 || currentTrack == QUEUE_SIZE) currentTrack = QUEUE_START_INDEX
+    else currentTrack += 1
     setPlaybackState(PlaybackState.STATE_PLAYING)
     mediaSession.setMetadata(metadataItems.get("" + currentTrack))
   }
@@ -102,14 +103,16 @@
   }
 
   fun forward() {
-    if (currentTrack == QUEUE_SIZE || currentTrack == -1) currentTrack = QUEUE_START_INDEX else currentTrack += 1
+    if (currentTrack == QUEUE_SIZE || currentTrack == -1) currentTrack = QUEUE_START_INDEX
+    else currentTrack += 1
     setPlaybackState(PlaybackState.STATE_SKIPPING_TO_NEXT)
     mediaSession.setMetadata(metadataItems.get("" + currentTrack))
     setPlaybackState(PlaybackState.STATE_PLAYING)
   }
 
   fun backward() {
-    if (currentTrack == QUEUE_START_INDEX || currentTrack == -1) currentTrack = QUEUE_SIZE else currentTrack -= 1
+    if (currentTrack == QUEUE_START_INDEX || currentTrack == -1) currentTrack = QUEUE_SIZE
+    else currentTrack -= 1
     setPlaybackState(PlaybackState.STATE_SKIPPING_TO_PREVIOUS)
     mediaSession.setMetadata(metadataItems.get("" + currentTrack))
     setPlaybackState(PlaybackState.STATE_PLAYING)
@@ -190,8 +193,14 @@
         MediaMetadata.Builder()
           .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, NOW_PLAYING_PREFIX + item)
           .putString(MediaMetadata.METADATA_KEY_TITLE, "Title$item")
-          .putString(MediaMetadata.METADATA_KEY_ARTIST, if (item != QUEUE_SIZE) "Artist$item" else generateAlphanumericString(512))
-          .putString(MediaMetadata.METADATA_KEY_ALBUM, if (item != QUEUE_SIZE) "Album$item" else generateAlphanumericString(512))
+          .putString(
+            MediaMetadata.METADATA_KEY_ARTIST,
+            if (item != QUEUE_SIZE) "Artist$item" else generateAlphanumericString(512)
+          )
+          .putString(
+            MediaMetadata.METADATA_KEY_ALBUM,
+            if (item != QUEUE_SIZE) "Album$item" else generateAlphanumericString(512)
+          )
           .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, item.toLong())
           .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, QUEUE_SIZE.toLong())
           .build()
@@ -252,6 +261,6 @@
     const val QUEUE_SIZE = 6
     const val NEW_QUEUE_ITEM_INDEX = 7
 
-    fun isInitialized() : Boolean = this::instance.isInitialized
+    fun isInitialized(): Boolean = this::instance.isInitialized
   }
 }
diff --git a/android/pandora/server/src/com/android/pandora/Pbap.kt b/android/pandora/server/src/com/android/pandora/Pbap.kt
index 279d6f0..2ae7729 100644
--- a/android/pandora/server/src/com/android/pandora/Pbap.kt
+++ b/android/pandora/server/src/com/android/pandora/Pbap.kt
@@ -61,7 +61,7 @@
 
     if (cursor.getCount() >= CONTACT_LIST_SIZE) return // return if contacts are present
 
-    for (item in cursor.getCount()+1..CONTACT_LIST_SIZE) {
+    for (item in cursor.getCount() + 1..CONTACT_LIST_SIZE) {
       addContact(item)
     }
   }
diff --git a/android/pandora/server/src/com/android/pandora/Security.kt b/android/pandora/server/src/com/android/pandora/Security.kt
index a3494e3..9fdc22e 100644
--- a/android/pandora/server/src/com/android/pandora/Security.kt
+++ b/android/pandora/server/src/com/android/pandora/Security.kt
@@ -34,7 +34,6 @@
 import android.util.Log
 import com.google.protobuf.ByteString
 import com.google.protobuf.Empty
-import io.grpc.Status
 import io.grpc.stub.StreamObserver
 import java.io.Closeable
 import kotlinx.coroutines.CoroutineScope
@@ -119,7 +118,8 @@
             check(request.getLevelCase() == SecureRequest.LevelCase.LE)
             val level = request.le
             if (level == LE_LEVEL1) true
-            else if (level == LE_LEVEL4) throw Status.UNKNOWN.asException()
+            else if (level == LE_LEVEL4)
+              throw RuntimeException("secure: Low-energy level 4 not supported")
             else {
               bluetoothDevice.createBond(transport)
               waitLESecurityLevel(bluetoothDevice, level)
@@ -129,13 +129,14 @@
             check(request.getLevelCase() == SecureRequest.LevelCase.CLASSIC)
             val level = request.classic
             if (level == LEVEL0) true
-            else if (level >= LEVEL3) throw Status.UNKNOWN.asException()
+            else if (level >= LEVEL3)
+              throw RuntimeException("secure: Classic level up to 3 not supported")
             else {
               bluetoothDevice.createBond(transport)
               waitBREDRSecurityLevel(bluetoothDevice, level)
             }
           }
-          else -> throw Status.UNKNOWN.asException()
+          else -> throw RuntimeException("secure: Invalid transport")
         }
       val secureResponseBuilder = SecureResponse.newBuilder()
       if (reached) secureResponseBuilder.setSuccess(Empty.getDefaultInstance())
@@ -159,7 +160,7 @@
     Log.i(TAG, "waitBREDRSecurityLevel")
     return when (level) {
       LEVEL0 -> true
-      LEVEL3 -> throw Status.UNKNOWN.asException()
+      LEVEL3 -> throw RuntimeException("waitSecurity: Classic level 3 not supported")
       else -> {
         val bondState = waitBondIntent(bluetoothDevice)
         val isEncrypted = bluetoothDevice.isEncrypted()
@@ -179,14 +180,14 @@
     Log.i(TAG, "waitLESecurityLevel")
     return when (level) {
       LE_LEVEL1 -> true
-      LE_LEVEL4 -> throw Status.UNKNOWN.asException()
+      LE_LEVEL4 -> throw RuntimeException("waitSecurity: Low-energy level 4 not supported")
       else -> {
         val bondState = waitBondIntent(bluetoothDevice)
         val isEncrypted = bluetoothDevice.isEncrypted()
         when (level) {
           LE_LEVEL2 -> isEncrypted
           LE_LEVEL3 -> isEncrypted && bondState == BOND_BONDED
-          else -> throw Status.UNKNOWN.asException()
+          else -> throw RuntimeException("waitSecurity: Low-energy level 4 not supported")
         }
       }
     }
@@ -210,7 +211,7 @@
             check(request.hasClassic())
             waitBREDRSecurityLevel(bluetoothDevice, request.classic)
           }
-          else -> throw Status.UNKNOWN.asException()
+          else -> throw RuntimeException("secure: Invalid transport")
         }
       val waitSecurityBuilder = WaitSecurityResponse.newBuilder()
       if (reached) waitSecurityBuilder.setSuccess(Empty.getDefaultInstance())
diff --git a/android/pandora/server/src/com/android/pandora/SecurityStorage.kt b/android/pandora/server/src/com/android/pandora/SecurityStorage.kt
index 8870e6c..f1e7f4a 100644
--- a/android/pandora/server/src/com/android/pandora/SecurityStorage.kt
+++ b/android/pandora/server/src/com/android/pandora/SecurityStorage.kt
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothDevice.BOND_BONDED
 import android.bluetooth.BluetoothDevice.BOND_NONE
 import android.bluetooth.BluetoothManager
 import android.content.Context
@@ -69,13 +68,11 @@
     grpcUnary(globalScope, responseObserver) {
       val bondedDevices = bluetoothAdapter.getBondedDevices()
       val bondedDevice =
-        when(request.getAddressCase()!!) {
-          IsBondedRequest.AddressCase.PUBLIC -> bondedDevices.firstOrNull {
-            it.toByteString() == request.public
-          }
-          IsBondedRequest.AddressCase.RANDOM ->  bondedDevices.firstOrNull {
-            it.toByteString() == request.random
-          }
+        when (request.getAddressCase()!!) {
+          IsBondedRequest.AddressCase.PUBLIC ->
+            bondedDevices.firstOrNull { it.toByteString() == request.public }
+          IsBondedRequest.AddressCase.RANDOM ->
+            bondedDevices.firstOrNull { it.toByteString() == request.random }
           IsBondedRequest.AddressCase.ADDRESS_NOT_SET -> throw Status.UNKNOWN.asException()
         }
       Log.i(TAG, "isBonded: device=$bondedDevice")
diff --git a/android/pandora/server/src/com/android/pandora/Utils.kt b/android/pandora/server/src/com/android/pandora/Utils.kt
index 877f000..0aa6dd6 100644
--- a/android/pandora/server/src/com/android/pandora/Utils.kt
+++ b/android/pandora/server/src/com/android/pandora/Utils.kt
@@ -27,15 +27,17 @@
 import android.media.*
 import android.net.MacAddress
 import android.os.ParcelFileDescriptor
-import android.os.ParcelUuid
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.protobuf.Any
 import com.google.protobuf.ByteString
+import io.grpc.Status
 import io.grpc.stub.ServerCallStreamObserver
 import io.grpc.stub.StreamObserver
 import java.io.BufferedReader
 import java.io.InputStreamReader
+import java.io.PrintWriter
+import java.io.StringWriter
 import java.util.concurrent.CancellationException
 import java.util.stream.Collectors
 import kotlinx.coroutines.CoroutineScope
@@ -103,7 +105,7 @@
  * @param scope coroutine scope used to run the coroutine.
  * @param responseObserver the gRPC stream observer on which to send the response.
  * @param timeout the duration in seconds after which the coroutine is automatically cancelled and
- * returns a timeout error. Default: 60s.
+ *   returns a timeout error. Default: 60s.
  * @param block the suspended function to execute to get the response.
  * @return reference to the coroutine as a Job.
  *
@@ -133,7 +135,11 @@
       responseObserver.onCompleted()
     } catch (e: Throwable) {
       e.printStackTrace()
-      responseObserver.onError(e)
+      val sw = StringWriter()
+      e.printStackTrace(PrintWriter(sw))
+      responseObserver.onError(
+        Status.UNKNOWN.withCause(e).withDescription(sw.toString()).asException()
+      )
     }
   }
 }
@@ -181,7 +187,11 @@
         }
         .catch {
           it.printStackTrace()
-          responseObserver.onError(it)
+          val sw = StringWriter()
+          it.printStackTrace(PrintWriter(sw))
+          responseObserver.onError(
+            Status.UNKNOWN.withCause(it).withDescription(sw.toString()).asException()
+          )
         }
         .launchIn(this)
     }
@@ -208,6 +218,11 @@
     override fun onError(e: Throwable) {
       job.cancel()
       e.printStackTrace()
+      val sw = StringWriter()
+      e.printStackTrace(PrintWriter(sw))
+      responseObserver.onError(
+        Status.UNKNOWN.withCause(e).withDescription(sw.toString()).asException()
+      )
     }
   }
 }
@@ -254,7 +269,11 @@
         }
         .catch {
           it.printStackTrace()
-          responseObserver.onError(it)
+          val sw = StringWriter()
+          it.printStackTrace(PrintWriter(sw))
+          responseObserver.onError(
+            Status.UNKNOWN.withCause(it).withDescription(sw.toString()).asException()
+          )
         }
         .launchIn(this)
     }