DatagramSocket.receive causes ANR when I ask for POST_NOTIFICATION permission

105 views Asked by At

I don't know what might be causing my issue because I didn't really made a lot of changes to it. The changes only involved the MainActivity, AndroidManifest, and the helper class I made that handle the creation of the Service, but somehow StrictMode was pointing at some illegal disk reads on other files. Also, the Service shouldn't immediately affect the app because I only start it on MainActivity's onStop because I just want it to listen to some stuff when the MainActivity is died. I think I already fixed the illegal reads warning somehow, but I'm still getting the ANR. StrictMode is saying that I have a leak, but LeakCanary isn't detecting anything. It did detect 1 thing, but I already fixed it. And still, I'm still getting the ANR occasionally. The last code that runs before I get the ANR is a method that sends UDP, which wasn't giving me any issue previously. I already changed that method's parameters to accept WeakReferences, but it didn't really help I think. All my I/O operations are either inside a lifecycleScope.launch or in a Thread, so I don't know why I should be getting an ANR, on an app that was already running fine previously.

Here's my code:

This is the helper class for creating the Service:

class ServiceHelper : Service() {
    private val socketHelper = SocketHelper.getInstance()!!
    private var lastShownNotificationId = 1

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        when(intent.action){
            Actions.START.toString() -> createAndShowForegroundNotification(this, lastShownNotificationId)
            Actions.STOP.toString() -> stopSelf()
        }

//        socketHelper.start(null)
//        socketHelper.socketRespObservers.clear()
//        socketHelper.socketRespObservers.add { socketResp ->
//            val respArr = socketResp.split(SocketHelper.SOCKET_DELIMITER)
//
//            if (respArr[0] == SocketHelper.SERVER_UDP_HEADER) {
//                processUDPMessage(respArr[1], respArr[2])
//            }
//        }

        return START_STICKY
    }

    private fun createAndShowForegroundNotification(yourService: Service, notificationId: Int) {
        val notifBuilder = NotificationCompat.Builder(this,
            "atlantis_service_notif_channel_id")
            .setOngoing(true)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle(yourService.getString(R.string.service_notif_title))
            .setContentText(yourService.getString(R.string.service_notif_content))

        val notification = notifBuilder.build()
        yourService.startForeground(notificationId, notification)

        lastShownNotificationId = notificationId
    }

    enum class Actions{
        START, STOP
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    private fun processUDPMessage(udpCommand: String, udpValue: String) {
        if (udpCommand == SocketHelper.APP_NOTIFICATION) {
            val srvNotif = Gson().fromJson(udpValue, ServerNotification::class.java)
            Log.i(TAG, "service notif: $srvNotif")
        }
    }

    companion object{
        const val TAG = ""
    }
}

Then this is that Socket helper class:

class SocketHelper {
    private val broadcastAddr: InetAddress = InetAddress.getByName("255.255.255.255")
    private lateinit var localBroadcastAddr: InetAddress
    private lateinit var basicSocket: DatagramSocket
    val socketRespObservers = mutableListOf<(String) -> Unit>()
    private var socketResp: String by Delegates.observable("") { _, _, newVal ->
        socketRespObservers.forEach { it(newVal) }
    }
    private lateinit var _ctx: WeakReference<Context>
    
    @Synchronized
    fun start(ctx: WeakReference<Context>?) {
        if(ctx != null){
            _ctx = ctx
        }

        Thread {
            try {
                if(!::basicSocket.isInitialized || basicSocket == null){
                    basicSocket = DatagramSocket(UDP_SERVER_PORT)

                    val udpPayload = StringBuilder()
                        .append(UDP_HEADER).append(SOCKET_DELIMITER)
                        .append(REQ_SERVER_IP_COMMAND).append(SOCKET_DELIMITER)
                        .append("").toString()

                    basicSendToUDP(WeakReference(udpPayload), getBroadcastAddr())
                }

                while (true) {
                    basicReceiveFromUDP()
                }
            } catch (e: Exception) {
                sendSnackbar("Socket Error: ${e.localizedMessage}")
            }
        }.start()
    }

    @Synchronized
    fun basicSendToUDP(payload: WeakReference<String>, iAddr: WeakReference<InetAddress>) {
        Log.i(TAG, "basicSendToUDP")
        Thread {
            try{
                val sendPacket =
                    DatagramPacket(
                        payload.get()!!.toByteArray(),
                        payload.get()!!.length,
                        iAddr.get(),
                        UDP_SERVER_PORT
                    )
                basicSocket.send(sendPacket)
            } catch (e: IOException) {
                Log.e(TAG, "sendUDP IO Error:", e)
                close("sendUDP IO Error")

                val msgArr = e.message?.split("(")
                var realErrorMsg = ""
                if(msgArr != null){
                    realErrorMsg = msgArr[1].replace(")", "")
                    ///I'm using gorm with my golang server, this is just some error msg parsing essentially
                }

                if(realErrorMsg != ""){
                    sendSnackbar("Network error: $realErrorMsg")
                }else{
                    sendSnackbar("Network error occurred")
                }
            }catch (e: Exception){
                Log.e(TAG, "sendUDP IO Error:", e)
                close("sendUDP IO Error")

                sendSnackbar("Network error: ${e.message}")
            }
        }.start()
        
    @Synchronized
    private fun basicReceiveFromUDP() {
        val receiveData = ByteArray(1024)
        val receivePacket = DatagramPacket(receiveData, receiveData.size)

        basicSocket.receive(receivePacket)

        socketResp = String(receivePacket.data, 0, receivePacket.length)

        val respArr = socketResp.split(SOCKET_DELIMITER)

        if (respArr[0] == SERVER_UDP_HEADER && respArr.size > 3 && respArr[3] != null) {
            sendAck(respArr[3].toInt())
        }
    }

    @Synchronized
    private fun sendAck(seqNum: Int) {
        val udpPayload = StringBuilder()
            .append(UDP_HEADER).append(SOCKET_DELIMITER)
            .append(ACK_PAYLOAD_RECEIVED).append(SOCKET_DELIMITER)
            .append(seqNum).toString()

        basicSendToUDP(WeakReference(udpPayload), getBroadcastAddr())
    }

    fun getBroadcastAddr(): WeakReference<InetAddress> {
        return WeakReference(broadcastAddr)
    }
    
    private fun sendSnackbar(message: String){
    //I was suspecting this to be a cause of the ANR, but it doesn't seem to be
//        if(::_ctx.isInitialized){
//            (_ctx as MainActivity).showSnackMessage(message, "SocketHelper")
//        }
    }
    
    fun close(closeReason: String) {
        basicSocket.close()
    }

On the call to basicSendToUDP inside sendAck, that seems to be when/where the ANR mostly gets triggered.

Here's how I added the Service to the AndroidManifest, also currently commented:

<service android:name=".utils.helpers.ServiceHelper"
            android:foregroundServiceType="dataSync"
            android:exported="false"></service>

I don't know if commenting that helped stop the ANR, because it's not appearing after doing that.

Then this are the code that "fixed" the illegal disk reads:

val oldPolicy = StrictMode.allowThreadDiskReads()
try{
    prefHelper = (requireActivity().application as MyApp).prefHelper
    /// do stuff with prefHelper
}finally {
    StrictMode.setThreadPolicy(oldPolicy)
}

This is what prefHelper actually looks like

class MyApp : Application() {
    ...
    val prefHelper by lazy { PreferenceHelper.customPrefs(this, "atlantis_prefs") }
}

Then this is what customPrefs looks like:

object PreferenceHelper {
    lateinit var instance: SharedPreferences
    
    fun customPrefs(context: Context, name: String): SharedPreferences {
        if(!::instance.isInitialized){
            instance = context.getSharedPreferences(name, Context.MODE_PRIVATE)
        }

        return instance
    }
}

Right now, if I disable/comment out all the code that is related to the Service (the ones in MainActivity and AndroidManifest), I'd still the get the leak warning from StrictMode, but it won't result into an ANR. I probably have some issue with my socket helper class that I just can't see.

UPDATE:

I think I found a very interesting cause of the ANR. After adding LeakCanary to my app, I realized that it asks for POST_NOTIFICATION permission. I was almost gonna go crazy that my app that I already commented out the part that asks for that permission, is still asking for it. And also, if I accept that request, that's when the ANR happens

UPDATE:

I tried "disabling" LeakCanary as I think the leaks aren't as important right now as the ANR because the leaks are mostly caused by DataBinding not releasing ConstraintLayout properly and I already added a fix to it (I'm still getting some leak notifications about ConstraintLayout but I don't want to spend more time on them right now). I also tried to ask the POST_NOTIFICATION permission myself, and well, the ANR appeared again. I got anrwatchdog now and it was pointing out to the line with basicSocket?.receive(receivePacket) in it. Again, all my code works perfectly if I don't ask for POST_NOTIFICATION permission, so I don't know why a permission would break it. Right now, even if I haven't accepted the POST_NOTIFICATION permission, anrwatchdog would already crash the app saying that basicSocket?.receive(receivePacket) would cause an ANR.

UPDATE:

I think I hit a metaphorical gas pipe. I filed an issue in issuetracker and, it's already being looked at by the people from AOSP. Here's the link to the issue.

0

There are 0 answers