androidkotlinnetworkingpacket-capture

How to Detect Websites Opened by User in Android Using Kotlin


I'm working on a kotlin Android app where I need to monitor the websites that a user is visiting on their device. I've tried many different variations of code that all, in one way or another, try to route traffic through a VPN, but all of them eventually run into the same form of issue where once the code is running sites will no longer load. I asked a question regarding this issue with code not long ago on this post, but I ended up dropping that code as I could not fix the bug I spoke about on that post.

I wrote this post to ask if anyone is aware of a method to detect and log the websites a user is visiting on a kotlin Android app?

Any guidance, examples, or references would be greatly appreciated. Thanks in advance!


Solution

  • I ended up figuring out a very obscure solution to the issue. It turns out that Android's Accessibility Service can track web traffic, not through packet sniffing or VPNs, but by utilizing the AccessibilityNodeInfo object. I do not fully understand it yet, but here is my best explanation.

    By monitoring the UI of browsers, specifically the android.widget.EditText view, you can figure out the site the user is currently on/loading. The android.widget.EditText view is used, at least by Chrome, to display the site's domain name in the search bar. So by checking this node and grabbing its value, we can find the site the user is on.

    I assume this code does have downsides though as it depends on what I assume is browser-specific code and UI, thus updates might break it.

    Here is the code I ended up using, though I did not filter for only the search bar view in this code instead scanning the whole screen.

    import android.accessibilityservice.AccessibilityService
    import android.accessibilityservice.AccessibilityServiceInfo
    import android.util.Log
    import android.view.accessibility.AccessibilityEvent
    import android.view.accessibility.AccessibilityNodeInfo
    
    class UrlLoggingService : AccessibilityService() {
    
        private val TAG = "UrlLoggingService"
    
        override fun onServiceConnected() {
            val info = AccessibilityServiceInfo().apply {
                eventTypes = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
                packageNames = arrayOf("com.android.chrome")
                feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
                notificationTimeout = 100
            }
            serviceInfo = info
            Log.d(TAG, "Accessibility Service connected")
        }
    
        override fun onAccessibilityEvent(event: AccessibilityEvent?) {
            event ?: return
    
            val rootNode = rootInActiveWindow ?: return
            Log.d(TAG, "Root node obtained, starting URL extraction")
    
            val url = extractUrl(rootNode)
            url?.let {
                Log.d(TAG, "Visited URL: $it")
            } ?: Log.d(TAG, "No URL found")
        }
    
        private fun extractUrl(node: AccessibilityNodeInfo?): String? {
            if (node == null) {
                Log.d(TAG, "Node is null, returning")
                return null
            }
    
            Log.d(TAG, "Processing node: ${node.className}, Text: ${node.text}")
    
            if (node.className == "android.widget.EditText" && node.text != null) {
                val text = node.text.toString()
                Log.d(TAG, "Found URL: $text")
                return text
    
            }
    
            for (i in 0 until node.childCount) {
                val childNode = node.getChild(i)
                val url = extractUrl(childNode)
                if (url != null) {
                    return url
                }
            }
            return null
        }
    
        override fun onInterrupt() {
            Log.d(TAG, "Accessibility Service interrupted")
        }
    }
    

    I should also note this code will need Accessibility permissions to run which can be requested with this code

    startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))