Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add ChromeWatcher #30

Merged
merged 1 commit into from
Sep 29, 2021
Merged
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
9 changes: 9 additions & 0 deletions mobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,14 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>

<service android:name=".watcher.ChromeWatcher"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package net.activitywatch.android.watcher

import android.accessibilityservice.AccessibilityService
import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.AppOpsManager
import android.app.PendingIntent
import android.app.usage.UsageEvents
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.*
import android.provider.Settings
import android.support.annotation.RequiresApi
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import net.activitywatch.android.R
import net.activitywatch.android.RustInterface
import net.activitywatch.android.models.Event
import org.json.JSONObject
import org.threeten.bp.DateTimeUtils
import org.threeten.bp.Duration
import org.threeten.bp.Instant
import java.net.URL
import java.text.ParseException
import java.text.SimpleDateFormat

class ChromeWatcher : AccessibilityService() {

private val TAG = "ChromeWatcher"
private val bucket_id = "aw-watcher-android-web-chrome"

private var ri : RustInterface? = null

var lastUrlTimestamp : Instant? = null
var lastUrl : String? = null
var lastTitle : String? = null

override fun onCreate() {
ri = RustInterface(applicationContext)
ri?.createBucketHelper(bucket_id, "web.tab.current")
}

override fun onAccessibilityEvent(event: AccessibilityEvent) {

// TODO: This method is called very often, which might affect performance. Future optimizations needed.

// Only track Chrome and System events
if (event.packageName != "com.android.chrome" && event.packageName != "com.android.systemui") {
onUrl(null)
return
}

try {
if (event != null && event.source != null) {
// Get URL
val urlBars = event.source.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
if (urlBars.any()) {
val newUrl = "http://" + urlBars[0].text.toString() // TODO: We can't access the URI scheme, so we assume HTTP.
onUrl(newUrl)
}

// Get title
var webView = findWebView(event.source)
if (webView != null) {
lastTitle = webView.text.toString()
Log.i(TAG, "Title: ${lastTitle}")
}
}
}
catch(ex : Exception) {
Log.e(TAG, ex.message)
}
}

fun findWebView(info : AccessibilityNodeInfo) : AccessibilityNodeInfo? {
if(info == null)
return null

if(info.className == "android.webkit.WebView" && info.text != null)
return info

for (i in 0 until info.childCount) {
val child = info.getChild(i)
val webView = findWebView(child)
if (webView != null) {
return webView
}
if(child != null){
child.recycle()
}
}

return null
}

fun onUrl(newUrl : String?) {
Log.i(TAG, "Url: ${newUrl}")
if (newUrl != lastUrl) { // URL changed
if (lastUrl != null) {
// Log last URL and title as a completed browser event.
// We wait for the event to complete (marked by a change in URL) to ensure that
// we had a chance to receive the title of the page, which often only arrives after
// the page loads completely and/or the user interacts with the page.
logBrowserEvent(lastUrl!!, lastTitle ?: "", lastUrlTimestamp!!)
}

lastUrlTimestamp = Instant.ofEpochMilli(System.currentTimeMillis())
lastUrl = newUrl
lastTitle = null
}
}

fun logBrowserEvent(url: String, title: String, lastUrlTimestamp : Instant) {
val now = Instant.ofEpochMilli(System.currentTimeMillis())
val start = lastUrlTimestamp
val end = now
val duration = Duration.between(start, end)

val data = JSONObject()
data.put("url", url)
data.put("title", title)
data.put("audible", false) // TODO
data.put("incognito", false) // TODO

ri?.heartbeatHelper(bucket_id, start, duration.seconds.toDouble(), data, 1.0)
}

override fun onInterrupt() {
TODO("not implemented")
}
}
1 change: 1 addition & 0 deletions mobile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"\n<b>If you\'re new to ActivityWatch</b>, you should also check out the <a href='https://activitywatch.net/'>desktop version of ActivityWatch</a>."
</string>
<string name="button_log_text">Click me to log data!</string>
<string name="accessibility_service_description">Allows ActivityWatch to read the URL and title from your browser.</string>

<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
Expand Down
8 changes: 8 additions & 0 deletions mobile/src/main/res/xml/accessibility_service_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowsChanged|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:accessibilityFlags="flagDefault|flagReportViewIds|flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true" />