We are developing on Samsung Tab Active 4 Pro using Android Studio, kotlin and java. We are getting constant at random times app deadlocks. I am not new to developing with threads and synchronization methods however I am very new to Android development. I assume I am doing something wrong and will share what I am doing. But to set up the situation, we are using OSMDroid for the map viewing and are using layers of the map for draw icons that represent aircraft flying over the map. The main android app is done in kotlin and the UDP over ethernet is written in java files. Aircraft telemetry is received at about 1 hz over ethernet inside protobuf packets. The protobuf is received and parsed in a Listener java class using a thread and implemented in the run() method. In the udp handler where we receive and parse protobuf we call a kotlin object "class" (unfortunate use of terms with kotlin) that has a method UpdateEntity(). Here is a snippet of this code that handles a proto message type called Entity. EntityClass is the java wrapper created. This part is working fine, no issues here, just showing for context.
Code:
if (msg.getMessageTypeEnum() == MessageWrapperClass.MessageWrapper.MessageTypeEnum.MESSAGE_TYPE_ENTITY) {
EntityClass.Entity entity = msg.getMessageEntity();
Log.d(TAG, "Listener:run (thread): Entity received\"" + entity.toString());
// 0 deg points north, vertically in map
double heading = Math.atan2(
entity.getKinematics().getEastSpeed(),
entity.getKinematics().getNorthSpeed()) * 180.0 / Math.PI;
// Put to [0,360] range
if (heading < 0) {
heading += 360.0;
}
Position position = new Position(
heading,
entity.getKinematics().getPosition().getLatitudeDeg(),
entity.getKinematics().getPosition().getLongitudeDeg(),
entity.getKinematics().getPosition().getAltitudeHaeMeters(),
0.0);
HostileEntity unit = new HostileEntity(
Utils.TrimUUID(entity.getEntityID().getValue().toString()),
//todo: we need callsign here
"red", //entity.getEntityID().getValue().toString(),
position, false);
Datasource.INSTANCE.UpdateEntity(unit); // <---------- This accesses the shared list
}
Datasource.INSTANCE.UpdateEntity(unit); call accesses the share list. Here is that method in total. Note Datasource is that singleton-like object construct that has all public "static" methods. (I'm a C++ guy mostly
New to kotlin)
Java:
/**
* [Datasource] holds the data that is utilized by the UI
*/
object Datasource {
var currentStatus = ""
var currentMode = Modes.Nav
var Settings:SettingsClass = SettingsClass()
//This is the list of automations
var AutomationList = mutableListOf<AutomationClass>(
WTP(),
EW()
)
var TabPages = mutableListOf<String>()
var TabPageContents = mutableListOf<TabPage>()
var KillPairs = mutableListOf<KillPair>()
var SelectedTab:Int = 0
var newTab:String = ""
var locklineschanged = false
val entityUnitListMutex = Mutex(false)
val killPairsMutex = Mutex(false)
/*
This is the list of units. Any unit added to this list will get added to the map.
*/
val entityUnitList = mutableStateListOf<EntityUnit>()
fun UpdateEntity(entity:EntityUnit)
{
if (!entity.isPositionValid()) {
Log.e(ContentValues.TAG, "Datasource::UpdateEntity " + "Invalid entity passed:")
return
}
entity.lifeTime_ms = System.currentTimeMillis() // update its life time
try {
// NOTE - FindEntity() call must remain OUTSIDE of runblocking and mutex because
// FindEntity also has mutex and we do not want to nest mutex calls. Deadlock may occur
val existingEntity = FindEntity(entity.id)
runBlocking {
entityUnitListMutex.withLock {
if (existingEntity != null) {
entity.status = existingEntity.status
entity.missionCount = existingEntity.missionCount
val index = entityUnitList.indexOf(existingEntity)
entityUnitList[index] = entity
} else {
entityUnitList.add(entity)
}
}// end mutex
}
}
catch(e:Exception)
{
Log.e(ContentValues.TAG, "Datasource::UpdateEntity " + e.toString())
}
}
/*
Find the entity in the list of active entities
*/
fun FindEntity(entityName: String): EntityUnit? {
try {
var returnval: EntityUnit? = null
runBlocking {
entityUnitListMutex.withLock {
for (i in entityUnitList.indices) {
if (entityUnitList[i].id == entityName) {
returnval = entityUnitList[i]
break
}
}
}// end mutex
}
return returnval
} catch (e: Exception) {
Log.e(ContentValues.TAG, "Datasource::FindEntity " + e.toString())
return null
}
}
My understanding of kotlin and android is that you need to use a coroutine if you want to use a mutex. I used a mutex to protect the shared resource, the entityUnitList of type mutableStateListOf<EntityUnit>
The Listener is started up on the main, UI thread however it uses its own thread to receive udp packets. The call to Datasource::UpdateEntity() is done in that thread, not the UI thread. Datasource is used by the UI thread as well and it needs access to the entityUnitLst to read from. The UI thread does not write to any of the elements in the list nor does it add or subtract elements from that list.
So the problem is the app will lock (not crash and disappear) and be non-responsive or frozen. This happens at various times but typically within a minute or two. My best guess is a thead race condition.
There was someone else developing the UI portions of this app and they said the UI only updates when a data element of it changes. So in UpdateEntity() these two lines do cause a UI refresh.
Code:
entityUnitList[index] = entity
...
entityUnitList.add(entity)
Here is another place accessing that list and is in the UI thread
Code:
fun PutStuffOnMap(
uiState: FOXUiState, mapView: MapView, onEntityClicked: (entityClicked: String) -> Unit
) {
try {
val items = ArrayList<OverlayItem>()
runBlocking {
Datasource.entityUnitListMutex.withLock {
for (i in entityUnitList.indices) {
val entity = entityUnitList[i]
PlotEntity(mapView, entity)?.let { items.add(it) }
}
}// end mutex
}
. . .
There is more to that function but that function is called from here in a global method tha tis used in the map drawing code.
Java:
@Composable
fun MapRow(
uiState: FOXUiState,
onEntityClicked: (entityClicked: String) -> Unit,
modifier: Modifier = Modifier){
Row(modifier = modifier) {
AndroidView(
factory = { context ->
MapView(context).apply {
SetUpMap( uiState, this, onEntityClicked)
}
},
update = {
mapView ->
PutStuffOnMap( uiState, mapView, onEntityClicked)
}
)
}
}
I am thinking we have a mutex locking unlocking issue where it stays locked and the UI blocks with that runBlocking{ } block waiting for the unlock. The Listener gets in and gets out when updating the entity pretty quick. The only link to the UI thread is Datasource::UpdateEntity(). When we comment out UpdateEntity() we never deaklock or freeze the app. We can even pre-load entityUnitList with entity class elements and interact with them again with no app freeze. Its only when we enable getting movement updates over ethernet (using USB/Ethernet adapter plugged into the tablet) when we freeze the app eventually.
So I think using runBlocking is wrong so what is the correct way to share that entityUnitList over multiple threads safely? We do not have to use a mutex and we could consider other list types that are better with thread synchronization.
-Steve
EDIT wow this post is long, I apologize! Just wanted to give enough details.