Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ServiceTestRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Configurator
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import net.activitywatch.android.RustInterface
import net.activitywatch.android.watcher.utils.MAX_CONDITION_WAIT_TIME_MILLIS
import net.activitywatch.android.watcher.utils.PAGE_MAX_WAIT_TIME_MILLIS
Expand All @@ -18,6 +22,7 @@ import net.activitywatch.android.watcher.utils.createCustomTabsWrapper
import org.awaitility.Awaitility.await
import org.hamcrest.TypeSafeMatcher
import org.junit.Assume
import org.junit.Assert.assertFalse
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Rule
Expand Down Expand Up @@ -58,6 +63,7 @@ class WebWatcherTest {

browsers.forEach { browser ->
openUris(uris = testWebPages.map { it.url }, browser = browser)
val focusedText = if (browser == "com.brave.browser") typeInFocusedBraveBar() else null
openHome() // to commit last event

val matchers = testWebPages.map { it.toMatcher(browser) }
Expand All @@ -68,6 +74,31 @@ class WebWatcherTest {

matchers.all { matcher -> events.any { matcher.matches(it) } }
}
if (focusedText != null) {
val events = ri.getEventsJSON(BUCKET_NAME, 100).asListOfJsonObjects()
assertFalse(events.any { it.getJSONObject("data").optString("url") == focusedText })
}
}
}

private fun typeInFocusedBraveBar(): String {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(testWebPages.first().url))
.setPackage("com.brave.browser").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)

val configurator = Configurator.getInstance()
val previousFlags = configurator.uiAutomationFlags
configurator.uiAutomationFlags = FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
return try {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
checkNotNull(device.wait(Until.findObject(By.res("com.brave.browser:id/url_bar")), 5_000)).click()
val bar = device.wait(Until.findObject(By.res("com.brave.browser:id/url_bar").focused(true)), 5_000)
val text = "focus-${System.currentTimeMillis()}.invalid"
checkNotNull(bar).text = text
device.waitForIdle()
text
} finally {
configurator.uiAutomationFlags = previousFlags
}
}

Expand Down Expand Up @@ -120,8 +151,9 @@ data class WebPage(val url: String, val title: String) {
expectedBrowser = expectedBrowser,
)

// Samsung Internet does not match title at all as no android.webkit.WebView node is present
private fun shouldMatchTitle(browser: String) = browser != "com.sec.android.app.sbrowser"
// Samsung Internet and Brave do not expose page titles through the WebView node.
private fun shouldMatchTitle(browser: String) =
browser != "com.sec.android.app.sbrowser" && browser != "com.brave.browser"
Comment thread
NikitaMGrimm marked this conversation as resolved.
}

class WebWatcherEventMatcher(
Expand Down Expand Up @@ -150,4 +182,4 @@ class WebWatcherEventMatcher(
&& expectedTitle?.let { it == title } ?: true
&& browser == expectedBrowser
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ import android.view.accessibility.AccessibilityNodeInfo
import net.activitywatch.android.RustInterface
import org.json.JSONObject

private fun extractTextByViewId(event: AccessibilityEvent, viewId: String): String? {
event.source?.let { source ->
val nodes = source.findAccessibilityNodeInfosByViewId(viewId)
try {
return processExtractedText(nodes.firstOrNull()?.text?.toString())
} finally {
nodes.forEach { it.recycle() }
source.recycle()
}
private fun extractTextByViewId(
source: AccessibilityNodeInfo?,
viewId: String,
ignoreFocused: Boolean = false,
): String? {
source ?: return null
val nodes = source.findAccessibilityNodeInfosByViewId(viewId)
try {
val node = nodes.firstOrNull()?.takeUnless { ignoreFocused && it.isFocused }
return processExtractedText(node?.text?.toString())
} finally {
nodes.forEach { it.recycle() }
source.recycle()
}
return null
}

private fun extractTextByViewId(event: AccessibilityEvent, viewId: String): String? =
extractTextByViewId(event.source, viewId)

class WebWatcher : AccessibilityService() {

// The toolbar is a sibling of the content area, so we search from the window root.
Expand Down Expand Up @@ -50,6 +56,8 @@ class WebWatcher : AccessibilityService() {
// formatted identically no matter which browser/view-variant produced it.
private fun extractUrl(packageName: String, event: AccessibilityEvent): String? = when (packageName) {
"com.android.chrome" -> extractTextByViewId(event, "com.android.chrome:id/url_bar")
// Page events omit Brave's toolbar; the focused address bar may show a search hint.
"com.brave.browser" -> extractTextByViewId(rootInActiveWindow, "com.brave.browser:id/url_bar", ignoreFocused = true)
"org.mozilla.firefox" ->
// Compose toolbar (current)
extractFirefoxUrl(event)
Expand Down Expand Up @@ -109,16 +117,17 @@ class WebWatcher : AccessibilityService() {
}

try {
val browser = packageName!!
val newUrl = extractUrl(browser, event)
if (newUrl == null) {
maybeDumpTree(browser)
if (windowChanged) handleUrl(null, newBrowser = null)
} else {
handleUrl(newUrl, newBrowser = browser)
}

event.source?.let { source ->
try {
val browser = packageName!!
val newUrl = extractUrl(browser, event)

if (newUrl == null) {
maybeDumpTree(browser)
} else {
handleUrl(newUrl, newBrowser = browser)
}
findWebView(source)?.let { webView ->
handleWindowTitle(webView.text.toString())
if (webView !== source) webView.recycle()
Expand Down Expand Up @@ -206,6 +215,7 @@ class WebWatcher : AccessibilityService() {
companion object {
internal val KNOWN_BROWSER_PACKAGES = setOf(
"com.android.chrome",
"com.brave.browser",
Comment thread
NikitaMGrimm marked this conversation as resolved.
"org.mozilla.firefox",
"com.sec.android.app.sbrowser",
"com.opera.browser",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ class BrowserSessionTrackerTest {
assertEquals("chrome", completed.browser)
}

@Test
fun `unavailable url ends the previous browser session`() {
val tracker = BrowserSessionTracker()
tracker.handleUrl("example.com", "chrome")

val completed = tracker.handleUrl(null, null)

checkNotNull(completed)
assertEquals("example.com", completed.url)
assertEquals("chrome", completed.browser)
}

@Test
fun `title set before the url changes is attached to the completed session`() {
val clock = FakeClock(Instant.ofEpochSecond(1000))
Expand Down